From b680ae6e43843bd8b76b8684a6e43006e0d83d69 Mon Sep 17 00:00:00 2001 From: Ermat Date: Wed, 12 Jun 2019 11:14:14 +0600 Subject: [PATCH 001/234] Update bundle --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b72406dd..7d2b2c8d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,10 +9,10 @@ GEM tzinfo (~> 1.1) atomos (0.1.3) claide (1.0.2) - cocoapods (1.7.0) + cocoapods (1.7.1) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.7.0) + cocoapods-core (= 1.7.1) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -28,7 +28,7 @@ GEM nap (~> 1.0) ruby-macho (~> 1.4) xcodeproj (>= 1.8.2, < 2.0) - cocoapods-core (1.7.0) + cocoapods-core (1.7.1) activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) @@ -45,7 +45,7 @@ GEM colored2 (3.1.2) concurrent-ruby (1.1.5) escape (0.0.4) - fourflusher (2.2.0) + fourflusher (2.3.0) fuzzy_match (2.0.4) gh_inspector (1.1.3) i18n (0.9.5) @@ -59,7 +59,7 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - xcodeproj (1.9.0) + xcodeproj (1.10.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) From 022ff1fced45bcf002ee099f6360e62d622bca3e Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 13 Jun 2019 15:25:25 +0600 Subject: [PATCH 002/234] Fix bug with updated checkpoint (#389) - If the last block in storage is older than the lastCheckpointBlock set to network, then it uses bip44CheckpointBlock --- .../BitcoinCore/Blocks/BlockSyncer.swift | 30 ++++-- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- .../Blocks/BlockSyncerTests.swift | 102 +++++++++++++----- 3 files changed, 102 insertions(+), 32 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift b/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift index 0238dfd9..d1f3d415 100644 --- a/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift +++ b/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift @@ -168,21 +168,39 @@ extension BlockSyncer: IBlockSyncer { extension BlockSyncer { - public static func instance(storage: IStorage, checkpointBlock: Block, factory: IFactory, listener: ISyncStateListener, transactionProcessor: ITransactionProcessor, - blockchain: IBlockchain, addressManager: IAddressManager, bloomFilterManager: IBloomFilterManager, + public static func instance(storage: IStorage, checkpointBlock: Block, factory: IFactory, listener: ISyncStateListener, + transactionProcessor: ITransactionProcessor, blockchain: IBlockchain, addressManager: IAddressManager, bloomFilterManager: IBloomFilterManager, hashCheckpointThreshold: Int = 100, logger: Logger? = nil, state: BlockSyncerState = BlockSyncerState()) -> BlockSyncer { let syncer = BlockSyncer(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: listener, transactionProcessor: transactionProcessor, blockchain: blockchain, addressManager: addressManager, bloomFilterManager: bloomFilterManager, hashCheckpointThreshold: hashCheckpointThreshold, logger: logger, state: state) - if storage.blocksCount == 0 { - storage.save(block: checkpointBlock) - } - listener.initialBestBlockHeightUpdated(height: syncer.localDownloadedBestBlockHeight) return syncer } + public static func checkpointBlock(network: INetwork, syncMode: BitcoinCore.SyncMode, storage: IStorage) -> Block { + let lastBlock = storage.lastBlock + let checkpointBlock: Block + + if syncMode == .full { + checkpointBlock = network.bip44CheckpointBlock + } else if let block = lastBlock, block.height < network.lastCheckpointBlock.height { + // When app is updated there may be case when the last block in DB is earlier than new checkpoint block. + // In this case we set the very first checkpoint block for bip44, + // since it surely will be earlier than the last block in DB + checkpointBlock = network.bip44CheckpointBlock + } else { + checkpointBlock = network.lastCheckpointBlock + } + + if lastBlock == nil { + storage.save(block: checkpointBlock) + } + + return checkpointBlock + } + } \ No newline at end of file diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 363f875d..fad3b277 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -188,7 +188,7 @@ public class BitcoinCoreBuilder { let blockValidatorChain = BlockValidatorChain(proofOfWorkValidator: ProofOfWorkValidator(difficultyEncoder: DifficultyEncoder())) let blockchain = Blockchain(storage: storage, blockValidator: blockValidatorChain, factory: factory, listener: dataProvider) - let checkpointBlock = syncMode == .full ? network.bip44CheckpointBlock : network.lastCheckpointBlock + let checkpointBlock = BlockSyncer.checkpointBlock(network: network, syncMode: syncMode, storage: storage) let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, addressManager: addressManager, bloomFilterManager: bloomFilterManager, logger: logger) let initialBlockDownload = InitialBlockDownload(blockSyncer: blockSyncer, peerManager: peerManager, merkleBlockValidator: merkleBlockValidator, syncStateListener: kitStateProvider, logger: logger) diff --git a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift index 5536c882..2fc11aeb 100644 --- a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift @@ -49,55 +49,107 @@ class BlockSyncerTests: QuickSpec { context("static methods") { describe("#instance") { + it("triggers #initialBestBlockHeightUpdated event on listener") { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(checkpointBlock) + } + stub(mockListener) { mock in + when(mock.initialBestBlockHeightUpdated(height: any())).thenDoNothing() + } + + + let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, + blockchain: mockBlockchain, addressManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100) + + verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) + verifyNoMoreInteractions(mockListener) + } + } + + describe("#checkpointBlock") { + let bip44CheckpointBlock = TestData.checkpointBlock + let lastCheckpointBlock = TestData.firstBlock + let mockNetwork = MockINetwork() + + beforeEach { + stub(mockNetwork) { mock in + when(mock.bip44CheckpointBlock.get).thenReturn(bip44CheckpointBlock) + when(mock.lastCheckpointBlock.get).thenReturn(lastCheckpointBlock) + } + } + + afterEach { + reset(mockNetwork) + } + context("when there are some blocks in storage") { + let lastBlock = TestData.secondBlock + beforeEach { stub(mockStorage) { mock in - when(mock.blocksCount.get).thenReturn(1) - when(mock.lastBlock.get).thenReturn(checkpointBlock) + when(mock.lastBlock.get).thenReturn(lastBlock) } - - stub(mockListener) { mock in - when(mock.initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height)))).thenDoNothing() - } - - let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, addressManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100) } it("doesn't save checkpointBlock to storage") { + _ = BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) + verify(mockStorage, never()).save(block: any()) - verify(mockStorage).blocksCount.get() verify(mockStorage).lastBlock.get() verifyNoMoreInteractions(mockStorage) } - it("triggers #initialBestBlockHeightUpdated event on listener") { - verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) - verifyNoMoreInteractions(mockListener) + context("when syncMode is .full") { + it("returns bip44CheckpointBlock") { + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) + } + } + + context("when syncMode is not .full") { + context("when lastBlock's height is more than lastCheckpointBlock") { + it("returns lastCheckpointBlock") { + lastBlock.height = lastCheckpointBlock.height + 1 + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) + } + } + + context("when lastBlock's height is less than lastCheckpointBlock") { + it("returns bip44CheckpointBlock") { + lastBlock.height = lastCheckpointBlock.height - 1 + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(bip44CheckpointBlock)) + } + } } } context("when there's no block in storage") { beforeEach { stub(mockStorage) { mock in - when(mock.blocksCount.get).thenReturn(0) - when(mock.lastBlock.get).thenReturn(checkpointBlock) - when(mock.save(block: sameInstance(as: checkpointBlock))).thenDoNothing() + when(mock.lastBlock.get).thenReturn(nil) + when(mock.save(block: any())).thenDoNothing() } - stub(mockListener) { mock in - when(mock.initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height)))).thenDoNothing() + } + + context("when syncMode is .full") { + it("returns bip44CheckpointBlock") { + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) } - let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, addressManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100) + it("saves bip44CheckpointBlock to storage") { + BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage) + verify(mockStorage).save(block: sameInstance(as: bip44CheckpointBlock)) + } } - it("saves checkpointBlock to storage") { - verify(mockStorage).save(block: sameInstance(as: checkpointBlock)) - } + context("when syncMode is not .full") { + it("returns lastCheckpointBlock") { + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) + } - it("triggers #initialBestBlockHeightUpdated event on listener") { - verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) + it("saves lastCheckpointBlock to storage") { + BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) + verify(mockStorage).save(block: sameInstance(as: lastCheckpointBlock)) + } } } } From 310b6a88422cfb3356c1dbaf77829980737d94e5 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Mon, 17 Jun 2019 15:39:53 +0600 Subject: [PATCH 003/234] Fix bug with wrong checkpoint block timestamp (#391) --- BitcoinKit/BitcoinKit/Network/MainNet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitcoinKit/BitcoinKit/Network/MainNet.swift b/BitcoinKit/BitcoinKit/Network/MainNet.swift index 6fab7053..40696e4d 100644 --- a/BitcoinKit/BitcoinKit/Network/MainNet.swift +++ b/BitcoinKit/BitcoinKit/Network/MainNet.swift @@ -45,7 +45,7 @@ class MainNet: INetwork { headerHash: "00000000000000000001791f463d849ce5363d751c91f7d3cd2ff18981ae221d".reversedData!, previousBlockHeaderHash: "0000000000000000000485ab94f5ea60203aacfc9740b3e42700d7e7012f76d7".reversedData!, merkleRoot: "2e76c50d3dcecc46264b7ff8e653d5c9f06680f4d88f5b239d58a531a3c12279".reversedData!, - timestamp: 1559277784, + timestamp: 1559256184, bits: 0x1725bb76, nonce: 0x423310ae ), From 21e99a6669b17cedbbe9b9a77a634b6b94f7ca82 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Mon, 17 Jun 2019 15:45:58 +0600 Subject: [PATCH 004/234] Increase pod versions to 0.6.1 --- BitcoinCashKit.swift.podspec | 4 ++-- BitcoinCore.swift.podspec | 2 +- BitcoinKit.swift.podspec | 4 ++-- DashKit.swift.podspec | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BitcoinCashKit.swift.podspec b/BitcoinCashKit.swift.podspec index aaea0c60..462d0dc7 100644 --- a/BitcoinCashKit.swift.podspec +++ b/BitcoinCashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCashKit.swift' spec.module_name = "BitcoinCashKit" - spec.version = '0.6' + spec.version = '0.6.1' spec.summary = 'BitcoinCash library for Swift' spec.description = <<-DESC BitcoinCashKit implements BitcoinCash protocol in Swift. It is an implementation of the BitcoinCash SPV protocol written (almost) entirely in swift. @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.6' + spec.dependency 'BitcoinCore.swift', '~> 0.6.1' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.1' spec.dependency 'Alamofire', '~> 4.0' diff --git a/BitcoinCore.swift.podspec b/BitcoinCore.swift.podspec index d54220b6..d81e48c9 100644 --- a/BitcoinCore.swift.podspec +++ b/BitcoinCore.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCore.swift' spec.module_name = "BitcoinCore" - spec.version = '0.6' + spec.version = '0.6.1' spec.summary = 'Core library Bitcoin derived wallets for Swift' spec.description = <<-DESC BitcoinCore implements Bitcoin core protocol in Swift. It is an implementation of the Bitcoin SPV protocol written (almost) entirely in swift. diff --git a/BitcoinKit.swift.podspec b/BitcoinKit.swift.podspec index 03410101..d383e720 100644 --- a/BitcoinKit.swift.podspec +++ b/BitcoinKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinKit.swift' spec.module_name = 'BitcoinKit' - spec.version = '0.6' + spec.version = '0.6.1' spec.summary = 'Bitcoin library for Swift' spec.description = <<-DESC BitcoinKit implements Bitcoin protocol in Swift. @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.6' + spec.dependency 'BitcoinCore.swift', '~> 0.6.1' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.1' spec.dependency 'Alamofire', '~> 4.0' diff --git a/DashKit.swift.podspec b/DashKit.swift.podspec index ab40788d..043056b7 100644 --- a/DashKit.swift.podspec +++ b/DashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'DashKit.swift' spec.module_name = 'DashKit' - spec.version = '0.6' + spec.version = '0.6.1' spec.summary = 'Dash library for Swift' spec.description = <<-DESC DashKit implements Dash protocol in Swift. @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.6' + spec.dependency 'BitcoinCore.swift', '~> 0.6.1' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.1' spec.dependency 'CryptoBLS.swift', '~> 1.1' From a772acffc02f33f31ac1ae1f310ae15a5ee2db79 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Mon, 17 Jun 2019 17:00:26 +0600 Subject: [PATCH 005/234] Add migration for change wrong checkpoint block timestamp --- BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift b/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift index 3abcac52..10c1c542 100644 --- a/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift +++ b/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift @@ -154,6 +154,10 @@ open class GrdbStorage { try db.execute(sql: "UPDATE \(Block.databaseTableName) SET \(Block.Columns.hasTransactions.name) = true") } + migrator.registerMigration("setCorrectTimestampForCheckpointBlock578592") { db in + try db.execute(sql: "UPDATE \(Block.databaseTableName) SET \(Block.Columns.timestamp.name) = 1559256184 WHERE \(Block.Columns.height.name) == 578592 AND \(Block.Columns.timestamp.name) == 1559277784") + } + return migrator } From af67e97b51960c81978cb281245b159fc0b26464 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Thu, 20 Jun 2019 10:54:36 +0600 Subject: [PATCH 006/234] Fix bug with wpkhSh transaction restoring. (#397) - Update testnet checkpoint block. --- .../Managers/BloomFilterManager.swift | 2 +- .../Transactions/TransactionProcessor.swift | 2 +- BitcoinKit/BitcoinKit/Network/TestNet.swift | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift index ebff3b7e..f7944794 100644 --- a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift @@ -51,7 +51,7 @@ extension BloomFilterManager: IBloomFilterManager { } var outputs = storage.outputsWithPublicKeys().filter { output in - return output.output.scriptType == ScriptType.p2wpkh || output.output.scriptType == ScriptType.p2pk + return output.output.scriptType == ScriptType.p2wpkh || output.output.scriptType == ScriptType.p2pk || output.output.scriptType == ScriptType.p2wpkhSh } if let bestBlockHeight = storage.lastBlock?.height { diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift index a083e325..b0ce18b5 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift @@ -29,7 +29,7 @@ class TransactionProcessor { private func hasUnspentOutputs(transaction: FullTransaction) -> Bool { for output in transaction.outputs { - if output.publicKeyPath != nil, (output.scriptType == .p2wpkh || output.scriptType == .p2pk) { + if output.publicKeyPath != nil, (output.scriptType == .p2wpkh || output.scriptType == .p2pk || output.scriptType == .p2wpkhSh) { return true } } diff --git a/BitcoinKit/BitcoinKit/Network/TestNet.swift b/BitcoinKit/BitcoinKit/Network/TestNet.swift index ce6b7585..7c15121c 100644 --- a/BitcoinKit/BitcoinKit/Network/TestNet.swift +++ b/BitcoinKit/BitcoinKit/Network/TestNet.swift @@ -41,15 +41,15 @@ class TestNet: INetwork { var lastCheckpointBlock: Block { return Block( withHeader: BlockHeader( - version: 0x20000000, - headerHash: "000000000000011b820755b3bbe03de7f7b8854b9f03307f41dafea4694eee7b".reversedData!, - previousBlockHeaderHash: "00000000000001d6874b4d88e387098c0b7100ff674d99781fc7045a78216a15".reversedData!, - merkleRoot: "d108b1c6229e1bc0c5506307779c6a51b1cb4c8edf3f91bef36dd1a2c30dfc99".reversedData!, - timestamp: 1558613325, - bits: 436289093, - nonce: 2472615319 + version: 0x20400000, + headerHash: "000000000000608edb398b7969795bc0681c1fa280234f44cb2c506f853ae41e".reversedData!, + previousBlockHeaderHash: "000000000001386a3904f0ba4f25dc7ace09b67a6fe8977e7aecc55813fa9ac5".reversedData!, + merkleRoot: "c75984136598aaec589cbe0f61b1caf6c1eb69614c412861b474aedce5d71b7a".reversedData!, + timestamp: 1560904860, + bits: 0x1b008690, + nonce: 0xcf731739 ), - height: 1518048) + height: 1560384) } } From 1ffc18140b796fbf29f155ee14c0bd2c64ed741d Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 21 Jun 2019 15:19:18 +0600 Subject: [PATCH 007/234] Bugfixes (#395) - TransactionProcessor doesn't emit 'onUpdate' event to it's listener if transaction existed in DB with blockHash and it's coming from mempool - TransactionProcessor trows exception in 'processCreated' if the transaction expires the bloom filter - TransactionCreator regenerates bloomfilter if transaction processor throws BloomFilterExpired exception - Remove bloomfilter.filter mutation check. It's useless, because filter is changed even if the elements are not changed --- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- .../Managers/BloomFilterManager.swift | 7 +-- .../Transactions/TransactionCreator.swift | 11 ++++- .../Transactions/TransactionProcessor.swift | 16 ++++--- .../Managers/BloomFilterManagerTests.swift | 19 -------- .../TransactionCreatorTests.swift | 40 +++++++++++++++-- .../TransactionProcessorTests.swift | 44 ++++++++++++++++++- 7 files changed, 102 insertions(+), 37 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index fad3b277..0c5adc33 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -201,7 +201,7 @@ public class BitcoinCoreBuilder { let scriptBuilder = ScriptBuilderChain() let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, addressManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) - let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender) + let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) let syncManager = SyncManager(reachabilityManager: reachabilityManager, initialSyncer: initialSyncer, peerGroup: peerGroup) diff --git a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift index f7944794..609578de 100644 --- a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift @@ -66,11 +66,8 @@ extension BloomFilterManager: IBloomFilterManager { } if !elements.isEmpty { - let bloomFilter = factory.bloomFilter(withElements: elements) - if self.bloomFilter?.filter != bloomFilter.filter { - self.bloomFilter = bloomFilter - delegate?.bloomFilterUpdated(bloomFilter: bloomFilter) - } + bloomFilter = factory.bloomFilter(withElements: elements) + delegate?.bloomFilterUpdated(bloomFilter: bloomFilter!) } } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index 8aa8bca2..fd9ec33c 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -6,11 +6,13 @@ class TransactionCreator { private let transactionBuilder: ITransactionBuilder private let transactionProcessor: ITransactionProcessor private let transactionSender: ITransactionSender + private let bloomFilterManager: IBloomFilterManager - init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender) { + init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender, bloomFilterManager: IBloomFilterManager) { self.transactionBuilder = transactionBuilder self.transactionProcessor = transactionProcessor self.transactionSender = transactionSender + self.bloomFilterManager = bloomFilterManager } } @@ -21,7 +23,12 @@ extension TransactionCreator: ITransactionCreator { try transactionSender.verifyCanSend() let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address) - try transactionProcessor.processCreated(transaction: transaction) + + do { + try transactionProcessor.processCreated(transaction: transaction) + } catch _ as BloomFilterManager.BloomFilterExpired { + bloomFilterManager.regenerateBloomFilter() + } try transactionSender.send(pendingTransaction: transaction) } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift index b0ce18b5..083c156c 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift @@ -27,8 +27,8 @@ class TransactionProcessor { self.queue = queue } - private func hasUnspentOutputs(transaction: FullTransaction) -> Bool { - for output in transaction.outputs { + private func expiresBloomFilter(outputs: [Output]) -> Bool { + for output in outputs { if output.publicKeyPath != nil, (output.scriptType == .p2wpkh || output.scriptType == .p2pk || output.scriptType == .p2wpkhSh) { return true } @@ -53,9 +53,6 @@ class TransactionProcessor { } private func relay(transaction: Transaction, withOrder order: Int, inBlock block: Block?) { - guard block != nil || transaction.blockHash == nil else { - return - } transaction.blockHash = block?.headerHash transaction.status = .relayed transaction.timestamp = block?.timestamp ?? Int(dateGenerator().timeIntervalSince1970) @@ -80,6 +77,9 @@ extension TransactionProcessor: ITransactionProcessor { try queue.sync { for (index, transaction) in transactions.inTopologicalOrder().enumerated() { if let existingTransaction = self.storage.transaction(byHash: transaction.header.dataHash) { + if existingTransaction.blockHash != nil && block == nil { + continue + } self.relay(transaction: existingTransaction, withOrder: index, inBlock: block) try self.storage.update(transaction: existingTransaction) updated.append(existingTransaction) @@ -95,7 +95,7 @@ extension TransactionProcessor: ITransactionProcessor { inserted.append(transaction.header) if !skipCheckBloomFilter { - needToUpdateBloomFilter = needToUpdateBloomFilter || self.addressManager.gapShifts() || self.hasUnspentOutputs(transaction: transaction) + needToUpdateBloomFilter = needToUpdateBloomFilter || self.addressManager.gapShifts() || self.expiresBloomFilter(outputs: transaction.outputs) } } } @@ -118,6 +118,10 @@ extension TransactionProcessor: ITransactionProcessor { process(transaction: transaction) try storage.add(transaction: transaction) listener?.onUpdate(updated: [], inserted: [transaction.header], inBlock: nil) + + if expiresBloomFilter(outputs: transaction.outputs) { + throw BloomFilterManager.BloomFilterExpired() + } } } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift index c04fad4f..8f123de0 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift @@ -164,25 +164,6 @@ class BloomFilterManagerTests: QuickSpec { verify(mockBloomFilterManagerDelegate, never()).bloomFilterUpdated(bloomFilter: any()) } } - - context("when no new element") { - it("doesn't trigger events") { - stub(mockStorage) { mock in - when(mock.publicKeys()).thenReturn([self.getPublicKey(withIndex: 0, chain: .external)]) - when(mock.outputsWithPublicKeys()).thenReturn([]) - } - - manager.regenerateBloomFilter() - verify(mockBloomFilterManagerDelegate).bloomFilterUpdated(bloomFilter: any()) - reset(mockBloomFilterManagerDelegate) - stub(mockBloomFilterManagerDelegate) { mock in - when(mock.bloomFilterUpdated(bloomFilter: any())).thenDoNothing() - } - - manager.regenerateBloomFilter() - verify(mockBloomFilterManagerDelegate, never()).bloomFilterUpdated(bloomFilter: any()) - } - } } } diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index 149afca4..f0c23538 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -9,12 +9,13 @@ class TransactionCreatorTests: QuickSpec { let mockTransactionBuilder = MockITransactionBuilder() let mockTransactionProcessor = MockITransactionProcessor() let mockTransactionSender = MockITransactionSender() + let mockBloomFilterManager = MockIBloomFilterManager() var transactionCreator: TransactionCreator! let transaction = TestData.p2pkhTransaction afterEach { - reset(mockTransactionBuilder, mockTransactionProcessor, mockTransactionSender) + reset(mockTransactionBuilder, mockTransactionProcessor, mockTransactionSender, mockBloomFilterManager) transactionCreator = nil } @@ -29,11 +30,40 @@ class TransactionCreatorTests: QuickSpec { stub(mockTransactionSender) { mock in when(mock.send(pendingTransaction: any())).thenDoNothing() } + stub(mockBloomFilterManager) { mock in + when(mock.regenerateBloomFilter()).thenDoNothing() + } + + transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, bloomFilterManager: mockBloomFilterManager) + } + + context("when BloomFilterManager.BloomFilterExpired error") { + beforeEach { + stub(mockTransactionProcessor) { mock in + when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) + } + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + + try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) + } + + it("does create transaction") { + verify(mockTransactionBuilder).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any()) + verify(mockTransactionProcessor).processCreated(transaction: any()) + } + + it("does send transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } - transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender) + it("regenerates bloomfilter") { + verify(mockBloomFilterManager).regenerateBloomFilter() + } } - context("when error") { + context("when other error") { beforeEach { stub(mockTransactionSender) { mock in when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) @@ -46,6 +76,10 @@ class TransactionCreatorTests: QuickSpec { verify(mockTransactionBuilder, never()).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any()) verify(mockTransactionProcessor, never()).processCreated(transaction: any()) } + + it("doesn't regenerate bloomfilter") { + verify(mockBloomFilterManager, never()).regenerateBloomFilter() + } } context("when success") { diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift index 285e223b..f12e28d4 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift @@ -129,6 +129,20 @@ class TransactionProcessorTests: XCTestCase { verify(mockInputExtractor, never()).extract(transaction: any()) } + func testProcessCreated_OutputsExpireBloomFiler() { + let transaction = TestData.p2wpkhTransaction + transaction.header.isMine = true + transaction.outputs[0].publicKeyPath = TestData.pubKey().path + + do { + try transactionProcessor.processCreated(transaction: transaction) + XCTFail("Expecting error") + } catch _ as BloomFilterManager.BloomFilterExpired { + } catch { + XCTFail("Unexpected error") + } + } + func testProcessReceived_TransactionExists() { let transaction = TestData.p2pkhTransaction transaction.header.status = .new @@ -189,12 +203,24 @@ class TransactionProcessorTests: XCTestCase { } try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) + verify(mockStorage).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) + + reset(mockStorage, mockBlockchainDataListener) + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + verify(mockStorage, never()).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener, never()).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) XCTAssertEqual(transaction.header.blockHash, block.headerHash) XCTAssertEqual(transaction.header.timestamp, block.timestamp) XCTAssertEqual(transaction.header.order, 0) + } func testProcessReceivedBlock_After_Block_TransactionExists() { @@ -208,8 +234,24 @@ class TransactionProcessorTests: XCTestCase { } try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) + verify(mockStorage).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) + + reset(mockStorage, mockBlockchainDataListener) + stub(mockStorage) { mock in + when(mock.update(transaction: any())).thenDoNothing() + when(mock.update(block: any())).thenDoNothing() + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + stub(mockBlockchainDataListener) { mock in + when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() + } + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nextBlock, skipCheckBloomFilter: false) + verify(mockStorage).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nextBlock)) + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) XCTAssertEqual(transaction.header.blockHash, nextBlock.headerHash) XCTAssertEqual(transaction.header.timestamp, nextBlock.timestamp) @@ -308,7 +350,7 @@ class TransactionProcessorTests: XCTestCase { XCTAssertEqual(transaction.header.blockHash, nil) } - func testProcessReceived_TransactionNotExists_Mine_HasUnspentOutputs() { + func testProcessReceived_TransactionNotExists_Mine_OutputsExpireBloomFiler() { let transaction = TestData.p2wpkhTransaction transaction.header.isMine = true transaction.outputs[0].publicKeyPath = TestData.pubKey().path From b9117dfab8409d36675072754c296610aa774021 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Tue, 25 Jun 2019 14:11:31 +0600 Subject: [PATCH 008/234] Bugfixes (#400, #401) - Wrap peers dictionary in SyncedReadyPeersManager to dispatch queue - 1 synced peer is enough for kit to be synced --- .../Blocks/InitialBlockDownload.swift | 7 +++---- .../BitcoinCore/Core/KitStateProvider.swift | 17 +++++++++------ BitcoinCore/BitcoinCore/Core/Protocols.swift | 3 +-- .../Managers/SyncedReadyPeerManager.swift | 21 ++++++++++++------- .../Network/TransactionSender.swift | 2 +- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift b/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift index 7b6252c8..23f3d292 100644 --- a/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift +++ b/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift @@ -33,7 +33,7 @@ public class InitialBlockDownload { public var syncedPeers = [IPeer]() init(blockSyncer: IBlockSyncer, peerManager: IPeerManager, merkleBlockValidator: IMerkleBlockValidator, syncStateListener: ISyncStateListener, - peersQueue: DispatchQueue = DispatchQueue(label: "PeerGroup Local Queue", qos: .userInitiated), + peersQueue: DispatchQueue = DispatchQueue(label: "InitialBlockDownload Local Queue", qos: .userInitiated), scheduler: SchedulerType = SerialDispatchQueueScheduler(qos: .background), logger: Logger? = nil) { self.blockSyncer = blockSyncer @@ -125,7 +125,6 @@ public class InitialBlockDownload { syncedPeers.append(peer) subject.onNext(.onPeerSynced(peer: peer)) - syncStateListener.syncFinished(all: allPeersSynced) } private func setPeerNotSynced(_ peer: IPeer) { @@ -154,8 +153,8 @@ public class InitialBlockDownload { .disposed(by: disposeBag) } - public var allPeersSynced: Bool { - return syncedPeers.count > 0 && syncedPeers.count >= peerManager.connected().count / 3 + public var hasSyncedPeer: Bool { + return syncedPeers.count > 0 } } diff --git a/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift b/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift index 44b2d939..9ef81154 100644 --- a/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift +++ b/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift @@ -27,10 +27,6 @@ extension KitStateProvider: ISyncStateListener { syncState = .notSynced } - func syncFinished(all: Bool) { - syncState = all ? .synced : .syncing(progress: 1) - } - func initialBestBlockHeightUpdated(height: Int32) { initialBestBlockHeight = height currentBestBlockHeight = height @@ -43,9 +39,18 @@ extension KitStateProvider: ISyncStateListener { let blocksDownloaded = currentBestBlockHeight - initialBestBlockHeight let allBlocksToDownload = maxBlockHeight - initialBestBlockHeight + var progress: Double = 0 + + if allBlocksToDownload <= 0 || allBlocksToDownload <= blocksDownloaded { + progress = 1.0 + } else { + progress = Double(blocksDownloaded) / Double(allBlocksToDownload) + } - if allBlocksToDownload > 0 && allBlocksToDownload > blocksDownloaded { - syncState = .syncing(progress: Double(blocksDownloaded) / Double(allBlocksToDownload)) + if progress >= 1 { + syncState = .synced + } else { + syncState = .syncing(progress: progress) } } diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 4a844264..dec7a7c5 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -225,7 +225,6 @@ protocol IConnectionTimeoutManager: class { protocol ISyncStateListener: class { func syncStarted() func syncStopped() - func syncFinished(all: Bool) func initialBestBlockHeightUpdated(height: Int32) func currentBestBlockHeightUpdated(height: Int32, maxBlockHeight: Int32) } @@ -500,7 +499,7 @@ public protocol IMessageSerializer { } public protocol IInitialBlockDownload { - var allPeersSynced: Bool { get } + var hasSyncedPeer: Bool { get } var observable: Observable { get } var syncedPeers: [IPeer] { get } func isSynced(peer: IPeer) -> Bool diff --git a/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift b/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift index 70e6967e..016d3e42 100644 --- a/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift @@ -7,20 +7,25 @@ public class SyncedReadyPeerManager { private var peerStates = [String: Bool]() private let peerSyncedAndReadySubject = PublishSubject() + private let peersQueue: DispatchQueue - init(peerGroup: IPeerGroup, initialBlockDownload: IInitialBlockDownload) { + init(peerGroup: IPeerGroup, initialBlockDownload: IInitialBlockDownload, + peersQueue: DispatchQueue = DispatchQueue(label: "SyncedReadyPeerManager Local Queue", qos: .userInitiated)) { self.peerGroup = peerGroup self.initialBlockDownload = initialBlockDownload + self.peersQueue = peersQueue } private func set(state: Bool, to peer: IPeer) { - let oldState = peerStates[peer.host] ?? false - peerStates[peer.host] = state - - if oldState != state { - if state { - peerSyncedAndReadySubject.onNext(peer) - } else { + peersQueue.async { + let oldState = self.peerStates[peer.host] ?? false + self.peerStates[peer.host] = state + + if oldState != state { + if state { + self.peerSyncedAndReadySubject.onNext(peer) + } else { + } } } } diff --git a/BitcoinCore/BitcoinCore/Network/TransactionSender.swift b/BitcoinCore/BitcoinCore/Network/TransactionSender.swift index 37f156b1..e39af89d 100644 --- a/BitcoinCore/BitcoinCore/Network/TransactionSender.swift +++ b/BitcoinCore/BitcoinCore/Network/TransactionSender.swift @@ -22,7 +22,7 @@ class TransactionSender { throw BitcoinCoreErrors.TransactionSendError.noConnectedPeers } - guard initialBlockDownload.allPeersSynced else { + guard initialBlockDownload.hasSyncedPeer else { throw BitcoinCoreErrors.TransactionSendError.peersNotSynced } From a4378f7639de54268c50bb7ca05172c8b613177b Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Tue, 25 Jun 2019 18:09:54 +0600 Subject: [PATCH 009/234] Notify syncFinished to syncStateListener when peer is synced (#400) --- BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift | 1 + BitcoinCore/BitcoinCore/Core/KitStateProvider.swift | 4 ++++ BitcoinCore/BitcoinCore/Core/Protocols.swift | 1 + 3 files changed, 6 insertions(+) diff --git a/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift b/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift index 23f3d292..49ef154a 100644 --- a/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift +++ b/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift @@ -125,6 +125,7 @@ public class InitialBlockDownload { syncedPeers.append(peer) subject.onNext(.onPeerSynced(peer: peer)) + syncStateListener.syncFinished() } private func setPeerNotSynced(_ peer: IPeer) { diff --git a/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift b/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift index 9ef81154..ab1c3e3c 100644 --- a/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift +++ b/BitcoinCore/BitcoinCore/Core/KitStateProvider.swift @@ -32,6 +32,10 @@ extension KitStateProvider: ISyncStateListener { currentBestBlockHeight = height } + func syncFinished() { + syncState = .synced + } + func currentBestBlockHeightUpdated(height: Int32, maxBlockHeight: Int32) { if currentBestBlockHeight < height { currentBestBlockHeight = height diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index dec7a7c5..4c2e16bf 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -223,6 +223,7 @@ protocol IConnectionTimeoutManager: class { } protocol ISyncStateListener: class { + func syncFinished() func syncStarted() func syncStopped() func initialBestBlockHeightUpdated(height: Int32) From 05211291ed548ac9c89a66347235355ba9a4817e Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Sat, 13 Jul 2019 12:29:21 +0600 Subject: [PATCH 010/234] Ability to redeem from p2sh output (#404) - New methods in AbstractKit: * redeem(from: Output, to: String, feeRate: Int, publicKey: PublicKey, signatureScriptFunction: (Data, Data) -> Data) * watch(transaction: BitcoinCore.TransactionFilter, delegate: IWatchedTransactionDelegate) * changePublicKey() throws -> PublicKey * receivePublicKey() throws -> PublicKey * publicKey(byPath path: String) throws -> PublicKey --- .../BitcoinCashKit/Network/TestNet.swift | 14 +-- .../BitcoinCore.xcodeproj/project.pbxproj | 12 +++ .../BitcoinCore/Core/AbstractKit.swift | 30 +++++- .../BitcoinCore/Core/BitcoinCore.swift | 38 +++++++- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 12 ++- BitcoinCore/BitcoinCore/Core/Protocols.swift | 26 ++++- BitcoinCore/BitcoinCore/Helpers/Helpers.swift | 15 ++- .../BitcoinCore/Managers/AddressManager.swift | 18 ++++ .../Managers/BloomFilterManager.swift | 20 ++-- .../Managers/WatchedTransactionManager.swift | 86 +++++++++++++++++ .../BitcoinCore/Models/DataObjects.swift | 7 ++ BitcoinCore/BitcoinCore/Models/Input.swift | 3 +- BitcoinCore/BitcoinCore/Models/Output.swift | 13 ++- .../BitcoinCore/Models/PublicKey.swift | 2 +- .../BitcoinCore/Models/Transaction.swift | 2 +- .../Serializers/DataListSerializer.swift | 2 +- .../SignatureScriptSerializer.swift | 43 +++++++++ .../TransactionInputSerializer.swift | 15 ++- .../Serializers/TransactionSerializer.swift | 14 ++- .../BitcoinCore/Storage/GrdbStorage.swift | 6 ++ .../Builder/TransactionBuilder.swift | 43 ++++++++- .../Transactions/Extractors/OpCode.swift | 6 ++ .../Transactions/TransactionCreator.swift | 26 +++-- .../Transactions/TransactionProcessor.swift | 2 + .../Managers/AddressManagerTests.swift | 73 ++++++++++++++ .../Managers/BloomFilterManagerTests.swift | 28 ++++++ .../WatchedTransactionManagerTests.swift | 86 +++++++++++++++++ .../Builder/TransactionBuilderTests.swift | 81 +++++++++++++++- .../TransactionCreatorTests.swift | 94 ++++++++++++++++++- .../TransactionProcessorTests.swift | 8 ++ DashKit/DashKit/Core/DashKit.swift | 4 +- Demo/Demo/Adapters/BaseAdapter.swift | 2 +- Demo/Demo/Adapters/BitcoinAdapter.swift | 2 +- Demo/Demo/Adapters/BitcoinCashAdapter.swift | 2 +- 34 files changed, 767 insertions(+), 68 deletions(-) create mode 100644 BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift create mode 100644 BitcoinCore/BitcoinCore/Serializers/SignatureScriptSerializer.swift create mode 100644 BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift diff --git a/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift b/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift index ddb78bdb..287e5822 100644 --- a/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift +++ b/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift @@ -39,14 +39,14 @@ class TestNet: INetwork { return Block( withHeader: BlockHeader( version: 0x20000000, - headerHash: "000000000000058417bfcbfaa5bd7c0449743d9a386331db58e4453bc77ae536".reversedData!, - previousBlockHeaderHash: "000000000000041abedc84c2ab85f72febbee655ed9d1dfdc9497126026e1bba".reversedData!, - merkleRoot: "cccf617e3ab704923dd45399649e7a5be11aa71ce344b7099b580c9d85445948".reversedData!, - timestamp: 1559627940, - bits: 0x1a065b0f, - nonce: 1911921100 + headerHash: "00000000000001c4a2ebbed0841005d527c5177f323cd3df5d9f70463d9c28c7".reversedData!, + previousBlockHeaderHash: "000000000000046bf1879dc49620b0b12d4faaeda6f0ee033fc2cb86382ce571".reversedData!, + merkleRoot: "a9e1f20ec48ce7fae824c0dd76b4c4ec354d623e0ad2fac7b66a06984c0c0f81".reversedData!, + timestamp: 1562665562, + bits: 0x1a0509d6, + nonce: 437871866 ), - height: 1307081) + height: 1313735) } } diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index f9ad9944..5002faf7 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -77,10 +77,13 @@ 2FA5D74EDAFB77BC86918E63 /* PeerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DE9CAB1F15B04E2C54C9 /* PeerDiscovery.swift */; }; 2FA5D806A7727E3BED0E54E7 /* DataObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D50922572F91F966DD4A /* DataObjects.swift */; }; 2FA5D8284C7C94155AC50B7B /* GetBlockHashesTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DFC7FA0A5363BFFB8E28 /* GetBlockHashesTaskTests.swift */; }; + 2FA5D84AA43DA05CAC68B912 /* WatchedTransactionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */; }; + 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */; }; 2FA5D91DCF7F320369162C5A /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */; }; 2FA5D9D3DA2DF9BD402CE5DD /* SentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF037E951950A0F7FF03 /* SentTransaction.swift */; }; 2FA5D9ED521660FA5EEF360E /* MerkleBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5C9BB0E185AF72B9174 /* MerkleBlock.swift */; }; 2FA5DA409AB91D54163250D7 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D9929AB239235F9B6895 /* Logger.swift */; }; + 2FA5DA939D3783E4377AF546 /* WatchedTransactionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D9C845F4D930457A5C6F /* WatchedTransactionManagerTests.swift */; }; 2FA5DB74D510BA9F6D9525A4 /* BloomFilterManagerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D0A6A8C6C5A4E2AAFB74 /* BloomFilterManagerDelegateTests.swift */; }; 2FA5DB7F5BD8E25DCAF9AC83 /* PeerAddressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF9ADB516A45C797889D /* PeerAddressManagerTests.swift */; }; 2FA5DBABD18D15E003686989 /* BlockSyncerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D2DDF60A9C5DA3776D8A /* BlockSyncerState.swift */; }; @@ -287,11 +290,14 @@ 2FA5D632A9EFB1F0CF2523DF /* PeerHostManagerDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerHostManagerDelegateTests.swift; sourceTree = ""; }; 2FA5D65667DA8BFD91E91C8B /* GetBlockHashesTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBlockHashesTask.swift; sourceTree = ""; }; 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = ""; }; + 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignatureScriptSerializer.swift; sourceTree = ""; }; 2FA5D82EFF381CE6233790A9 /* KitStateProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitStateProviderTests.swift; sourceTree = ""; }; 2FA5D859C070F9718CACF08F /* KitStateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitStateProvider.swift; sourceTree = ""; }; 2FA5D880471F191DBCDAE574 /* TransactionSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionSerializer.swift; sourceTree = ""; }; + 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchedTransactionManager.swift; sourceTree = ""; }; 2FA5D9929AB239235F9B6895 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 2FA5D99586B15F4477D695F7 /* IPeerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPeerTests.swift; sourceTree = ""; }; + 2FA5D9C845F4D930457A5C6F /* WatchedTransactionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchedTransactionManagerTests.swift; sourceTree = ""; }; 2FA5DA9D7F78A26107F7209C /* InputSignerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSignerTests.swift; sourceTree = ""; }; 2FA5DAAAC2F759FD42948DDE /* IPeerTaskRequesterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPeerTaskRequesterTests.swift; sourceTree = ""; }; 2FA5DADEB0FD09138FD308A1 /* HDPrivateKeyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HDPrivateKeyTests.swift; sourceTree = ""; }; @@ -501,6 +507,7 @@ 2FA5DAE996A847B90E5D033A /* UnspentOutputSelectorTests.swift */, 58AAAECE6429C4CBD7FD2394 /* UnspentOutputProviderTests.swift */, 58AAAA81938DAE46B8074C9D /* UnspentOutputSelectorSingleNoChangeTests.swift */, + 2FA5D9C845F4D930457A5C6F /* WatchedTransactionManagerTests.swift */, ); path = Managers; sourceTree = ""; @@ -707,6 +714,7 @@ 58AAA481A82D05C914AB0CCA /* UnspentOutputProvider.swift */, 58AAA7A391E4B2F8C4E99B97 /* SyncedReadyPeerManager.swift */, 58AAA67094BBC66D3A385341 /* UnspentOutputSelectorSingleNoChange.swift */, + 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */, ); path = Managers; sourceTree = ""; @@ -939,6 +947,7 @@ 2FA5DCD21585D41C520DF764 /* TransactionInputSerializer.swift */, 2FA5DEC34722F594F85688C0 /* TransactionOutputSerializer.swift */, 2FA5D880471F191DBCDAE574 /* TransactionSerializer.swift */, + 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */, ); path = Serializers; sourceTree = ""; @@ -1293,6 +1302,7 @@ 58AAA7B5F60F31613515F273 /* BlockDiscoveryBatchTest.swift in Sources */, 58AAAA0E73B45A6262D8272F /* MerkleBranchTests.swift in Sources */, 58AAAE80BE4074DB3B1F0B1C /* UnspentOutputSelectorSingleNoChangeTests.swift in Sources */, + 2FA5DA939D3783E4377AF546 /* WatchedTransactionManagerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1438,6 +1448,8 @@ 58AAAF1F346D79776D04CEA6 /* UnspentOutputSelectorChain.swift in Sources */, 58AAAE8B6F9F832A13753E78 /* UnspentOutputSelectorSingleNoChange.swift in Sources */, 58AAAF671C4B3988EF6764B1 /* InsightApi.swift in Sources */, + 2FA5D84AA43DA05CAC68B912 /* WatchedTransactionManager.swift in Sources */, + 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index b2521711..c954c5a0 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -34,8 +34,16 @@ open class AbstractKit { return bitcoinCore.transactions(fromHash: fromHash, limit: limit) } - open func send(to address: String, value: Int, feeRate: Int) throws { - try bitcoinCore.send(to: address, value: value, feeRate: feeRate) + open func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { + return try bitcoinCore.send(to: address, value: value, feeRate: feeRate) + } + + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { + return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate) + } + + public func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { + return try bitcoinCore.redeem(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) } open func validate(address: String) throws { @@ -54,8 +62,24 @@ open class AbstractKit { return bitcoinCore.receiveAddress(for: type) } + open func changePublicKey() throws -> PublicKey { + return try bitcoinCore.changePublicKey() + } + + open func receivePublicKey() throws -> PublicKey { + return try bitcoinCore.receivePublicKey() + } + + public func publicKey(byPath path: String) throws -> PublicKey { + return try bitcoinCore.publicKey(byPath: path) + } + + open func watch(transaction: BitcoinCore.TransactionFilter, delegate: IWatchedTransactionDelegate) { + bitcoinCore.watch(transaction: transaction, delegate: delegate) + } + open var debugInfo: String { return bitcoinCore.debugInfo } -} \ No newline at end of file +} diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 8e655bee..2c7e418c 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -14,6 +14,7 @@ public class BitcoinCore { private let cache: OutputsCache private var dataProvider: IDataProvider private let addressManager: IAddressManager + private let watchedTransactionManager: IWatchedTransactionManager private let addressConverter: AddressConverterChain private let unspentOutputSelector: UnspentOutputSelectorChain private let kitStateProvider: IKitStateProvider & ISyncStateListener @@ -63,6 +64,10 @@ public class BitcoinCore { return self } + func publicKey(byPath path: String) throws -> PublicKey { + return try addressManager.publicKey(byPath: path) + } + public func prepend(scriptBuilder: IScriptBuilder) { self.scriptBuilder.prepend(scriptBuilder: scriptBuilder) } @@ -86,7 +91,7 @@ public class BitcoinCore { blockValidatorChain: BlockValidatorChain, addressManager: IAddressManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, scriptBuilder: ScriptBuilderChain, transactionBuilder: ITransactionBuilder, transactionCreator: ITransactionCreator, paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, - syncManager: SyncManager) { + syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager) { self.storage = storage self.cache = cache self.dataProvider = dataProvider @@ -109,6 +114,7 @@ public class BitcoinCore { self.networkMessageSerializer = networkMessageSerializer self.syncManager = syncManager + self.watchedTransactionManager = watchedTransactionManager } } @@ -143,8 +149,17 @@ extension BitcoinCore { return dataProvider.transactions(fromHash: fromHash, limit: limit) } - public func send(to address: String, value: Int, feeRate: Int) throws { - try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true) + public func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { + return try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true) + } + + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { + let address = try addressConverter.convert(keyHash: hash, type: scriptType) + return try send(to: address.stringValue, value: value, feeRate: feeRate) + } + + func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { + return try transactionCreator.create(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) } public func validate(address: String) throws { @@ -163,6 +178,18 @@ extension BitcoinCore { return (try? addressManager.receiveAddress(for: type)) ?? "" } + public func changePublicKey() throws -> PublicKey { + return try addressManager.changePublicKey() + } + + public func receivePublicKey() throws -> PublicKey { + return try addressManager.receivePublicKey() + } + + func watch(transaction: BitcoinCore.TransactionFilter, delegate: IWatchedTransactionDelegate) { + watchedTransactionManager.add(transactionFilter: transaction, delegatedTo: delegate) + } + public var debugInfo: String { return dataProvider.debugInfo } @@ -244,6 +271,11 @@ extension BitcoinCore { case newWallet // Sync from lastCheckpointBlock. Api restore enabled } + public enum TransactionFilter { + case p2shOutput(scriptHash: Data) + case outpoint(transactionHash: Data, outputIndex: Int) + } + } extension BitcoinCore.KitState { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 0c5adc33..9b099ba3 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -185,6 +185,7 @@ public class BitcoinCoreBuilder { let initialSyncer = InitialSyncer(storage: storage, listener: kitStateProvider, stateManager: stateManager, blockDiscovery: blockDiscovery, addressManager: addressManager, logger: logger) let bloomFilterLoader = BloomFilterLoader(bloomFilterManager: bloomFilterManager, peerManager: peerManager) + let watchedTransactionManager = WatchedTransactionManager(bloomFilterManager: bloomFilterManager) let blockValidatorChain = BlockValidatorChain(proofOfWorkValidator: ProofOfWorkValidator(difficultyEncoder: DifficultyEncoder())) let blockchain = Blockchain(storage: storage, blockValidator: blockValidatorChain, factory: factory, listener: dataProvider) @@ -199,7 +200,8 @@ public class BitcoinCoreBuilder { let inputSigner = InputSigner(hdWallet: hdWallet, network: network) let scriptBuilder = ScriptBuilderChain() - let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, addressManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory) + let transactionSizeCalculator = TransactionSizeCalculator() + let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, addressManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, transactionSizeCalculator: transactionSizeCalculator) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) @@ -224,20 +226,22 @@ public class BitcoinCoreBuilder { paymentAddressParser: paymentAddressParser, networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer, - syncManager: syncManager) + syncManager: syncManager, + watchedTransactionManager: watchedTransactionManager) initialSyncer.delegate = syncManager bloomFilterManager.delegate = bloomFilterLoader dataProvider.delegate = bitcoinCore kitStateProvider.delegate = bitcoinCore + transactionProcessor.transactionListener = watchedTransactionManager + + bloomFilterManager.add(provider: watchedTransactionManager) peerGroup.peerTaskHandler = bitcoinCore.peerTaskHandlerChain peerGroup.inventoryItemsHandler = bitcoinCore.inventoryItemsHandlerChain bitcoinCore.prepend(scriptBuilder: ScriptBuilder()) bitcoinCore.prepend(addressConverter: Base58AddressConverter(addressVersion: network.pubKeyHash, addressScriptVersion: network.scriptHash)) - - let transactionSizeCalculator = TransactionSizeCalculator() bitcoinCore.prepend(unspentOutputSelector: UnspentOutputSelector(calculator: transactionSizeCalculator, provider: unspentOutputProvider)) bitcoinCore.prepend(unspentOutputSelector: UnspentOutputSelectorSingleNoChange(calculator: transactionSizeCalculator, provider: unspentOutputProvider)) // this part can be moved to another place diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 4c2e16bf..94b6c5a8 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -126,6 +126,7 @@ public protocol IStorage { func publicKey(byRawOrKeyHash: Data) -> PublicKey? func add(publicKeys: [PublicKey]) func publicKeysWithUsedState() -> [PublicKeyWithUsedState] + func publicKey(byPath: String) -> PublicKey? } public protocol IAddressSelector { @@ -134,17 +135,19 @@ public protocol IAddressSelector { public protocol IAddressManager { func changePublicKey() throws -> PublicKey + func receivePublicKey() throws -> PublicKey func receiveAddress(for type: ScriptType) throws -> String func fillGap() throws func addKeys(keys: [PublicKey]) throws func gapShifts() -> Bool + func publicKey(byPath: String) throws -> PublicKey } public protocol IBloomFilterManagerDelegate: class { func bloomFilterUpdated(bloomFilter: BloomFilter) } -public protocol IBloomFilterManager { +public protocol IBloomFilterManager: AnyObject { var delegate: IBloomFilterManagerDelegate? { get set } var bloomFilter: BloomFilter? { get } func regenerateBloomFilter() @@ -340,12 +343,14 @@ public protocol ITransactionSyncer: class { } public protocol ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction + func create(from: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionBuilder { func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction + func buildTransaction(from: UnspentOutput, to: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol IBlockchain { @@ -527,3 +532,20 @@ protocol ITransactionSender { protocol IMerkleBlockHandler: AnyObject { func handle(merkleBlock: MerkleBlock) throws } + +protocol ITransactionListener: class { + func onReceive(transaction: FullTransaction) +} + +public protocol IWatchedTransactionDelegate { + func transactionReceived(transaction: FullTransaction, outputIndex: Int) + func transactionReceived(transaction: FullTransaction, inputIndex: Int) +} + +protocol IWatchedTransactionManager { + func add(transactionFilter: BitcoinCore.TransactionFilter, delegatedTo: IWatchedTransactionDelegate) +} + +protocol IBloomFilterProvider { + func filterElements() -> [Data] +} diff --git a/BitcoinCore/BitcoinCore/Helpers/Helpers.swift b/BitcoinCore/BitcoinCore/Helpers/Helpers.swift index 37e37bef..07337ba1 100644 --- a/BitcoinCore/BitcoinCore/Helpers/Helpers.swift +++ b/BitcoinCore/BitcoinCore/Helpers/Helpers.swift @@ -19,12 +19,11 @@ func pton(_ address: String) -> Data { return buffer } -/// Version = 1 byte of 0 (zero); on the test network, this is 1 byte of 111 -/// Key hash = Version concatenated with RIPEMD-160(SHA-256(public key)) -/// Checksum = 1st 4 bytes of SHA-256(SHA-256(Key hash)) -/// Bitcoin Address = Base58Encode(Key hash concatenated with Checksum) -func publicKeyHashToAddress(_ hash: Data) -> String { - let checksum = CryptoKit.sha256sha256(hash).prefix(4) - let address = Base58.encode(hash + checksum) - return address +func byteArrayLittleEndian(int: Int) -> [UInt8] { + return [ + UInt8(int & 0x000000FF), + UInt8((int & 0x0000FF00) >> 8), + UInt8((int & 0x00FF0000) >> 16), + UInt8((int & 0xFF000000) >> 24) + ] } diff --git a/BitcoinCore/BitcoinCore/Managers/AddressManager.swift b/BitcoinCore/BitcoinCore/Managers/AddressManager.swift index 3338c14a..bdf0e5dd 100644 --- a/BitcoinCore/BitcoinCore/Managers/AddressManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/AddressManager.swift @@ -4,6 +4,7 @@ class AddressManager { enum AddressManagerError: Error { case noUnusedPublicKey + case invalidPath } private let storage: IStorage @@ -62,6 +63,10 @@ extension AddressManager: IAddressManager { return try publicKey(external: false) } + func receivePublicKey() throws -> PublicKey { + return try publicKey(external: true) + } + func receiveAddress(for type: ScriptType) throws -> String { let keyHash = try publicKey(external: true).keyHash let correctKeyHash = addressKeyHashConverter?.convert(keyHash: keyHash, type: type) ?? keyHash @@ -113,6 +118,19 @@ extension AddressManager: IAddressManager { return false } + public func publicKey(byPath path: String) throws -> PublicKey { + let parts = path.split(separator: "/") + + guard parts.count == 3, let account = Int(parts[0]), let external = Int(parts[1]), let index = Int(parts[2]) else { + throw AddressManagerError.invalidPath + } + + if let publicKey = storage.publicKey(byPath: path) { + return publicKey + } + + return try hdWallet.publicKey(account: account, index: index, external: external == 1) + } } extension AddressManager { diff --git a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift index 609578de..d1e2230b 100644 --- a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift @@ -1,6 +1,8 @@ class BloomFilterManager { class BloomFilterExpired: Error {} + private var providers = [IBloomFilterProvider]() + private let storage: IStorage private let factory: IFactory weak var delegate: IBloomFilterManagerDelegate? @@ -12,16 +14,6 @@ class BloomFilterManager { self.factory = factory } - // This method is a workaround - private func byteArrayLittleEndian(int: Int) -> [UInt8] { - return [ - UInt8(int & 0x000000FF), - UInt8((int & 0x0000FF00) >> 8), - UInt8((int & 0x00FF0000) >> 16), - UInt8((int & 0xFF000000) >> 24) - ] - } - private func needToSetToBloomFilter(output: OutputWithPublicKey, bestBlockHeight: Int) -> Bool { // Need to set if output is unspent guard let _ = output.spendingInput else { @@ -40,6 +32,10 @@ class BloomFilterManager { extension BloomFilterManager: IBloomFilterManager { + func add(provider: IBloomFilterProvider) { + providers.append(provider) + } + func regenerateBloomFilter() { var elements = [Data]() @@ -65,6 +61,10 @@ extension BloomFilterManager: IBloomFilterManager { elements.append(outpoint) } + for provider in providers { + elements.append(contentsOf: provider.filterElements()) + } + if !elements.isEmpty { bloomFilter = factory.bloomFilter(withElements: elements) delegate?.bloomFilterUpdated(bloomFilter: bloomFilter!) diff --git a/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift b/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift new file mode 100644 index 00000000..a8c3ecaf --- /dev/null +++ b/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift @@ -0,0 +1,86 @@ +class WatchedTransactionManager { + + struct P2ShOutputFilter { + let hash: Data + let delegate: IWatchedTransactionDelegate + } + + struct OutpointFilter { + let transactionHash: Data + let outputIndex: Int + let delegate: IWatchedTransactionDelegate + } + + private var p2ShOutputFilters = [P2ShOutputFilter]() + private var outpointFilters = [OutpointFilter]() + private let queue: DispatchQueue + private weak var bloomFilterManager: IBloomFilterManager? + + init(bloomFilterManager: IBloomFilterManager, queue: DispatchQueue = DispatchQueue(label: "WatchedTransactionManager Queue", qos: .background)) { + self.bloomFilterManager = bloomFilterManager + self.queue = queue + } + + private func scan(transaction: FullTransaction) { + for filter in p2ShOutputFilters { + for output in transaction.outputs { + if output.scriptType == .p2sh && output.keyHash == filter.hash { + filter.delegate.transactionReceived(transaction: transaction, outputIndex: output.index) + return + } + } + } + + for filter in outpointFilters { + for (index, input) in transaction.inputs.enumerated() { + if input.previousOutputTxHash == filter.transactionHash && input.previousOutputIndex == filter.outputIndex { + filter.delegate.transactionReceived(transaction: transaction, inputIndex: index) + return + } + } + } + } + +} + +extension WatchedTransactionManager : IWatchedTransactionManager { + + func add(transactionFilter: BitcoinCore.TransactionFilter, delegatedTo delegate: IWatchedTransactionDelegate) { + switch transactionFilter { + case .p2shOutput(let scriptHash): + p2ShOutputFilters.append(P2ShOutputFilter(hash: scriptHash, delegate: delegate)) + case .outpoint(let transactionHash, let outputIndex): + outpointFilters.append(OutpointFilter(transactionHash: transactionHash, outputIndex: outputIndex, delegate: delegate)) + } + bloomFilterManager?.regenerateBloomFilter() + } + +} + +extension WatchedTransactionManager : ITransactionListener { + + func onReceive(transaction: FullTransaction) { + queue.async { + self.scan(transaction: transaction) + } + } + +} + +extension WatchedTransactionManager : IBloomFilterProvider { + + func filterElements() -> [Data] { + var elements = [Data]() + + for filter in p2ShOutputFilters { + elements.append(filter.hash) + } + + for filter in outpointFilters { + elements.append(filter.transactionHash + byteArrayLittleEndian(int: filter.outputIndex)) + } + + return elements + } + +} diff --git a/BitcoinCore/BitcoinCore/Models/DataObjects.swift b/BitcoinCore/BitcoinCore/Models/DataObjects.swift index 55aefd7a..1146d077 100644 --- a/BitcoinCore/BitcoinCore/Models/DataObjects.swift +++ b/BitcoinCore/BitcoinCore/Models/DataObjects.swift @@ -82,6 +82,13 @@ public struct UnspentOutput { public let transaction: Transaction public let blockHeight: Int? + public init(output: Output, publicKey: PublicKey, transaction: Transaction, blockHeight: Int? = nil) { + self.output = output + self.publicKey = publicKey + self.transaction = transaction + self.blockHeight = blockHeight + } + } public struct FullTransactionForInfo { diff --git a/BitcoinCore/BitcoinCore/Models/Input.swift b/BitcoinCore/BitcoinCore/Models/Input.swift index e769ecbd..22d14bbc 100644 --- a/BitcoinCore/BitcoinCore/Models/Input.swift +++ b/BitcoinCore/BitcoinCore/Models/Input.swift @@ -5,7 +5,7 @@ public class Input: Record { public var previousOutputTxHash: Data var previousOutputIndex: Int - var signatureScript: Data + public var signatureScript: Data var sequence: Int var transactionHash = Data() var keyHash: Data? = nil @@ -66,4 +66,5 @@ public class Input: Record { enum SerializationError: Error { case noPreviousOutput case noPreviousTransaction + case noPreviousOutputScript } diff --git a/BitcoinCore/BitcoinCore/Models/Output.swift b/BitcoinCore/BitcoinCore/Models/Output.swift index b0b7688a..542f3c57 100644 --- a/BitcoinCore/BitcoinCore/Models/Output.swift +++ b/BitcoinCore/BitcoinCore/Models/Output.swift @@ -44,19 +44,22 @@ public enum ScriptType: Int, DatabaseValueConvertible { public class Output: Record { public var value: Int - var lockingScript: Data - var index: Int - var transactionHash = Data() + public var lockingScript: Data + public var index: Int + var transactionHash: Data var publicKeyPath: String? = nil public var scriptType: ScriptType = .unknown - var keyHash: Data? = nil + public var redeemScript: Data? = nil + public var keyHash: Data? = nil var address: String? = nil - init(withValue value: Int, index: Int, lockingScript script: Data, type: ScriptType = .unknown, address: String? = nil, keyHash: Data? = nil, publicKey: PublicKey? = nil) { + public init(withValue value: Int, index: Int, lockingScript script: Data, transactionHash: Data = Data(), type: ScriptType = .unknown, redeemScript: Data? = nil, address: String? = nil, keyHash: Data? = nil, publicKey: PublicKey? = nil) { self.value = value self.lockingScript = script self.index = index + self.transactionHash = transactionHash self.scriptType = type + self.redeemScript = redeemScript self.address = address self.keyHash = keyHash self.publicKeyPath = publicKey?.path diff --git a/BitcoinCore/BitcoinCore/Models/PublicKey.swift b/BitcoinCore/BitcoinCore/Models/PublicKey.swift index 9153c1b8..078dd6d7 100644 --- a/BitcoinCore/BitcoinCore/Models/PublicKey.swift +++ b/BitcoinCore/BitcoinCore/Models/PublicKey.swift @@ -9,7 +9,7 @@ public class PublicKey: Record { case wrongNetwork } - let path: String + public let path: String public let account: Int public let index: Int public let external: Bool diff --git a/BitcoinCore/BitcoinCore/Models/Transaction.swift b/BitcoinCore/BitcoinCore/Models/Transaction.swift index be51b4a4..c9a676d5 100644 --- a/BitcoinCore/BitcoinCore/Models/Transaction.swift +++ b/BitcoinCore/BitcoinCore/Models/Transaction.swift @@ -16,7 +16,7 @@ public class Transaction: Record { public var status: TransactionStatus = .relayed public var segWit: Bool = false - init(version: Int = 0, lockTime: Int = 0, timestamp: Int? = nil) { + public init(version: Int = 0, lockTime: Int = 0, timestamp: Int? = nil) { self.version = version self.lockTime = lockTime self.timestamp = timestamp ?? Int(Date().timeIntervalSince1970) diff --git a/BitcoinCore/BitcoinCore/Serializers/DataListSerializer.swift b/BitcoinCore/BitcoinCore/Serializers/DataListSerializer.swift index 2955839b..07b225f4 100644 --- a/BitcoinCore/BitcoinCore/Serializers/DataListSerializer.swift +++ b/BitcoinCore/BitcoinCore/Serializers/DataListSerializer.swift @@ -1,6 +1,6 @@ import Foundation -class DataListSerializer { +public class DataListSerializer { static func serialize(dataList: [Data]) -> Data { var data = Data() diff --git a/BitcoinCore/BitcoinCore/Serializers/SignatureScriptSerializer.swift b/BitcoinCore/BitcoinCore/Serializers/SignatureScriptSerializer.swift new file mode 100644 index 00000000..00675dc5 --- /dev/null +++ b/BitcoinCore/BitcoinCore/Serializers/SignatureScriptSerializer.swift @@ -0,0 +1,43 @@ +import Foundation + +public class SignatureScriptSerializer { + + static func deserialize(byteStream: ByteStream) -> [Data] { + var data = [Data]() + + while byteStream.availableBytes > 0 { + let dataSize = byteStream.read(VarInt.self) + + switch dataSize.underlyingValue { + case 0x00: + data.append(Data()) + case 0x01...0x4b: + data.append(byteStream.read(Data.self, count: Int(dataSize.underlyingValue))) + case 0x4c: + let dataSize2 = byteStream.read(UInt8.self).littleEndian + data.append(byteStream.read(Data.self, count: Int(dataSize2))) + case 0x4d: + let dataSize2 = byteStream.read(UInt16.self).littleEndian + data.append(byteStream.read(Data.self, count: Int(dataSize2))) + case 0x4e: + let dataSize2 = byteStream.read(UInt32.self).littleEndian + data.append(byteStream.read(Data.self, count: Int(dataSize2))) + case 0x4f: + data.append(Data(from: Int8(-1))) + case 0x51: + data.append(Data([UInt8(0x51)])) + case 0x52...0x60: + data.append(Data([UInt8(dataSize.underlyingValue - 0x50)])) + default: + () + } + } + + return data + } + + public static func deserialize(data: Data) -> [Data] { + return deserialize(byteStream: ByteStream(data)) + } + +} diff --git a/BitcoinCore/BitcoinCore/Serializers/TransactionInputSerializer.swift b/BitcoinCore/BitcoinCore/Serializers/TransactionInputSerializer.swift index cfd19d68..6fac812c 100644 --- a/BitcoinCore/BitcoinCore/Serializers/TransactionInputSerializer.swift +++ b/BitcoinCore/BitcoinCore/Serializers/TransactionInputSerializer.swift @@ -34,9 +34,20 @@ class TransactionInputSerializer { data += UInt32(output.index) if forCurrentInputSignature { - let scriptLength = VarInt(output.lockingScript.count) + let script: Data + switch inputToSign.previousOutput.scriptType { + case .p2sh: + guard let redeemScript = inputToSign.previousOutput.redeemScript else { + throw SerializationError.noPreviousOutputScript + } + script = redeemScript + default: + script = output.lockingScript + } + + let scriptLength = VarInt(script.count) data += scriptLength.serialized() - data += output.lockingScript + data += script } else { data += VarInt(0).serialized() } diff --git a/BitcoinCore/BitcoinCore/Serializers/TransactionSerializer.swift b/BitcoinCore/BitcoinCore/Serializers/TransactionSerializer.swift index 1249f9b8..f8c6a1bf 100644 --- a/BitcoinCore/BitcoinCore/Serializers/TransactionSerializer.swift +++ b/BitcoinCore/BitcoinCore/Serializers/TransactionSerializer.swift @@ -46,7 +46,19 @@ public class TransactionSerializer { let inputToSign = inputsToSign[inputIndex] data += try TransactionInputSerializer.serializedOutPoint(input: inputToSign) - data += OpCode.push(OpCode.p2pkhStart + OpCode.push(inputToSign.previousOutput.keyHash!) + OpCode.p2pkhFinish) + + switch inputToSign.previousOutput.scriptType { + case .p2sh: + guard let script = inputToSign.previousOutput.redeemScript else { + throw SerializationError.noPreviousOutputScript + } + let scriptLength = VarInt(script.count) + data += scriptLength.serialized() + data += script + default: + data += OpCode.push(OpCode.p2pkhStart + OpCode.push(inputToSign.previousOutput.keyHash!) + OpCode.p2pkhFinish) + } + data += inputToSign.previousOutput.value data += UInt32(inputToSign.input.sequence) diff --git a/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift b/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift index 10c1c542..e5411d3f 100644 --- a/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift +++ b/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift @@ -753,4 +753,10 @@ extension GrdbStorage: IStorage { } } + public func publicKey(byPath path: String) -> PublicKey? { + return try! dbPool.read { db in + try PublicKey.filter(PublicKey.Columns.path == path).fetchOne(db) + } + } + } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 0614ce5c..73d8ed59 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -12,10 +12,12 @@ class TransactionBuilder { private let addressConverter: IAddressConverter private let inputSigner: IInputSigner private let factory: IFactory + private let transactionSizeCalculator: ITransactionSizeCalculator var scriptBuilder: IScriptBuilder - init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, addressManager: IAddressManager, addressConverter: IAddressConverter, inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory) { + init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, addressManager: IAddressManager, addressConverter: IAddressConverter, + inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator) { self.unspentOutputSelector = unspentOutputSelector self.unspentOutputProvider = unspentOutputProvider self.addressManager = addressManager @@ -23,6 +25,7 @@ class TransactionBuilder { self.inputSigner = inputSigner self.scriptBuilder = scriptBuilder self.factory = factory + self.transactionSizeCalculator = transactionSizeCalculator } private func input(fromUnspentOutput unspentOutput: UnspentOutput) throws -> InputToSign { @@ -128,4 +131,42 @@ extension TransactionBuilder: ITransactionBuilder { return FullTransaction(header: transaction, inputs: inputsToSign.map{ $0.input }, outputs: outputs) } + func buildTransaction(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { + let address = try addressConverter.convert(address: address) + + // Calculate fee + let emptySignature = Data(repeating: 0, count: TransactionSizeCalculator.signatureLength) + let emptyPublicKey = Data(repeating: 0, count: TransactionSizeCalculator.pubKeyLength) + + let transactionSize = transactionSizeCalculator.transactionSize(inputs: [unspentOutput.output.scriptType], outputScriptTypes: [address.scriptType]) + + signatureScriptFunction(emptySignature, emptyPublicKey).count + let fee = transactionSize * feeRate + + guard fee < unspentOutput.output.value else { + throw BuildError.feeMoreThanValue + } + + // Add input without unlocking scripts + let inputToSign = try input(fromUnspentOutput: unspentOutput) + + // Calculate receiveValue + let receivedValue = unspentOutput.output.value - fee + + // Add :to output + let output = try self.output(withIndex: 0, address: address, value: receivedValue) + + // Build transaction + let transaction = factory.transaction(version: 1, lockTime: 0) + + // Sign inputs + let sigScriptData = try inputSigner.sigScriptData(transaction: transaction, inputsToSign: [inputToSign], outputs: [output], index: 0) + inputToSign.input.signatureScript = signatureScriptFunction(sigScriptData[0], sigScriptData[1]) + + transaction.status = .new + transaction.isMine = true + transaction.isOutgoing = false + + return FullTransaction(header: transaction, inputs: [inputToSign.input], outputs: [output]) + } + } diff --git a/BitcoinCore/BitcoinCore/Transactions/Extractors/OpCode.swift b/BitcoinCore/BitcoinCore/Transactions/Extractors/OpCode.swift index 859e4996..3e230049 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Extractors/OpCode.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Extractors/OpCode.swift @@ -15,14 +15,20 @@ public class OpCode { public static let pushData1: UInt8 = 0x4c public static let pushData2: UInt8 = 0x4d public static let pushData4: UInt8 = 0x4e + public static let drop: UInt8 = 0x75 public static let dup: UInt8 = 0x76 + public static let sha256: UInt8 = 0xA8 public static let hash160: UInt8 = 0xA9 + public static let size: UInt8 = 0x82 public static let equal: UInt8 = 0x87 public static let equalVerify: UInt8 = 0x88 public static let checkSig: UInt8 = 0xAC public static let checkSigVerify: UInt8 = 0xAD public static let checkMultiSig: UInt8 = 0xAE public static let checkMultiSigVerify: UInt8 = 0xAF + public static let checkLockTimeVerify: UInt8 = 0xB1 + public static let _if: UInt8 = 0x63 + public static let _else: UInt8 = 0x67 public static let endIf: UInt8 = 0x68 public static func value(fromPush code: UInt8) -> UInt8? { diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index fd9ec33c..9e30c5c5 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -15,15 +15,9 @@ class TransactionCreator { self.bloomFilterManager = bloomFilterManager } -} - -extension TransactionCreator: ITransactionCreator { - - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws { + func processAndSend(transaction: FullTransaction) throws { try transactionSender.verifyCanSend() - let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address) - do { try transactionProcessor.processCreated(transaction: transaction) } catch _ as BloomFilterManager.BloomFilterExpired { @@ -34,3 +28,21 @@ extension TransactionCreator: ITransactionCreator { } } + +extension TransactionCreator: ITransactionCreator { + + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { + let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address) + + try processAndSend(transaction: transaction) + return transaction + } + + func create(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { + let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + + try processAndSend(transaction: transaction) + return transaction + } + +} diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift index 083c156c..82782954 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift @@ -9,6 +9,7 @@ class TransactionProcessor { private let addressManager: IAddressManager weak var listener: IBlockchainDataListener? + weak var transactionListener: ITransactionListener? private let dateGenerator: () -> Date private let queue: DispatchQueue @@ -87,6 +88,7 @@ extension TransactionProcessor: ITransactionProcessor { } self.process(transaction: transaction) + self.transactionListener?.onReceive(transaction: transaction) if transaction.header.isMine { self.relay(transaction: transaction.header, withOrder: index, inBlock: block) diff --git a/BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift index 589f38f1..6a5b5775 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift @@ -73,6 +73,41 @@ class AddressManagerTests: XCTestCase { } } + func testReceivePublicKey() { + let publicKeys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false) + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) + } + + let changePublicKey = try! manager.receivePublicKey() + XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) + } + + func testReceivePublicKey_NoUnusedPublicKey() { + let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true) + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) + } + + do { + let _ = try manager.receivePublicKey() + XCTFail("Should throw exception") + } catch let error as AddressManager.AddressManagerError { + XCTAssertEqual(error, AddressManager.AddressManagerError.noUnusedPublicKey) + } catch { + XCTFail("Unexpected exception thrown") + } + } + func testReceiveAddress() { let publicKeys = [ PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), @@ -344,9 +379,47 @@ class AddressManagerTests: XCTestCase { XCTAssertEqual(manager.gapShifts(), false) } + func testPublicKey_byPath_ExistsInStorage() { + let key = getPublicKey(withAccount: 10, index: 20, chain: .internal) + + stub(mockStorage) { mock in + when(mock.publicKey(byPath: equal(to: "10/0/20"))).thenReturn(key) + } + + XCTAssertEqual(try! manager.publicKey(byPath: "10/0/20"), key) + verify(mockStorage).publicKey(byPath: equal(to: "10/0/20")) + verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) + } + + func testPublicKey_byPath_DoesNotExistsInStorage() { + let key = getPublicKey(withAccount: 10, index: 20, chain: .external) + + stub(mockStorage) { mock in + when(mock.publicKey(byPath: equal(to: "10/1/20"))).thenReturn(nil) + } + stub(mockHDWallet) { mock in + when(mock.publicKey(account: 10, index: 20, external: true)).thenReturn(key) + } + + XCTAssertEqual(try! manager.publicKey(byPath: "10/1/20"), key) + verify(mockStorage).publicKey(byPath: equal(to: "10/1/20")) + verify(mockHDWallet).publicKey(account: 10, index: 20, external: true) + } + + func testPublicKey_byPath_InvalidPath() { + do { + _ = try manager.publicKey(byPath: "0/0") + XCTFail("Expected exception") + } catch let error as AddressManager.AddressManagerError { + XCTAssertEqual(error, .invalidPath) + } catch { + XCTFail("Unexpected exception") + } + } private func getPublicKey(withAccount account: Int, index: Int, chain: HDWallet.Chain) -> PublicKey { let hdPrivKeyData = try! hdWallet.privateKeyData(account: account, index: index, external: chain == .external) return PublicKey(withAccount: account, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) } + } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift index 8f123de0..b666c304 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift @@ -10,6 +10,7 @@ class BloomFilterManagerTests: QuickSpec { let mockStorage = MockIStorage() let mockFactory = MockIFactory() let mockBloomFilterManagerDelegate = MockIBloomFilterManagerDelegate() + let mockBloomFilterProvider = MockIBloomFilterProvider() let bloomFilter = BloomFilter(elements: [Data(from: 9999999)]) let lastBlock = TestData.checkpointBlock @@ -152,6 +153,33 @@ class BloomFilterManagerTests: QuickSpec { } } + context("when has providers") { + let elements = [Data(repeating: 0, count: 32), Data(repeating: 1, count: 20)] + + beforeEach { + stub(mockStorage) { mock in + when(mock.publicKeys()).thenReturn([]) + when(mock.outputsWithPublicKeys()).thenReturn([]) + } + stub(mockBloomFilterProvider) { mock in + when(mock.filterElements()).thenReturn(elements) + } + + manager.add(provider: mockBloomFilterProvider) + } + + afterEach { + reset(mockBloomFilterProvider) + } + + it("adds elements to bloom filter") { + manager.regenerateBloomFilter() + + verify(mockFactory).bloomFilter(withElements: equal(to: elements)) + verify(mockBloomFilterManagerDelegate).bloomFilterUpdated(bloomFilter: equal(to: bloomFilter, equalWhen: { $0.filter == $1.filter })) + } + } + context("when no elements") { it("doesn't trigger events") { stub(mockStorage) { mock in diff --git a/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift new file mode 100644 index 00000000..5f1cfca7 --- /dev/null +++ b/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift @@ -0,0 +1,86 @@ +import Quick +import Nimble +import XCTest +import Cuckoo +import HSHDWalletKit +@testable import BitcoinCore + +class WatchedTransactionManagerTests: QuickSpec { + override func spec() { + let mockBloomFilterManager = MockIBloomFilterManager() + let mockP2ShFilterDelegate = MockIWatchedTransactionDelegate() + let mockOutpointFilterDelegate = MockIWatchedTransactionDelegate() + var manager: WatchedTransactionManager! + + let scriptHash = Data(repeating: 0, count: 32) + let transactionHash = Data(repeating: 1, count: 32) + let outputIndex = 1 + + beforeEach { + stub(mockBloomFilterManager) { mock in + when(mock.regenerateBloomFilter()).thenDoNothing() + } + stub(mockP2ShFilterDelegate) { mock in + when(mock.transactionReceived(transaction: any(), outputIndex: any())).thenDoNothing() + } + stub(mockOutpointFilterDelegate) { mock in + when(mock.transactionReceived(transaction: any(), inputIndex: any())).thenDoNothing() + } + + manager = WatchedTransactionManager(bloomFilterManager: mockBloomFilterManager, queue: DispatchQueue.main) + manager.add(transactionFilter: .p2shOutput(scriptHash: scriptHash), delegatedTo: mockP2ShFilterDelegate) + manager.add(transactionFilter: .outpoint(transactionHash: transactionHash, outputIndex: outputIndex), delegatedTo: mockOutpointFilterDelegate) + } + + afterEach { + reset(mockBloomFilterManager, mockP2ShFilterDelegate, mockOutpointFilterDelegate) + + manager = nil + } + + describe("#add(transactionFilter:delegatedTo:)") { + it("calls regenerateBloomFilter") { + manager.add(transactionFilter: .p2shOutput(scriptHash: scriptHash), delegatedTo: mockP2ShFilterDelegate) + verify(mockBloomFilterManager, times(3)).regenerateBloomFilter() + } + } + + describe("#onReceive") { + it("matches against p2shOutput filters") { + let transaction = TestData.p2shTransaction + transaction.outputs[0].keyHash = scriptHash + + manager.onReceive(transaction: transaction) + self.waitForMainQueue() + + verify(mockP2ShFilterDelegate).transactionReceived(transaction: equal(to: transaction), outputIndex: transaction.outputs[0].index) + verify(mockOutpointFilterDelegate, never()).transactionReceived(transaction: any(), inputIndex: any()) + } + + it("matches against outpoint filters") { + let transaction = TestData.p2shTransaction + transaction.inputs[0].previousOutputTxHash = transactionHash + transaction.inputs[0].previousOutputIndex = outputIndex + + manager.onReceive(transaction: transaction) + self.waitForMainQueue() + + verify(mockP2ShFilterDelegate, never()).transactionReceived(transaction: any(), outputIndex: any()) + verify(mockOutpointFilterDelegate).transactionReceived(transaction: equal(to: transaction), inputIndex: 0) + } + } + + describe("#getFilterElements") { + it("returns p2shOutput filters") { + let elements = manager.filterElements() + expect(elements).to(contain(scriptHash)) + } + + it("returns outpoint filters") { + let elements = manager.filterElements() + expect(elements).to(contain(transactionHash + byteArrayLittleEndian(int: outputIndex))) + } + } + } + +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index 99f2fbc0..a8327e7b 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -39,6 +39,7 @@ class TransactionBuilderTests: XCTestCase { private var mockInputSigner: MockIInputSigner! private var mockScriptBuilder: MockIScriptBuilder! private var mockFactory: MockIFactory! + private var mockTransactionSizeCalculator: MockITransactionSizeCalculator! private var transactionBuilder: TransactionBuilder! @@ -72,8 +73,9 @@ class TransactionBuilderTests: XCTestCase { mockInputSigner = MockIInputSigner() mockScriptBuilder = MockIScriptBuilder() mockFactory = MockIFactory() + mockTransactionSizeCalculator = MockITransactionSizeCalculator() - transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, addressManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory) + transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, addressManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory, transactionSizeCalculator: mockTransactionSizeCalculator) changePubKey = TestData.pubKey() changePubKeyAddress = "Rsfz3aRmCwTe2J8pSWSYRNYmweJ" @@ -149,6 +151,7 @@ class TransactionBuilderTests: XCTestCase { mockAddressConverter = nil mockInputSigner = nil mockFactory = nil + mockTransactionSizeCalculator = nil transactionBuilder = nil changePubKey = nil toAddressPKH = nil @@ -356,4 +359,80 @@ class TransactionBuilderTests: XCTestCase { } } + func testBuildTransaction_FromUnspentOutput_P2SH() { + let sigData = [Data(hex: "000001")!, Data(hex: "000002")!] + let value = 1000000 + + stub(mockInputSigner) { mock in + when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) + } + stub(mockTransactionSizeCalculator) { mock in + when(mock.transactionSize(inputs: equal(to: [ScriptType.p2sh]), outputScriptTypes: equal(to: [ScriptType.p2pkh]))).thenReturn(90) + } + + let previousTransaction = TestData.p2shTransaction + previousTransaction.outputs[0].value = value + let unspentOutput = UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil) + let signatureScript = Data(repeating: 0, count: 10) + var calledWithSignatureAndPublicKey = false + let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in + if signature == sigData[0] { + XCTAssertEqual(signature, sigData[0]) + XCTAssertEqual(publicKey, sigData[1]) + calledWithSignatureAndPublicKey = true + } + return signatureScript + } + let fee = (10 + 90) * feeRate + + let resultTx = try! transactionBuilder.buildTransaction(from: unspentOutput, to: toAddressPKH, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + + XCTAssertNotEqual(resultTx.header.dataHash, Data()) + XCTAssertEqual(resultTx.header.status, .new) + XCTAssertEqual(resultTx.header.isMine, true) + XCTAssertEqual(resultTx.header.segWit, false) + XCTAssertEqual(resultTx.header.isOutgoing, false) + XCTAssertEqual(resultTx.inputs.count, 1) + XCTAssertEqual(resultTx.outputs.count, 1) + XCTAssertEqual(resultTx.inputs[0].signatureScript, signatureScript) + + verify(mockFactory).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: equal(to: Data()), sequence: 0xFFFFFFFF) + verify(mockFactory).output(withValue: value - fee, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) + XCTAssertTrue(calledWithSignatureAndPublicKey) + } + + func testBuildTransaction_FromUnspentOutput_FeeMoreThanValue() { + let sigData = [Data(hex: "000001")!, Data(hex: "000002")!] + stub(mockInputSigner) { mock in + when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) + } + stub(mockTransactionSizeCalculator) { mock in + when(mock.transactionSize(inputs: equal(to: [ScriptType.p2sh]), outputScriptTypes: equal(to: [ScriptType.p2pkh]))).thenReturn(90) + } + + let previousTransaction = TestData.p2shTransaction + let unspentOutput = UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil) + let signatureScript = Data(repeating: 0, count: 10) + var calledWithSignatureAndPublicKey = false + let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in + if signature == sigData[0] { + XCTAssertEqual(signature, sigData[0]) + XCTAssertEqual(publicKey, sigData[1]) + calledWithSignatureAndPublicKey = true + } + return signatureScript + } + let fee = (10 + 90) * feeRate + previousTransaction.outputs[0].value = fee - 1 + + do { + _ = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddressPKH, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + } catch let error as TransactionBuilder.BuildError { + XCTAssertEqual(error, TransactionBuilder.BuildError.feeMoreThanValue) + } catch let error { + XCTFail(error.localizedDescription) + } + + } + } diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index f0c23538..7e96d918 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -19,7 +19,7 @@ class TransactionCreatorTests: QuickSpec { transactionCreator = nil } - describe("#create") { + describe("#create(to:value:feeRate:senderPay:)") { beforeEach { stub(mockTransactionBuilder) { mock in when(mock.buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any())).thenReturn(transaction) @@ -46,7 +46,7 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenDoNothing() } - try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) + _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } it("does create transaction") { @@ -69,11 +69,10 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) } - try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) + _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } it("doesn't create transaction") { - verify(mockTransactionBuilder, never()).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any()) verify(mockTransactionProcessor, never()).processCreated(transaction: any()) } @@ -88,7 +87,7 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenDoNothing() } - try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) + _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } it("creates transaction") { @@ -101,5 +100,90 @@ class TransactionCreatorTests: QuickSpec { } } } + + describe("#create(from:to:feeRate:signatureScriptFunction:)") { + let unspentOutput = UnspentOutput(output: TestData.p2shTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: Transaction(), blockHeight: nil) + let signatureScriptFunction: (Data, Data) -> Data = { return $0 + $1 } + + beforeEach { + stub(mockTransactionBuilder) { mock in + when(mock.buildTransaction(from: any(), to: any(), feeRate: any(), signatureScriptFunction: any())).thenReturn(transaction) + } + stub(mockTransactionProcessor) { mock in + when(mock.processCreated(transaction: any())).thenDoNothing() + } + stub(mockTransactionSender) { mock in + when(mock.send(pendingTransaction: any())).thenDoNothing() + } + stub(mockBloomFilterManager) { mock in + when(mock.regenerateBloomFilter()).thenDoNothing() + } + + transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, bloomFilterManager: mockBloomFilterManager) + } + + context("when BloomFilterManager.BloomFilterExpired error") { + beforeEach { + stub(mockTransactionProcessor) { mock in + when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) + } + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + + _ = try? transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) + } + + it("does create transaction") { + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: "", feeRate: 0, signatureScriptFunction: any()) + verify(mockTransactionProcessor).processCreated(transaction: any()) + } + + it("does send transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + + it("regenerates bloomfilter") { + verify(mockBloomFilterManager).regenerateBloomFilter() + } + } + + context("when other error") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) + } + + _ = try? transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) + } + + it("doesn't create transaction") { + verify(mockTransactionProcessor, never()).processCreated(transaction: any()) + } + + it("doesn't regenerate bloomfilter") { + verify(mockBloomFilterManager, never()).regenerateBloomFilter() + } + } + + context("when success") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + + _ = try! transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) + } + + it("creates transaction") { + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: "", feeRate: 0, signatureScriptFunction: any()) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + } + + it("sends transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + } + } } } diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift index f12e28d4..46a0123d 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift @@ -10,6 +10,7 @@ class TransactionProcessorTests: XCTestCase { private var mockOutputsCache: MockIOutputsCache! private var mockAddressManager: MockIAddressManager! private var mockBlockchainDataListener: MockIBlockchainDataListener! + private var mockTransactionListener: MockITransactionListener! private var generatedDate: Date! private var dateGenerator: (() -> Date)! @@ -31,6 +32,7 @@ class TransactionProcessorTests: XCTestCase { mockOutputsCache = MockIOutputsCache() mockAddressManager = MockIAddressManager() mockBlockchainDataListener = MockIBlockchainDataListener() + mockTransactionListener = MockITransactionListener() stub(mockStorage) { mock in when(mock.transaction(byHash: any())).thenReturn(nil) @@ -59,8 +61,12 @@ class TransactionProcessorTests: XCTestCase { when(mock.onDelete(transactionHashes: any())).thenDoNothing() when(mock.onInsert(block: any())).thenDoNothing() } + stub(mockTransactionListener) { mock in + when(mock.onReceive(transaction: any())).thenDoNothing() + } transactionProcessor = TransactionProcessor(storage: mockStorage, outputExtractor: mockOutputExtractor, inputExtractor: mockInputExtractor, outputsCache: mockOutputsCache, outputAddressExtractor: mockOutputAddressExtractor, addressManager: mockAddressManager, listener: mockBlockchainDataListener, dateGenerator: dateGenerator) + transactionProcessor.transactionListener = mockTransactionListener } override func tearDown() { @@ -304,6 +310,7 @@ class TransactionProcessorTests: XCTestCase { verify(mockStorage).add(transaction: equal(to: transaction)) verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) verify(mockInputExtractor).extract(transaction: equal(to: transaction)) + verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) XCTAssertEqual(transaction.header.blockHash, nil) @@ -318,6 +325,7 @@ class TransactionProcessorTests: XCTestCase { verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) verify(mockStorage, never()).add(transaction: any()) + verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) verifyNoMoreInteractions(mockBlockchainDataListener) verifyNoMoreInteractions(mockOutputAddressExtractor) verifyNoMoreInteractions(mockInputExtractor) diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index 898c5109..0fc98775 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -149,8 +149,8 @@ public class DashKit: AbstractKit { return transactionInfos.compactMap { $0 as? DashTransactionInfo } } - public override func send(to address: String, value: Int, feeRate: Int) throws { - try super.send(to: address, value: value, feeRate: feeRate) + public override func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { + return try super.send(to: address, value: value, feeRate: feeRate) } public func transactions(fromHash: String?, limit: Int?) -> Single<[DashTransactionInfo]> { diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 587f5477..7413fd7b 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -121,7 +121,7 @@ extension BaseAdapter { return Single.create { [unowned self] observer in do { - try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate) + _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate) observer(.success(())) } catch { observer(.error(error)) diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index 09418e2f..e41cf061 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -3,7 +3,7 @@ import BitcoinCore import RxSwift class BitcoinAdapter: BaseAdapter { - private let bitcoinKit: BitcoinKit + let bitcoinKit: BitcoinKit override var changeableAddressType: Bool { return true } init(words: [String], testMode: Bool, syncMode: BitcoinCore.SyncMode) { diff --git a/Demo/Demo/Adapters/BitcoinCashAdapter.swift b/Demo/Demo/Adapters/BitcoinCashAdapter.swift index c91a8f9f..ed0cb1f4 100644 --- a/Demo/Demo/Adapters/BitcoinCashAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinCashAdapter.swift @@ -3,7 +3,7 @@ import BitcoinCore import RxSwift class BitcoinCashAdapter: BaseAdapter { - private let bitcoinCashKit: BitcoinCashKit + let bitcoinCashKit: BitcoinCashKit init(words: [String], testMode: Bool, syncMode: BitcoinCore.SyncMode) { let networkType: BitcoinCashKit.NetworkType = testMode ? .testNet : .mainNet From eb9ea536078a207b36e2759c5b33b0975b15e6bb Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 15 Jul 2019 16:51:07 +0600 Subject: [PATCH 011/234] BloomFilterManager sets itself to the providers it's adding (#404) --- BitcoinCashKit.swift.podspec | 4 ++-- BitcoinCore.swift.podspec | 2 +- BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 3 ++- BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift | 1 + .../BitcoinCore/Managers/WatchedTransactionManager.swift | 5 ++--- .../BitcoinCoreTests/Managers/BloomFilterManagerTests.swift | 1 + .../Managers/WatchedTransactionManagerTests.swift | 3 ++- .../Transactions/Builder/TransactionBuilderTests.swift | 6 ------ BitcoinKit.swift.podspec | 4 ++-- DashKit.swift.podspec | 4 ++-- Podfile.lock | 2 +- 12 files changed, 17 insertions(+), 20 deletions(-) diff --git a/BitcoinCashKit.swift.podspec b/BitcoinCashKit.swift.podspec index 462d0dc7..edda63b5 100644 --- a/BitcoinCashKit.swift.podspec +++ b/BitcoinCashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCashKit.swift' spec.module_name = "BitcoinCashKit" - spec.version = '0.6.1' + spec.version = '0.7.0' spec.summary = 'BitcoinCash library for Swift' spec.description = <<-DESC BitcoinCashKit implements BitcoinCash protocol in Swift. It is an implementation of the BitcoinCash SPV protocol written (almost) entirely in swift. @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.6.1' + spec.dependency 'BitcoinCore.swift', '~> 0.7.0' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.1' spec.dependency 'Alamofire', '~> 4.0' diff --git a/BitcoinCore.swift.podspec b/BitcoinCore.swift.podspec index d81e48c9..4d8fbdce 100644 --- a/BitcoinCore.swift.podspec +++ b/BitcoinCore.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCore.swift' spec.module_name = "BitcoinCore" - spec.version = '0.6.1' + spec.version = '0.7.0' spec.summary = 'Core library Bitcoin derived wallets for Swift' spec.description = <<-DESC BitcoinCore implements Bitcoin core protocol in Swift. It is an implementation of the Bitcoin SPV protocol written (almost) entirely in swift. diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 9b099ba3..7b15398e 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -185,7 +185,7 @@ public class BitcoinCoreBuilder { let initialSyncer = InitialSyncer(storage: storage, listener: kitStateProvider, stateManager: stateManager, blockDiscovery: blockDiscovery, addressManager: addressManager, logger: logger) let bloomFilterLoader = BloomFilterLoader(bloomFilterManager: bloomFilterManager, peerManager: peerManager) - let watchedTransactionManager = WatchedTransactionManager(bloomFilterManager: bloomFilterManager) + let watchedTransactionManager = WatchedTransactionManager() let blockValidatorChain = BlockValidatorChain(proofOfWorkValidator: ProofOfWorkValidator(difficultyEncoder: DifficultyEncoder())) let blockchain = Blockchain(storage: storage, blockValidator: blockValidatorChain, factory: factory, listener: dataProvider) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 94b6c5a8..9ead7bd3 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -546,6 +546,7 @@ protocol IWatchedTransactionManager { func add(transactionFilter: BitcoinCore.TransactionFilter, delegatedTo: IWatchedTransactionDelegate) } -protocol IBloomFilterProvider { +protocol IBloomFilterProvider: AnyObject { + var bloomFilterManager: IBloomFilterManager? { set get } func filterElements() -> [Data] } diff --git a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift index d1e2230b..d182c23e 100644 --- a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift @@ -33,6 +33,7 @@ class BloomFilterManager { extension BloomFilterManager: IBloomFilterManager { func add(provider: IBloomFilterProvider) { + provider.bloomFilterManager = self providers.append(provider) } diff --git a/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift b/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift index a8c3ecaf..8e847755 100644 --- a/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/WatchedTransactionManager.swift @@ -14,10 +14,9 @@ class WatchedTransactionManager { private var p2ShOutputFilters = [P2ShOutputFilter]() private var outpointFilters = [OutpointFilter]() private let queue: DispatchQueue - private weak var bloomFilterManager: IBloomFilterManager? + weak var bloomFilterManager: IBloomFilterManager? - init(bloomFilterManager: IBloomFilterManager, queue: DispatchQueue = DispatchQueue(label: "WatchedTransactionManager Queue", qos: .background)) { - self.bloomFilterManager = bloomFilterManager + init(queue: DispatchQueue = DispatchQueue(label: "WatchedTransactionManager Queue", qos: .background)) { self.queue = queue } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift index b666c304..5563d65e 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift @@ -162,6 +162,7 @@ class BloomFilterManagerTests: QuickSpec { when(mock.outputsWithPublicKeys()).thenReturn([]) } stub(mockBloomFilterProvider) { mock in + when(mock.bloomFilterManager.set(_: any())).thenDoNothing() when(mock.filterElements()).thenReturn(elements) } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift index 5f1cfca7..0d621596 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/WatchedTransactionManagerTests.swift @@ -27,7 +27,8 @@ class WatchedTransactionManagerTests: QuickSpec { when(mock.transactionReceived(transaction: any(), inputIndex: any())).thenDoNothing() } - manager = WatchedTransactionManager(bloomFilterManager: mockBloomFilterManager, queue: DispatchQueue.main) + manager = WatchedTransactionManager(queue: DispatchQueue.main) + manager.bloomFilterManager = mockBloomFilterManager manager.add(transactionFilter: .p2shOutput(scriptHash: scriptHash), delegatedTo: mockP2ShFilterDelegate) manager.add(transactionFilter: .outpoint(transactionHash: transactionHash, outputIndex: outputIndex), delegatedTo: mockOutpointFilterDelegate) } diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index a8327e7b..2b451f3f 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -413,13 +413,7 @@ class TransactionBuilderTests: XCTestCase { let previousTransaction = TestData.p2shTransaction let unspentOutput = UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil) let signatureScript = Data(repeating: 0, count: 10) - var calledWithSignatureAndPublicKey = false let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in - if signature == sigData[0] { - XCTAssertEqual(signature, sigData[0]) - XCTAssertEqual(publicKey, sigData[1]) - calledWithSignatureAndPublicKey = true - } return signatureScript } let fee = (10 + 90) * feeRate diff --git a/BitcoinKit.swift.podspec b/BitcoinKit.swift.podspec index d383e720..042e99ff 100644 --- a/BitcoinKit.swift.podspec +++ b/BitcoinKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinKit.swift' spec.module_name = 'BitcoinKit' - spec.version = '0.6.1' + spec.version = '0.7.0' spec.summary = 'Bitcoin library for Swift' spec.description = <<-DESC BitcoinKit implements Bitcoin protocol in Swift. @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.6.1' + spec.dependency 'BitcoinCore.swift', '~> 0.7.0' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.1' spec.dependency 'Alamofire', '~> 4.0' diff --git a/DashKit.swift.podspec b/DashKit.swift.podspec index 043056b7..3bffffed 100644 --- a/DashKit.swift.podspec +++ b/DashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'DashKit.swift' spec.module_name = 'DashKit' - spec.version = '0.6.1' + spec.version = '0.7.0' spec.summary = 'Dash library for Swift' spec.description = <<-DESC DashKit implements Dash protocol in Swift. @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.6.1' + spec.dependency 'BitcoinCore.swift', '~> 0.7.0' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.1' spec.dependency 'CryptoBLS.swift', '~> 1.1' diff --git a/Podfile.lock b/Podfile.lock index cc6900a9..ad4efa61 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -65,4 +65,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b45f4c6b3e83ecd66793f2ae2479d2038bcfc8e9 -COCOAPODS: 1.7.0 +COCOAPODS: 1.7.4 From 7d7c292716de2a44a885a3cb78ca3f9849a3c9d5 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 2 Aug 2019 12:46:50 +0600 Subject: [PATCH 012/234] Ability to set change output script type (#407) --- .../BitcoinCore/Core/AbstractKit.swift | 12 +++---- .../BitcoinCore/Core/BitcoinCore.swift | 13 +++---- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 3 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 6 ++-- .../Builder/TransactionBuilder.swift | 15 ++++---- .../Transactions/TransactionCreator.swift | 4 +-- .../Builder/TransactionBuilderTests.swift | 22 ++++++------ .../TransactionCreatorTests.swift | 34 +++++++++++++------ DashKit/DashKit/Core/DashKit.swift | 4 +-- Demo/Demo/Adapters/BaseAdapter.swift | 6 ++-- Demo/Demo/Adapters/BitcoinAdapter.swift | 2 +- Demo/Demo/Configuration.swift | 3 +- 12 files changed, 70 insertions(+), 54 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index c954c5a0..9071689a 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -34,12 +34,12 @@ open class AbstractKit { return bitcoinCore.transactions(fromHash: fromHash, limit: limit) } - open func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { - return try bitcoinCore.send(to: address, value: value, feeRate: feeRate) + open func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + return try bitcoinCore.send(to: address, value: value, feeRate: feeRate, changeScriptType: changeScriptType) } - public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { - return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate) + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate, changeScriptType: changeScriptType) } public func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { @@ -54,8 +54,8 @@ open class AbstractKit { return bitcoinCore.parse(paymentAddress: paymentAddress) } - open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate) + open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, changeScriptType: ScriptType) throws -> Int { + return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate, changeScriptType: changeScriptType) } open func receiveAddress(for type: ScriptType) -> String { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 2c7e418c..0db7bb9d 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -149,13 +149,14 @@ extension BitcoinCore { return dataProvider.transactions(fromHash: fromHash, limit: limit) } - public func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { - return try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true) + public func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + return try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true, changeScriptType: changeScriptType) } - public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + // TODO: convert to scriptWPKH when scriptType is P2WPKHSH ? let address = try addressConverter.convert(keyHash: hash, type: scriptType) - return try send(to: address.stringValue, value: value, feeRate: feeRate) + return try send(to: address.stringValue, value: value, feeRate: feeRate, changeScriptType: changeScriptType) } func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { @@ -170,8 +171,8 @@ extension BitcoinCore { return paymentAddressParser.parse(paymentAddress: paymentAddress) } - public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress) + public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, changeScriptType: ScriptType) throws -> Int { + return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress, changeScriptType: changeScriptType) } public func receiveAddress(for type: ScriptType) -> String { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 7b15398e..00dd4885 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -201,7 +201,8 @@ public class BitcoinCoreBuilder { let inputSigner = InputSigner(hdWallet: hdWallet, network: network) let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() - let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, addressManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, transactionSizeCalculator: transactionSizeCalculator) + let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, addressManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, + transactionSizeCalculator: transactionSizeCalculator, addressKeyHashConverter: addressKeyHashConverter) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 9ead7bd3..21115a12 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -343,13 +343,13 @@ public protocol ITransactionSyncer: class { } public protocol ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, changeScriptType: ScriptType) throws -> FullTransaction func create(from: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionBuilder { - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction + func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?, changeScriptType: ScriptType) throws -> Int + func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String, changeScriptType: ScriptType) throws -> FullTransaction func buildTransaction(from: UnspentOutput, to: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 73d8ed59..8f851b0e 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -10,6 +10,7 @@ class TransactionBuilder { private let unspentOutputProvider: IUnspentOutputProvider private let addressManager: IAddressManager private let addressConverter: IAddressConverter + private let addressKeyHashConverter: IAddressKeyHashConverter? private let inputSigner: IInputSigner private let factory: IFactory private let transactionSizeCalculator: ITransactionSizeCalculator @@ -17,11 +18,13 @@ class TransactionBuilder { var scriptBuilder: IScriptBuilder init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, addressManager: IAddressManager, addressConverter: IAddressConverter, - inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator) { + inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator, + addressKeyHashConverter: IAddressKeyHashConverter? = nil) { self.unspentOutputSelector = unspentOutputSelector self.unspentOutputProvider = unspentOutputProvider self.addressManager = addressManager self.addressConverter = addressConverter + self.addressKeyHashConverter = addressKeyHashConverter self.inputSigner = inputSigner self.scriptBuilder = scriptBuilder self.factory = factory @@ -52,10 +55,10 @@ extension TransactionBuilder: ITransactionBuilder { // :fee method returns the fee for the given amount // If address given and it's valid, it returns the actual fee // Otherwise, it returns the estimated fee - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String? = nil) throws -> Int { + func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String? = nil, changeScriptType: ScriptType) throws -> Int { if let string = address, let _ = try? addressConverter.convert(address: string) { // Actual fee - let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: string) + let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: string, changeScriptType: changeScriptType) return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate } else { // Estimated fee @@ -65,12 +68,11 @@ extension TransactionBuilder: ITransactionBuilder { } } - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction { + func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String, changeScriptType: ScriptType) throws -> FullTransaction { guard let changePubKey = try? addressManager.changePublicKey() else { throw BuildError.noChangeAddress } - let changeScriptType = ScriptType.p2pkh let address = try addressConverter.convert(address: toAddress) let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: changeScriptType, senderPay: senderPay) @@ -97,7 +99,8 @@ extension TransactionBuilder: ITransactionBuilder { // Add :change output if needed if selectedOutputsInfo.addChangeOutput { - let changeAddress = try addressConverter.convert(keyHash: changePubKey.keyHash, type: changeScriptType) + let correctKeyHash = addressKeyHashConverter?.convert(keyHash: changePubKey.keyHash, type: changeScriptType) ?? changePubKey.keyHash + let changeAddress = try addressConverter.convert(keyHash: correctKeyHash, type: changeScriptType) outputs.append(try output(withIndex: 1, address: changeAddress, value: selectedOutputsInfo.totalValue - sentValue)) } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index 9e30c5c5..fb7bb7f6 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -31,8 +31,8 @@ class TransactionCreator { extension TransactionCreator: ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { - let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address) + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, changeScriptType: ScriptType) throws -> FullTransaction { + let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address, changeScriptType: changeScriptType) try processAndSend(transaction: transaction) return transaction diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index 2b451f3f..a88d0015 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -164,7 +164,7 @@ class TransactionBuilderTests: XCTestCase { } func testFee_AddressGiven() { - let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH) + let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH, changeScriptType: .p2pkh) XCTAssertEqual(resultFee, 546) } @@ -174,7 +174,7 @@ class TransactionBuilderTests: XCTestCase { } do { - let _ = try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH) + let _ = try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH, changeScriptType: .p2pkh) } catch let error as BitcoinCoreErrors.AddressConversion { XCTAssertEqual(error, BitcoinCoreErrors.AddressConversion.invalidAddressLength) } catch let error { @@ -183,12 +183,12 @@ class TransactionBuilderTests: XCTestCase { } func testFee_AddressNotGiven_Error() { - let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false) + let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, changeScriptType: .p2pkh) XCTAssertEqual(resultFee, fee) } func testBuildTransaction_P2PKH() { - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) XCTAssertNotEqual(resultTx.header.dataHash, Data()) XCTAssertEqual(resultTx.header.status, .new) @@ -259,7 +259,7 @@ class TransactionBuilderTests: XCTestCase { // } func testBuildTransaction_P2SH() { - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressSH) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressSH, changeScriptType: .p2pkh) XCTAssertNotEqual(resultTx.header.dataHash, Data()) XCTAssertEqual(resultTx.header.status, .new) @@ -277,7 +277,7 @@ class TransactionBuilderTests: XCTestCase { } func testBuildTransactionSenderPay() { - _ = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: true, toAddress: toAddressPKH) + _ = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: true, toAddress: toAddressPKH, changeScriptType: .p2pkh) verify(mockFactory).output(withValue: value, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) verify(mockFactory).output(withValue: unspentOutputs.unspentOutputs[0].output.value - value - fee, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any()) @@ -290,7 +290,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) XCTAssertEqual(resultTx.inputs.count, 1) XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) @@ -307,7 +307,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) XCTAssertEqual(resultTx.inputs.count, 1) XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) @@ -325,7 +325,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) XCTAssertEqual(resultTx.inputs[0].signatureScript, sigScript) } @@ -336,7 +336,7 @@ class TransactionBuilderTests: XCTestCase { ) do { - let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) } catch let error as TransactionBuilder.BuildError { XCTAssertEqual(error, TransactionBuilder.BuildError.feeMoreThanValue) } catch let error { @@ -350,7 +350,7 @@ class TransactionBuilderTests: XCTestCase { } do { - let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) XCTFail("No exception!") } catch let error as TransactionBuilder.BuildError { XCTAssertEqual(error, TransactionBuilder.BuildError.noChangeAddress) diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index 7e96d918..39ab9548 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -22,7 +22,7 @@ class TransactionCreatorTests: QuickSpec { describe("#create(to:value:feeRate:senderPay:)") { beforeEach { stub(mockTransactionBuilder) { mock in - when(mock.buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any())).thenReturn(transaction) + when(mock.buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any(), changeScriptType: any())).thenReturn(transaction) } stub(mockTransactionProcessor) { mock in when(mock.processCreated(transaction: any())).thenDoNothing() @@ -46,11 +46,11 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenDoNothing() } - _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) + _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2pkh) } it("does create transaction") { - verify(mockTransactionBuilder).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any()) + verify(mockTransactionBuilder).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any(), changeScriptType: any()) verify(mockTransactionProcessor).processCreated(transaction: any()) } @@ -69,7 +69,7 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) } - _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) + _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2pkh) } it("doesn't create transaction") { @@ -86,17 +86,29 @@ class TransactionCreatorTests: QuickSpec { stub(mockTransactionSender) { mock in when(mock.verifyCanSend()).thenDoNothing() } - - _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } - it("creates transaction") { - verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "") - verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + context("when changeScriptType is .p2pkh") { + beforeEach { + _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2pkh) + } + + it("creates transaction") { + verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "", changeScriptType: equal(to: ScriptType.p2pkh)) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + } + + it("sends transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } } - it("sends transaction") { - verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + context("when changeScriptType is .p2wpkh") { + it("create transaction with p2wpkh change output") { + _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2wpkh) + verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "", changeScriptType: equal(to: ScriptType.p2wpkh)) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + } } } } diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index 0fc98775..0eba1cd7 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -149,8 +149,8 @@ public class DashKit: AbstractKit { return transactionInfos.compactMap { $0 as? DashTransactionInfo } } - public override func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { - return try super.send(to: address, value: value, feeRate: feeRate) + public override func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + return try super.send(to: address, value: value, feeRate: feeRate, changeScriptType: changeScriptType) } public func transactions(fromHash: String?, limit: Int?) -> Single<[DashTransactionInfo]> { diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 7413fd7b..c3d4dd26 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -8,7 +8,7 @@ class BaseAdapter { let name: String let coinCode: String - var changeableAddressType: Bool { return false } + var changeAddressScriptType: ScriptType { return .p2pkh } private let abstractKit: AbstractKit @@ -121,7 +121,7 @@ extension BaseAdapter { return Single.create { [unowned self] observer in do { - _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate) + _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate, changeScriptType: self.changeAddressScriptType) observer(.success(())) } catch { observer(.error(error)) @@ -138,7 +138,7 @@ extension BaseAdapter { func fee(for value: Decimal, address: String?) -> Decimal { do { let amount = convertToSatoshi(value: value) - let fee = try abstractKit.fee(for: amount, toAddress: address, senderPay: true, feeRate: feeRate) + let fee = try abstractKit.fee(for: amount, toAddress: address, senderPay: true, feeRate: feeRate, changeScriptType: self.changeAddressScriptType) return Decimal(fee) / coinRate } catch BitcoinCoreErrors.UnspentOutputSelection.notEnough(let maxFee) { return Decimal(maxFee) / coinRate diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index e41cf061..0b28d94e 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -4,7 +4,7 @@ import RxSwift class BitcoinAdapter: BaseAdapter { let bitcoinKit: BitcoinKit - override var changeableAddressType: Bool { return true } + override var changeAddressScriptType: ScriptType { return .p2pkh } init(words: [String], testMode: Bool, syncMode: BitcoinCore.SyncMode) { let networkType: BitcoinKit.NetworkType = testMode ? .testNet : .mainNet diff --git a/Demo/Demo/Configuration.swift b/Demo/Demo/Configuration.swift index d9d7fa6d..5e3889c3 100644 --- a/Demo/Demo/Configuration.swift +++ b/Demo/Demo/Configuration.swift @@ -7,10 +7,9 @@ class Configuration { let testNet = true let mainNet = false let defaultWords = [ - "used ugly meat glad balance divorce inner artwork hire invest already piano", - "razor noodle horse vital dilemma drum civil account grow turn genre turtle", "current force clump paper shrug extra zebra employ prefer upon mobile hire", "popular game latin harvest silly excess much valid elegant illness edge silk", ] } +// \ No newline at end of file From 8da43b201843ad4768268e17dc2ced3acc30133d Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 2 Aug 2019 14:37:22 +0600 Subject: [PATCH 013/234] Wrap peerStates removal in SyncedReadyPeerManager (#407) --- BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift b/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift index 016d3e42..aeda9eb0 100644 --- a/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/SyncedReadyPeerManager.swift @@ -79,7 +79,9 @@ extension SyncedReadyPeerManager { } private func onPeerDisconnect(peer: IPeer, error: Error?) { - peerStates.removeValue(forKey: peer.host) + peersQueue.async { + self.peerStates.removeValue(forKey: peer.host) + } } private func onPeerReady(peer: IPeer) { From 7155d6119e33702a28a5e92417610e8275e37fa6 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 2 Aug 2019 15:11:50 +0600 Subject: [PATCH 014/234] Set default changeScriptType (#407) --- BitcoinCore/BitcoinCore/Core/AbstractKit.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index 9071689a..358cb1a2 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -34,11 +34,11 @@ open class AbstractKit { return bitcoinCore.transactions(fromHash: fromHash, limit: limit) } - open func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + open func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType = .p2pkh) throws -> FullTransaction { return try bitcoinCore.send(to: address, value: value, feeRate: feeRate, changeScriptType: changeScriptType) } - public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, changeScriptType: ScriptType = .p2pkh) throws -> FullTransaction { return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate, changeScriptType: changeScriptType) } @@ -54,7 +54,7 @@ open class AbstractKit { return bitcoinCore.parse(paymentAddress: paymentAddress) } - open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, changeScriptType: ScriptType) throws -> Int { + open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, changeScriptType: ScriptType = .p2pkh) throws -> Int { return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate, changeScriptType: changeScriptType) } From 7bc5acd18e441f9d0df241347303cee3b3e4359d Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 19 Aug 2019 16:28:42 +0600 Subject: [PATCH 015/234] Ability to clear wallets except specified ones - All 3 kit classes' #clear methods receive a list of wallet ids which must remain after clearing - DirectoryHelper#removeAll method iterates over files in given directory and removes files not listed(match fully or partially) in 'excludedFiles' parameter - Increased pods version --- BitcoinCashKit.swift.podspec | 2 +- .../BitcoinCashKit/Core/BitcoinCashKit.swift | 25 ++++++++++++++----- BitcoinCore.swift.podspec | 2 +- .../BitcoinCore/Helpers/DirectoryHelper.swift | 13 +++++++++- BitcoinKit.swift.podspec | 2 +- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 24 ++++++++++++++---- DashKit.swift.podspec | 2 +- DashKit/DashKit/Core/DashKit.swift | 25 ++++++++++++++----- Demo/Demo/Adapters/BitcoinAdapter.swift | 2 +- Demo/Demo/Adapters/BitcoinCashAdapter.swift | 2 +- Demo/Demo/Adapters/DashAdapter.swift | 2 +- 11 files changed, 76 insertions(+), 25 deletions(-) diff --git a/BitcoinCashKit.swift.podspec b/BitcoinCashKit.swift.podspec index edda63b5..51aba3e1 100644 --- a/BitcoinCashKit.swift.podspec +++ b/BitcoinCashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCashKit.swift' spec.module_name = "BitcoinCashKit" - spec.version = '0.7.0' + spec.version = '0.7.1' spec.summary = 'BitcoinCash library for Swift' spec.description = <<-DESC BitcoinCashKit implements BitcoinCash protocol in Swift. It is an implementation of the BitcoinCash SPV protocol written (almost) entirely in swift. diff --git a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift index 1b8af32b..57ba383c 100644 --- a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift +++ b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift @@ -5,6 +5,7 @@ import HSCryptoKit import RxSwift public class BitcoinCashKit: AbstractKit { + private static let name = "BitcoinCashKit" private static let svChainForkHeight = 556767 // 2018 November 14 private static let abcChainForkBlockHash = "0000000000000000004626ff6e3b936941d341c5932ece4357eeccac44e6d56c".reversedData! @@ -13,11 +14,7 @@ public class BitcoinCashKit: AbstractKit { private static let targetSpacing = 10 * 60 // Time to mining one block ( 10 min. same as Bitcoin ) private static let maxTargetBits = 0x1d00ffff // Initially and max. target difficulty for blocks - public static func clear() throws { - try DirectoryHelper.removeDirectory("BitcoinCashKit") - } - - public enum NetworkType { case mainNet, testNet } + public enum NetworkType: String, CaseIterable { case mainNet, testNet } public weak var delegate: BitcoinCoreDelegate? { didSet { @@ -44,7 +41,7 @@ public class BitcoinCashKit: AbstractKit { } let initialSyncApi = InsightApi(url: initialSyncApiUrl) - let databaseFilePath = try DirectoryHelper.directoryURL(for: "BitcoinCashKit").appendingPathComponent("\(walletId)-\(networkType)").path + let databaseFilePath = try DirectoryHelper.directoryURL(for: BitcoinCashKit.name).appendingPathComponent("\(walletId)-\(networkType)").path let storage = BitcoinCashGrdbStorage(databaseFilePath: databaseFilePath) self.storage = storage @@ -88,3 +85,19 @@ public class BitcoinCashKit: AbstractKit { } } + +extension BitcoinCashKit { + + public static func clear(exceptFor walletIdsToExclude: [String] = []) throws { + var excludedFileNames = [String]() + + for walletId in walletIdsToExclude { + for type in NetworkType.allCases { + excludedFileNames.append("\(walletId)-\(type.rawValue)") + } + } + + try DirectoryHelper.removeAll(inDirectory: BitcoinCashKit.name, except: excludedFileNames) + } + +} diff --git a/BitcoinCore.swift.podspec b/BitcoinCore.swift.podspec index 4d8fbdce..5d195f18 100644 --- a/BitcoinCore.swift.podspec +++ b/BitcoinCore.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCore.swift' spec.module_name = "BitcoinCore" - spec.version = '0.7.0' + spec.version = '0.7.1' spec.summary = 'Core library Bitcoin derived wallets for Swift' spec.description = <<-DESC BitcoinCore implements Bitcoin core protocol in Swift. It is an implementation of the Bitcoin SPV protocol written (almost) entirely in swift. diff --git a/BitcoinCore/BitcoinCore/Helpers/DirectoryHelper.swift b/BitcoinCore/BitcoinCore/Helpers/DirectoryHelper.swift index feb0dedf..7c5f0364 100644 --- a/BitcoinCore/BitcoinCore/Helpers/DirectoryHelper.swift +++ b/BitcoinCore/BitcoinCore/Helpers/DirectoryHelper.swift @@ -16,4 +16,15 @@ public class DirectoryHelper { try FileManager.default.removeItem(at: directoryURL(for: name)) } -} \ No newline at end of file + public static func removeAll(inDirectory directoryName: String, except excludedFiles: [String]) throws { + let fileManager = FileManager.default + let fileUrls = try fileManager.contentsOfDirectory(at: directoryURL(for: directoryName), includingPropertiesForKeys: nil) + + for filename in fileUrls { + if !excludedFiles.contains(where: { filename.lastPathComponent.contains($0) }) { + try fileManager.removeItem(at: filename) + } + } + } + +} diff --git a/BitcoinKit.swift.podspec b/BitcoinKit.swift.podspec index 042e99ff..eb147e9d 100644 --- a/BitcoinKit.swift.podspec +++ b/BitcoinKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinKit.swift' spec.module_name = 'BitcoinKit' - spec.version = '0.7.0' + spec.version = '0.7.1' spec.summary = 'Bitcoin library for Swift' spec.description = <<-DESC BitcoinKit implements Bitcoin protocol in Swift. diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index 769f349a..159c901b 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -5,11 +5,9 @@ import HSCryptoKit import RxSwift public class BitcoinKit: AbstractKit { - public static func clear() throws { - try DirectoryHelper.removeDirectory("BitcoinKit") - } + private static let name = "BitcoinKit" - public enum NetworkType { case mainNet, testNet, regTest } + public enum NetworkType: String, CaseIterable { case mainNet, testNet, regTest } private let storage: IStorage private let bech32AddressConverter: IAddressConverter @@ -37,7 +35,7 @@ public class BitcoinKit: AbstractKit { } let initialSyncApi = BCoinApi(url: initialSyncApiUrl) - let databaseFilePath = try DirectoryHelper.directoryURL(for: "BitcoinKit").appendingPathComponent("\(walletId)-\(networkType)").path + let databaseFilePath = try DirectoryHelper.directoryURL(for: BitcoinKit.name).appendingPathComponent("\(walletId)-\(networkType.rawValue)").path let storage = GrdbStorage(databaseFilePath: databaseFilePath) self.storage = storage @@ -95,3 +93,19 @@ public class BitcoinKit: AbstractKit { } } + +extension BitcoinKit { + + public static func clear(exceptFor walletIdsToExclude: [String] = []) throws { + var excludedFileNames = [String]() + + for walletId in walletIdsToExclude { + for type in NetworkType.allCases { + excludedFileNames.append("\(walletId)-\(type.rawValue)") + } + } + + try DirectoryHelper.removeAll(inDirectory: BitcoinKit.name, except: excludedFileNames) + } + +} diff --git a/DashKit.swift.podspec b/DashKit.swift.podspec index 3bffffed..07f9e420 100644 --- a/DashKit.swift.podspec +++ b/DashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'DashKit.swift' spec.module_name = 'DashKit' - spec.version = '0.7.0' + spec.version = '0.7.1' spec.summary = 'Dash library for Swift' spec.description = <<-DESC DashKit implements Dash protocol in Swift. diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index 0eba1cd7..3e15cf13 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -5,15 +5,12 @@ import HSCryptoKit import RxSwift public class DashKit: AbstractKit { + private static let name = "DashKit" private static let heightInterval = 24 // Blocks count in window for calculating difficulty private static let targetSpacing = 150 // Time to mining one block ( 2.5 min. Dash ) private static let maxTargetBits = 0x1e0fffff // Initially and max. target difficulty for blocks ( Dash ) - public static func clear() throws { - try DirectoryHelper.removeDirectory("DashKit") - } - - public enum NetworkType { case mainNet, testNet } + public enum NetworkType: String, CaseIterable { case mainNet, testNet } weak public var delegate: DashKitDelegate? @@ -39,7 +36,7 @@ public class DashKit: AbstractKit { let logger = Logger(network: network, minLogLevel: minLogLevel) - let databaseFilePath = try DirectoryHelper.directoryURL(for: "DashKit").appendingPathComponent("\(walletId)-\(networkType)").path + let databaseFilePath = try DirectoryHelper.directoryURL(for: DashKit.name).appendingPathComponent("\(walletId)-\(networkType)").path let storage = DashGrdbStorage(databaseFilePath: databaseFilePath) self.storage = storage @@ -201,3 +198,19 @@ extension DashKit: IInstantTransactionDelegate { } } + +extension DashKit { + + public static func clear(exceptFor walletIdsToExclude: [String] = []) throws { + var excludedFileNames = [String]() + + for walletId in walletIdsToExclude { + for type in NetworkType.allCases { + excludedFileNames.append("\(walletId)-\(type.rawValue)") + } + } + + try DirectoryHelper.removeAll(inDirectory: DashKit.name, except: excludedFileNames) + } + +} diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index 0b28d94e..858f505d 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -15,7 +15,7 @@ class BitcoinAdapter: BaseAdapter { } class func clear() { - try? BitcoinKit.clear() + try? BitcoinKit.clear(exceptFor: ["walletId"]) } } diff --git a/Demo/Demo/Adapters/BitcoinCashAdapter.swift b/Demo/Demo/Adapters/BitcoinCashAdapter.swift index ed0cb1f4..a350f821 100644 --- a/Demo/Demo/Adapters/BitcoinCashAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinCashAdapter.swift @@ -14,7 +14,7 @@ class BitcoinCashAdapter: BaseAdapter { } class func clear() { - try? BitcoinCashKit.clear() + try? BitcoinCashKit.clear(exceptFor: ["walletId"]) } } diff --git a/Demo/Demo/Adapters/DashAdapter.swift b/Demo/Demo/Adapters/DashAdapter.swift index d04cc01d..b0396402 100644 --- a/Demo/Demo/Adapters/DashAdapter.swift +++ b/Demo/Demo/Adapters/DashAdapter.swift @@ -34,7 +34,7 @@ class DashAdapter: BaseAdapter { } class func clear() { - try? DashKit.clear() + try? DashKit.clear(exceptFor: ["walletId"]) } } From f3558c47e5a0dca2aa63a27817f29713e6de6cc7 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Tue, 20 Aug 2019 10:40:00 +0600 Subject: [PATCH 016/234] Use method to generate database file name --- BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift | 8 ++++++-- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 8 ++++++-- DashKit/DashKit/Core/DashKit.swift | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift index 57ba383c..6579413a 100644 --- a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift +++ b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift @@ -41,7 +41,7 @@ public class BitcoinCashKit: AbstractKit { } let initialSyncApi = InsightApi(url: initialSyncApiUrl) - let databaseFilePath = try DirectoryHelper.directoryURL(for: BitcoinCashKit.name).appendingPathComponent("\(walletId)-\(networkType)").path + let databaseFilePath = try DirectoryHelper.directoryURL(for: BitcoinCashKit.name).appendingPathComponent(BitcoinCashKit.databaseFileName(walletId: walletId, networkType: networkType)).path let storage = BitcoinCashGrdbStorage(databaseFilePath: databaseFilePath) self.storage = storage @@ -93,11 +93,15 @@ extension BitcoinCashKit { for walletId in walletIdsToExclude { for type in NetworkType.allCases { - excludedFileNames.append("\(walletId)-\(type.rawValue)") + excludedFileNames.append(databaseFileName(walletId: walletId, networkType: type)) } } try DirectoryHelper.removeAll(inDirectory: BitcoinCashKit.name, except: excludedFileNames) } + private static func databaseFileName(walletId: String, networkType: NetworkType) -> String { + return "\(walletId)-\(networkType.rawValue)" + } + } diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index 159c901b..83d0350b 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -35,7 +35,7 @@ public class BitcoinKit: AbstractKit { } let initialSyncApi = BCoinApi(url: initialSyncApiUrl) - let databaseFilePath = try DirectoryHelper.directoryURL(for: BitcoinKit.name).appendingPathComponent("\(walletId)-\(networkType.rawValue)").path + let databaseFilePath = try DirectoryHelper.directoryURL(for: BitcoinKit.name).appendingPathComponent(BitcoinKit.databaseFileName(walletId: walletId, networkType: networkType)).path let storage = GrdbStorage(databaseFilePath: databaseFilePath) self.storage = storage @@ -101,11 +101,15 @@ extension BitcoinKit { for walletId in walletIdsToExclude { for type in NetworkType.allCases { - excludedFileNames.append("\(walletId)-\(type.rawValue)") + excludedFileNames.append(databaseFileName(walletId: walletId, networkType: type)) } } try DirectoryHelper.removeAll(inDirectory: BitcoinKit.name, except: excludedFileNames) } + private static func databaseFileName(walletId: String, networkType: NetworkType) -> String { + return "\(walletId)-\(networkType.rawValue)" + } + } diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index 3e15cf13..e22acd2f 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -36,7 +36,7 @@ public class DashKit: AbstractKit { let logger = Logger(network: network, minLogLevel: minLogLevel) - let databaseFilePath = try DirectoryHelper.directoryURL(for: DashKit.name).appendingPathComponent("\(walletId)-\(networkType)").path + let databaseFilePath = try DirectoryHelper.directoryURL(for: DashKit.name).appendingPathComponent(DashKit.databaseFileName(walletId: walletId, networkType: networkType)).path let storage = DashGrdbStorage(databaseFilePath: databaseFilePath) self.storage = storage @@ -206,11 +206,15 @@ extension DashKit { for walletId in walletIdsToExclude { for type in NetworkType.allCases { - excludedFileNames.append("\(walletId)-\(type.rawValue)") + excludedFileNames.append(databaseFileName(walletId: walletId, networkType: type)) } } try DirectoryHelper.removeAll(inDirectory: DashKit.name, except: excludedFileNames) } + private static func databaseFileName(walletId: String, networkType: NetworkType) -> String { + return "\(walletId)-\(networkType.rawValue)" + } + } From 4842b7a65de6e531fe377378b795b00c225d28ff Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Wed, 21 Aug 2019 16:38:48 +0600 Subject: [PATCH 017/234] Add fee to TransactionInfo - When transaction has only 'mine' inputs, calculate transaction fee in BaseTransactionInfoConverter and subtract it out from amount - Add transaction fee in Demo app --- .../Core/BaseTransactionInfoConverter.swift | 21 +++++++++++++------ BitcoinCore/BitcoinCore/Core/Protocols.swift | 2 +- .../BitcoinCore/Models/TransactionInfo.swift | 4 +++- .../DashKit/Models/DashTransactionInfo.swift | 4 ++-- Demo/Demo/Adapters/BaseAdapter.swift | 1 + .../Controllers/Cells/TransactionCell.swift | 2 ++ Demo/Demo/Core/TransactionRecord.swift | 1 + 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BaseTransactionInfoConverter.swift b/BitcoinCore/BitcoinCore/Core/BaseTransactionInfoConverter.swift index a27d2ea0..95abb801 100644 --- a/BitcoinCore/BitcoinCore/Core/BaseTransactionInfoConverter.swift +++ b/BitcoinCore/BitcoinCore/Core/BaseTransactionInfoConverter.swift @@ -11,15 +11,16 @@ public class BaseTransactionInfoConverter: IBaseTransactionInfoConverter { var totalMineOutput: Int = 0 var fromAddresses = [TransactionAddressInfo]() var toAddresses = [TransactionAddressInfo]() + var hasOnlyMyInputs = true for inputWithPreviousOutput in transactionForInfo.inputsWithPreviousOutputs { var mine = false - if let previousOutput = inputWithPreviousOutput.previousOutput { - if previousOutput.publicKeyPath != nil { - totalMineInput += previousOutput.value - mine = true - } + if let previousOutput = inputWithPreviousOutput.previousOutput, previousOutput.publicKeyPath != nil { + totalMineInput += previousOutput.value + mine = true + } else { + hasOnlyMyInputs = false } if let address = inputWithPreviousOutput.input.address { @@ -40,7 +41,14 @@ public class BaseTransactionInfoConverter: IBaseTransactionInfoConverter { } } - let amount = totalMineOutput - totalMineInput + var amount = totalMineOutput - totalMineInput + + var resolvedFee: Int? = nil + if hasOnlyMyInputs { + let fee = totalMineInput - transactionForInfo.outputs.reduce(0) { totalOutput, output in totalOutput + output.value } + amount += fee + resolvedFee = fee + } return T( transactionHash: transactionForInfo.transactionWithBlock.transaction.dataHash.reversedHex, @@ -48,6 +56,7 @@ public class BaseTransactionInfoConverter: IBaseTransactionInfoConverter { from: fromAddresses, to: toAddresses, amount: amount, + fee: resolvedFee, blockHeight: transactionForInfo.transactionWithBlock.blockHeight, timestamp: transactionForInfo.transactionWithBlock.transaction.timestamp ) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 21115a12..62b88a7c 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -417,7 +417,7 @@ protocol IKitStateProviderDelegate: class { } public protocol ITransactionInfo: class { - init(transactionHash: String, transactionIndex: Int, from: [TransactionAddressInfo], to: [TransactionAddressInfo], amount: Int, blockHeight: Int?, timestamp: Int) + init(transactionHash: String, transactionIndex: Int, from: [TransactionAddressInfo], to: [TransactionAddressInfo], amount: Int, fee: Int?, blockHeight: Int?, timestamp: Int) } public protocol ITransactionInfoConverter { diff --git a/BitcoinCore/BitcoinCore/Models/TransactionInfo.swift b/BitcoinCore/BitcoinCore/Models/TransactionInfo.swift index e34cbb36..de44efef 100644 --- a/BitcoinCore/BitcoinCore/Models/TransactionInfo.swift +++ b/BitcoinCore/BitcoinCore/Models/TransactionInfo.swift @@ -6,15 +6,17 @@ open class TransactionInfo: ITransactionInfo { public let from: [TransactionAddressInfo] public let to: [TransactionAddressInfo] public let amount: Int + public let fee: Int? public let blockHeight: Int? public let timestamp: Int - public required init(transactionHash: String, transactionIndex: Int, from: [TransactionAddressInfo], to: [TransactionAddressInfo], amount: Int, blockHeight: Int?, timestamp: Int) { + public required init(transactionHash: String, transactionIndex: Int, from: [TransactionAddressInfo], to: [TransactionAddressInfo], amount: Int, fee: Int?, blockHeight: Int?, timestamp: Int) { self.transactionHash = transactionHash self.transactionIndex = transactionIndex self.from = from self.to = to self.amount = amount + self.fee = fee self.blockHeight = blockHeight self.timestamp = timestamp } diff --git a/DashKit/DashKit/Models/DashTransactionInfo.swift b/DashKit/DashKit/Models/DashTransactionInfo.swift index bed7a784..f4592459 100644 --- a/DashKit/DashKit/Models/DashTransactionInfo.swift +++ b/DashKit/DashKit/Models/DashTransactionInfo.swift @@ -3,8 +3,8 @@ import BitcoinCore public class DashTransactionInfo: TransactionInfo { public var instantTx: Bool = false - public required init(transactionHash: String, transactionIndex: Int, from: [TransactionAddressInfo], to: [TransactionAddressInfo], amount: Int, blockHeight: Int?, timestamp: Int) { - super.init(transactionHash: transactionHash, transactionIndex: transactionIndex, from: from, to: to, amount: amount, blockHeight: blockHeight, timestamp: timestamp) + public required init(transactionHash: String, transactionIndex: Int, from: [TransactionAddressInfo], to: [TransactionAddressInfo], amount: Int, fee: Int?, blockHeight: Int?, timestamp: Int) { + super.init(transactionHash: transactionHash, transactionIndex: transactionIndex, from: from, to: to, amount: amount, fee: fee, blockHeight: blockHeight, timestamp: timestamp) } } \ No newline at end of file diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index c3d4dd26..5a28c07c 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -40,6 +40,7 @@ class BaseAdapter { transactionHash: transaction.transactionHash, transactionIndex: transaction.transactionIndex, amount: Decimal(transaction.amount) / coinRate, + fee: transaction.fee.map { Decimal($0) / coinRate }, timestamp: Double(transaction.timestamp), from: fromAddresses, to: toAddresses, diff --git a/Demo/Demo/Controllers/Cells/TransactionCell.swift b/Demo/Demo/Controllers/Cells/TransactionCell.swift index 9d4f15fc..c80326b0 100644 --- a/Demo/Demo/Controllers/Cells/TransactionCell.swift +++ b/Demo/Demo/Controllers/Cells/TransactionCell.swift @@ -41,6 +41,7 @@ class TransactionCell: UITableViewCell { Tx Index: Date: Amount: + Fee: Block: Confirmations: \(from.map { _ in "From:" }.joined(separator: "\n")) @@ -52,6 +53,7 @@ class TransactionCell: UITableViewCell { \(transaction.transactionIndex) \(TransactionCell.dateFormatter.string(from: Date(timeIntervalSince1970: transaction.timestamp))) \(transaction.amount) \(coinCode) + \(transaction.fee?.description ?? "") \(coinCode) \(transaction.blockHeight.map { "# \($0)" } ?? "n/a") \(confirmations) \(from.joined(separator: "\n")) diff --git a/Demo/Demo/Core/TransactionRecord.swift b/Demo/Demo/Core/TransactionRecord.swift index fe683b95..a385ae5c 100644 --- a/Demo/Demo/Core/TransactionRecord.swift +++ b/Demo/Demo/Core/TransactionRecord.swift @@ -5,6 +5,7 @@ struct TransactionRecord { let transactionIndex: Int let amount: Decimal + let fee: Decimal? let timestamp: Double let from: [TransactionAddress] From ea36be9f72a9ea88305f9e2402988c33aff4ca1f Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 23 Aug 2019 12:14:15 +0600 Subject: [PATCH 018/234] Update checkpoints for all blockchains --- .../BitcoinCashKit/Network/MainNet.swift | 16 +++++++-------- .../BitcoinCashKit/Network/TestNet.swift | 16 +++++++-------- BitcoinKit/BitcoinKit/Network/MainNet.swift | 16 +++++++-------- BitcoinKit/BitcoinKit/Network/TestNet.swift | 16 +++++++-------- DashKit/DashKit/Network/MainNet.swift | 16 +++++++-------- DashKit/DashKit/Network/TestNet.swift | 14 ++++++------- Demo/Demo/Configuration.swift | 1 - Gemfile.lock | 20 +++++++++---------- Podfile.lock | 2 +- 9 files changed, 58 insertions(+), 59 deletions(-) diff --git a/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift b/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift index ecfd3764..f97cd1ac 100644 --- a/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift +++ b/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift @@ -38,15 +38,15 @@ class MainNet: INetwork { var lastCheckpointBlock: Block { return Block( withHeader: BlockHeader( - version: 0x2000e000, - headerHash: "00000000000000000040f26002e04126dc84700d6f82c0785efab2293080fe68".reversedData!, - previousBlockHeaderHash: "000000000000000002a1f5acfab47e5e1afcac9f50eb9b7c875e6c736d099763".reversedData!, - merkleRoot: "e6a8e517f708d294f426895c255cfd0a443d7f55a768b04398eadde0c516027c".reversedData!, - timestamp: 1559650598, - bits: 0x1803769a, - nonce: 0xed7bb8ff + version: 536870912, + headerHash: "00000000000000000376c7174b3d07ca1797dbbda4247e0df02147a8748489e8".reversedData!, + previousBlockHeaderHash: "000000000000000000553bfbe09e381b8bf90aaf5918aec2a9ca04cbb380de4c".reversedData!, + merkleRoot: "eddba5abfaf472c82adfdb0585eb672fbcb142b4ca485d00e76afd9e71184991".reversedData!, + timestamp: 1566454348, + bits: 0x18039165, + nonce: 2279816485 ), - height: 585504) + height: 596855) } } diff --git a/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift b/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift index 287e5822..305c1198 100644 --- a/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift +++ b/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift @@ -38,15 +38,15 @@ class TestNet: INetwork { var lastCheckpointBlock: Block { return Block( withHeader: BlockHeader( - version: 0x20000000, - headerHash: "00000000000001c4a2ebbed0841005d527c5177f323cd3df5d9f70463d9c28c7".reversedData!, - previousBlockHeaderHash: "000000000000046bf1879dc49620b0b12d4faaeda6f0ee033fc2cb86382ce571".reversedData!, - merkleRoot: "a9e1f20ec48ce7fae824c0dd76b4c4ec354d623e0ad2fac7b66a06984c0c0f81".reversedData!, - timestamp: 1562665562, - bits: 0x1a0509d6, - nonce: 437871866 + version: 541065216, + headerHash: "000000000000036ab860e5f4fabd32910018cba8dcac7388a9fe39696a8c44e7".reversedData!, + previousBlockHeaderHash: "000000000000030f449112975b4b6e354d97a5c518289a59f6b56549ff3368bd".reversedData!, + merkleRoot: "ad38ccce41340b04a0f56dbb8336a79bf7bcc081e6f2fc0547e70944e5f6cda5".reversedData!, + timestamp: 1566231970, + bits: 0x1a050d88, + nonce: 2919755716 ), - height: 1313735) + height: 1322849) } } diff --git a/BitcoinKit/BitcoinKit/Network/MainNet.swift b/BitcoinKit/BitcoinKit/Network/MainNet.swift index 40696e4d..13efad3b 100644 --- a/BitcoinKit/BitcoinKit/Network/MainNet.swift +++ b/BitcoinKit/BitcoinKit/Network/MainNet.swift @@ -41,15 +41,15 @@ class MainNet: INetwork { var lastCheckpointBlock: Block { return Block( withHeader: BlockHeader( - version: 0x20000000, - headerHash: "00000000000000000001791f463d849ce5363d751c91f7d3cd2ff18981ae221d".reversedData!, - previousBlockHeaderHash: "0000000000000000000485ab94f5ea60203aacfc9740b3e42700d7e7012f76d7".reversedData!, - merkleRoot: "2e76c50d3dcecc46264b7ff8e653d5c9f06680f4d88f5b239d58a531a3c12279".reversedData!, - timestamp: 1559256184, - bits: 0x1725bb76, - nonce: 0x423310ae + version: 549453824, + headerHash: "0000000000000000000bab9600a8e7593e2b13ea061c88f1c107a282ee75830b".reversedData!, + previousBlockHeaderHash: "00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff".reversedData!, + merkleRoot: "00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff".reversedData!, + timestamp: 1566161382, + bits: 387687377, + nonce: 5141340 ), - height: 578592) + height: 590688) } } diff --git a/BitcoinKit/BitcoinKit/Network/TestNet.swift b/BitcoinKit/BitcoinKit/Network/TestNet.swift index 7c15121c..5763d326 100644 --- a/BitcoinKit/BitcoinKit/Network/TestNet.swift +++ b/BitcoinKit/BitcoinKit/Network/TestNet.swift @@ -41,15 +41,15 @@ class TestNet: INetwork { var lastCheckpointBlock: Block { return Block( withHeader: BlockHeader( - version: 0x20400000, - headerHash: "000000000000608edb398b7969795bc0681c1fa280234f44cb2c506f853ae41e".reversedData!, - previousBlockHeaderHash: "000000000001386a3904f0ba4f25dc7ace09b67a6fe8977e7aecc55813fa9ac5".reversedData!, - merkleRoot: "c75984136598aaec589cbe0f61b1caf6c1eb69614c412861b474aedce5d71b7a".reversedData!, - timestamp: 1560904860, - bits: 0x1b008690, - nonce: 0xcf731739 + version: 1073676288, + headerHash: "00000000000001d6d3fcee88c80ac371d82b370f0158cc2737bebad6d13ff47b".reversedData!, + previousBlockHeaderHash: "00000000000000525de83fba2439549ef0ed78d6d08516a0513abb972b0fca95".reversedData!, + merkleRoot: "1c2f9a7885b2334096fbc1cd1373a1ac6d42b3eacee91d32758bb6f9f5018670".reversedData!, + timestamp: 1565789713, + bits: 436336433, + nonce: 1552477045 ), - height: 1560384) + height: 1574496) } } diff --git a/DashKit/DashKit/Network/MainNet.swift b/DashKit/DashKit/Network/MainNet.swift index 634116c3..6c7eb76d 100644 --- a/DashKit/DashKit/Network/MainNet.swift +++ b/DashKit/DashKit/Network/MainNet.swift @@ -41,15 +41,15 @@ class MainNet: INetwork { var lastCheckpointBlock: Block { return Block( withHeader: BlockHeader( - version: 536870928, - headerHash: "00000000000000011ce58a8bb55333277640b015e97689f9277d582a4c1f9999".reversedData!, - previousBlockHeaderHash: "000000000000000fcbac491b68a0774d1b9f82edeae8742eb492815e8fa76ca5".reversedData!, - merkleRoot: "91e15e6045c20d06abc41eb5feb17813ccc723f6f018ca8fd01485e8837bc761".reversedData!, - timestamp: 1559624664, - bits: 0x191a414a, - nonce: 838341360 + version: 536870912, + headerHash: "0000000000000007025cba534229ad1aea320e71396c81a567ee73d1d4d08dbd".reversedData!, + previousBlockHeaderHash: "000000000000000a80683bb332ddb2d29d2404addd6b84ba4ec574d3347726c6".reversedData!, + merkleRoot: "f9f9916a421d732ac78661fad94f8b605c57cd6653f533fd2460912950147e6b".reversedData!, + timestamp: 1566523466, + bits: 421091794, + nonce: 565120927 ), - height: 1081358) + height: 1125153) } } diff --git a/DashKit/DashKit/Network/TestNet.swift b/DashKit/DashKit/Network/TestNet.swift index d7f92f92..e5b045bc 100644 --- a/DashKit/DashKit/Network/TestNet.swift +++ b/DashKit/DashKit/Network/TestNet.swift @@ -41,14 +41,14 @@ class TestNet: INetwork { return Block( withHeader: BlockHeader( version: 536870912, - headerHash: "000000000cf1ebc27139b55559f2a0e312e566e1fd7dcac7ccf4e58d973794f5".reversedData!, - previousBlockHeaderHash: "000000001099bd5d3c903f2ab865b2c49c8bd29bddc9c990db43acd99617362c".reversedData!, - merkleRoot: "e58aeda83f17834baedb488c5276a37376c61c375848761f9a02c1981fe0d507".reversedData!, - timestamp: 1559651035, - bits: 0x1c0f8fa9, - nonce: 1118140024 + headerHash: "000000000d5cbf42cd0da22e4a2dc4aab275a5e8d5a8ab39025b1bd2d588ebfb".reversedData!, + previousBlockHeaderHash: "000000000e34afe7600a439c89dbbb90908a6bf2bc117dcf30e82c89a83ec280".reversedData!, + merkleRoot: "334130f690b9e58bfc61c767c89251913e87dd7a44a1eb30bdd0668d85313527".reversedData!, + timestamp: 1566527727, + bits: 0x1c0f2298, + nonce: 958408694 ), - height: 111324) + height: 160834) } } diff --git a/Demo/Demo/Configuration.swift b/Demo/Demo/Configuration.swift index 5e3889c3..94c6d4f2 100644 --- a/Demo/Demo/Configuration.swift +++ b/Demo/Demo/Configuration.swift @@ -5,7 +5,6 @@ class Configuration { let minLogLevel: Logger.Level = .error let testNet = true - let mainNet = false let defaultWords = [ "current force clump paper shrug extra zebra employ prefer upon mobile hire", "popular game latin harvest silly excess much valid elegant illness edge silk", diff --git a/Gemfile.lock b/Gemfile.lock index 7d2b2c8d..83ad8c33 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,18 +1,18 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.0) + CFPropertyList (3.0.1) activesupport (4.2.11.1) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) atomos (0.1.3) - claide (1.0.2) - cocoapods (1.7.1) + claide (1.0.3) + cocoapods (1.7.5) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.7.1) + cocoapods-core (= 1.7.5) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -22,13 +22,13 @@ GEM cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) - fourflusher (>= 2.2.0, < 3.0) + fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) molinillo (~> 0.6.6) nap (~> 1.0) ruby-macho (~> 1.4) - xcodeproj (>= 1.8.2, < 2.0) - cocoapods-core (1.7.1) + xcodeproj (>= 1.10.0, < 2.0) + cocoapods-core (1.7.5) activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) @@ -38,14 +38,14 @@ GEM nap cocoapods-search (1.0.0) cocoapods-stats (1.1.0) - cocoapods-trunk (1.3.1) + cocoapods-trunk (1.4.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.1.0) colored2 (3.1.2) concurrent-ruby (1.1.5) escape (0.0.4) - fourflusher (2.3.0) + fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) i18n (0.9.5) @@ -59,7 +59,7 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - xcodeproj (1.10.0) + xcodeproj (1.12.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/Podfile.lock b/Podfile.lock index ad4efa61..969fa973 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -65,4 +65,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b45f4c6b3e83ecd66793f2ae2479d2038bcfc8e9 -COCOAPODS: 1.7.4 +COCOAPODS: 1.7.5 From 5d5ade9d24d6cf1295c9e25d91a21714a2507dad Mon Sep 17 00:00:00 2001 From: Ermat Date: Fri, 30 Aug 2019 15:03:03 +0600 Subject: [PATCH 019/234] Fix Bitcoin MainNet checkpoint --- BitcoinKit/BitcoinKit/Network/MainNet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BitcoinKit/BitcoinKit/Network/MainNet.swift b/BitcoinKit/BitcoinKit/Network/MainNet.swift index 13efad3b..cebabc5a 100644 --- a/BitcoinKit/BitcoinKit/Network/MainNet.swift +++ b/BitcoinKit/BitcoinKit/Network/MainNet.swift @@ -44,7 +44,7 @@ class MainNet: INetwork { version: 549453824, headerHash: "0000000000000000000bab9600a8e7593e2b13ea061c88f1c107a282ee75830b".reversedData!, previousBlockHeaderHash: "00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff".reversedData!, - merkleRoot: "00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff".reversedData!, + merkleRoot: "e99b5d4feb6d70c056022b579c3ed70d249e66a1cd2fde6b06fa52dc68b9e480".reversedData!, timestamp: 1566161382, bits: 387687377, nonce: 5141340 From f7d4807f39158e489a3322b813ff1ab1be0deb2c Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 5 Sep 2019 14:37:34 +0600 Subject: [PATCH 020/234] Pass master key derivation protocol (BIP) to BitcoinKit --- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 8 ++++- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 3 +- Demo/Demo/Adapters/BitcoinAdapter.swift | 7 ++-- Demo/Demo/Adapters/BitcoinCashAdapter.swift | 2 +- Demo/Demo/Adapters/DashAdapter.swift | 2 +- Demo/Demo/Core/Manager.swift | 2 +- Podfile | 2 +- Podfile.lock | 34 +++++++++---------- 8 files changed, 34 insertions(+), 26 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 00dd4885..99af0abc 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -7,6 +7,7 @@ public class BitcoinCoreBuilder { // required parameters private var seed: Data? private var words: [String]? + private var bip: Purpose = .bip44 private var network: INetwork? private var paymentAddressParser: IPaymentAddressParser? private var addressSelector: IAddressSelector? @@ -36,6 +37,11 @@ public class BitcoinCoreBuilder { return self } + public func set(bip: Purpose) -> BitcoinCoreBuilder { + self.bip = bip + return self + } + public func set(network: INetwork) -> BitcoinCoreBuilder { self.network = network return self @@ -141,7 +147,7 @@ public class BitcoinCoreBuilder { let reachabilityManager = ReachabilityManager() - let hdWallet = HDWallet(seed: seed, coinType: network.coinType, xPrivKey: network.xPrivKey, xPubKey: network.xPubKey, gapLimit: 20) + let hdWallet = HDWallet(seed: seed, coinType: network.coinType, xPrivKey: network.xPrivKey, xPubKey: network.xPubKey, gapLimit: 20, purpose: bip) let networkMessageParser = NetworkMessageParser(magic: network.magic) let networkMessageSerializer = NetworkMessageSerializer(magic: network.magic) diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index 83d0350b..44deb164 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -18,7 +18,7 @@ public class BitcoinKit: AbstractKit { } } - public init(withWords words: [String], walletId: String, syncMode: BitcoinCore.SyncMode = .api, networkType: NetworkType = .mainNet, confirmationsThreshold: Int = 6, minLogLevel: Logger.Level = .verbose) throws { + public init(withWords words: [String], bip: Purpose, walletId: String, syncMode: BitcoinCore.SyncMode = .api, networkType: NetworkType = .mainNet, confirmationsThreshold: Int = 6, minLogLevel: Logger.Level = .verbose) throws { let network: INetwork let initialSyncApiUrl: String @@ -47,6 +47,7 @@ public class BitcoinKit: AbstractKit { .set(network: network) .set(initialSyncApi: initialSyncApi) .set(words: words) + .set(bip: bip) .set(paymentAddressParser: paymentAddressParser) .set(addressSelector: addressSelector) .set(addressKeyHashConverter: addressKeyHashConverter) diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index 858f505d..7d703797 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -1,21 +1,22 @@ import BitcoinKit import BitcoinCore +import HSHDWalletKit import RxSwift class BitcoinAdapter: BaseAdapter { let bitcoinKit: BitcoinKit override var changeAddressScriptType: ScriptType { return .p2pkh } - init(words: [String], testMode: Bool, syncMode: BitcoinCore.SyncMode) { + init(words: [String], bip: Purpose, testMode: Bool, syncMode: BitcoinCore.SyncMode) { let networkType: BitcoinKit.NetworkType = testMode ? .testNet : .mainNet - bitcoinKit = try! BitcoinKit(withWords: words, walletId: "walletId", syncMode: syncMode, networkType: networkType, minLogLevel: Configuration.shared.minLogLevel) + bitcoinKit = try! BitcoinKit(withWords: words, bip: bip, walletId: "walletId", syncMode: syncMode, networkType: networkType, minLogLevel: Configuration.shared.minLogLevel) super.init(name: "Bitcoin", coinCode: "BTC", abstractKit: bitcoinKit) bitcoinKit.delegate = self } class func clear() { - try? BitcoinKit.clear(exceptFor: ["walletId"]) + try? BitcoinKit.clear() } } diff --git a/Demo/Demo/Adapters/BitcoinCashAdapter.swift b/Demo/Demo/Adapters/BitcoinCashAdapter.swift index a350f821..ed0cb1f4 100644 --- a/Demo/Demo/Adapters/BitcoinCashAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinCashAdapter.swift @@ -14,7 +14,7 @@ class BitcoinCashAdapter: BaseAdapter { } class func clear() { - try? BitcoinCashKit.clear(exceptFor: ["walletId"]) + try? BitcoinCashKit.clear() } } diff --git a/Demo/Demo/Adapters/DashAdapter.swift b/Demo/Demo/Adapters/DashAdapter.swift index b0396402..d04cc01d 100644 --- a/Demo/Demo/Adapters/DashAdapter.swift +++ b/Demo/Demo/Adapters/DashAdapter.swift @@ -34,7 +34,7 @@ class DashAdapter: BaseAdapter { } class func clear() { - try? DashKit.clear(exceptFor: ["walletId"]) + try? DashKit.clear() } } diff --git a/Demo/Demo/Core/Manager.swift b/Demo/Demo/Core/Manager.swift index 34f847ec..b8cff329 100644 --- a/Demo/Demo/Core/Manager.swift +++ b/Demo/Demo/Core/Manager.swift @@ -38,7 +38,7 @@ class Manager { let configuration = Configuration.shared adapters = [ - BitcoinAdapter(words: words, testMode: configuration.testNet, syncMode: syncMode), + BitcoinAdapter(words: words, bip: .bip49, testMode: configuration.testNet, syncMode: syncMode), BitcoinCashAdapter(words: words, testMode: configuration.testNet, syncMode: syncMode), DashAdapter(words: words, testMode: configuration.testNet, syncMode: syncMode), ] diff --git a/Podfile b/Podfile index dc803462..abd46dc8 100644 --- a/Podfile +++ b/Podfile @@ -13,7 +13,7 @@ project 'DashKit/DashKit' def internal_pods pod 'HSCryptoKit', '~> 1.4' - pod 'HSHDWalletKit', '~> 1.1' + pod 'HSHDWalletKit', '~> 1.2' end def kit_pods diff --git a/Podfile.lock b/Podfile.lock index 969fa973..e7129256 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,15 +3,15 @@ PODS: - BigInt (4.0.0) - CryptoBLS.swift (1.1) - CryptoX11.swift (1.1) - - Cuckoo (1.0.5) - - GRDB.swift (4.0.1): - - GRDB.swift/standard (= 4.0.1) - - GRDB.swift/standard (4.0.1) - - HSCryptoKit (1.4.1) - - HSHDWalletKit (1.1): - - HSCryptoKit (~> 1.4) - - Nimble (8.0.1) - - ObjectMapper (3.4.2) + - Cuckoo (1.1.1) + - GRDB.swift (4.3.0): + - GRDB.swift/standard (= 4.3.0) + - GRDB.swift/standard (4.3.0) + - HSCryptoKit (1.5.0) + - HSHDWalletKit (1.2.0): + - HSCryptoKit (~> 1) + - Nimble (8.0.2) + - ObjectMapper (3.5.1) - Quick (2.1.0) - RxBlocking (5.0.0): - RxSwift (~> 5) @@ -25,7 +25,7 @@ DEPENDENCIES: - Cuckoo - GRDB.swift (~> 4.0) - HSCryptoKit (~> 1.4) - - HSHDWalletKit (~> 1.1) + - HSHDWalletKit (~> 1.2) - Nimble - ObjectMapper (~> 3.0) - Quick @@ -53,16 +53,16 @@ SPEC CHECKSUMS: BigInt: 2aad1a9942dc932ec8b84290d2c564a3d76f97ab CryptoBLS.swift: 89828d8e3013e45ce4f913d26008b91fc5d0f3a0 CryptoX11.swift: 8f0ca5fbfefbe49126220ce23b5de106928cf0f0 - Cuckoo: 56ceda7442986ad2aa0615f462b0e431253b97a5 - GRDB.swift: 106a830decf1d92a3fc63c6d6a2f6586f6187297 - HSCryptoKit: 639ab56381fa4c4b9da520e286c9f993b5b8b028 - HSHDWalletKit: 1d946bd24bd307cc494141f4e518d25db814f050 - Nimble: 45f786ae66faa9a709624227fae502db55a8bdd0 - ObjectMapper: 0d4402610f4e468903ae64629eec4784531e5c51 + Cuckoo: 6193c50e46403cc9417aa98752671a2a5ae6f3a6 + GRDB.swift: 083ec261ce573888d8ed2eae1b49bc0bf40d9e2c + HSCryptoKit: 514a3111fd37ad18b7261d4a55271daaba3b0eb9 + HSHDWalletKit: 6f5d5b1f9b1f950c948bdff13f5f52e5c5bcb7bb + Nimble: 622629381bda1dd5678162f21f1368cec7cbba60 + ObjectMapper: 70187b8941977c62ccfb423caf6b50be405cabf0 Quick: 4be43f6634acfa727dd106bdf3929ce125ffa79d RxBlocking: c67185d26498ea3cbe3e121917c3c16739e43123 RxSwift: 8b0671caa829a763bbce7271095859121cbd895f -PODFILE CHECKSUM: b45f4c6b3e83ecd66793f2ae2479d2038bcfc8e9 +PODFILE CHECKSUM: ef2010399b017fbc3b49c80f38b5b921edb0a5df COCOAPODS: 1.7.5 From 8a7f354543889fd24220145d1b2ebb588bdf8489 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 5 Sep 2019 14:46:01 +0600 Subject: [PATCH 021/234] Rename AddressManager to PublicKeyManager --- .../BitcoinCore.xcodeproj/project.pbxproj | 16 +++++++-------- .../BitcoinCore/Blocks/BlockSyncer.swift | 12 +++++------ .../BitcoinCore/Core/BitcoinCore.swift | 14 ++++++------- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 10 +++++----- BitcoinCore/BitcoinCore/Core/Protocols.swift | 2 +- .../Managers/InitialSync/InitialSyncer.swift | 8 ++++---- ...ssManager.swift => PublicKeyManager.swift} | 10 +++++----- .../Builder/TransactionBuilder.swift | 8 ++++---- .../Transactions/TransactionProcessor.swift | 8 ++++---- .../Transactions/TransactionSyncer.swift | 8 ++++---- .../Blocks/BlockSyncerTests.swift | 6 +++--- .../Core/DataProviderTests.swift | 4 ++-- .../Managers/InitialSyncerTests.swift | 4 ++-- ...ests.swift => PublicKeyManagerTests.swift} | 20 +++++++++---------- .../Builder/TransactionBuilderTests.swift | 8 ++++---- .../TransactionProcessorTests.swift | 4 ++-- .../Transactions/TransactionSyncerTests.swift | 4 ++-- 17 files changed, 73 insertions(+), 73 deletions(-) rename BitcoinCore/BitcoinCore/Managers/{AddressManager.swift => PublicKeyManager.swift} (94%) rename BitcoinCore/BitcoinCoreTests/Managers/{AddressManagerTests.swift => PublicKeyManagerTests.swift} (96%) diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index 5002faf7..9f6d378c 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -53,7 +53,7 @@ 2FA5D1F7E6DABFC12A0AD064 /* HDPrivateKeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DADEB0FD09138FD308A1 /* HDPrivateKeyTests.swift */; }; 2FA5D2239DF0C24C4CC6366B /* PeerHostManagerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D632A9EFB1F0CF2523DF /* PeerHostManagerDelegateTests.swift */; }; 2FA5D26905936BB701998750 /* GetBlockHashesTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D65667DA8BFD91E91C8B /* GetBlockHashesTask.swift */; }; - 2FA5D2E6BA34E1A932C9C16B /* AddressManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5A9656117A6FD41E814 /* AddressManager.swift */; }; + 2FA5D2E6BA34E1A932C9C16B /* PublicKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5A9656117A6FD41E814 /* PublicKeyManager.swift */; }; 2FA5D371584B554068CCAD8E /* IPeerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D99586B15F4477D695F7 /* IPeerTests.swift */; }; 2FA5D37E8B4A6D9B89410173 /* SendTransactionTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D2DAB8485F5F9E2E23B6 /* SendTransactionTaskTests.swift */; }; 2FA5D39FBC802B5EF03AD53A /* TransactionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D11405173CD80A6B9904 /* TransactionBuilder.swift */; }; @@ -69,7 +69,7 @@ 2FA5D571931CA46B417C18BA /* PeerGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D602DBCFC5121F7C245F /* PeerGroupTests.swift */; }; 2FA5D599E444F6847B6E9A4C /* Peer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5A744085AD28EFBB868 /* Peer.swift */; }; 2FA5D5FB8B7F7B1E65A156BF /* GetMerkleBlocksTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D2907674257196C4F586 /* GetMerkleBlocksTaskTests.swift */; }; - 2FA5D5FE71A528D363227585 /* AddressManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D05FCBFFE93D46F8C292 /* AddressManagerTests.swift */; }; + 2FA5D5FE71A528D363227585 /* PublicKeyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D05FCBFFE93D46F8C292 /* PublicKeyManagerTests.swift */; }; 2FA5D6129186D79E7C7DD57F /* IPeerTaskDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DFB5B9A13478ACBB61EB /* IPeerTaskDelegateTests.swift */; }; 2FA5D65869320A18AFB8CE98 /* BlockHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DB1F056B264F829BFDDD /* BlockHash.swift */; }; 2FA5D67384D58067F71CD6B2 /* InputSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF9995FA7DD7556893F7 /* InputSigner.swift */; }; @@ -266,7 +266,7 @@ 11B35FE0F5B2A705A5C42439 /* PeerAddressManagerState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAddressManagerState.swift; sourceTree = ""; }; 11B35FF2F94006C73B7773BC /* GetHeadersMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetHeadersMessage.swift; sourceTree = ""; }; 2FA5D05597A1DC332C67BDF7 /* Blockchain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blockchain.swift; sourceTree = ""; }; - 2FA5D05FCBFFE93D46F8C292 /* AddressManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressManagerTests.swift; sourceTree = ""; }; + 2FA5D05FCBFFE93D46F8C292 /* PublicKeyManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicKeyManagerTests.swift; sourceTree = ""; }; 2FA5D0A6A8C6C5A4E2AAFB74 /* BloomFilterManagerDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloomFilterManagerDelegateTests.swift; sourceTree = ""; }; 2FA5D0B8032597D7392B0637 /* TransactionProcessorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionProcessorTests.swift; sourceTree = ""; }; 2FA5D11405173CD80A6B9904 /* TransactionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionBuilder.swift; sourceTree = ""; }; @@ -284,7 +284,7 @@ 2FA5D4E64129FF62BED7ADE7 /* BloomFilterManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloomFilterManagerTests.swift; sourceTree = ""; }; 2FA5D50922572F91F966DD4A /* DataObjects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataObjects.swift; sourceTree = ""; }; 2FA5D5A744085AD28EFBB868 /* Peer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Peer.swift; sourceTree = ""; }; - 2FA5D5A9656117A6FD41E814 /* AddressManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressManager.swift; sourceTree = ""; }; + 2FA5D5A9656117A6FD41E814 /* PublicKeyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicKeyManager.swift; sourceTree = ""; }; 2FA5D5C9BB0E185AF72B9174 /* MerkleBlock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MerkleBlock.swift; sourceTree = ""; }; 2FA5D602DBCFC5121F7C245F /* PeerGroupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerGroupTests.swift; sourceTree = ""; }; 2FA5D632A9EFB1F0CF2523DF /* PeerHostManagerDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerHostManagerDelegateTests.swift; sourceTree = ""; }; @@ -500,7 +500,7 @@ isa = PBXGroup; children = ( 58AAA656C4EDCE966289B71F /* InitialSync */, - 2FA5D05FCBFFE93D46F8C292 /* AddressManagerTests.swift */, + 2FA5D05FCBFFE93D46F8C292 /* PublicKeyManagerTests.swift */, 2FA5D4E64129FF62BED7ADE7 /* BloomFilterManagerTests.swift */, 11B35F9A349EC7EF0DD5BA96 /* InitialSyncerTests.swift */, 2FA5DFBF60F56D8A2FC86ED2 /* StateManagerTests.swift */, @@ -703,7 +703,7 @@ isa = PBXGroup; children = ( 58AAAEC3E09DB4796FF009DE /* InitialSync */, - 2FA5D5A9656117A6FD41E814 /* AddressManager.swift */, + 2FA5D5A9656117A6FD41E814 /* PublicKeyManager.swift */, 11B3597DE3169FD250A982D5 /* ApiManager.swift */, 58AAA55E49708A950F951E2D /* BitcoinAddressSelector.swift */, 2FA5DE7E06652FCED7B288E7 /* BloomFilterManager.swift */, @@ -1258,7 +1258,7 @@ 58AAA68C0D710ED3FCDE76DE /* ScriptTests.swift in Sources */, 11B3566FF1557B650F44D89E /* InitialSyncerTests.swift in Sources */, 58AAA6D586BB4E3E39C15E36 /* TransactionSizeCalculatorTests.swift in Sources */, - 2FA5D5FE71A528D363227585 /* AddressManagerTests.swift in Sources */, + 2FA5D5FE71A528D363227585 /* PublicKeyManagerTests.swift in Sources */, 2FA5D1F7E6DABFC12A0AD064 /* HDPrivateKeyTests.swift in Sources */, 58AAAD9224BAB440C1B6E033 /* BitsValidatorTests.swift in Sources */, 58AAA2EC7A7825B8E4408AA7 /* ProofOfWorkValidatorTests.swift in Sources */, @@ -1363,7 +1363,7 @@ 58AAA1E87B62806CD5891D43 /* Chunk.swift in Sources */, 11B3580E3ED73322D657644F /* StateManager.swift in Sources */, 58AAA6A3D98BB1CF6360F639 /* TransactionSizeCalculator.swift in Sources */, - 2FA5D2E6BA34E1A932C9C16B /* AddressManager.swift in Sources */, + 2FA5D2E6BA34E1A932C9C16B /* PublicKeyManager.swift in Sources */, 2FA5D1EE5C8EF298CD8A6733 /* TransactionSerializer.swift in Sources */, 2FA5DDD47BF94625B499ECC2 /* TransactionOutputSerializer.swift in Sources */, 2FA5D56E3AE0E2E9817FC370 /* TransactionInputSerializer.swift in Sources */, diff --git a/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift b/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift index d1f3d415..9a3502e9 100644 --- a/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift +++ b/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift @@ -8,7 +8,7 @@ class BlockSyncer { private let factory: IFactory private let transactionProcessor: ITransactionProcessor private let blockchain: IBlockchain - private let addressManager: IAddressManager + private let publicKeyManager: IPublicKeyManager private let bloomFilterManager: IBloomFilterManager private let hashCheckpointThreshold: Int @@ -17,7 +17,7 @@ class BlockSyncer { private let logger: Logger? init(storage: IStorage, checkpointBlock: Block, factory: IFactory, listener: ISyncStateListener, transactionProcessor: ITransactionProcessor, - blockchain: IBlockchain, addressManager: IAddressManager, bloomFilterManager: IBloomFilterManager, + blockchain: IBlockchain, publicKeyManager: IPublicKeyManager, bloomFilterManager: IBloomFilterManager, hashCheckpointThreshold: Int, logger: Logger?, state: BlockSyncerState ) { self.storage = storage @@ -25,7 +25,7 @@ class BlockSyncer { self.factory = factory self.transactionProcessor = transactionProcessor self.blockchain = blockchain - self.addressManager = addressManager + self.publicKeyManager = publicKeyManager self.bloomFilterManager = bloomFilterManager self.hashCheckpointThreshold = hashCheckpointThreshold self.listener = listener @@ -57,7 +57,7 @@ class BlockSyncer { } private func handlePartialBlocks() throws { - try addressManager.fillGap() + try publicKeyManager.fillGap() bloomFilterManager.regenerateBloomFilter() state.iteration(hasPartialBlocks: false) } @@ -169,11 +169,11 @@ extension BlockSyncer: IBlockSyncer { extension BlockSyncer { public static func instance(storage: IStorage, checkpointBlock: Block, factory: IFactory, listener: ISyncStateListener, - transactionProcessor: ITransactionProcessor, blockchain: IBlockchain, addressManager: IAddressManager, bloomFilterManager: IBloomFilterManager, + transactionProcessor: ITransactionProcessor, blockchain: IBlockchain, publicKeyManager: IPublicKeyManager, bloomFilterManager: IBloomFilterManager, hashCheckpointThreshold: Int = 100, logger: Logger? = nil, state: BlockSyncerState = BlockSyncerState()) -> BlockSyncer { let syncer = BlockSyncer(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: listener, transactionProcessor: transactionProcessor, - blockchain: blockchain, addressManager: addressManager, bloomFilterManager: bloomFilterManager, + blockchain: blockchain, publicKeyManager: publicKeyManager, bloomFilterManager: bloomFilterManager, hashCheckpointThreshold: hashCheckpointThreshold, logger: logger, state: state) listener.initialBestBlockHeightUpdated(height: syncer.localDownloadedBestBlockHeight) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 0db7bb9d..13a91155 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -13,7 +13,7 @@ public class BitcoinCore { private let storage: IStorage private let cache: OutputsCache private var dataProvider: IDataProvider - private let addressManager: IAddressManager + private let publicKeyManager: IPublicKeyManager private let watchedTransactionManager: IWatchedTransactionManager private let addressConverter: AddressConverterChain private let unspentOutputSelector: UnspentOutputSelectorChain @@ -65,7 +65,7 @@ public class BitcoinCore { } func publicKey(byPath path: String) throws -> PublicKey { - return try addressManager.publicKey(byPath: path) + return try publicKeyManager.publicKey(byPath: path) } public func prepend(scriptBuilder: IScriptBuilder) { @@ -88,7 +88,7 @@ public class BitcoinCore { init(storage: IStorage, cache: OutputsCache, dataProvider: IDataProvider, peerGroup: IPeerGroup, initialBlockDownload: IInitialBlockDownload, bloomFilterLoader: BloomFilterLoader, syncedReadyPeerManager: ISyncedReadyPeerManager, transactionSyncer: ITransactionSyncer, - blockValidatorChain: BlockValidatorChain, addressManager: IAddressManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, + blockValidatorChain: BlockValidatorChain, addressManager: IPublicKeyManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, scriptBuilder: ScriptBuilderChain, transactionBuilder: ITransactionBuilder, transactionCreator: ITransactionCreator, paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager) { @@ -101,7 +101,7 @@ public class BitcoinCore { self.syncedReadyPeerManager = syncedReadyPeerManager self.transactionSyncer = transactionSyncer self.blockValidatorChain = blockValidatorChain - self.addressManager = addressManager + self.publicKeyManager = addressManager self.addressConverter = addressConverter self.unspentOutputSelector = unspentOutputSelector self.kitStateProvider = kitStateProvider @@ -176,15 +176,15 @@ extension BitcoinCore { } public func receiveAddress(for type: ScriptType) -> String { - return (try? addressManager.receiveAddress(for: type)) ?? "" + return (try? publicKeyManager.receiveAddress(for: type)) ?? "" } public func changePublicKey() throws -> PublicKey { - return try addressManager.changePublicKey() + return try publicKeyManager.changePublicKey() } public func receivePublicKey() throws -> PublicKey { - return try addressManager.receivePublicKey() + return try publicKeyManager.receivePublicKey() } func watch(transaction: BitcoinCore.TransactionFilter, delegate: IWatchedTransactionDelegate) { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 99af0abc..c0d2f632 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -158,7 +158,7 @@ public class BitcoinCoreBuilder { let factory = Factory(network: network, networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer) - let addressManager = AddressManager.instance(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter, addressKeyHashConverter: addressKeyHashConverter) + let addressManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter, addressKeyHashConverter: addressKeyHashConverter) let myOutputsCache = OutputsCache.instance(storage: storage) let scriptConverter = ScriptConverter() @@ -180,7 +180,7 @@ public class BitcoinCoreBuilder { let peerManager = PeerManager() let unspentOutputSelector = UnspentOutputSelectorChain() - let transactionSyncer = TransactionSyncer(storage: storage, processor: transactionProcessor, addressManager: addressManager, bloomFilterManager: bloomFilterManager) + let transactionSyncer = TransactionSyncer(storage: storage, processor: transactionProcessor, publicKeyManager: addressManager, bloomFilterManager: bloomFilterManager) let mempoolTransactions = MempoolTransactions(transactionSyncer: transactionSyncer) let blockHashFetcher = BlockHashFetcher(addressSelector: addressSelector, apiManager: initialSyncApi, addressConverter: addressConverter, helper: BlockHashFetcherHelper()) @@ -188,7 +188,7 @@ public class BitcoinCoreBuilder { let stateManager = StateManager(storage: storage, restoreFromApi: network.syncableFromApi && syncMode == BitcoinCore.SyncMode.api) - let initialSyncer = InitialSyncer(storage: storage, listener: kitStateProvider, stateManager: stateManager, blockDiscovery: blockDiscovery, addressManager: addressManager, logger: logger) + let initialSyncer = InitialSyncer(storage: storage, listener: kitStateProvider, stateManager: stateManager, blockDiscovery: blockDiscovery, publicKeyManager: addressManager, logger: logger) let bloomFilterLoader = BloomFilterLoader(bloomFilterManager: bloomFilterManager, peerManager: peerManager) let watchedTransactionManager = WatchedTransactionManager() @@ -196,7 +196,7 @@ public class BitcoinCoreBuilder { let blockValidatorChain = BlockValidatorChain(proofOfWorkValidator: ProofOfWorkValidator(difficultyEncoder: DifficultyEncoder())) let blockchain = Blockchain(storage: storage, blockValidator: blockValidatorChain, factory: factory, listener: dataProvider) let checkpointBlock = BlockSyncer.checkpointBlock(network: network, syncMode: syncMode, storage: storage) - let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, addressManager: addressManager, bloomFilterManager: bloomFilterManager, logger: logger) + let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, publicKeyManager: addressManager, bloomFilterManager: bloomFilterManager, logger: logger) let initialBlockDownload = InitialBlockDownload(blockSyncer: blockSyncer, peerManager: peerManager, merkleBlockValidator: merkleBlockValidator, syncStateListener: kitStateProvider, logger: logger) let peerGroup = PeerGroup(factory: factory, reachabilityManager: reachabilityManager, @@ -207,7 +207,7 @@ public class BitcoinCoreBuilder { let inputSigner = InputSigner(hdWallet: hdWallet, network: network) let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() - let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, addressManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, + let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, publicKeyManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, transactionSizeCalculator: transactionSizeCalculator, addressKeyHashConverter: addressKeyHashConverter) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 62b88a7c..c0a34f16 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -133,7 +133,7 @@ public protocol IAddressSelector { func getAddressVariants(addressConverter: IAddressConverter, publicKey: PublicKey) -> [String] } -public protocol IAddressManager { +public protocol IPublicKeyManager { func changePublicKey() throws -> PublicKey func receivePublicKey() throws -> PublicKey func receiveAddress(for type: ScriptType) throws -> String diff --git a/BitcoinCore/BitcoinCore/Managers/InitialSync/InitialSyncer.swift b/BitcoinCore/BitcoinCore/Managers/InitialSync/InitialSyncer.swift index 24ff6f22..2d8dde2f 100644 --- a/BitcoinCore/BitcoinCore/Managers/InitialSync/InitialSyncer.swift +++ b/BitcoinCore/BitcoinCore/Managers/InitialSync/InitialSyncer.swift @@ -10,19 +10,19 @@ class InitialSyncer { private let listener: ISyncStateListener private var stateManager: IStateManager private let blockDiscovery: IBlockDiscovery - private let addressManager: IAddressManager + private let publicKeyManager: IPublicKeyManager private let logger: Logger? private let async: Bool private var restoring = false - init(storage: IStorage, listener: ISyncStateListener, stateManager: IStateManager, blockDiscovery: IBlockDiscovery, addressManager: IAddressManager, async: Bool = true, logger: Logger? = nil) { + init(storage: IStorage, listener: ISyncStateListener, stateManager: IStateManager, blockDiscovery: IBlockDiscovery, publicKeyManager: IPublicKeyManager, async: Bool = true, logger: Logger? = nil) { self.storage = storage self.listener = listener self.stateManager = stateManager self.blockDiscovery = blockDiscovery - self.addressManager = addressManager + self.publicKeyManager = publicKeyManager self.logger = logger self.async = async @@ -58,7 +58,7 @@ class InitialSyncer { private func handle(forAccount account: Int, keys: [PublicKey], blockHashes: [BlockHash]) { do { logger?.debug("Account \(account) has \(keys.count) keys and \(blockHashes.count) blocks") - try addressManager.addKeys(keys: keys) + try publicKeyManager.addKeys(keys: keys) // If gap shift is found if blockHashes.isEmpty { diff --git a/BitcoinCore/BitcoinCore/Managers/AddressManager.swift b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift similarity index 94% rename from BitcoinCore/BitcoinCore/Managers/AddressManager.swift rename to BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift index bdf0e5dd..3805515c 100644 --- a/BitcoinCore/BitcoinCore/Managers/AddressManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift @@ -1,6 +1,6 @@ import HSHDWalletKit -class AddressManager { +class PublicKeyManager { enum AddressManagerError: Error { case noUnusedPublicKey @@ -57,7 +57,7 @@ class AddressManager { } } -extension AddressManager: IAddressManager { +extension PublicKeyManager: IPublicKeyManager { func changePublicKey() throws -> PublicKey { return try publicKey(external: false) @@ -133,10 +133,10 @@ extension AddressManager: IAddressManager { } } -extension AddressManager { +extension PublicKeyManager { - public static func instance(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter, addressKeyHashConverter: IAddressKeyHashConverter? = nil) -> AddressManager { - let addressManager = AddressManager(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter, addressKeyHashConverter: addressKeyHashConverter) + public static func instance(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter, addressKeyHashConverter: IAddressKeyHashConverter? = nil) -> PublicKeyManager { + let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter, addressKeyHashConverter: addressKeyHashConverter) try? addressManager.fillGap() return addressManager } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 8f851b0e..29e2544b 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -8,7 +8,7 @@ class TransactionBuilder { private let unspentOutputSelector: IUnspentOutputSelector private let unspentOutputProvider: IUnspentOutputProvider - private let addressManager: IAddressManager + private let publicKeyManager: IPublicKeyManager private let addressConverter: IAddressConverter private let addressKeyHashConverter: IAddressKeyHashConverter? private let inputSigner: IInputSigner @@ -17,12 +17,12 @@ class TransactionBuilder { var scriptBuilder: IScriptBuilder - init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, addressManager: IAddressManager, addressConverter: IAddressConverter, + init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, publicKeyManager: IPublicKeyManager, addressConverter: IAddressConverter, inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator, addressKeyHashConverter: IAddressKeyHashConverter? = nil) { self.unspentOutputSelector = unspentOutputSelector self.unspentOutputProvider = unspentOutputProvider - self.addressManager = addressManager + self.publicKeyManager = publicKeyManager self.addressConverter = addressConverter self.addressKeyHashConverter = addressKeyHashConverter self.inputSigner = inputSigner @@ -69,7 +69,7 @@ extension TransactionBuilder: ITransactionBuilder { } func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String, changeScriptType: ScriptType) throws -> FullTransaction { - guard let changePubKey = try? addressManager.changePublicKey() else { + guard let changePubKey = try? publicKeyManager.changePublicKey() else { throw BuildError.noChangeAddress } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift index 82782954..bc135a45 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift @@ -6,7 +6,7 @@ class TransactionProcessor { private let inputExtractor: ITransactionExtractor private let outputAddressExtractor: ITransactionOutputAddressExtractor private let outputsCache: IOutputsCache - private let addressManager: IAddressManager + private let publicKeyManager: IPublicKeyManager weak var listener: IBlockchainDataListener? weak var transactionListener: ITransactionListener? @@ -14,7 +14,7 @@ class TransactionProcessor { private let dateGenerator: () -> Date private let queue: DispatchQueue - init(storage: IStorage, outputExtractor: ITransactionExtractor, inputExtractor: ITransactionExtractor, outputsCache: IOutputsCache, outputAddressExtractor: ITransactionOutputAddressExtractor, addressManager: IAddressManager, listener: IBlockchainDataListener? = nil, + init(storage: IStorage, outputExtractor: ITransactionExtractor, inputExtractor: ITransactionExtractor, outputsCache: IOutputsCache, outputAddressExtractor: ITransactionOutputAddressExtractor, addressManager: IPublicKeyManager, listener: IBlockchainDataListener? = nil, dateGenerator: @escaping () -> Date = Date.init, queue: DispatchQueue = DispatchQueue(label: "Transactions", qos: .background )) { self.storage = storage @@ -22,7 +22,7 @@ class TransactionProcessor { self.inputExtractor = inputExtractor self.outputAddressExtractor = outputAddressExtractor self.outputsCache = outputsCache - self.addressManager = addressManager + self.publicKeyManager = addressManager self.listener = listener self.dateGenerator = dateGenerator self.queue = queue @@ -97,7 +97,7 @@ extension TransactionProcessor: ITransactionProcessor { inserted.append(transaction.header) if !skipCheckBloomFilter { - needToUpdateBloomFilter = needToUpdateBloomFilter || self.addressManager.gapShifts() || self.expiresBloomFilter(outputs: transaction.outputs) + needToUpdateBloomFilter = needToUpdateBloomFilter || self.publicKeyManager.gapShifts() || self.expiresBloomFilter(outputs: transaction.outputs) } } } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift index 211aea5c..5627ebd0 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift @@ -3,17 +3,17 @@ import Foundation public class TransactionSyncer { private let storage: IStorage private let transactionProcessor: ITransactionProcessor - private let addressManager: IAddressManager + private let publicKeyManager: IPublicKeyManager private let bloomFilterManager: IBloomFilterManager private let maxRetriesCount: Int private let retriesPeriod: Double // seconds private let totalRetriesPeriod: Double // seconds - init(storage: IStorage, processor: ITransactionProcessor, addressManager: IAddressManager, bloomFilterManager: IBloomFilterManager, + init(storage: IStorage, processor: ITransactionProcessor, publicKeyManager: IPublicKeyManager, bloomFilterManager: IBloomFilterManager, maxRetriesCount: Int = 3, retriesPeriod: Double = 60, totalRetriesPeriod: Double = 60 * 60 * 24) { self.storage = storage self.transactionProcessor = processor - self.addressManager = addressManager + self.publicKeyManager = publicKeyManager self.bloomFilterManager = bloomFilterManager self.maxRetriesCount = maxRetriesCount self.retriesPeriod = retriesPeriod @@ -67,7 +67,7 @@ extension TransactionSyncer: ITransactionSyncer { } if needToUpdateBloomFilter { - try? addressManager.fillGap() + try? publicKeyManager.fillGap() bloomFilterManager.regenerateBloomFilter() } } diff --git a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift index 2fc11aeb..0454f120 100644 --- a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift @@ -11,7 +11,7 @@ class BlockSyncerTests: QuickSpec { let mockListener = MockISyncStateListener() let mockTransactionProcessor = MockITransactionProcessor() let mockBlockchain = MockIBlockchain() - let mockAddressManager = MockIAddressManager() + let mockAddressManager = MockIPublicKeyManager() let mockBloomFilterManager = MockIBloomFilterManager() let mockState = MockBlockSyncerState() @@ -59,7 +59,7 @@ class BlockSyncerTests: QuickSpec { let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, addressManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100) + blockchain: mockBlockchain, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100) verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) verifyNoMoreInteractions(mockListener) @@ -158,7 +158,7 @@ class BlockSyncerTests: QuickSpec { context("instance methods") { beforeEach { syncer = BlockSyncer(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, addressManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, + blockchain: mockBlockchain, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100, logger: nil, state: mockState) } diff --git a/BitcoinCore/BitcoinCoreTests/Core/DataProviderTests.swift b/BitcoinCore/BitcoinCoreTests/Core/DataProviderTests.swift index 1ba302b9..39804b05 100644 --- a/BitcoinCore/BitcoinCoreTests/Core/DataProviderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Core/DataProviderTests.swift @@ -5,7 +5,7 @@ // //class DataProviderTests: XCTestCase { // private var mockStorage: MockIStorage! -// private var mockAddressManager: MockIAddressManager! +// private var mockAddressManager: MockIPublicKeyManager! // private var mockAddressConverter: MockIAddressConverter! // private var mockPaymentAddressParser: MockIPaymentAddressParser! // private var mockUnspentOutputProvider: MockIUnspentOutputProvider! @@ -20,7 +20,7 @@ // super.setUp() // // mockStorage = MockIStorage() -// mockAddressManager = MockIAddressManager() +// mockAddressManager = MockIPublicKeyManager() // mockAddressConverter = MockIAddressConverter() // mockPaymentAddressParser = MockIPaymentAddressParser() // mockUnspentOutputProvider = MockIUnspentOutputProvider() diff --git a/BitcoinCore/BitcoinCoreTests/Managers/InitialSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/InitialSyncerTests.swift index 6b28ae25..055f4737 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/InitialSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/InitialSyncerTests.swift @@ -11,7 +11,7 @@ class InitialSyncerTests: QuickSpec { let mockListener = MockISyncStateListener() let mockStateManager = MockIStateManager() let mockBlockDiscovery = MockIBlockDiscovery() - let mockAddressManager = MockIAddressManager() + let mockAddressManager = MockIPublicKeyManager() let mockDelegate = MockIInitialSyncerDelegate() var syncer: InitialSyncer! @@ -30,7 +30,7 @@ class InitialSyncerTests: QuickSpec { syncer = InitialSyncer( storage: mockStorage, listener: mockListener, stateManager: mockStateManager, blockDiscovery: mockBlockDiscovery, - addressManager: mockAddressManager, async: false + publicKeyManager: mockAddressManager, async: false ) syncer.delegate = mockDelegate diff --git a/BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift similarity index 96% rename from BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift rename to BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift index 6a5b5775..8d053362 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/AddressManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift @@ -3,14 +3,14 @@ import Cuckoo import HSHDWalletKit @testable import BitcoinCore -class AddressManagerTests: XCTestCase { +class PublicKeyManagerTests: XCTestCase { private var mockStorage: MockIStorage! private var mockHDWallet: MockIHDWallet! private var mockAddressConverter: MockIAddressConverter! private var hdWallet: IHDWallet! - private var manager: AddressManager! + private var manager: PublicKeyManager! override func setUp() { super.setUp() @@ -24,7 +24,7 @@ class AddressManagerTests: XCTestCase { } hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) - manager = AddressManager(storage: mockStorage, hdWallet: mockHDWallet, addressConverter: mockAddressConverter) + manager = PublicKeyManager(storage: mockStorage, hdWallet: mockHDWallet, addressConverter: mockAddressConverter) } override func tearDown() { @@ -66,8 +66,8 @@ class AddressManagerTests: XCTestCase { do { let _ = try manager.changePublicKey() XCTFail("Should throw exception") - } catch let error as AddressManager.AddressManagerError { - XCTAssertEqual(error, AddressManager.AddressManagerError.noUnusedPublicKey) + } catch let error as PublicKeyManager.AddressManagerError { + XCTAssertEqual(error, PublicKeyManager.AddressManagerError.noUnusedPublicKey) } catch { XCTFail("Unexpected exception thrown") } @@ -101,8 +101,8 @@ class AddressManagerTests: XCTestCase { do { let _ = try manager.receivePublicKey() XCTFail("Should throw exception") - } catch let error as AddressManager.AddressManagerError { - XCTAssertEqual(error, AddressManager.AddressManagerError.noUnusedPublicKey) + } catch let error as PublicKeyManager.AddressManagerError { + XCTAssertEqual(error, PublicKeyManager.AddressManagerError.noUnusedPublicKey) } catch { XCTFail("Unexpected exception thrown") } @@ -141,8 +141,8 @@ class AddressManagerTests: XCTestCase { do { let _ = try manager.receiveAddress(for: .p2pkh) XCTFail("Should throw exception") - } catch let error as AddressManager.AddressManagerError { - XCTAssertEqual(error, AddressManager.AddressManagerError.noUnusedPublicKey) + } catch let error as PublicKeyManager.AddressManagerError { + XCTAssertEqual(error, PublicKeyManager.AddressManagerError.noUnusedPublicKey) } catch { XCTFail("Unexpected exception thrown") } @@ -410,7 +410,7 @@ class AddressManagerTests: XCTestCase { do { _ = try manager.publicKey(byPath: "0/0") XCTFail("Expected exception") - } catch let error as AddressManager.AddressManagerError { + } catch let error as PublicKeyManager.AddressManagerError { XCTAssertEqual(error, .invalidPath) } catch { XCTFail("Unexpected exception") diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index a88d0015..b9b45824 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -34,7 +34,7 @@ class TransactionBuilderTests: XCTestCase { private var mockUnspentOutputSelector: MockIUnspentOutputSelector! private var mockUnspentOutputProvider: MockIUnspentOutputProvider! - private var mockAddressManager: MockIAddressManager! + private var mockAddressManager: MockIPublicKeyManager! private var mockAddressConverter: MockIAddressConverter! private var mockInputSigner: MockIInputSigner! private var mockScriptBuilder: MockIScriptBuilder! @@ -68,14 +68,14 @@ class TransactionBuilderTests: XCTestCase { mockUnspentOutputSelector = MockIUnspentOutputSelector() mockUnspentOutputProvider = MockIUnspentOutputProvider() - mockAddressManager = MockIAddressManager() + mockAddressManager = MockIPublicKeyManager() mockAddressConverter = MockIAddressConverter() mockInputSigner = MockIInputSigner() mockScriptBuilder = MockIScriptBuilder() mockFactory = MockIFactory() mockTransactionSizeCalculator = MockITransactionSizeCalculator() - transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, addressManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory, transactionSizeCalculator: mockTransactionSizeCalculator) + transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, publicKeyManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory, transactionSizeCalculator: mockTransactionSizeCalculator) changePubKey = TestData.pubKey() changePubKeyAddress = "Rsfz3aRmCwTe2J8pSWSYRNYmweJ" @@ -346,7 +346,7 @@ class TransactionBuilderTests: XCTestCase { func testBuildTransaction_noChangeAddress() { stub(mockAddressManager) { mock in - when(mock.changePublicKey()).thenThrow(AddressManager.AddressManagerError.noUnusedPublicKey) + when(mock.changePublicKey()).thenThrow(PublicKeyManager.AddressManagerError.noUnusedPublicKey) } do { diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift index 46a0123d..f0310374 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift @@ -8,7 +8,7 @@ class TransactionProcessorTests: XCTestCase { private var mockOutputAddressExtractor: MockITransactionOutputAddressExtractor! private var mockInputExtractor: MockITransactionExtractor! private var mockOutputsCache: MockIOutputsCache! - private var mockAddressManager: MockIAddressManager! + private var mockAddressManager: MockIPublicKeyManager! private var mockBlockchainDataListener: MockIBlockchainDataListener! private var mockTransactionListener: MockITransactionListener! @@ -30,7 +30,7 @@ class TransactionProcessorTests: XCTestCase { mockOutputAddressExtractor = MockITransactionOutputAddressExtractor() mockInputExtractor = MockITransactionExtractor() mockOutputsCache = MockIOutputsCache() - mockAddressManager = MockIAddressManager() + mockAddressManager = MockIPublicKeyManager() mockBlockchainDataListener = MockIBlockchainDataListener() mockTransactionListener = MockITransactionListener() diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift index eb28d4d8..c99dd357 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift @@ -8,7 +8,7 @@ class TransactionSyncerTests: QuickSpec { override func spec() { let mockStorage = MockIStorage() let mockTransactionProcessor = MockITransactionProcessor() - let mockAddressManager = MockIAddressManager() + let mockAddressManager = MockIPublicKeyManager() let mockBloomFilterManager = MockIBloomFilterManager() let maxRetriesCount = 3 let retriesPeriod: Double = 60 @@ -32,7 +32,7 @@ class TransactionSyncerTests: QuickSpec { } syncer = TransactionSyncer( - storage: mockStorage, processor: mockTransactionProcessor, addressManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, + storage: mockStorage, processor: mockTransactionProcessor, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, maxRetriesCount: maxRetriesCount, retriesPeriod: retriesPeriod, totalRetriesPeriod: totalRetriesPeriod) } From 23ba7279bb84e48174efa8cde29130cc946516ff Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 5 Sep 2019 16:56:25 +0600 Subject: [PATCH 022/234] Remove "type" parameter from BitcoinCore#receiveAddress - Add "scriptType" parameter to BitcoinCore - Remove IAddressKeyHashConverter - Add convert(publicKey:type:) throws -> Address method to IAddressConverter - Remove PublicKeyManager#receiveAddress(type:) --- .../Bech32/CashBech32AddressConverter.swift | 3 ++ .../BitcoinCore/Core/AbstractKit.swift | 4 +- .../BitcoinCore/Core/BitcoinCore.swift | 14 +++++-- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 19 ++++----- BitcoinCore/BitcoinCore/Core/Protocols.swift | 6 +-- .../Helpers/AddressConverterChain.swift | 15 +++++++ .../Helpers/Base58AddressConverter.swift | 5 +++ .../Managers/PublicKeyManager.swift | 15 ++----- .../Builder/TransactionBuilder.swift | 8 +--- .../Managers/PublicKeyManagerTests.swift | 40 ------------------- .../Builder/TransactionBuilderTests.swift | 2 +- .../BitcoinKit.xcodeproj/project.pbxproj | 4 -- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 2 - .../SegWit/SegWitBech32AddressConverter.swift | 5 +++ .../SegWit/SegWitBech32KeyHashConverter.swift | 14 ------- Demo/Demo/Adapters/BaseAdapter.swift | 4 +- Demo/Demo/Controllers/ReceiveController.swift | 19 +-------- Demo/Demo/Controllers/ReceiveController.xib | 21 ++-------- 18 files changed, 65 insertions(+), 135 deletions(-) delete mode 100644 BitcoinKit/BitcoinKit/SegWit/SegWitBech32KeyHashConverter.swift diff --git a/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift b/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift index 26784f53..973b97cf 100644 --- a/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift +++ b/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift @@ -57,4 +57,7 @@ public class CashBech32AddressConverter: IAddressConverter { return CashAddress(type: addressType, keyHash: keyHash, cashAddrBech32: bech32, version: versionByte) } + public func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + return try convert(keyHash: publicKey.keyHash, type: type) + } } diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index 358cb1a2..e3591cb7 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -58,8 +58,8 @@ open class AbstractKit { return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate, changeScriptType: changeScriptType) } - open func receiveAddress(for type: ScriptType) -> String { - return bitcoinCore.receiveAddress(for: type) + open func receiveAddress() -> String { + return bitcoinCore.receiveAddress() } open func changePublicKey() throws -> PublicKey { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 13a91155..98e77b3e 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -30,6 +30,8 @@ public class BitcoinCore { private let syncManager: SyncManager + private let scriptType: ScriptType + // START: Extending public let peerGroup: IPeerGroup @@ -91,7 +93,7 @@ public class BitcoinCore { blockValidatorChain: BlockValidatorChain, addressManager: IPublicKeyManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, scriptBuilder: ScriptBuilderChain, transactionBuilder: ITransactionBuilder, transactionCreator: ITransactionCreator, paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, - syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager) { + syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, scriptType: ScriptType) { self.storage = storage self.cache = cache self.dataProvider = dataProvider @@ -115,6 +117,7 @@ public class BitcoinCore { self.syncManager = syncManager self.watchedTransactionManager = watchedTransactionManager + self.scriptType = scriptType } } @@ -175,8 +178,13 @@ extension BitcoinCore { return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress, changeScriptType: changeScriptType) } - public func receiveAddress(for type: ScriptType) -> String { - return (try? publicKeyManager.receiveAddress(for: type)) ?? "" + public func receiveAddress() -> String { + guard let publicKey = try? publicKeyManager.receivePublicKey(), + let address = try? addressConverter.convert(publicKey: publicKey, type: scriptType) else { + return "" + } + + return address.stringValue } public func changePublicKey() throws -> PublicKey { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index c0d2f632..5a910e68 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -8,10 +8,10 @@ public class BitcoinCoreBuilder { private var seed: Data? private var words: [String]? private var bip: Purpose = .bip44 + private var scriptType: ScriptType = .p2pkh private var network: INetwork? private var paymentAddressParser: IPaymentAddressParser? private var addressSelector: IAddressSelector? - private var addressKeyHashConverter: IAddressKeyHashConverter? private var walletId: String? private var initialSyncApi: ISyncTransactionApi? private var logger: Logger @@ -39,6 +39,11 @@ public class BitcoinCoreBuilder { public func set(bip: Purpose) -> BitcoinCoreBuilder { self.bip = bip + switch bip { + case .bip44: scriptType = .p2pkh + case .bip49: scriptType = .p2wpkhSh + case .bip84: scriptType = .p2wpkh + } return self } @@ -57,11 +62,6 @@ public class BitcoinCoreBuilder { return self } - public func set(addressKeyHashConverter: IAddressKeyHashConverter) -> BitcoinCoreBuilder { - self.addressKeyHashConverter = addressKeyHashConverter - return self - } - public func set(walletId: String) -> BitcoinCoreBuilder { self.walletId = walletId return self @@ -158,7 +158,7 @@ public class BitcoinCoreBuilder { let factory = Factory(network: network, networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer) - let addressManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter, addressKeyHashConverter: addressKeyHashConverter) + let addressManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter) let myOutputsCache = OutputsCache.instance(storage: storage) let scriptConverter = ScriptConverter() @@ -208,7 +208,7 @@ public class BitcoinCoreBuilder { let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, publicKeyManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, - transactionSizeCalculator: transactionSizeCalculator, addressKeyHashConverter: addressKeyHashConverter) + transactionSizeCalculator: transactionSizeCalculator) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) @@ -234,7 +234,8 @@ public class BitcoinCoreBuilder { networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer, syncManager: syncManager, - watchedTransactionManager: watchedTransactionManager) + watchedTransactionManager: watchedTransactionManager, + scriptType: scriptType) initialSyncer.delegate = syncManager bloomFilterManager.delegate = bloomFilterLoader diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index c0a34f16..955fe350 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -136,7 +136,6 @@ public protocol IAddressSelector { public protocol IPublicKeyManager { func changePublicKey() throws -> PublicKey func receivePublicKey() throws -> PublicKey - func receiveAddress(for type: ScriptType) throws -> String func fillGap() throws func addKeys(keys: [PublicKey]) throws func gapShifts() -> Bool @@ -288,13 +287,10 @@ protocol IPaymentAddressParser { func parse(paymentAddress: String) -> BitcoinPaymentData } -public protocol IAddressKeyHashConverter { - func convert(keyHash: Data, type: ScriptType) -> Data -} - public protocol IAddressConverter { func convert(address: String) throws -> Address func convert(keyHash: Data, type: ScriptType) throws -> Address + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address } public protocol IScriptConverter { diff --git a/BitcoinCore/BitcoinCore/Helpers/AddressConverterChain.swift b/BitcoinCore/BitcoinCore/Helpers/AddressConverterChain.swift index b30412f1..38b2befa 100644 --- a/BitcoinCore/BitcoinCore/Helpers/AddressConverterChain.swift +++ b/BitcoinCore/BitcoinCore/Helpers/AddressConverterChain.swift @@ -35,4 +35,19 @@ class AddressConverterChain: IAddressConverter { throw BitcoinCoreErrors.AddressConversionErrors(errors: errors) } + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + var errors = [Error]() + + for converter in concreteConverters { + do { + let converted = try converter.convert(publicKey: publicKey, type: type) + return converted + } catch { + errors.append(error) + } + } + + throw BitcoinCoreErrors.AddressConversionErrors(errors: errors) + } + } diff --git a/BitcoinCore/BitcoinCore/Helpers/Base58AddressConverter.swift b/BitcoinCore/BitcoinCore/Helpers/Base58AddressConverter.swift index e991e0a7..80309603 100644 --- a/BitcoinCore/BitcoinCore/Helpers/Base58AddressConverter.swift +++ b/BitcoinCore/BitcoinCore/Helpers/Base58AddressConverter.swift @@ -61,4 +61,9 @@ class Base58AddressConverter: IAddressConverter { return LegacyAddress(type: addressType, keyHash: keyHash, base58: base58) } + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + let keyHash = type == .p2wpkhSh ? publicKey.scriptHashForP2WPKH : publicKey.keyHash + return try convert(keyHash: keyHash, type: type) + } + } diff --git a/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift index 3805515c..12e502c5 100644 --- a/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift @@ -9,13 +9,11 @@ class PublicKeyManager { private let storage: IStorage private let hdWallet: IHDWallet - private let addressKeyHashConverter: IAddressKeyHashConverter? private let addressConverter: IAddressConverter - init(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter, addressKeyHashConverter: IAddressKeyHashConverter? = nil) { + init(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter) { self.storage = storage self.addressConverter = addressConverter - self.addressKeyHashConverter = addressKeyHashConverter self.hdWallet = hdWallet } @@ -67,13 +65,6 @@ extension PublicKeyManager: IPublicKeyManager { return try publicKey(external: true) } - func receiveAddress(for type: ScriptType) throws -> String { - let keyHash = try publicKey(external: true).keyHash - let correctKeyHash = addressKeyHashConverter?.convert(keyHash: keyHash, type: type) ?? keyHash - - return try addressConverter.convert(keyHash: correctKeyHash, type: type).stringValue - } - func fillGap() throws { let publicKeysWithUsedStates = storage.publicKeysWithUsedState() let requiredAccountsCount: Int @@ -135,8 +126,8 @@ extension PublicKeyManager: IPublicKeyManager { extension PublicKeyManager { - public static func instance(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter, addressKeyHashConverter: IAddressKeyHashConverter? = nil) -> PublicKeyManager { - let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter, addressKeyHashConverter: addressKeyHashConverter) + public static func instance(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter) -> PublicKeyManager { + let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter) try? addressManager.fillGap() return addressManager } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 29e2544b..4749017b 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -10,7 +10,6 @@ class TransactionBuilder { private let unspentOutputProvider: IUnspentOutputProvider private let publicKeyManager: IPublicKeyManager private let addressConverter: IAddressConverter - private let addressKeyHashConverter: IAddressKeyHashConverter? private let inputSigner: IInputSigner private let factory: IFactory private let transactionSizeCalculator: ITransactionSizeCalculator @@ -18,13 +17,11 @@ class TransactionBuilder { var scriptBuilder: IScriptBuilder init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, publicKeyManager: IPublicKeyManager, addressConverter: IAddressConverter, - inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator, - addressKeyHashConverter: IAddressKeyHashConverter? = nil) { + inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator) { self.unspentOutputSelector = unspentOutputSelector self.unspentOutputProvider = unspentOutputProvider self.publicKeyManager = publicKeyManager self.addressConverter = addressConverter - self.addressKeyHashConverter = addressKeyHashConverter self.inputSigner = inputSigner self.scriptBuilder = scriptBuilder self.factory = factory @@ -99,8 +96,7 @@ extension TransactionBuilder: ITransactionBuilder { // Add :change output if needed if selectedOutputsInfo.addChangeOutput { - let correctKeyHash = addressKeyHashConverter?.convert(keyHash: changePubKey.keyHash, type: changeScriptType) ?? changePubKey.keyHash - let changeAddress = try addressConverter.convert(keyHash: correctKeyHash, type: changeScriptType) + let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: changeScriptType) outputs.append(try output(withIndex: 1, address: changeAddress, value: selectedOutputsInfo.totalValue - sentValue)) } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift index 8d053362..9af88232 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift @@ -108,46 +108,6 @@ class PublicKeyManagerTests: XCTestCase { } } - func testReceiveAddress() { - let publicKeys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false) - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) - } - - let address = LegacyAddress(type: .pubKeyHash, keyHash: publicKeys[3].publicKey.keyHash, base58: "receiveAddress") - stub(mockAddressConverter) { mock in - when(mock.convert(keyHash: equal(to: publicKeys[3].publicKey.keyHash), type: equal(to: ScriptType.p2pkh))).thenReturn(address) - } - - XCTAssertEqual(try? manager.receiveAddress(for: .p2pkh), address.stringValue) - verify(mockAddressConverter).convert(keyHash: equal(to: publicKeys[3].publicKey.keyHash), type: equal(to: ScriptType.p2pkh)) - } - - func testReceiveAddress_NoUnusedPublicKey() { - let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true) - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) - } - - do { - let _ = try manager.receiveAddress(for: .p2pkh) - XCTFail("Should throw exception") - } catch let error as PublicKeyManager.AddressManagerError { - XCTAssertEqual(error, PublicKeyManager.AddressManagerError.noUnusedPublicKey) - } catch { - XCTFail("Unexpected exception thrown") - } - } - func testFillGap() { let keys = [ PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index b9b45824..9dd02eae 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -126,7 +126,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.convert(address: toAddressSH)).thenReturn(LegacyAddress(type: .scriptHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, base58: toAddressSH)) when(mock.convert(address: toAddressWPKH)).thenReturn(SegWitAddress(type: .pubKeyHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, bech32: "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2", version: 0)) when(mock.convert(address: changePubKeyAddress)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) - when(mock.convert(keyHash: equal(to: changePubKey.keyHash), type: equal(to: .p2pkh))).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) + when(mock.convert(publicKey: equal(to: changePubKey), type: equal(to: .p2pkh))).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) // when(mock.convert(address: any())).thenReturn(Address(type: .pubKeyHash, keyHash: Data(), base58: "")) } diff --git a/BitcoinKit/BitcoinKit.xcodeproj/project.pbxproj b/BitcoinKit/BitcoinKit.xcodeproj/project.pbxproj index 4e73f42f..496ff6b8 100644 --- a/BitcoinKit/BitcoinKit.xcodeproj/project.pbxproj +++ b/BitcoinKit/BitcoinKit.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 58AAA323FA5EDE02F99E25AE /* SegWitAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA71716FABEBBE3FFAB45 /* SegWitAddress.swift */; }; 58AAA49B8012402356C135CE /* TestNet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6A33CBF3D1A0782C365 /* TestNet.swift */; }; 58AAA4F949AA4261158F54B1 /* SegWitBech32AddressConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA5A526DD240851A5115F /* SegWitBech32AddressConverterTests.swift */; }; - 58AAA57AF8182EA3F11C0FD9 /* SegWitBech32KeyHashConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAABD4F082706A8D771796 /* SegWitBech32KeyHashConverter.swift */; }; 58AAA63A4D5CB80B7D260F71 /* MainNet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA5DC79D60B7733E52BEC /* MainNet.swift */; }; 58AAA64BD96CE9A79484D42E /* SegWitBech32AddressConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA79DC50FFF8761BAFD89 /* SegWitBech32AddressConverter.swift */; }; 58AAA663ECDC727231CA8863 /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA97073BF8F4DCA68CBE1 /* Bech32.swift */; }; @@ -60,7 +59,6 @@ 58AAA8AA13E5099C73B8E79C /* GeneratedMocks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; 58AAA97073BF8F4DCA68CBE1 /* Bech32.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = ""; }; 58AAAA628741DC6017350532 /* SegWitScriptBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegWitScriptBuilderTests.swift; sourceTree = ""; }; - 58AAABD4F082706A8D771796 /* SegWitBech32KeyHashConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegWitBech32KeyHashConverter.swift; sourceTree = ""; }; 58AAAC5F67BD7DA7392C6D82 /* SegWitBech32.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegWitBech32.swift; sourceTree = ""; }; 58AAACD1AA455F909A7E4378 /* RegTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegTest.swift; sourceTree = ""; }; 58AAACD3F4604233235F3FDD /* SegWitScriptBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegWitScriptBuilder.swift; sourceTree = ""; }; @@ -183,7 +181,6 @@ 58AAA97073BF8F4DCA68CBE1 /* Bech32.swift */, 58AAA71716FABEBBE3FFAB45 /* SegWitAddress.swift */, 58AAACD3F4604233235F3FDD /* SegWitScriptBuilder.swift */, - 58AAABD4F082706A8D771796 /* SegWitBech32KeyHashConverter.swift */, ); path = SegWit; sourceTree = ""; @@ -396,7 +393,6 @@ 58AAA323FA5EDE02F99E25AE /* SegWitAddress.swift in Sources */, 58AAAC2EE10933D65DA4B027 /* SegWitScriptBuilder.swift in Sources */, 58AAAAAAA18E7D96FE94AADE /* BitcoinKitErrors.swift in Sources */, - 58AAA57AF8182EA3F11C0FD9 /* SegWitBech32KeyHashConverter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index 44deb164..fde48d95 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -41,7 +41,6 @@ public class BitcoinKit: AbstractKit { let paymentAddressParser = PaymentAddressParser(validScheme: "bitcoin", removeScheme: true) let addressSelector = BitcoinAddressSelector() - let addressKeyHashConverter = SegWitBech32KeyHashConverter() let bitcoinCore = try BitcoinCoreBuilder(minLogLevel: minLogLevel) .set(network: network) @@ -50,7 +49,6 @@ public class BitcoinKit: AbstractKit { .set(bip: bip) .set(paymentAddressParser: paymentAddressParser) .set(addressSelector: addressSelector) - .set(addressKeyHashConverter: addressKeyHashConverter) .set(walletId: walletId) .set(confirmationsThreshold: confirmationsThreshold) .set(peerSize: 10) diff --git a/BitcoinKit/BitcoinKit/SegWit/SegWitBech32AddressConverter.swift b/BitcoinKit/BitcoinKit/SegWit/SegWitBech32AddressConverter.swift index 747999bc..2f86838d 100644 --- a/BitcoinKit/BitcoinKit/SegWit/SegWitBech32AddressConverter.swift +++ b/BitcoinKit/BitcoinKit/SegWit/SegWitBech32AddressConverter.swift @@ -1,4 +1,5 @@ import BitcoinCore +import HSCryptoKit class SegWitBech32AddressConverter: IAddressConverter { private let prefix: String @@ -43,4 +44,8 @@ class SegWitBech32AddressConverter: IAddressConverter { return SegWitAddress(type: addressType, keyHash: keyHash, bech32: bech32, version: versionByte) } + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { + return try convert(keyHash: OpCode.scriptWPKH(publicKey.keyHash), type: type) + } + } diff --git a/BitcoinKit/BitcoinKit/SegWit/SegWitBech32KeyHashConverter.swift b/BitcoinKit/BitcoinKit/SegWit/SegWitBech32KeyHashConverter.swift deleted file mode 100644 index 4c786925..00000000 --- a/BitcoinKit/BitcoinKit/SegWit/SegWitBech32KeyHashConverter.swift +++ /dev/null @@ -1,14 +0,0 @@ -import BitcoinCore -import HSCryptoKit - -class SegWitBech32KeyHashConverter: IAddressKeyHashConverter { - - func convert(keyHash: Data, type: ScriptType) -> Data { - switch type { - case .p2wpkh: return OpCode.scriptWPKH(keyHash) - case .p2wpkhSh:return CryptoKit.sha256ripemd160(OpCode.scriptWPKH(keyHash)) - default: return keyHash - } - } - -} diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 5a28c07c..2d0c910d 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -103,8 +103,8 @@ extension BaseAdapter { return abstractKit.syncState } - func receiveAddress(for type: ScriptType) -> String { - return abstractKit.receiveAddress(for: type) + func receiveAddress() -> String { + return abstractKit.receiveAddress() } func validate(address: String) throws { diff --git a/Demo/Demo/Controllers/ReceiveController.swift b/Demo/Demo/Controllers/ReceiveController.swift index 01d93546..d41ab269 100644 --- a/Demo/Demo/Controllers/ReceiveController.swift +++ b/Demo/Demo/Controllers/ReceiveController.swift @@ -6,8 +6,7 @@ class ReceiveController: UIViewController { private let disposeBag = DisposeBag() @IBOutlet weak var addressLabel: UILabel? - @IBOutlet weak var addressTypeControl: UISegmentedControl! - + private var adapters = [BaseAdapter]() private let segmentedControl = UISegmentedControl() @@ -45,9 +44,6 @@ class ReceiveController: UIViewController { segmentedControl.selectedSegmentIndex = 0 segmentedControl.sendActions(for: .valueChanged) - - addressTypeControl.selectedSegmentIndex = 0 - addressTypeControl.isHidden = false } override func viewWillAppear(_ animated: Bool) { @@ -56,17 +52,7 @@ class ReceiveController: UIViewController { segmentedControl.sendActions(for: .valueChanged) } - func type(segment: Int) -> ScriptType { - switch segment { - case 1: return .p2wpkh - case 2: return .p2wpkhSh - default: return .p2pkh - } - } - @objc func onSegmentChanged() { - addressTypeControl.isHidden = segmentedControl.selectedSegmentIndex != 0 - addressTypeControl.selectedSegmentIndex = 0 updateAddress() if let adapter = currentAdapter { @@ -74,8 +60,7 @@ class ReceiveController: UIViewController { } } func updateAddress() { - let segment = addressTypeControl.selectedSegmentIndex - addressLabel?.text = " \(currentAdapter?.receiveAddress(for: type(segment: segment)) ?? "") " + addressLabel?.text = " \(currentAdapter?.receiveAddress() ?? "") " } @IBAction func onAddressTypeChanged(_ sender: Any) { diff --git a/Demo/Demo/Controllers/ReceiveController.xib b/Demo/Demo/Controllers/ReceiveController.xib index 54928982..9a27aaaf 100644 --- a/Demo/Demo/Controllers/ReceiveController.xib +++ b/Demo/Demo/Controllers/ReceiveController.xib @@ -13,7 +13,6 @@ - @@ -39,38 +38,24 @@ - - - - - - - - - - - - - - - + + - From 9c3e3298608d5b04543605c75e10b93edcb95397 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 5 Sep 2019 18:16:42 +0600 Subject: [PATCH 023/234] Remove "changeScriptType" parameter from BitcoinCore#send and BitcoinCore#fee - Add Bip enum - Inject Bip instance to TransactionBuilder --- .../BitcoinCore.xcodeproj/project.pbxproj | 4 +++ .../BitcoinCore/Core/AbstractKit.swift | 12 +++---- BitcoinCore/BitcoinCore/Core/Bip.swift | 24 ++++++++++++++ .../BitcoinCore/Core/BitcoinCore.swift | 30 ++++++++--------- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 16 +++------ BitcoinCore/BitcoinCore/Core/Protocols.swift | 6 ++-- .../Builder/TransactionBuilder.swift | 14 ++++---- .../Transactions/TransactionCreator.swift | 4 +-- .../Builder/TransactionBuilderTests.swift | 24 +++++++------- .../TransactionCreatorTests.swift | 33 ++++++------------- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 2 +- DashKit/DashKit/Core/DashKit.swift | 4 +-- Demo/Demo/Adapters/BaseAdapter.swift | 6 ++-- Demo/Demo/Adapters/BitcoinAdapter.swift | 3 +- 14 files changed, 95 insertions(+), 87 deletions(-) create mode 100644 BitcoinCore/BitcoinCore/Core/Bip.swift diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index 9f6d378c..3c4feab1 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ 2FA5D806A7727E3BED0E54E7 /* DataObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D50922572F91F966DD4A /* DataObjects.swift */; }; 2FA5D8284C7C94155AC50B7B /* GetBlockHashesTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DFC7FA0A5363BFFB8E28 /* GetBlockHashesTaskTests.swift */; }; 2FA5D84AA43DA05CAC68B912 /* WatchedTransactionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */; }; + 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D7A29489EA7A15A4B25D /* Bip.swift */; }; 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */; }; 2FA5D91DCF7F320369162C5A /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */; }; 2FA5D9D3DA2DF9BD402CE5DD /* SentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF037E951950A0F7FF03 /* SentTransaction.swift */; }; @@ -291,6 +292,7 @@ 2FA5D65667DA8BFD91E91C8B /* GetBlockHashesTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBlockHashesTask.swift; sourceTree = ""; }; 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = ""; }; 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignatureScriptSerializer.swift; sourceTree = ""; }; + 2FA5D7A29489EA7A15A4B25D /* Bip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bip.swift; sourceTree = ""; }; 2FA5D82EFF381CE6233790A9 /* KitStateProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitStateProviderTests.swift; sourceTree = ""; }; 2FA5D859C070F9718CACF08F /* KitStateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitStateProvider.swift; sourceTree = ""; }; 2FA5D880471F191DBCDAE574 /* TransactionSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionSerializer.swift; sourceTree = ""; }; @@ -492,6 +494,7 @@ 58AAA6603AEA2159F6126D10 /* Signal.swift */, 58AAA0FE0496F520DDA29BB1 /* TransactionInfoConverter.swift */, 58AAAFE91DB09FA5E32DD881 /* BaseTransactionInfoConverter.swift */, + 2FA5D7A29489EA7A15A4B25D /* Bip.swift */, ); path = Core; sourceTree = ""; @@ -1450,6 +1453,7 @@ 58AAAF671C4B3988EF6764B1 /* InsightApi.swift in Sources */, 2FA5D84AA43DA05CAC68B912 /* WatchedTransactionManager.swift in Sources */, 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */, + 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index e3591cb7..3a1268a1 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -34,12 +34,12 @@ open class AbstractKit { return bitcoinCore.transactions(fromHash: fromHash, limit: limit) } - open func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType = .p2pkh) throws -> FullTransaction { - return try bitcoinCore.send(to: address, value: value, feeRate: feeRate, changeScriptType: changeScriptType) + open func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { + return try bitcoinCore.send(to: address, value: value, feeRate: feeRate) } - public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, changeScriptType: ScriptType = .p2pkh) throws -> FullTransaction { - return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate, changeScriptType: changeScriptType) + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { + return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate) } public func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { @@ -54,8 +54,8 @@ open class AbstractKit { return bitcoinCore.parse(paymentAddress: paymentAddress) } - open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, changeScriptType: ScriptType = .p2pkh) throws -> Int { - return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate, changeScriptType: changeScriptType) + open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { + return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate) } open func receiveAddress() -> String { diff --git a/BitcoinCore/BitcoinCore/Core/Bip.swift b/BitcoinCore/BitcoinCore/Core/Bip.swift new file mode 100644 index 00000000..56efde4e --- /dev/null +++ b/BitcoinCore/BitcoinCore/Core/Bip.swift @@ -0,0 +1,24 @@ +import HSHDWalletKit + +public enum Bip { + case bip44 + case bip49 + case bip84 + + var scriptType: ScriptType { + switch self { + case .bip44: return .p2pkh + case .bip49: return .p2wpkhSh + case .bip84: return .p2wpkh + } + } + + var purpose: Purpose { + switch self { + case .bip44: return Purpose.bip44 + case .bip49: return Purpose.bip49 + case .bip84: return Purpose.bip84 + } + } + +} diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 98e77b3e..77305bc8 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -30,7 +30,7 @@ public class BitcoinCore { private let syncManager: SyncManager - private let scriptType: ScriptType + private let bip: Bip // START: Extending @@ -88,12 +88,12 @@ public class BitcoinCore { public weak var delegate: BitcoinCoreDelegate? init(storage: IStorage, cache: OutputsCache, dataProvider: IDataProvider, - peerGroup: IPeerGroup, initialBlockDownload: IInitialBlockDownload, bloomFilterLoader: BloomFilterLoader, - syncedReadyPeerManager: ISyncedReadyPeerManager, transactionSyncer: ITransactionSyncer, - blockValidatorChain: BlockValidatorChain, addressManager: IPublicKeyManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, - scriptBuilder: ScriptBuilderChain, transactionBuilder: ITransactionBuilder, transactionCreator: ITransactionCreator, - paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, - syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, scriptType: ScriptType) { + peerGroup: IPeerGroup, initialBlockDownload: IInitialBlockDownload, bloomFilterLoader: BloomFilterLoader, + syncedReadyPeerManager: ISyncedReadyPeerManager, transactionSyncer: ITransactionSyncer, + blockValidatorChain: BlockValidatorChain, addressManager: IPublicKeyManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, + scriptBuilder: ScriptBuilderChain, transactionBuilder: ITransactionBuilder, transactionCreator: ITransactionCreator, + paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, + syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, bip: Bip) { self.storage = storage self.cache = cache self.dataProvider = dataProvider @@ -117,7 +117,7 @@ public class BitcoinCore { self.syncManager = syncManager self.watchedTransactionManager = watchedTransactionManager - self.scriptType = scriptType + self.bip = bip } } @@ -152,14 +152,14 @@ extension BitcoinCore { return dataProvider.transactions(fromHash: fromHash, limit: limit) } - public func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { - return try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true, changeScriptType: changeScriptType) + public func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { + return try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true) } - public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { + public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { // TODO: convert to scriptWPKH when scriptType is P2WPKHSH ? let address = try addressConverter.convert(keyHash: hash, type: scriptType) - return try send(to: address.stringValue, value: value, feeRate: feeRate, changeScriptType: changeScriptType) + return try send(to: address.stringValue, value: value, feeRate: feeRate) } func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { @@ -174,13 +174,13 @@ extension BitcoinCore { return paymentAddressParser.parse(paymentAddress: paymentAddress) } - public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, changeScriptType: ScriptType) throws -> Int { - return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress, changeScriptType: changeScriptType) + public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { + return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress) } public func receiveAddress() -> String { guard let publicKey = try? publicKeyManager.receivePublicKey(), - let address = try? addressConverter.convert(publicKey: publicKey, type: scriptType) else { + let address = try? addressConverter.convert(publicKey: publicKey, type: bip.scriptType) else { return "" } diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 5a910e68..aa92f3f7 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -7,8 +7,7 @@ public class BitcoinCoreBuilder { // required parameters private var seed: Data? private var words: [String]? - private var bip: Purpose = .bip44 - private var scriptType: ScriptType = .p2pkh + private var bip: Bip = .bip44 private var network: INetwork? private var paymentAddressParser: IPaymentAddressParser? private var addressSelector: IAddressSelector? @@ -37,13 +36,8 @@ public class BitcoinCoreBuilder { return self } - public func set(bip: Purpose) -> BitcoinCoreBuilder { + public func set(bip: Bip) -> BitcoinCoreBuilder { self.bip = bip - switch bip { - case .bip44: scriptType = .p2pkh - case .bip49: scriptType = .p2wpkhSh - case .bip84: scriptType = .p2wpkh - } return self } @@ -147,7 +141,7 @@ public class BitcoinCoreBuilder { let reachabilityManager = ReachabilityManager() - let hdWallet = HDWallet(seed: seed, coinType: network.coinType, xPrivKey: network.xPrivKey, xPubKey: network.xPubKey, gapLimit: 20, purpose: bip) + let hdWallet = HDWallet(seed: seed, coinType: network.coinType, xPrivKey: network.xPrivKey, xPubKey: network.xPubKey, gapLimit: 20, purpose: bip.purpose) let networkMessageParser = NetworkMessageParser(magic: network.magic) let networkMessageSerializer = NetworkMessageSerializer(magic: network.magic) @@ -208,7 +202,7 @@ public class BitcoinCoreBuilder { let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, publicKeyManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, - transactionSizeCalculator: transactionSizeCalculator) + transactionSizeCalculator: transactionSizeCalculator, bip: bip) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) @@ -235,7 +229,7 @@ public class BitcoinCoreBuilder { networkMessageSerializer: networkMessageSerializer, syncManager: syncManager, watchedTransactionManager: watchedTransactionManager, - scriptType: scriptType) + bip: bip) initialSyncer.delegate = syncManager bloomFilterManager.delegate = bloomFilterLoader diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 955fe350..73216849 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -339,13 +339,13 @@ public protocol ITransactionSyncer: class { } public protocol ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, changeScriptType: ScriptType) throws -> FullTransaction + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction func create(from: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionBuilder { - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?, changeScriptType: ScriptType) throws -> Int - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String, changeScriptType: ScriptType) throws -> FullTransaction + func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int + func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction func buildTransaction(from: UnspentOutput, to: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 4749017b..405a23e6 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -13,11 +13,12 @@ class TransactionBuilder { private let inputSigner: IInputSigner private let factory: IFactory private let transactionSizeCalculator: ITransactionSizeCalculator + private let bip: Bip var scriptBuilder: IScriptBuilder init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, publicKeyManager: IPublicKeyManager, addressConverter: IAddressConverter, - inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator) { + inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator, bip: Bip) { self.unspentOutputSelector = unspentOutputSelector self.unspentOutputProvider = unspentOutputProvider self.publicKeyManager = publicKeyManager @@ -26,6 +27,7 @@ class TransactionBuilder { self.scriptBuilder = scriptBuilder self.factory = factory self.transactionSizeCalculator = transactionSizeCalculator + self.bip = bip } private func input(fromUnspentOutput unspentOutput: UnspentOutput) throws -> InputToSign { @@ -52,10 +54,10 @@ extension TransactionBuilder: ITransactionBuilder { // :fee method returns the fee for the given amount // If address given and it's valid, it returns the actual fee // Otherwise, it returns the estimated fee - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String? = nil, changeScriptType: ScriptType) throws -> Int { + func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String? = nil) throws -> Int { if let string = address, let _ = try? addressConverter.convert(address: string) { // Actual fee - let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: string, changeScriptType: changeScriptType) + let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: string) return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate } else { // Estimated fee @@ -65,13 +67,13 @@ extension TransactionBuilder: ITransactionBuilder { } } - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String, changeScriptType: ScriptType) throws -> FullTransaction { + func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction { guard let changePubKey = try? publicKeyManager.changePublicKey() else { throw BuildError.noChangeAddress } let address = try addressConverter.convert(address: toAddress) - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: changeScriptType, senderPay: senderPay) + let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: bip.scriptType, senderPay: senderPay) if !senderPay { guard selectedOutputsInfo.fee < value else { @@ -96,7 +98,7 @@ extension TransactionBuilder: ITransactionBuilder { // Add :change output if needed if selectedOutputsInfo.addChangeOutput { - let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: changeScriptType) + let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) outputs.append(try output(withIndex: 1, address: changeAddress, value: selectedOutputsInfo.totalValue - sentValue)) } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index fb7bb7f6..9e30c5c5 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -31,8 +31,8 @@ class TransactionCreator { extension TransactionCreator: ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, changeScriptType: ScriptType) throws -> FullTransaction { - let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address, changeScriptType: changeScriptType) + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { + let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address) try processAndSend(transaction: transaction) return transaction diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index 9dd02eae..4adbdb73 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -75,7 +75,7 @@ class TransactionBuilderTests: XCTestCase { mockFactory = MockIFactory() mockTransactionSizeCalculator = MockITransactionSizeCalculator() - transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, publicKeyManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory, transactionSizeCalculator: mockTransactionSizeCalculator) + transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, publicKeyManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory, transactionSizeCalculator: mockTransactionSizeCalculator, bip: .bip44) changePubKey = TestData.pubKey() changePubKeyAddress = "Rsfz3aRmCwTe2J8pSWSYRNYmweJ" @@ -164,7 +164,7 @@ class TransactionBuilderTests: XCTestCase { } func testFee_AddressGiven() { - let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH, changeScriptType: .p2pkh) + let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH) XCTAssertEqual(resultFee, 546) } @@ -174,7 +174,7 @@ class TransactionBuilderTests: XCTestCase { } do { - let _ = try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH, changeScriptType: .p2pkh) + let _ = try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH) } catch let error as BitcoinCoreErrors.AddressConversion { XCTAssertEqual(error, BitcoinCoreErrors.AddressConversion.invalidAddressLength) } catch let error { @@ -183,12 +183,12 @@ class TransactionBuilderTests: XCTestCase { } func testFee_AddressNotGiven_Error() { - let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, changeScriptType: .p2pkh) + let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false) XCTAssertEqual(resultFee, fee) } func testBuildTransaction_P2PKH() { - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) XCTAssertNotEqual(resultTx.header.dataHash, Data()) XCTAssertEqual(resultTx.header.status, .new) @@ -259,7 +259,7 @@ class TransactionBuilderTests: XCTestCase { // } func testBuildTransaction_P2SH() { - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressSH, changeScriptType: .p2pkh) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressSH) XCTAssertNotEqual(resultTx.header.dataHash, Data()) XCTAssertEqual(resultTx.header.status, .new) @@ -277,7 +277,7 @@ class TransactionBuilderTests: XCTestCase { } func testBuildTransactionSenderPay() { - _ = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: true, toAddress: toAddressPKH, changeScriptType: .p2pkh) + _ = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: true, toAddress: toAddressPKH) verify(mockFactory).output(withValue: value, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) verify(mockFactory).output(withValue: unspentOutputs.unspentOutputs[0].output.value - value - fee, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any()) @@ -290,7 +290,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) XCTAssertEqual(resultTx.inputs.count, 1) XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) @@ -307,7 +307,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) XCTAssertEqual(resultTx.inputs.count, 1) XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) @@ -325,7 +325,7 @@ class TransactionBuilderTests: XCTestCase { when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) + let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) XCTAssertEqual(resultTx.inputs[0].signatureScript, sigScript) } @@ -336,7 +336,7 @@ class TransactionBuilderTests: XCTestCase { ) do { - let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) + let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) } catch let error as TransactionBuilder.BuildError { XCTAssertEqual(error, TransactionBuilder.BuildError.feeMoreThanValue) } catch let error { @@ -350,7 +350,7 @@ class TransactionBuilderTests: XCTestCase { } do { - let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH, changeScriptType: .p2pkh) + let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) XCTFail("No exception!") } catch let error as TransactionBuilder.BuildError { XCTAssertEqual(error, TransactionBuilder.BuildError.noChangeAddress) diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index 39ab9548..ec1fd617 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -22,7 +22,7 @@ class TransactionCreatorTests: QuickSpec { describe("#create(to:value:feeRate:senderPay:)") { beforeEach { stub(mockTransactionBuilder) { mock in - when(mock.buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any(), changeScriptType: any())).thenReturn(transaction) + when(mock.buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any())).thenReturn(transaction) } stub(mockTransactionProcessor) { mock in when(mock.processCreated(transaction: any())).thenDoNothing() @@ -46,11 +46,11 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenDoNothing() } - _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2pkh) + _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } it("does create transaction") { - verify(mockTransactionBuilder).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any(), changeScriptType: any()) + verify(mockTransactionBuilder).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any()) verify(mockTransactionProcessor).processCreated(transaction: any()) } @@ -69,7 +69,7 @@ class TransactionCreatorTests: QuickSpec { when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) } - _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2pkh) + _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } it("doesn't create transaction") { @@ -86,29 +86,16 @@ class TransactionCreatorTests: QuickSpec { stub(mockTransactionSender) { mock in when(mock.verifyCanSend()).thenDoNothing() } + _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) } - context("when changeScriptType is .p2pkh") { - beforeEach { - _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2pkh) - } - - it("creates transaction") { - verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "", changeScriptType: equal(to: ScriptType.p2pkh)) - verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) - } - - it("sends transaction") { - verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) - } + it("creates transaction") { + verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "") + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) } - context("when changeScriptType is .p2wpkh") { - it("create transaction with p2wpkh change output") { - _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false, changeScriptType: .p2wpkh) - verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "", changeScriptType: equal(to: ScriptType.p2wpkh)) - verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) - } + it("sends transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) } } } diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index fde48d95..336d6e06 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -18,7 +18,7 @@ public class BitcoinKit: AbstractKit { } } - public init(withWords words: [String], bip: Purpose, walletId: String, syncMode: BitcoinCore.SyncMode = .api, networkType: NetworkType = .mainNet, confirmationsThreshold: Int = 6, minLogLevel: Logger.Level = .verbose) throws { + public init(withWords words: [String], bip: Bip, walletId: String, syncMode: BitcoinCore.SyncMode = .api, networkType: NetworkType = .mainNet, confirmationsThreshold: Int = 6, minLogLevel: Logger.Level = .verbose) throws { let network: INetwork let initialSyncApiUrl: String diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index e22acd2f..9f155919 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -146,8 +146,8 @@ public class DashKit: AbstractKit { return transactionInfos.compactMap { $0 as? DashTransactionInfo } } - public override func send(to address: String, value: Int, feeRate: Int, changeScriptType: ScriptType) throws -> FullTransaction { - return try super.send(to: address, value: value, feeRate: feeRate, changeScriptType: changeScriptType) + public override func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { + return try super.send(to: address, value: value, feeRate: feeRate) } public func transactions(fromHash: String?, limit: Int?) -> Single<[DashTransactionInfo]> { diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 2d0c910d..569b3f3b 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -8,8 +8,6 @@ class BaseAdapter { let name: String let coinCode: String - var changeAddressScriptType: ScriptType { return .p2pkh } - private let abstractKit: AbstractKit let lastBlockSignal = Signal() @@ -122,7 +120,7 @@ extension BaseAdapter { return Single.create { [unowned self] observer in do { - _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate, changeScriptType: self.changeAddressScriptType) + _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate) observer(.success(())) } catch { observer(.error(error)) @@ -139,7 +137,7 @@ extension BaseAdapter { func fee(for value: Decimal, address: String?) -> Decimal { do { let amount = convertToSatoshi(value: value) - let fee = try abstractKit.fee(for: amount, toAddress: address, senderPay: true, feeRate: feeRate, changeScriptType: self.changeAddressScriptType) + let fee = try abstractKit.fee(for: amount, toAddress: address, senderPay: true, feeRate: feeRate) return Decimal(fee) / coinRate } catch BitcoinCoreErrors.UnspentOutputSelection.notEnough(let maxFee) { return Decimal(maxFee) / coinRate diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index 7d703797..3191517d 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -5,9 +5,8 @@ import RxSwift class BitcoinAdapter: BaseAdapter { let bitcoinKit: BitcoinKit - override var changeAddressScriptType: ScriptType { return .p2pkh } - init(words: [String], bip: Purpose, testMode: Bool, syncMode: BitcoinCore.SyncMode) { + init(words: [String], bip: Bip, testMode: Bool, syncMode: BitcoinCore.SyncMode) { let networkType: BitcoinKit.NetworkType = testMode ? .testNet : .mainNet bitcoinKit = try! BitcoinKit(withWords: words, bip: bip, walletId: "walletId", syncMode: syncMode, networkType: networkType, minLogLevel: Configuration.shared.minLogLevel) From fa94bcb0fb9cd51c3aa22fc3668fa538e832eb44 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 9 Sep 2019 15:11:05 +0600 Subject: [PATCH 024/234] Code Refactor - Remove blockchain dedicated IAddressSelect implementations. - Introduce IRestoreKeyConverter with BIP dedicated implementations instead - Remove TransactionBuilder instance from BitcoinCore. Use TransactionCreator#fee instead - Extract UnspentOutputProvider, PublicKeyManager, AddressConverter and Bip instances from TransactionBuilder out to TransactionCreator --- .../BitcoinCashKit.xcodeproj/project.pbxproj | 5 -- .../Bech32/CashBech32AddressConverter.swift | 1 + .../BitcoinCashKit/Core/BitcoinCashKit.swift | 4 +- .../BitcoinCashAddressSelector.swift | 10 --- .../BitcoinCore.xcodeproj/project.pbxproj | 8 +- BitcoinCore/BitcoinCore/Core/Bip.swift | 10 ++- .../BitcoinCore/Core/BitcoinCore.swift | 21 ++--- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 32 +++----- BitcoinCore/BitcoinCore/Core/Protocols.swift | 13 +-- .../Core/RestoreKeyConverter.swift | 79 +++++++++++++++++++ .../Managers/BitcoinAddressSelector.swift | 12 --- .../InitialSync/BlockHashFetcher.swift | 10 +-- .../Managers/PublicKeyManager.swift | 14 ++-- .../Builder/TransactionBuilder.swift | 37 +++------ .../Transactions/TransactionCreator.swift | 41 ++++++++-- .../Managers/PublicKeyManagerTests.swift | 10 +-- .../Builder/TransactionBuilderTests.swift | 2 +- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 8 +- DashKit/DashKit.xcodeproj/project.pbxproj | 12 --- DashKit/DashKit/Core/DashKit.swift | 4 +- .../Managers/DashAddressSelector.swift | 12 --- 21 files changed, 195 insertions(+), 150 deletions(-) delete mode 100644 BitcoinCashKit/BitcoinCashKit/InitialSync/BitcoinCashAddressSelector.swift create mode 100644 BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift delete mode 100644 BitcoinCore/BitcoinCore/Managers/BitcoinAddressSelector.swift delete mode 100644 DashKit/DashKit/Managers/DashAddressSelector.swift diff --git a/BitcoinCashKit/BitcoinCashKit.xcodeproj/project.pbxproj b/BitcoinCashKit/BitcoinCashKit.xcodeproj/project.pbxproj index 26586e93..e4d4463d 100644 --- a/BitcoinCashKit/BitcoinCashKit.xcodeproj/project.pbxproj +++ b/BitcoinCashKit/BitcoinCashKit.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 58AAA44EAA5A88BD01DB5F99 /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA05B23F61CC6B6057F2F /* Protocols.swift */; }; 58AAA47795614383562E1602 /* BitcoinCashValidatorHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA6A735B4CB392E2643A0 /* BitcoinCashValidatorHelperTests.swift */; }; 58AAA4B27824731070116037 /* DAAValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAE58DE2E10255CD84550 /* DAAValidator.swift */; }; - 58AAA52BC5EB99E129E1F686 /* BitcoinCashAddressSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA60C6D03771882923E33 /* BitcoinCashAddressSelector.swift */; }; 58AAA56B03E2B4CD86EA7E6A /* EDAValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF54D4B5DE9187D77429 /* EDAValidatorTests.swift */; }; 58AAA63D42A4BA25DE72F1BD /* BitcoinCoreCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAE93C44463A5D9297539 /* BitcoinCoreCompatibility.swift */; }; 58AAA6AB3292B8674FD56812 /* CashBech32AddressConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA729DEE6879404255A7C /* CashBech32AddressConverterTests.swift */; }; @@ -60,7 +59,6 @@ 58AAA3F964711BDAEC84F01A /* CashBech32AddressConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CashBech32AddressConverter.swift; sourceTree = ""; }; 58AAA52A0C026AFF6327EF38 /* TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; 58AAA600D45B8BDA5F96433F /* TestNet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNet.swift; sourceTree = ""; }; - 58AAA60C6D03771882923E33 /* BitcoinCashAddressSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinCashAddressSelector.swift; sourceTree = ""; }; 58AAA6A735B4CB392E2643A0 /* BitcoinCashValidatorHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinCashValidatorHelperTests.swift; sourceTree = ""; }; 58AAA6F67B9BCCEB9212EF94 /* EDAValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EDAValidator.swift; sourceTree = ""; }; 58AAA729DEE6879404255A7C /* CashBech32AddressConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CashBech32AddressConverterTests.swift; sourceTree = ""; }; @@ -130,7 +128,6 @@ children = ( 58AAAE4FC18678BDC8990B26 /* Blocks */, 58AAA9B8B9C2CD7BE720AD34 /* Core */, - 58AAAE936FB46FD1053E8F21 /* InitialSync */, 58AAA8763996C7D71EEDD2A7 /* Network */, 58AAA71B1D2679E9B65B9B6B /* Storage */, 3A7A7D3C226842CB0063D6AD /* BitcoinCashKit.h */, @@ -239,7 +236,6 @@ 58AAAE936FB46FD1053E8F21 /* InitialSync */ = { isa = PBXGroup; children = ( - 58AAA60C6D03771882923E33 /* BitcoinCashAddressSelector.swift */, ); path = InitialSync; sourceTree = ""; @@ -457,7 +453,6 @@ 58AAAAD670B7A3FF24536C5D /* BitcoinCashBlockValidatorHelper.swift in Sources */, 58AAAEC09B3582BE2C6EAE3A /* TestNet.swift in Sources */, 58AAAB634B23381028A6C7CB /* MainNet.swift in Sources */, - 58AAA52BC5EB99E129E1F686 /* BitcoinCashAddressSelector.swift in Sources */, 58AAA44EAA5A88BD01DB5F99 /* Protocols.swift in Sources */, 58AAA63D42A4BA25DE72F1BD /* BitcoinCoreCompatibility.swift in Sources */, 58AAAF23A0B8737FA1CE3AED /* CashBech32AddressConverter.swift in Sources */, diff --git a/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift b/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift index 973b97cf..acd9246a 100644 --- a/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift +++ b/BitcoinCashKit/BitcoinCashKit/Bech32/CashBech32AddressConverter.swift @@ -60,4 +60,5 @@ public class CashBech32AddressConverter: IAddressConverter { public func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { return try convert(keyHash: publicKey.keyHash, type: type) } + } diff --git a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift index 6579413a..8e87649d 100644 --- a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift +++ b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift @@ -46,14 +46,12 @@ public class BitcoinCashKit: AbstractKit { self.storage = storage let paymentAddressParser = PaymentAddressParser(validScheme: validScheme, removeScheme: false) - let addressSelector = BitcoinCashAddressSelector() let bitcoinCore = try BitcoinCoreBuilder(minLogLevel: minLogLevel) .set(network: network) .set(initialSyncApi: initialSyncApi) .set(words: words) .set(paymentAddressParser: paymentAddressParser) - .set(addressSelector: addressSelector) .set(walletId: walletId) .set(confirmationsThreshold: confirmationsThreshold) .set(peerSize: 10) @@ -82,6 +80,8 @@ public class BitcoinCashKit: AbstractKit { case .testNet: () // not use test validators } + + bitcoinCore.add(restoreKeyConverterForBip: .bip44) } } diff --git a/BitcoinCashKit/BitcoinCashKit/InitialSync/BitcoinCashAddressSelector.swift b/BitcoinCashKit/BitcoinCashKit/InitialSync/BitcoinCashAddressSelector.swift deleted file mode 100644 index e22758b6..00000000 --- a/BitcoinCashKit/BitcoinCashKit/InitialSync/BitcoinCashAddressSelector.swift +++ /dev/null @@ -1,10 +0,0 @@ -import BitcoinCore - -class BitcoinCashAddressSelector: IAddressSelector { - - func getAddressVariants(addressConverter: IAddressConverter, publicKey: PublicKey) -> [String] { - let legacyAddress = (try? addressConverter.convert(keyHash: publicKey.keyHash, type: .p2pkh))?.stringValue - return [legacyAddress].compactMap { $0 } - } - -} diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index 3c4feab1..9760edf9 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D7A29489EA7A15A4B25D /* Bip.swift */; }; 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */; }; 2FA5D91DCF7F320369162C5A /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */; }; + 2FA5D97434F2504E5B4AC072 /* RestoreKeyConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D706F48D5B7F0629BCB6 /* RestoreKeyConverter.swift */; }; 2FA5D9D3DA2DF9BD402CE5DD /* SentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF037E951950A0F7FF03 /* SentTransaction.swift */; }; 2FA5D9ED521660FA5EEF360E /* MerkleBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5C9BB0E185AF72B9174 /* MerkleBlock.swift */; }; 2FA5DA409AB91D54163250D7 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D9929AB239235F9B6895 /* Logger.swift */; }; @@ -149,7 +150,6 @@ 58AAA744EF8A7193BB2EF860 /* NetworkMessageSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA201AEA32587BD5E2E12 /* NetworkMessageSerializer.swift */; }; 58AAA74E89561DE6CCDC5EC3 /* BitcoinCoreErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF2AC5C1330DFE1FAAD3 /* BitcoinCoreErrors.swift */; }; 58AAA789081293EB75B29A78 /* MempoolTransactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAD73659B838E541862BA /* MempoolTransactions.swift */; }; - 58AAA7912B48C0C21F50DBF2 /* BitcoinAddressSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA55E49708A950F951E2D /* BitcoinAddressSelector.swift */; }; 58AAA79D7763A69B80757392 /* BitsValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB994FBA1E42999E4968 /* BitsValidator.swift */; }; 58AAA7B5F60F31613515F273 /* BlockDiscoveryBatchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9BB5301D7CB5292473B /* BlockDiscoveryBatchTest.swift */; }; 58AAA8C5908EBFA5BCC2B5FC /* MerkleBlockValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC9FD7A9AD220E52F974 /* MerkleBlockValidator.swift */; }; @@ -291,6 +291,7 @@ 2FA5D632A9EFB1F0CF2523DF /* PeerHostManagerDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerHostManagerDelegateTests.swift; sourceTree = ""; }; 2FA5D65667DA8BFD91E91C8B /* GetBlockHashesTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetBlockHashesTask.swift; sourceTree = ""; }; 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReachabilityManager.swift; sourceTree = ""; }; + 2FA5D706F48D5B7F0629BCB6 /* RestoreKeyConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreKeyConverter.swift; sourceTree = ""; }; 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignatureScriptSerializer.swift; sourceTree = ""; }; 2FA5D7A29489EA7A15A4B25D /* Bip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bip.swift; sourceTree = ""; }; 2FA5D82EFF381CE6233790A9 /* KitStateProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitStateProviderTests.swift; sourceTree = ""; }; @@ -354,7 +355,6 @@ 58AAA4852F5313674AA9E2B3 /* SynchronizedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SynchronizedArray.swift; sourceTree = ""; }; 58AAA4F887DF0F5E9527C761 /* ScriptBuilderChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScriptBuilderChain.swift; sourceTree = ""; }; 58AAA5081672879DFB3D483F /* PeerTaskHandlerChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerTaskHandlerChain.swift; sourceTree = ""; }; - 58AAA55E49708A950F951E2D /* BitcoinAddressSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BitcoinAddressSelector.swift; sourceTree = ""; }; 58AAA5D8E61487D82ACFD9A6 /* NetworkMessageParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkMessageParser.swift; sourceTree = ""; }; 58AAA5EE018C74544938121F /* MerkleBlockValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MerkleBlockValidatorTests.swift; sourceTree = ""; }; 58AAA62C7C37CA2D7FD668B5 /* TransactionOutputExtractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionOutputExtractor.swift; sourceTree = ""; }; @@ -495,6 +495,7 @@ 58AAA0FE0496F520DDA29BB1 /* TransactionInfoConverter.swift */, 58AAAFE91DB09FA5E32DD881 /* BaseTransactionInfoConverter.swift */, 2FA5D7A29489EA7A15A4B25D /* Bip.swift */, + 2FA5D706F48D5B7F0629BCB6 /* RestoreKeyConverter.swift */, ); path = Core; sourceTree = ""; @@ -708,7 +709,6 @@ 58AAAEC3E09DB4796FF009DE /* InitialSync */, 2FA5D5A9656117A6FD41E814 /* PublicKeyManager.swift */, 11B3597DE3169FD250A982D5 /* ApiManager.swift */, - 58AAA55E49708A950F951E2D /* BitcoinAddressSelector.swift */, 2FA5DE7E06652FCED7B288E7 /* BloomFilterManager.swift */, 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */, 11B358481D7E7EB156912F52 /* StateManager.swift */, @@ -1392,7 +1392,6 @@ 11B35468A2ACF4B8534BB719 /* DataProvider.swift in Sources */, 11B358B84B9C6B09ADB8A0B0 /* HDWallet.swift in Sources */, 58AAA2C85BFE1C624D1A1997 /* SigHashType.swift in Sources */, - 58AAA7912B48C0C21F50DBF2 /* BitcoinAddressSelector.swift in Sources */, 2FA5DBF35EDF66F6977BA032 /* ConnectionTimeoutManager.swift in Sources */, 2FA5D024DF2A429D6606350F /* KitStateProvider.swift in Sources */, 2FA5D91DCF7F320369162C5A /* ReachabilityManager.swift in Sources */, @@ -1454,6 +1453,7 @@ 2FA5D84AA43DA05CAC68B912 /* WatchedTransactionManager.swift in Sources */, 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */, 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */, + 2FA5D97434F2504E5B4AC072 /* RestoreKeyConverter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitcoinCore/BitcoinCore/Core/Bip.swift b/BitcoinCore/BitcoinCore/Core/Bip.swift index 56efde4e..b4b3429a 100644 --- a/BitcoinCore/BitcoinCore/Core/Bip.swift +++ b/BitcoinCore/BitcoinCore/Core/Bip.swift @@ -5,7 +5,7 @@ public enum Bip { case bip49 case bip84 - var scriptType: ScriptType { + public var scriptType: ScriptType { switch self { case .bip44: return .p2pkh case .bip49: return .p2wpkhSh @@ -21,4 +21,12 @@ public enum Bip { } } + func restoreKeyConverter(addressConverter: AddressConverterChain) -> IRestoreKeyConverter { + switch self { + case .bip44: return Bip44RestoreKeyConverter(addressConverter: addressConverter) + case .bip49: return Bip49RestoreKeyConverter(addressConverter: addressConverter) + case .bip84: return Bip84RestoreKeyConverter(addressConverter: addressConverter) + } + } + } diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 77305bc8..6d44d85b 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -16,11 +16,11 @@ public class BitcoinCore { private let publicKeyManager: IPublicKeyManager private let watchedTransactionManager: IWatchedTransactionManager private let addressConverter: AddressConverterChain + private let restoreKeyConverterChain: RestoreKeyConverterChain private let unspentOutputSelector: UnspentOutputSelectorChain private let kitStateProvider: IKitStateProvider & ISyncStateListener private let scriptBuilder: ScriptBuilderChain - private let transactionBuilder: ITransactionBuilder private let transactionCreator: ITransactionCreator private let paymentAddressParser: IPaymentAddressParser @@ -56,6 +56,10 @@ public class BitcoinCore { peerTaskHandlerChain.add(handler: peerTaskHandler) } + public func add(restoreKeyConverterForBip bip: Bip) { + restoreKeyConverterChain.add(converter: bip.restoreKeyConverter(addressConverter: addressConverter)) + } + @discardableResult public func add(messageParser: IMessageParser) -> Self { networkMessageParser.add(parser: messageParser) return self @@ -90,8 +94,9 @@ public class BitcoinCore { init(storage: IStorage, cache: OutputsCache, dataProvider: IDataProvider, peerGroup: IPeerGroup, initialBlockDownload: IInitialBlockDownload, bloomFilterLoader: BloomFilterLoader, syncedReadyPeerManager: ISyncedReadyPeerManager, transactionSyncer: ITransactionSyncer, - blockValidatorChain: BlockValidatorChain, addressManager: IPublicKeyManager, addressConverter: AddressConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, - scriptBuilder: ScriptBuilderChain, transactionBuilder: ITransactionBuilder, transactionCreator: ITransactionCreator, + blockValidatorChain: BlockValidatorChain, publicKeyManager: IPublicKeyManager, addressConverter: AddressConverterChain, restoreKeyConverterChain: RestoreKeyConverterChain, + unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, + scriptBuilder: ScriptBuilderChain, transactionCreator: ITransactionCreator, paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, bip: Bip) { self.storage = storage @@ -103,12 +108,12 @@ public class BitcoinCore { self.syncedReadyPeerManager = syncedReadyPeerManager self.transactionSyncer = transactionSyncer self.blockValidatorChain = blockValidatorChain - self.publicKeyManager = addressManager + self.publicKeyManager = publicKeyManager self.addressConverter = addressConverter + self.restoreKeyConverterChain = restoreKeyConverterChain self.unspentOutputSelector = unspentOutputSelector self.kitStateProvider = kitStateProvider self.scriptBuilder = scriptBuilder - self.transactionBuilder = transactionBuilder self.transactionCreator = transactionCreator self.paymentAddressParser = paymentAddressParser @@ -157,9 +162,7 @@ extension BitcoinCore { } public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { - // TODO: convert to scriptWPKH when scriptType is P2WPKHSH ? - let address = try addressConverter.convert(keyHash: hash, type: scriptType) - return try send(to: address.stringValue, value: value, feeRate: feeRate) + return try transactionCreator.create(to: hash, scriptType: scriptType, value: value, feeRate: feeRate, senderPay: true) } func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { @@ -175,7 +178,7 @@ extension BitcoinCore { } public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress) + return try transactionCreator.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress) } public func receiveAddress() -> String { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index aa92f3f7..7e0babc6 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -10,7 +10,6 @@ public class BitcoinCoreBuilder { private var bip: Bip = .bip44 private var network: INetwork? private var paymentAddressParser: IPaymentAddressParser? - private var addressSelector: IAddressSelector? private var walletId: String? private var initialSyncApi: ISyncTransactionApi? private var logger: Logger @@ -51,11 +50,6 @@ public class BitcoinCoreBuilder { return self } - public func set(addressSelector: IAddressSelector) -> BitcoinCoreBuilder { - self.addressSelector = addressSelector - return self - } - public func set(walletId: String) -> BitcoinCoreBuilder { self.walletId = walletId return self @@ -118,9 +112,6 @@ public class BitcoinCoreBuilder { guard let paymentAddressParser = self.paymentAddressParser else { throw BuildError.noPaymentAddressParser } - guard let addressSelector = self.addressSelector else { - throw BuildError.noAddressSelector - } guard let storage = self.storage else { throw BuildError.noStorage } @@ -129,6 +120,7 @@ public class BitcoinCoreBuilder { } let addressConverter = AddressConverterChain() + let restoreKeyConverterChain = RestoreKeyConverterChain() // let dbName = "bitcoinkit-${network.javaClass}-$walletId" // let database = KitDatabase.getInstance(context, dbName) @@ -152,7 +144,7 @@ public class BitcoinCoreBuilder { let factory = Factory(network: network, networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer) - let addressManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter) + let publicKeyManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet) let myOutputsCache = OutputsCache.instance(storage: storage) let scriptConverter = ScriptConverter() @@ -163,7 +155,7 @@ public class BitcoinCoreBuilder { let transactionProcessor = TransactionProcessor(storage: storage, outputExtractor: transactionOutputExtractor, inputExtractor: transactionInputExtractor, outputsCache: myOutputsCache, outputAddressExtractor: transactionAddressExtractor, - addressManager: addressManager, listener: dataProvider) + addressManager: publicKeyManager, listener: dataProvider) let kitStateProvider = KitStateProvider() @@ -174,15 +166,15 @@ public class BitcoinCoreBuilder { let peerManager = PeerManager() let unspentOutputSelector = UnspentOutputSelectorChain() - let transactionSyncer = TransactionSyncer(storage: storage, processor: transactionProcessor, publicKeyManager: addressManager, bloomFilterManager: bloomFilterManager) + let transactionSyncer = TransactionSyncer(storage: storage, processor: transactionProcessor, publicKeyManager: publicKeyManager, bloomFilterManager: bloomFilterManager) let mempoolTransactions = MempoolTransactions(transactionSyncer: transactionSyncer) - let blockHashFetcher = BlockHashFetcher(addressSelector: addressSelector, apiManager: initialSyncApi, addressConverter: addressConverter, helper: BlockHashFetcherHelper()) + let blockHashFetcher = BlockHashFetcher(restoreKeyConverter: restoreKeyConverterChain, apiManager: initialSyncApi, helper: BlockHashFetcherHelper()) let blockDiscovery = BlockDiscoveryBatch(network: network, wallet: hdWallet, blockHashFetcher: blockHashFetcher, logger: logger) let stateManager = StateManager(storage: storage, restoreFromApi: network.syncableFromApi && syncMode == BitcoinCore.SyncMode.api) - let initialSyncer = InitialSyncer(storage: storage, listener: kitStateProvider, stateManager: stateManager, blockDiscovery: blockDiscovery, publicKeyManager: addressManager, logger: logger) + let initialSyncer = InitialSyncer(storage: storage, listener: kitStateProvider, stateManager: stateManager, blockDiscovery: blockDiscovery, publicKeyManager: publicKeyManager, logger: logger) let bloomFilterLoader = BloomFilterLoader(bloomFilterManager: bloomFilterManager, peerManager: peerManager) let watchedTransactionManager = WatchedTransactionManager() @@ -190,7 +182,7 @@ public class BitcoinCoreBuilder { let blockValidatorChain = BlockValidatorChain(proofOfWorkValidator: ProofOfWorkValidator(difficultyEncoder: DifficultyEncoder())) let blockchain = Blockchain(storage: storage, blockValidator: blockValidatorChain, factory: factory, listener: dataProvider) let checkpointBlock = BlockSyncer.checkpointBlock(network: network, syncMode: syncMode, storage: storage) - let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, publicKeyManager: addressManager, bloomFilterManager: bloomFilterManager, logger: logger) + let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, publicKeyManager: publicKeyManager, bloomFilterManager: bloomFilterManager, logger: logger) let initialBlockDownload = InitialBlockDownload(blockSyncer: blockSyncer, peerManager: peerManager, merkleBlockValidator: merkleBlockValidator, syncStateListener: kitStateProvider, logger: logger) let peerGroup = PeerGroup(factory: factory, reachabilityManager: reachabilityManager, @@ -201,10 +193,10 @@ public class BitcoinCoreBuilder { let inputSigner = InputSigner(hdWallet: hdWallet, network: network) let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() - let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, unspentOutputProvider: unspentOutputProvider, publicKeyManager: addressManager, addressConverter: addressConverter, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, - transactionSizeCalculator: transactionSizeCalculator, bip: bip) + let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, transactionSizeCalculator: transactionSizeCalculator) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) - let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager) + let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager, + addressConverter: addressConverter, publicKeyManager: publicKeyManager, bip: bip) let syncManager = SyncManager(reachabilityManager: reachabilityManager, initialSyncer: initialSyncer, peerGroup: peerGroup) @@ -217,12 +209,12 @@ public class BitcoinCoreBuilder { syncedReadyPeerManager: syncedReadyPeerManager, transactionSyncer: transactionSyncer, blockValidatorChain: blockValidatorChain, - addressManager: addressManager, + publicKeyManager: publicKeyManager, addressConverter: addressConverter, + restoreKeyConverterChain: restoreKeyConverterChain, unspentOutputSelector: unspentOutputSelector, kitStateProvider: kitStateProvider, scriptBuilder: scriptBuilder, - transactionBuilder: transactionBuilder, transactionCreator: transactionCreator, paymentAddressParser: paymentAddressParser, networkMessageParser: networkMessageParser, diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 73216849..e287d38f 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -129,8 +129,9 @@ public protocol IStorage { func publicKey(byPath: String) -> PublicKey? } -public protocol IAddressSelector { - func getAddressVariants(addressConverter: IAddressConverter, publicKey: PublicKey) -> [String] +public protocol IRestoreKeyConverter { + func keysForApiRestore(publicKey: PublicKey) -> [String] +// func bloomFilterElements(publicKey: PublicKey) -> [Data] } public protocol IPublicKeyManager { @@ -339,14 +340,16 @@ public protocol ITransactionSyncer: class { } public protocol ITransactionCreator { + func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction + func create(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction func create(from: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionBuilder { - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction - func buildTransaction(from: UnspentOutput, to: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction + func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int + func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: Address, changeAddress: Address) throws -> FullTransaction + func buildTransaction(from: UnspentOutput, to: Address, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol IBlockchain { diff --git a/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift b/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift new file mode 100644 index 00000000..aa344ab8 --- /dev/null +++ b/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift @@ -0,0 +1,79 @@ +class RestoreKeyConverterChain : IRestoreKeyConverter { + + var converters = [IRestoreKeyConverter]() + + func add(converter: IRestoreKeyConverter) { + converters.append(converter) + } + + func keysForApiRestore(publicKey: PublicKey) -> [String] { + var keys = [String]() + for converter in converters { + keys.append(contentsOf: converter.keysForApiRestore(publicKey: publicKey)) + } + + return keys.unique + } + +} + +class Bip44RestoreKeyConverter { + + let addressConverter: IAddressConverter + + init(addressConverter: IAddressConverter) { + self.addressConverter = addressConverter + } + +} + +extension Bip44RestoreKeyConverter : IRestoreKeyConverter { + + func keysForApiRestore(publicKey: PublicKey) -> [String] { + let legacyAddress = try? addressConverter.convert(publicKey: publicKey, type: .p2pkh).stringValue + + return [legacyAddress].compactMap { $0 } + } + +} + +class Bip49RestoreKeyConverter { + + let addressConverter: IAddressConverter + + init(addressConverter: IAddressConverter) { + self.addressConverter = addressConverter + } + +} + +extension Bip49RestoreKeyConverter : IRestoreKeyConverter { + + func keysForApiRestore(publicKey: PublicKey) -> [String] { + let wpkhShAddress = try? addressConverter.convert(publicKey: publicKey, type: .p2wpkhSh).stringValue + + return [wpkhShAddress].compactMap { $0 } + } + +} + + +class Bip84RestoreKeyConverter { + + let addressConverter: IAddressConverter + + init(addressConverter: IAddressConverter) { + self.addressConverter = addressConverter + } + +} + +extension Bip84RestoreKeyConverter : IRestoreKeyConverter { + + func keysForApiRestore(publicKey: PublicKey) -> [String] { + let segwitAddress = try? addressConverter.convert(publicKey: publicKey, type: .p2wpkh).stringValue + + return [segwitAddress].compactMap { $0 } + } + +} diff --git a/BitcoinCore/BitcoinCore/Managers/BitcoinAddressSelector.swift b/BitcoinCore/BitcoinCore/Managers/BitcoinAddressSelector.swift deleted file mode 100644 index 13ff5cea..00000000 --- a/BitcoinCore/BitcoinCore/Managers/BitcoinAddressSelector.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Foundation - -public class BitcoinAddressSelector: IAddressSelector { - - public init() {} - - public func getAddressVariants(addressConverter: IAddressConverter, publicKey: PublicKey) -> [String] { - let wpkhShAddress = try? addressConverter.convert(keyHash: publicKey.scriptHashForP2WPKH, type: .p2sh).stringValue - return [wpkhShAddress, publicKey.keyHash.hex].compactMap { $0 } - } - -} diff --git a/BitcoinCore/BitcoinCore/Managers/InitialSync/BlockHashFetcher.swift b/BitcoinCore/BitcoinCore/Managers/InitialSync/BlockHashFetcher.swift index 1839c963..fde5f99c 100644 --- a/BitcoinCore/BitcoinCore/Managers/InitialSync/BlockHashFetcher.swift +++ b/BitcoinCore/BitcoinCore/Managers/InitialSync/BlockHashFetcher.swift @@ -1,14 +1,12 @@ import RxSwift class BlockHashFetcher { - private let addressSelector: IAddressSelector - private let addressConverter: IAddressConverter + private let restoreKeyConverter: IRestoreKeyConverter private let apiManager: ISyncTransactionApi private let helper: IBlockHashFetcherHelper - init(addressSelector: IAddressSelector, apiManager: ISyncTransactionApi, addressConverter: IAddressConverter, helper: IBlockHashFetcherHelper) { - self.addressSelector = addressSelector - self.addressConverter = addressConverter + init(restoreKeyConverter: IRestoreKeyConverter, apiManager: ISyncTransactionApi, helper: IBlockHashFetcherHelper) { + self.restoreKeyConverter = restoreKeyConverter self.apiManager = apiManager self.helper = helper } @@ -19,7 +17,7 @@ extension BlockHashFetcher: IBlockHashFetcher { func getBlockHashes(publicKeys: [PublicKey]) -> Observable<(responses: [BlockHash], lastUsedIndex: Int)> { let addresses = publicKeys.map { - addressSelector.getAddressVariants(addressConverter: addressConverter, publicKey: $0) + restoreKeyConverter.keysForApiRestore(publicKey: $0) } return apiManager.getTransactions(addresses: addresses.flatMap { $0 }).map { [weak self] transactionResponses -> (responses: [BlockHash], lastUsedIndex: Int) in diff --git a/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift index 12e502c5..9e31427c 100644 --- a/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift @@ -2,18 +2,16 @@ import HSHDWalletKit class PublicKeyManager { - enum AddressManagerError: Error { + enum PublicKeyManagerError: Error { case noUnusedPublicKey case invalidPath } private let storage: IStorage private let hdWallet: IHDWallet - private let addressConverter: IAddressConverter - init(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter) { + init(storage: IStorage, hdWallet: IHDWallet) { self.storage = storage - self.addressConverter = addressConverter self.hdWallet = hdWallet } @@ -48,7 +46,7 @@ class PublicKeyManager { .filter({ $0.publicKey.external == external && $0.publicKey.account == 0 && !$0.used }) .sorted(by: { $0.publicKey.index < $1.publicKey.index }) .first else { - throw AddressManagerError.noUnusedPublicKey + throw PublicKeyManagerError.noUnusedPublicKey } return unusedKey.publicKey @@ -113,7 +111,7 @@ extension PublicKeyManager: IPublicKeyManager { let parts = path.split(separator: "/") guard parts.count == 3, let account = Int(parts[0]), let external = Int(parts[1]), let index = Int(parts[2]) else { - throw AddressManagerError.invalidPath + throw PublicKeyManagerError.invalidPath } if let publicKey = storage.publicKey(byPath: path) { @@ -126,8 +124,8 @@ extension PublicKeyManager: IPublicKeyManager { extension PublicKeyManager { - public static func instance(storage: IStorage, hdWallet: IHDWallet, addressConverter: IAddressConverter) -> PublicKeyManager { - let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet, addressConverter: addressConverter) + public static func instance(storage: IStorage, hdWallet: IHDWallet) -> PublicKeyManager { + let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet) try? addressManager.fillGap() return addressManager } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 405a23e6..80632d60 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -2,32 +2,23 @@ import HSCryptoKit class TransactionBuilder { enum BuildError: Error { - case noChangeAddress case feeMoreThanValue } private let unspentOutputSelector: IUnspentOutputSelector - private let unspentOutputProvider: IUnspentOutputProvider - private let publicKeyManager: IPublicKeyManager - private let addressConverter: IAddressConverter private let inputSigner: IInputSigner private let factory: IFactory private let transactionSizeCalculator: ITransactionSizeCalculator - private let bip: Bip var scriptBuilder: IScriptBuilder - init(unspentOutputSelector: IUnspentOutputSelector, unspentOutputProvider: IUnspentOutputProvider, publicKeyManager: IPublicKeyManager, addressConverter: IAddressConverter, - inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator, bip: Bip) { + init(unspentOutputSelector: IUnspentOutputSelector, inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, + factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator) { self.unspentOutputSelector = unspentOutputSelector - self.unspentOutputProvider = unspentOutputProvider - self.publicKeyManager = publicKeyManager - self.addressConverter = addressConverter self.inputSigner = inputSigner self.scriptBuilder = scriptBuilder self.factory = factory self.transactionSizeCalculator = transactionSizeCalculator - self.bip = bip } private func input(fromUnspentOutput unspentOutput: UnspentOutput) throws -> InputToSign { @@ -54,26 +45,21 @@ extension TransactionBuilder: ITransactionBuilder { // :fee method returns the fee for the given amount // If address given and it's valid, it returns the actual fee // Otherwise, it returns the estimated fee - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String? = nil) throws -> Int { - if let string = address, let _ = try? addressConverter.convert(address: string) { + func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int { + if let address = toAddress { // Actual fee - let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: string) + let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address, changeAddress: changeAddress) return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate } else { // Estimated fee // Default to .p2pkh address - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: .p2pkh, changeType: .p2pkh, senderPay: senderPay) + let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: changeAddress.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) return selectedOutputsInfo.fee } } - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: String) throws -> FullTransaction { - guard let changePubKey = try? publicKeyManager.changePublicKey() else { - throw BuildError.noChangeAddress - } - - let address = try addressConverter.convert(address: toAddress) - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: bip.scriptType, senderPay: senderPay) + func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: Address, changeAddress: Address) throws -> FullTransaction { + let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: toAddress.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) if !senderPay { guard selectedOutputsInfo.fee < value else { @@ -94,11 +80,10 @@ extension TransactionBuilder: ITransactionBuilder { let sentValue = senderPay ? value + selectedOutputsInfo.fee : value // Add :to output - outputs.append(try output(withIndex: 0, address: address, value: receivedValue)) + outputs.append(try output(withIndex: 0, address: toAddress, value: receivedValue)) // Add :change output if needed if selectedOutputsInfo.addChangeOutput { - let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) outputs.append(try output(withIndex: 1, address: changeAddress, value: selectedOutputsInfo.totalValue - sentValue)) } @@ -132,9 +117,7 @@ extension TransactionBuilder: ITransactionBuilder { return FullTransaction(header: transaction, inputs: inputsToSign.map{ $0.input }, outputs: outputs) } - func buildTransaction(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { - let address = try addressConverter.convert(address: address) - + func buildTransaction(from unspentOutput: UnspentOutput, to address: Address, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { // Calculate fee let emptySignature = Data(repeating: 0, count: TransactionSizeCalculator.signatureLength) let emptyPublicKey = Data(repeating: 0, count: TransactionSizeCalculator.pubKeyLength) diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index 9e30c5c5..26c8dd26 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -7,15 +7,22 @@ class TransactionCreator { private let transactionProcessor: ITransactionProcessor private let transactionSender: ITransactionSender private let bloomFilterManager: IBloomFilterManager + private let addressConverter: IAddressConverter + private let publicKeyManager: IPublicKeyManager + private let bip: Bip - init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender, bloomFilterManager: IBloomFilterManager) { + init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender, bloomFilterManager: IBloomFilterManager, + addressConverter: IAddressConverter, publicKeyManager: IPublicKeyManager, bip: Bip) { self.transactionBuilder = transactionBuilder self.transactionProcessor = transactionProcessor self.transactionSender = transactionSender self.bloomFilterManager = bloomFilterManager + self.addressConverter = addressConverter + self.publicKeyManager = publicKeyManager + self.bip = bip } - func processAndSend(transaction: FullTransaction) throws { + private func processAndSend(transaction: FullTransaction) throws { try transactionSender.verifyCanSend() do { @@ -27,19 +34,41 @@ class TransactionCreator { try transactionSender.send(pendingTransaction: transaction) } + private func create(to toAddress: Address, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { + let changePubKey = try publicKeyManager.changePublicKey() + let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) + + let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) + + try processAndSend(transaction: transaction) + return transaction + } + } extension TransactionCreator: ITransactionCreator { + func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int { + let toAddress = try address.map { try addressConverter.convert(address: $0) } + let changePubKey = try publicKeyManager.changePublicKey() + let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) + + return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) + } + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { - let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address) + let toAddress = try addressConverter.convert(address: address) + return try create(to: toAddress, value: value, feeRate: feeRate, senderPay: senderPay) + } - try processAndSend(transaction: transaction) - return transaction + func create(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { + let toAddress = try addressConverter.convert(keyHash: hash, type: scriptType) + return try create(to: toAddress, value: value, feeRate: feeRate, senderPay: senderPay) } func create(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { - let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + let toAddress = try addressConverter.convert(address: address) + let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddress, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) try processAndSend(transaction: transaction) return transaction diff --git a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift index 9af88232..25a33469 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift @@ -66,8 +66,8 @@ class PublicKeyManagerTests: XCTestCase { do { let _ = try manager.changePublicKey() XCTFail("Should throw exception") - } catch let error as PublicKeyManager.AddressManagerError { - XCTAssertEqual(error, PublicKeyManager.AddressManagerError.noUnusedPublicKey) + } catch let error as PublicKeyManager.PublicKeyManagerError { + XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) } catch { XCTFail("Unexpected exception thrown") } @@ -101,8 +101,8 @@ class PublicKeyManagerTests: XCTestCase { do { let _ = try manager.receivePublicKey() XCTFail("Should throw exception") - } catch let error as PublicKeyManager.AddressManagerError { - XCTAssertEqual(error, PublicKeyManager.AddressManagerError.noUnusedPublicKey) + } catch let error as PublicKeyManager.PublicKeyManagerError { + XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) } catch { XCTFail("Unexpected exception thrown") } @@ -370,7 +370,7 @@ class PublicKeyManagerTests: XCTestCase { do { _ = try manager.publicKey(byPath: "0/0") XCTFail("Expected exception") - } catch let error as PublicKeyManager.AddressManagerError { + } catch let error as PublicKeyManager.PublicKeyManagerError { XCTAssertEqual(error, .invalidPath) } catch { XCTFail("Unexpected exception") diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index 4adbdb73..fadd4ad5 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -346,7 +346,7 @@ class TransactionBuilderTests: XCTestCase { func testBuildTransaction_noChangeAddress() { stub(mockAddressManager) { mock in - when(mock.changePublicKey()).thenThrow(PublicKeyManager.AddressManagerError.noUnusedPublicKey) + when(mock.changePublicKey()).thenThrow(PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) } do { diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index 336d6e06..0f2f7b81 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -40,7 +40,6 @@ public class BitcoinKit: AbstractKit { self.storage = storage let paymentAddressParser = PaymentAddressParser(validScheme: "bitcoin", removeScheme: true) - let addressSelector = BitcoinAddressSelector() let bitcoinCore = try BitcoinCoreBuilder(minLogLevel: minLogLevel) .set(network: network) @@ -48,7 +47,6 @@ public class BitcoinKit: AbstractKit { .set(words: words) .set(bip: bip) .set(paymentAddressParser: paymentAddressParser) - .set(addressSelector: addressSelector) .set(walletId: walletId) .set(confirmationsThreshold: confirmationsThreshold) .set(peerSize: 10) @@ -77,6 +75,12 @@ public class BitcoinKit: AbstractKit { bitcoinCore.add(blockValidator: LegacyDifficultyAdjustmentValidator(encoder: difficultyEncoder, blockValidatorHelper: blockHelper, heightInterval: BitcoinCore.heightInterval, targetTimespan: BitcoinCore.heightInterval * BitcoinCore.targetSpacing, maxTargetBits: BitcoinCore.maxTargetBits)) bitcoinCore.add(blockValidator: LegacyTestNetDifficultyValidator(blockHelper: blockHelper, heightInterval: BitcoinCore.heightInterval, targetSpacing: BitcoinCore.targetSpacing, maxTargetBits: BitcoinCore.maxTargetBits)) } + + bitcoinCore.add(restoreKeyConverterForBip: bip) + if bip == .bip44 { + bitcoinCore.add(restoreKeyConverterForBip: .bip49) + bitcoinCore.add(restoreKeyConverterForBip: .bip84) + } } override open var debugInfo: String { diff --git a/DashKit/DashKit.xcodeproj/project.pbxproj b/DashKit/DashKit.xcodeproj/project.pbxproj index 19814a55..48d13a72 100644 --- a/DashKit/DashKit.xcodeproj/project.pbxproj +++ b/DashKit/DashKit.xcodeproj/project.pbxproj @@ -61,7 +61,6 @@ 58AAA99C55992149EBC964E0 /* DashExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA96578DB9BE7A8E1AA65 /* DashExtensions.swift */; }; 58AAA9A24E2E28955B893151 /* QuorumSortedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA879961D8A283F21BB9D /* QuorumSortedList.swift */; }; 58AAAA13B65A94F50CDE64C1 /* ISLockMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA9C4B7A2015BBE7EEF4B /* ISLockMessage.swift */; }; - 58AAAA292D3A413E0E9F44A7 /* DashAddressSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA1A86BE6A17469E53400 /* DashAddressSelector.swift */; }; 58AAAA59995E41B1661BBBAB /* MasternodeListDiffMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB4372CC0F55CBBE60C9 /* MasternodeListDiffMessage.swift */; }; 58AAAA8392E527FFED910B01 /* MainNet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAA839FB2C7EA770A036D /* MainNet.swift */; }; 58AAAA9B99A71DC2D87BBCDA /* InstantSend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA0399119E61DC3C34677 /* InstantSend.swift */; }; @@ -122,7 +121,6 @@ 58AAA0731BC44688D2072C72 /* TransactionLockVoteManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionLockVoteManagerTests.swift; sourceTree = ""; }; 58AAA135CAE12BD33A6D7D2E /* ConfirmedUnspentOutputProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmedUnspentOutputProvider.swift; sourceTree = ""; }; 58AAA1776A1D0A594FC7FD18 /* TransactionLockVoteMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionLockVoteMessage.swift; sourceTree = ""; }; - 58AAA1A86BE6A17469E53400 /* DashAddressSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashAddressSelector.swift; sourceTree = ""; }; 58AAA1C1D2E2538CBC33B663 /* TransactionLockMessageParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionLockMessageParser.swift; sourceTree = ""; }; 58AAA2017DB5CEA5C9B9D720 /* DarkGravityWaveTestNetValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarkGravityWaveTestNetValidator.swift; sourceTree = ""; }; 58AAA25F98854859029BE603 /* InstantTransactionState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstantTransactionState.swift; sourceTree = ""; }; @@ -288,13 +286,6 @@ name = Frameworks; sourceTree = ""; }; - 3C7B97DDBAAB290971492CBC /* InitialSync */ = { - isa = PBXGroup; - children = ( - ); - path = InitialSync; - sourceTree = ""; - }; 58AAA07E0767C45516BDD9E6 /* Storage */ = { isa = PBXGroup; children = ( @@ -383,8 +374,6 @@ 58AAA940CCEC533F3402FB59 /* Managers */ = { isa = PBXGroup; children = ( - 3C7B97DDBAAB290971492CBC /* InitialSync */, - 58AAA1A86BE6A17469E53400 /* DashAddressSelector.swift */, 58AAA135CAE12BD33A6D7D2E /* ConfirmedUnspentOutputProvider.swift */, ); path = Managers; @@ -794,7 +783,6 @@ 58AAA5F7AF7CF8E3A909BD56 /* InstantTransactionHash.swift in Sources */, 58AAAE9A737EE5D91AD2884A /* InstantTransactionState.swift in Sources */, 58AAA1992857F107D48947E9 /* DashTransactionInfo.swift in Sources */, - 58AAAA292D3A413E0E9F44A7 /* DashAddressSelector.swift in Sources */, 58AAA5020E3111ADB26B2261 /* ConfirmedUnspentOutputProvider.swift in Sources */, 58AAAA13B65A94F50CDE64C1 /* ISLockMessage.swift in Sources */, 58AAA65979EDF654C84E786F /* ISLockParser.swift in Sources */, diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index 9f155919..bdee1aa1 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -41,7 +41,6 @@ public class DashKit: AbstractKit { self.storage = storage let paymentAddressParser = PaymentAddressParser(validScheme: "dash", removeScheme: true) - let addressSelector = DashAddressSelector() let singleHasher = SingleHasher() // Use single sha256 for hash let doubleShaHasher = DoubleShaHasher() // Use doubleSha256 for hash @@ -58,7 +57,6 @@ public class DashKit: AbstractKit { .set(words: words) .set(initialSyncApi: initialSyncApi) .set(paymentAddressParser: paymentAddressParser) - .set(addressSelector: addressSelector) .set(walletId: walletId) .set(confirmationsThreshold: confirmationsThreshold) .set(peerSize: 10) @@ -139,7 +137,7 @@ public class DashKit: AbstractKit { bitcoinCore.add(peerTaskHandler: instantSend) bitcoinCore.add(inventoryItemsHandler: instantSend) // -------------------------------------- - + bitcoinCore.add(restoreKeyConverterForBip: .bip44) } private func cast(transactionInfos:[TransactionInfo]) -> [DashTransactionInfo] { diff --git a/DashKit/DashKit/Managers/DashAddressSelector.swift b/DashKit/DashKit/Managers/DashAddressSelector.swift deleted file mode 100644 index 8a0ee781..00000000 --- a/DashKit/DashKit/Managers/DashAddressSelector.swift +++ /dev/null @@ -1,12 +0,0 @@ -import BitcoinCore - -public class DashAddressSelector: IAddressSelector { - - public init() {} - - public func getAddressVariants(addressConverter: IAddressConverter, publicKey: PublicKey) -> [String] { - let address = try? addressConverter.convert(keyHash: publicKey.keyHash, type: .p2pkh).stringValue - return [address].compactMap { $0 } - } - -} From d5469b5aa367b50d4abc5bb315462255829db723 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 9 Sep 2019 16:20:19 +0600 Subject: [PATCH 025/234] PublicKeyManager as IBloomFilterProvider - Add PublicKeyManager to BloomFilterManager as IBloomFilterProvider - Call bloomFilterManager#regenerateBloomFilter in PublicKeyManager#fillGap - Remove bloomFilterManager#regenerateBloomFilter calls from BlockSyncer and TransactionSyncer --- .../BitcoinCore/Blocks/BlockSyncer.swift | 11 +++----- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 7 +++--- BitcoinCore/BitcoinCore/Core/Protocols.swift | 2 +- .../Core/RestoreKeyConverter.swift | 21 ++++++++++++++++ .../Managers/BloomFilterManager.swift | 7 ------ .../Managers/PublicKeyManager.swift | 25 ++++++++++++++++--- .../Transactions/TransactionSyncer.swift | 5 +--- .../Blocks/BlockSyncerTests.swift | 2 +- 8 files changed, 53 insertions(+), 27 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift b/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift index 9a3502e9..d6d82806 100644 --- a/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift +++ b/BitcoinCore/BitcoinCore/Blocks/BlockSyncer.swift @@ -9,7 +9,6 @@ class BlockSyncer { private let transactionProcessor: ITransactionProcessor private let blockchain: IBlockchain private let publicKeyManager: IPublicKeyManager - private let bloomFilterManager: IBloomFilterManager private let hashCheckpointThreshold: Int private var state: BlockSyncerState @@ -17,8 +16,7 @@ class BlockSyncer { private let logger: Logger? init(storage: IStorage, checkpointBlock: Block, factory: IFactory, listener: ISyncStateListener, transactionProcessor: ITransactionProcessor, - blockchain: IBlockchain, publicKeyManager: IPublicKeyManager, bloomFilterManager: IBloomFilterManager, - hashCheckpointThreshold: Int, logger: Logger?, state: BlockSyncerState + blockchain: IBlockchain, publicKeyManager: IPublicKeyManager, hashCheckpointThreshold: Int, logger: Logger?, state: BlockSyncerState ) { self.storage = storage self.checkpointBlock = checkpointBlock @@ -26,7 +24,6 @@ class BlockSyncer { self.transactionProcessor = transactionProcessor self.blockchain = blockchain self.publicKeyManager = publicKeyManager - self.bloomFilterManager = bloomFilterManager self.hashCheckpointThreshold = hashCheckpointThreshold self.listener = listener self.logger = logger @@ -58,7 +55,6 @@ class BlockSyncer { private func handlePartialBlocks() throws { try publicKeyManager.fillGap() - bloomFilterManager.regenerateBloomFilter() state.iteration(hasPartialBlocks: false) } @@ -169,12 +165,11 @@ extension BlockSyncer: IBlockSyncer { extension BlockSyncer { public static func instance(storage: IStorage, checkpointBlock: Block, factory: IFactory, listener: ISyncStateListener, - transactionProcessor: ITransactionProcessor, blockchain: IBlockchain, publicKeyManager: IPublicKeyManager, bloomFilterManager: IBloomFilterManager, + transactionProcessor: ITransactionProcessor, blockchain: IBlockchain, publicKeyManager: IPublicKeyManager, hashCheckpointThreshold: Int = 100, logger: Logger? = nil, state: BlockSyncerState = BlockSyncerState()) -> BlockSyncer { let syncer = BlockSyncer(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: listener, transactionProcessor: transactionProcessor, - blockchain: blockchain, publicKeyManager: publicKeyManager, bloomFilterManager: bloomFilterManager, - hashCheckpointThreshold: hashCheckpointThreshold, logger: logger, state: state) + blockchain: blockchain, publicKeyManager: publicKeyManager, hashCheckpointThreshold: hashCheckpointThreshold, logger: logger, state: state) listener.initialBestBlockHeightUpdated(height: syncer.localDownloadedBestBlockHeight) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 7e0babc6..7d21bc2b 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -144,7 +144,7 @@ public class BitcoinCoreBuilder { let factory = Factory(network: network, networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer) - let publicKeyManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet) + let publicKeyManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet, restoreKeyConverter: restoreKeyConverterChain) let myOutputsCache = OutputsCache.instance(storage: storage) let scriptConverter = ScriptConverter() @@ -166,7 +166,7 @@ public class BitcoinCoreBuilder { let peerManager = PeerManager() let unspentOutputSelector = UnspentOutputSelectorChain() - let transactionSyncer = TransactionSyncer(storage: storage, processor: transactionProcessor, publicKeyManager: publicKeyManager, bloomFilterManager: bloomFilterManager) + let transactionSyncer = TransactionSyncer(storage: storage, processor: transactionProcessor, publicKeyManager: publicKeyManager) let mempoolTransactions = MempoolTransactions(transactionSyncer: transactionSyncer) let blockHashFetcher = BlockHashFetcher(restoreKeyConverter: restoreKeyConverterChain, apiManager: initialSyncApi, helper: BlockHashFetcherHelper()) @@ -182,7 +182,7 @@ public class BitcoinCoreBuilder { let blockValidatorChain = BlockValidatorChain(proofOfWorkValidator: ProofOfWorkValidator(difficultyEncoder: DifficultyEncoder())) let blockchain = Blockchain(storage: storage, blockValidator: blockValidatorChain, factory: factory, listener: dataProvider) let checkpointBlock = BlockSyncer.checkpointBlock(network: network, syncMode: syncMode, storage: storage) - let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, publicKeyManager: publicKeyManager, bloomFilterManager: bloomFilterManager, logger: logger) + let blockSyncer = BlockSyncer.instance(storage: storage, checkpointBlock: checkpointBlock, factory: factory, listener: kitStateProvider, transactionProcessor: transactionProcessor, blockchain: blockchain, publicKeyManager: publicKeyManager, logger: logger) let initialBlockDownload = InitialBlockDownload(blockSyncer: blockSyncer, peerManager: peerManager, merkleBlockValidator: merkleBlockValidator, syncStateListener: kitStateProvider, logger: logger) let peerGroup = PeerGroup(factory: factory, reachabilityManager: reachabilityManager, @@ -230,6 +230,7 @@ public class BitcoinCoreBuilder { transactionProcessor.transactionListener = watchedTransactionManager bloomFilterManager.add(provider: watchedTransactionManager) + bloomFilterManager.add(provider: publicKeyManager) peerGroup.peerTaskHandler = bitcoinCore.peerTaskHandlerChain peerGroup.inventoryItemsHandler = bitcoinCore.inventoryItemsHandlerChain diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index e287d38f..0db0c8e3 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -131,7 +131,7 @@ public protocol IStorage { public protocol IRestoreKeyConverter { func keysForApiRestore(publicKey: PublicKey) -> [String] -// func bloomFilterElements(publicKey: PublicKey) -> [Data] + func bloomFilterElements(publicKey: PublicKey) -> [Data] } public protocol IPublicKeyManager { diff --git a/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift b/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift index aa344ab8..e8359fd7 100644 --- a/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift +++ b/BitcoinCore/BitcoinCore/Core/RestoreKeyConverter.swift @@ -15,6 +15,15 @@ class RestoreKeyConverterChain : IRestoreKeyConverter { return keys.unique } + func bloomFilterElements(publicKey: PublicKey) -> [Data] { + var keys = [Data]() + for converter in converters { + keys.append(contentsOf: converter.bloomFilterElements(publicKey: publicKey)) + } + + return keys.unique + } + } class Bip44RestoreKeyConverter { @@ -35,6 +44,10 @@ extension Bip44RestoreKeyConverter : IRestoreKeyConverter { return [legacyAddress].compactMap { $0 } } + func bloomFilterElements(publicKey: PublicKey) -> [Data] { + return [publicKey.keyHash, publicKey.raw] + } + } class Bip49RestoreKeyConverter { @@ -55,6 +68,10 @@ extension Bip49RestoreKeyConverter : IRestoreKeyConverter { return [wpkhShAddress].compactMap { $0 } } + func bloomFilterElements(publicKey: PublicKey) -> [Data] { + return [publicKey.scriptHashForP2WPKH] + } + } @@ -76,4 +93,8 @@ extension Bip84RestoreKeyConverter : IRestoreKeyConverter { return [segwitAddress].compactMap { $0 } } + func bloomFilterElements(publicKey: PublicKey) -> [Data] { + return [publicKey.keyHash] + } + } diff --git a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift index d182c23e..00dca7e2 100644 --- a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift @@ -40,13 +40,6 @@ extension BloomFilterManager: IBloomFilterManager { func regenerateBloomFilter() { var elements = [Data]() - let publicKeys = storage.publicKeys() - for publicKey in publicKeys { - elements.append(publicKey.keyHash) - elements.append(publicKey.raw) - elements.append(publicKey.scriptHashForP2WPKH) - } - var outputs = storage.outputsWithPublicKeys().filter { output in return output.output.scriptType == ScriptType.p2wpkh || output.output.scriptType == ScriptType.p2pk || output.output.scriptType == ScriptType.p2wpkhSh } diff --git a/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift index 9e31427c..ca32ad7e 100644 --- a/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PublicKeyManager.swift @@ -7,12 +7,15 @@ class PublicKeyManager { case invalidPath } + private let restoreKeyConverter: IRestoreKeyConverter private let storage: IStorage private let hdWallet: IHDWallet + weak var bloomFilterManager: IBloomFilterManager? - init(storage: IStorage, hdWallet: IHDWallet) { + init(storage: IStorage, hdWallet: IHDWallet, restoreKeyConverter: IRestoreKeyConverter) { self.storage = storage self.hdWallet = hdWallet + self.restoreKeyConverter = restoreKeyConverter } private func fillGap(publicKeysWithUsedStates: [PublicKeyWithUsedState], account: Int, external: Bool) throws { @@ -77,6 +80,8 @@ extension PublicKeyManager: IPublicKeyManager { try fillGap(publicKeysWithUsedStates: publicKeysWithUsedStates, account: i, external: true) try fillGap(publicKeysWithUsedStates: publicKeysWithUsedStates, account: i, external: false) } + + bloomFilterManager?.regenerateBloomFilter() } func addKeys(keys: [PublicKey]) throws { @@ -122,10 +127,24 @@ extension PublicKeyManager: IPublicKeyManager { } } +extension PublicKeyManager: IBloomFilterProvider { + + func filterElements() -> [Data] { + var elements = [Data]() + + for publicKey in storage.publicKeys() { + elements.append(contentsOf: restoreKeyConverter.bloomFilterElements(publicKey: publicKey)) + } + + return elements + } + +} + extension PublicKeyManager { - public static func instance(storage: IStorage, hdWallet: IHDWallet) -> PublicKeyManager { - let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet) + public static func instance(storage: IStorage, hdWallet: IHDWallet, restoreKeyConverter: IRestoreKeyConverter) -> PublicKeyManager { + let addressManager = PublicKeyManager(storage: storage, hdWallet: hdWallet, restoreKeyConverter: restoreKeyConverter) try? addressManager.fillGap() return addressManager } diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift index 5627ebd0..d922f3ff 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionSyncer.swift @@ -4,17 +4,15 @@ public class TransactionSyncer { private let storage: IStorage private let transactionProcessor: ITransactionProcessor private let publicKeyManager: IPublicKeyManager - private let bloomFilterManager: IBloomFilterManager private let maxRetriesCount: Int private let retriesPeriod: Double // seconds private let totalRetriesPeriod: Double // seconds - init(storage: IStorage, processor: ITransactionProcessor, publicKeyManager: IPublicKeyManager, bloomFilterManager: IBloomFilterManager, + init(storage: IStorage, processor: ITransactionProcessor, publicKeyManager: IPublicKeyManager, maxRetriesCount: Int = 3, retriesPeriod: Double = 60, totalRetriesPeriod: Double = 60 * 60 * 24) { self.storage = storage self.transactionProcessor = processor self.publicKeyManager = publicKeyManager - self.bloomFilterManager = bloomFilterManager self.maxRetriesCount = maxRetriesCount self.retriesPeriod = retriesPeriod self.totalRetriesPeriod = totalRetriesPeriod @@ -68,7 +66,6 @@ extension TransactionSyncer: ITransactionSyncer { if needToUpdateBloomFilter { try? publicKeyManager.fillGap() - bloomFilterManager.regenerateBloomFilter() } } diff --git a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift index 0454f120..ffa7f064 100644 --- a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift @@ -59,7 +59,7 @@ class BlockSyncerTests: QuickSpec { let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, hashCheckpointThreshold: 100) + blockchain: mockBlockchain, publicKeyManager: mockAddressManager, hashCheckpointThreshold: 100) verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) verifyNoMoreInteractions(mockListener) From b801a27dc4a1d48c8b2f46e8e011b6491fae0ea9 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 9 Sep 2019 18:13:14 +0600 Subject: [PATCH 026/234] Extract outpoints bloom filter elements to IrregularOutputFinder class --- .../BitcoinCore.xcodeproj/project.pbxproj | 4 ++ .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 6 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 4 ++ .../Managers/BloomFilterManager.swift | 34 +--------- .../Managers/IrregularOutputFinder.swift | 63 +++++++++++++++++++ .../Transactions/TransactionProcessor.swift | 21 +++---- .../Managers/BloomFilterManagerTests.swift | 2 +- 7 files changed, 84 insertions(+), 50 deletions(-) create mode 100644 BitcoinCore/BitcoinCore/Managers/IrregularOutputFinder.swift diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index 9760edf9..44192a55 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -75,6 +75,7 @@ 2FA5D67384D58067F71CD6B2 /* InputSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF9995FA7DD7556893F7 /* InputSigner.swift */; }; 2FA5D6EC6F4A32E3489F89B0 /* TransactionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D307095385625960B0A0 /* TransactionMessage.swift */; }; 2FA5D74EDAFB77BC86918E63 /* PeerDiscovery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DE9CAB1F15B04E2C54C9 /* PeerDiscovery.swift */; }; + 2FA5D761883BB2CCC770B790 /* IrregularOutputFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DE58A18A9451DFC02356 /* IrregularOutputFinder.swift */; }; 2FA5D806A7727E3BED0E54E7 /* DataObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D50922572F91F966DD4A /* DataObjects.swift */; }; 2FA5D8284C7C94155AC50B7B /* GetBlockHashesTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DFC7FA0A5363BFFB8E28 /* GetBlockHashesTaskTests.swift */; }; 2FA5D84AA43DA05CAC68B912 /* WatchedTransactionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */; }; @@ -312,6 +313,7 @@ 2FA5DCD21585D41C520DF764 /* TransactionInputSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionInputSerializer.swift; sourceTree = ""; }; 2FA5DDAE64200E386070668C /* TransactionBuilderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionBuilderTests.swift; sourceTree = ""; }; 2FA5DDED90E11DEC3647B164 /* PeerAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAddress.swift; sourceTree = ""; }; + 2FA5DE58A18A9451DFC02356 /* IrregularOutputFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IrregularOutputFinder.swift; sourceTree = ""; }; 2FA5DE7E06652FCED7B288E7 /* BloomFilterManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BloomFilterManager.swift; sourceTree = ""; }; 2FA5DE9CAB1F15B04E2C54C9 /* PeerDiscovery.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerDiscovery.swift; sourceTree = ""; }; 2FA5DEC11B1DA4DA336DBB28 /* ConnectionTimeoutManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionTimeoutManager.swift; sourceTree = ""; }; @@ -718,6 +720,7 @@ 58AAA7A391E4B2F8C4E99B97 /* SyncedReadyPeerManager.swift */, 58AAA67094BBC66D3A385341 /* UnspentOutputSelectorSingleNoChange.swift */, 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */, + 2FA5DE58A18A9451DFC02356 /* IrregularOutputFinder.swift */, ); path = Managers; sourceTree = ""; @@ -1454,6 +1457,7 @@ 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */, 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */, 2FA5D97434F2504E5B4AC072 /* RestoreKeyConverter.swift in Sources */, + 2FA5D761883BB2CCC770B790 /* IrregularOutputFinder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 7d21bc2b..929581a1 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -147,6 +147,7 @@ public class BitcoinCoreBuilder { let publicKeyManager = PublicKeyManager.instance(storage: storage, hdWallet: hdWallet, restoreKeyConverter: restoreKeyConverterChain) let myOutputsCache = OutputsCache.instance(storage: storage) + let irregularOutputFinder = IrregularOutputFinder(storage: storage) let scriptConverter = ScriptConverter() let transactionInputExtractor = TransactionInputExtractor(storage: storage, scriptConverter: scriptConverter, addressConverter: addressConverter, logger: logger) let transactionKeySetter = TransactionPublicKeySetter(storage: storage) @@ -155,14 +156,14 @@ public class BitcoinCoreBuilder { let transactionProcessor = TransactionProcessor(storage: storage, outputExtractor: transactionOutputExtractor, inputExtractor: transactionInputExtractor, outputsCache: myOutputsCache, outputAddressExtractor: transactionAddressExtractor, - addressManager: publicKeyManager, listener: dataProvider) + addressManager: publicKeyManager, irregularOutputFinder: irregularOutputFinder, listener: dataProvider) let kitStateProvider = KitStateProvider() let peerDiscovery = PeerDiscovery() let peerAddressManager = PeerAddressManager(storage: storage, dnsSeeds: network.dnsSeeds, peerDiscovery: peerDiscovery, logger: logger) peerDiscovery.peerAddressManager = peerAddressManager - let bloomFilterManager = BloomFilterManager(storage: storage, factory: factory) + let bloomFilterManager = BloomFilterManager(factory: factory) let peerManager = PeerManager() let unspentOutputSelector = UnspentOutputSelectorChain() @@ -231,6 +232,7 @@ public class BitcoinCoreBuilder { bloomFilterManager.add(provider: watchedTransactionManager) bloomFilterManager.add(provider: publicKeyManager) + bloomFilterManager.add(provider: irregularOutputFinder) peerGroup.peerTaskHandler = bitcoinCore.peerTaskHandlerChain peerGroup.inventoryItemsHandler = bitcoinCore.inventoryItemsHandlerChain diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 0db0c8e3..3c8641fb 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -549,3 +549,7 @@ protocol IBloomFilterProvider: AnyObject { var bloomFilterManager: IBloomFilterManager? { set get } func filterElements() -> [Data] } + +protocol IIrregularOutputFinder { + func hasIrregularOutput(outputs: [Output]) -> Bool +} \ No newline at end of file diff --git a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift index 00dca7e2..6e984db4 100644 --- a/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/BloomFilterManager.swift @@ -3,31 +3,14 @@ class BloomFilterManager { private var providers = [IBloomFilterProvider]() - private let storage: IStorage private let factory: IFactory weak var delegate: IBloomFilterManagerDelegate? var bloomFilter: BloomFilter? - init(storage: IStorage, factory: IFactory) { - self.storage = storage + init(factory: IFactory) { self.factory = factory } - - private func needToSetToBloomFilter(output: OutputWithPublicKey, bestBlockHeight: Int) -> Bool { - // Need to set if output is unspent - guard let _ = output.spendingInput else { - return true - } - - if let spendingBlockHeight = output.spendingBlockHeight { - // If output is spent, we still need to set to bloom filter if it hasn't at least 100 confirmations - return bestBlockHeight - spendingBlockHeight < 100 - } - - // if output is spent by a mempool transaction, that is, spending input's transaction has not a block - return true - } } extension BloomFilterManager: IBloomFilterManager { @@ -40,21 +23,6 @@ extension BloomFilterManager: IBloomFilterManager { func regenerateBloomFilter() { var elements = [Data]() - var outputs = storage.outputsWithPublicKeys().filter { output in - return output.output.scriptType == ScriptType.p2wpkh || output.output.scriptType == ScriptType.p2pk || output.output.scriptType == ScriptType.p2wpkhSh - } - - if let bestBlockHeight = storage.lastBlock?.height { - outputs = outputs.filter { - self.needToSetToBloomFilter(output: $0, bestBlockHeight: bestBlockHeight) - } - } - - for outputWithPublicKey in outputs { - let outpoint = outputWithPublicKey.output.transactionHash + byteArrayLittleEndian(int: outputWithPublicKey.output.index) - elements.append(outpoint) - } - for provider in providers { elements.append(contentsOf: provider.filterElements()) } diff --git a/BitcoinCore/BitcoinCore/Managers/IrregularOutputFinder.swift b/BitcoinCore/BitcoinCore/Managers/IrregularOutputFinder.swift new file mode 100644 index 00000000..ea16e134 --- /dev/null +++ b/BitcoinCore/BitcoinCore/Managers/IrregularOutputFinder.swift @@ -0,0 +1,63 @@ +class IrregularOutputFinder { + + private let irregularScriptTypes: [ScriptType] = [.p2wpkh, .p2pk, .p2wpkhSh] + private let storage: IStorage + var bloomFilterManager: IBloomFilterManager? = nil + + init(storage: IStorage) { + self.storage = storage + } + + private func needToSetToBloomFilter(output: OutputWithPublicKey, bestBlockHeight: Int) -> Bool { + // Need to set if output is unspent + guard let _ = output.spendingInput else { + return true + } + + if let spendingBlockHeight = output.spendingBlockHeight { + // If output is spent, we still need to set to bloom filter if it hasn't at least 100 confirmations + return bestBlockHeight - spendingBlockHeight < 100 + } + + // if output is spent by a mempool transaction, that is, spending input's transaction has not a block + return true + } + +} + +extension IrregularOutputFinder: IIrregularOutputFinder { + + func hasIrregularOutput(outputs: [Output]) -> Bool { + for output in outputs { + if output.publicKeyPath != nil, irregularScriptTypes.contains(output.scriptType) { + return true + } + } + + return false + } + +} + +extension IrregularOutputFinder: IBloomFilterProvider { + + func filterElements() -> [Data] { + var elements = [Data]() + + var outputs = storage.outputsWithPublicKeys().filter { irregularScriptTypes.contains($0.output.scriptType) } + + if let bestBlockHeight = storage.lastBlock?.height { + outputs = outputs.filter { + self.needToSetToBloomFilter(output: $0, bestBlockHeight: bestBlockHeight) + } + } + + for outputWithPublicKey in outputs { + let outpoint = outputWithPublicKey.output.transactionHash + byteArrayLittleEndian(int: outputWithPublicKey.output.index) + elements.append(outpoint) + } + + return elements + } + +} diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift index bc135a45..68e37038 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionProcessor.swift @@ -7,6 +7,7 @@ class TransactionProcessor { private let outputAddressExtractor: ITransactionOutputAddressExtractor private let outputsCache: IOutputsCache private let publicKeyManager: IPublicKeyManager + private let irregularOutputFinder: IIrregularOutputFinder weak var listener: IBlockchainDataListener? weak var transactionListener: ITransactionListener? @@ -14,8 +15,9 @@ class TransactionProcessor { private let dateGenerator: () -> Date private let queue: DispatchQueue - init(storage: IStorage, outputExtractor: ITransactionExtractor, inputExtractor: ITransactionExtractor, outputsCache: IOutputsCache, outputAddressExtractor: ITransactionOutputAddressExtractor, addressManager: IPublicKeyManager, listener: IBlockchainDataListener? = nil, - dateGenerator: @escaping () -> Date = Date.init, queue: DispatchQueue = DispatchQueue(label: "Transactions", qos: .background + init(storage: IStorage, outputExtractor: ITransactionExtractor, inputExtractor: ITransactionExtractor, outputsCache: IOutputsCache, + outputAddressExtractor: ITransactionOutputAddressExtractor, addressManager: IPublicKeyManager, irregularOutputFinder: IIrregularOutputFinder, + listener: IBlockchainDataListener? = nil, dateGenerator: @escaping () -> Date = Date.init, queue: DispatchQueue = DispatchQueue(label: "Transactions", qos: .background )) { self.storage = storage self.outputExtractor = outputExtractor @@ -23,21 +25,12 @@ class TransactionProcessor { self.outputAddressExtractor = outputAddressExtractor self.outputsCache = outputsCache self.publicKeyManager = addressManager + self.irregularOutputFinder = irregularOutputFinder self.listener = listener self.dateGenerator = dateGenerator self.queue = queue } - private func expiresBloomFilter(outputs: [Output]) -> Bool { - for output in outputs { - if output.publicKeyPath != nil, (output.scriptType == .p2wpkh || output.scriptType == .p2pk || output.scriptType == .p2wpkhSh) { - return true - } - } - - return false - } - private func process(transaction: FullTransaction) { outputExtractor.extract(transaction: transaction) if outputsCache.hasOutputs(forInputs: transaction.inputs) { @@ -97,7 +90,7 @@ extension TransactionProcessor: ITransactionProcessor { inserted.append(transaction.header) if !skipCheckBloomFilter { - needToUpdateBloomFilter = needToUpdateBloomFilter || self.publicKeyManager.gapShifts() || self.expiresBloomFilter(outputs: transaction.outputs) + needToUpdateBloomFilter = needToUpdateBloomFilter || self.publicKeyManager.gapShifts() || self.irregularOutputFinder.hasIrregularOutput(outputs: transaction.outputs) } } } @@ -121,7 +114,7 @@ extension TransactionProcessor: ITransactionProcessor { try storage.add(transaction: transaction) listener?.onUpdate(updated: [], inserted: [transaction.header], inBlock: nil) - if expiresBloomFilter(outputs: transaction.outputs) { + if irregularOutputFinder.hasIrregularOutput(outputs: transaction.outputs) { throw BloomFilterManager.BloomFilterExpired() } } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift index 5563d65e..057f9b81 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift @@ -28,7 +28,7 @@ class BloomFilterManagerTests: QuickSpec { when(mock.lastBlock.get).thenReturn(lastBlock) } - manager = BloomFilterManager(storage: mockStorage, factory: mockFactory) + manager = BloomFilterManager(factory: mockFactory) manager.delegate = mockBloomFilterManagerDelegate } From af6778247d0cd3e1111ff1251eadba56fcc82d5e Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Wed, 11 Sep 2019 13:52:57 +0600 Subject: [PATCH 027/234] Add TransactionFeeCalculator - Extract UnspentOutputSelector and TransactionSizeCalculator from TransactionBuilder to TransactionFeeCalculator - Move "fee" method from TransactionCreator to TransactionFeeCalculator --- .../BitcoinCore.xcodeproj/project.pbxproj | 8 + .../BitcoinCore/Core/BitcoinCore.swift | 10 +- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 8 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 10 +- .../Builder/TransactionBuilder.swift | 71 +- .../Transactions/TransactionCreator.swift | 31 +- .../TransactionFeeCalculator.swift | 48 + .../Blocks/BlockSyncerTests.swift | 1118 ++++++++--------- BitcoinCore/BitcoinCoreTests/Extensions.swift | 16 + .../Managers/PublicKeyManagerTests.swift | 770 ++++++------ BitcoinCore/BitcoinCoreTests/TestData.swift | 9 + .../Transactions/Builder/SegWitAddress.swift | 27 + .../Builder/TransactionBuilderTests.swift | 740 +++++------ .../TransactionCreatorTests.swift | 421 ++++--- .../TransactionProcessorTests.swift | 1068 ++++++++-------- .../Transactions/TransactionSyncerTests.swift | 494 ++++---- 16 files changed, 2473 insertions(+), 2376 deletions(-) create mode 100644 BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift create mode 100644 BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index 44192a55..da85a628 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ 2FA5D2239DF0C24C4CC6366B /* PeerHostManagerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D632A9EFB1F0CF2523DF /* PeerHostManagerDelegateTests.swift */; }; 2FA5D26905936BB701998750 /* GetBlockHashesTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D65667DA8BFD91E91C8B /* GetBlockHashesTask.swift */; }; 2FA5D2E6BA34E1A932C9C16B /* PublicKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5A9656117A6FD41E814 /* PublicKeyManager.swift */; }; + 2FA5D2E6CAD495129575C402 /* SegWitAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF8081FB47483ABBA87E /* SegWitAddress.swift */; }; 2FA5D371584B554068CCAD8E /* IPeerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D99586B15F4477D695F7 /* IPeerTests.swift */; }; 2FA5D37E8B4A6D9B89410173 /* SendTransactionTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D2DAB8485F5F9E2E23B6 /* SendTransactionTaskTests.swift */; }; 2FA5D39FBC802B5EF03AD53A /* TransactionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D11405173CD80A6B9904 /* TransactionBuilder.swift */; }; @@ -71,6 +72,7 @@ 2FA5D5FB8B7F7B1E65A156BF /* GetMerkleBlocksTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D2907674257196C4F586 /* GetMerkleBlocksTaskTests.swift */; }; 2FA5D5FE71A528D363227585 /* PublicKeyManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D05FCBFFE93D46F8C292 /* PublicKeyManagerTests.swift */; }; 2FA5D6129186D79E7C7DD57F /* IPeerTaskDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DFB5B9A13478ACBB61EB /* IPeerTaskDelegateTests.swift */; }; + 2FA5D653017C871A9F2E387D /* TransactionFeeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D23F2B71A6AC790350FF /* TransactionFeeCalculator.swift */; }; 2FA5D65869320A18AFB8CE98 /* BlockHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DB1F056B264F829BFDDD /* BlockHash.swift */; }; 2FA5D67384D58067F71CD6B2 /* InputSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF9995FA7DD7556893F7 /* InputSigner.swift */; }; 2FA5D6EC6F4A32E3489F89B0 /* TransactionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D307095385625960B0A0 /* TransactionMessage.swift */; }; @@ -274,6 +276,7 @@ 2FA5D11405173CD80A6B9904 /* TransactionBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionBuilder.swift; sourceTree = ""; }; 2FA5D131E769F7E4C62CC49F /* DirectoryHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryHelper.swift; sourceTree = ""; }; 2FA5D23A8EC7BB3569F51ABD /* AddressConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressConverterTests.swift; sourceTree = ""; }; + 2FA5D23F2B71A6AC790350FF /* TransactionFeeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFeeCalculator.swift; sourceTree = ""; }; 2FA5D27387B0CF46C2B0A22A /* PeerManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerManagerTests.swift; sourceTree = ""; }; 2FA5D2907674257196C4F586 /* GetMerkleBlocksTaskTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMerkleBlocksTaskTests.swift; sourceTree = ""; }; 2FA5D2DAB8485F5F9E2E23B6 /* SendTransactionTaskTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTransactionTaskTests.swift; sourceTree = ""; }; @@ -322,6 +325,7 @@ 2FA5DF037E951950A0F7FF03 /* SentTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentTransaction.swift; sourceTree = ""; }; 2FA5DF1470723462D2CF707B /* TransactionProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionProcessor.swift; sourceTree = ""; }; 2FA5DF41CAF034B375E58E7A /* IPeerGroupTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPeerGroupTests.swift; sourceTree = ""; }; + 2FA5DF8081FB47483ABBA87E /* SegWitAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegWitAddress.swift; sourceTree = ""; }; 2FA5DF96100D81EACBBC87F6 /* PeerConnectionDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerConnectionDelegateTests.swift; sourceTree = ""; }; 2FA5DF9995FA7DD7556893F7 /* InputSigner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputSigner.swift; sourceTree = ""; }; 2FA5DF9ADB516A45C797889D /* PeerAddressManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerAddressManagerTests.swift; sourceTree = ""; }; @@ -659,6 +663,7 @@ 3C7B983C4EC7F79E44560A7C /* TransactionPublicKeySetter.swift */, 58AAAA7C33E2529A70D0E529 /* TransactionSizeCalculator.swift */, D373EB82215A2A6C00124298 /* TransactionSyncer.swift */, + 2FA5D23F2B71A6AC790350FF /* TransactionFeeCalculator.swift */, ); path = Transactions; sourceTree = ""; @@ -805,6 +810,7 @@ 2FA5DA9D7F78A26107F7209C /* InputSignerTests.swift */, 2FA5DDAE64200E386070668C /* TransactionBuilderTests.swift */, 58AAA422686CE355CACA11DF /* ScriptBuilderTests.swift */, + 2FA5DF8081FB47483ABBA87E /* SegWitAddress.swift */, ); path = Builder; sourceTree = ""; @@ -1458,6 +1464,8 @@ 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */, 2FA5D97434F2504E5B4AC072 /* RestoreKeyConverter.swift in Sources */, 2FA5D761883BB2CCC770B790 /* IrregularOutputFinder.swift in Sources */, + 2FA5D2E6CAD495129575C402 /* SegWitAddress.swift in Sources */, + 2FA5D653017C871A9F2E387D /* TransactionFeeCalculator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 6d44d85b..1348fe0a 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -23,6 +23,7 @@ public class BitcoinCore { private let scriptBuilder: ScriptBuilderChain private let transactionCreator: ITransactionCreator + private let transactionFeeCalculator: ITransactionFeeCalculator private let paymentAddressParser: IPaymentAddressParser private let networkMessageSerializer: NetworkMessageSerializer @@ -96,7 +97,7 @@ public class BitcoinCore { syncedReadyPeerManager: ISyncedReadyPeerManager, transactionSyncer: ITransactionSyncer, blockValidatorChain: BlockValidatorChain, publicKeyManager: IPublicKeyManager, addressConverter: AddressConverterChain, restoreKeyConverterChain: RestoreKeyConverterChain, unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, - scriptBuilder: ScriptBuilderChain, transactionCreator: ITransactionCreator, + scriptBuilder: ScriptBuilderChain, transactionCreator: ITransactionCreator, transactionFeeCalculator: ITransactionFeeCalculator, paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, bip: Bip) { self.storage = storage @@ -115,6 +116,7 @@ public class BitcoinCore { self.kitStateProvider = kitStateProvider self.scriptBuilder = scriptBuilder self.transactionCreator = transactionCreator + self.transactionFeeCalculator = transactionFeeCalculator self.paymentAddressParser = paymentAddressParser self.networkMessageParser = networkMessageParser @@ -178,7 +180,11 @@ extension BitcoinCore { } public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - return try transactionCreator.fee(for: value, feeRate: feeRate, senderPay: senderPay, address: toAddress) + let toAddress = try toAddress.map { try addressConverter.convert(address: $0) } + let changePubKey = try publicKeyManager.changePublicKey() + let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) + + return try transactionFeeCalculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) } public func receiveAddress() -> String { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 929581a1..cdd11a3e 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -194,10 +194,11 @@ public class BitcoinCoreBuilder { let inputSigner = InputSigner(hdWallet: hdWallet, network: network) let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() - let transactionBuilder = TransactionBuilder(unspentOutputSelector: unspentOutputSelector, inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory, transactionSizeCalculator: transactionSizeCalculator) + let transactionBuilder = TransactionBuilder(inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory) + let transactionFeeCalculator = TransactionFeeCalculator(unspentOutputSelector: unspentOutputSelector, transactionSizeCalculator: transactionSizeCalculator, transactionBuilder: transactionBuilder) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) - let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, bloomFilterManager: bloomFilterManager, - addressConverter: addressConverter, publicKeyManager: publicKeyManager, bip: bip) + let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, transactionFeeCalculator: transactionFeeCalculator, + bloomFilterManager: bloomFilterManager, addressConverter: addressConverter, publicKeyManager: publicKeyManager, bip: bip) let syncManager = SyncManager(reachabilityManager: reachabilityManager, initialSyncer: initialSyncer, peerGroup: peerGroup) @@ -217,6 +218,7 @@ public class BitcoinCoreBuilder { kitStateProvider: kitStateProvider, scriptBuilder: scriptBuilder, transactionCreator: transactionCreator, + transactionFeeCalculator: transactionFeeCalculator, paymentAddressParser: paymentAddressParser, networkMessageParser: networkMessageParser, networkMessageSerializer: networkMessageSerializer, diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 3c8641fb..c866bbd0 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -340,16 +340,20 @@ public protocol ITransactionSyncer: class { } public protocol ITransactionCreator { - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction func create(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction func create(from: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionBuilder { + func buildTransaction(value: Int, unspentOutputs: [UnspentOutput], fee: Int, senderPay: Bool, toAddress: Address, changeAddress: Address?) throws -> FullTransaction + func buildTransaction(from: UnspentOutput, to: Address, fee: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction +} + +protocol ITransactionFeeCalculator { func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: Address, changeAddress: Address) throws -> FullTransaction - func buildTransaction(from: UnspentOutput, to: Address, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction + func feeWithUnspentOutputs(value: Int, feeRate: Int, toScriptType: ScriptType, changeScriptType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo + func fee(inputScriptType: ScriptType, outputScriptType: ScriptType, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) -> Int } protocol IBlockchain { diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 80632d60..5228dd33 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -3,22 +3,17 @@ import HSCryptoKit class TransactionBuilder { enum BuildError: Error { case feeMoreThanValue + case notSupportedScriptType } - private let unspentOutputSelector: IUnspentOutputSelector private let inputSigner: IInputSigner + private let scriptBuilder: IScriptBuilder private let factory: IFactory - private let transactionSizeCalculator: ITransactionSizeCalculator - var scriptBuilder: IScriptBuilder - - init(unspentOutputSelector: IUnspentOutputSelector, inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, - factory: IFactory, transactionSizeCalculator: ITransactionSizeCalculator) { - self.unspentOutputSelector = unspentOutputSelector + init(inputSigner: IInputSigner, scriptBuilder: IScriptBuilder, factory: IFactory) { self.inputSigner = inputSigner self.scriptBuilder = scriptBuilder self.factory = factory - self.transactionSizeCalculator = transactionSizeCalculator } private func input(fromUnspentOutput unspentOutput: UnspentOutput) throws -> InputToSign { @@ -32,37 +27,18 @@ class TransactionBuilder { return factory.inputToSign(withPreviousOutput: unspentOutput, script: Data(), sequence: 0xFFFFFFFF) } - private func output(withIndex index: Int, address: Address, pubKey: PublicKey? = nil, value: Int) throws -> Output { + private func output(withIndex index: Int, address: Address, value: Int) throws -> Output { let script = try scriptBuilder.lockingScript(for: address) - let output = factory.output(withValue: value, index: index, lockingScript: script, type: address.scriptType, address: address.stringValue, keyHash: address.keyHash, publicKey: pubKey) - return output + return factory.output(withValue: value, index: index, lockingScript: script, type: address.scriptType, address: address.stringValue, keyHash: address.keyHash, publicKey: nil) } } extension TransactionBuilder: ITransactionBuilder { - // :fee method returns the fee for the given amount - // If address given and it's valid, it returns the actual fee - // Otherwise, it returns the estimated fee - func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int { - if let address = toAddress { - // Actual fee - let transaction = try buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: address, changeAddress: changeAddress) - return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate - } else { - // Estimated fee - // Default to .p2pkh address - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: changeAddress.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) - return selectedOutputsInfo.fee - } - } - - func buildTransaction(value: Int, feeRate: Int, senderPay: Bool, toAddress: Address, changeAddress: Address) throws -> FullTransaction { - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: toAddress.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) - + func buildTransaction(value: Int, unspentOutputs: [UnspentOutput], fee: Int, senderPay: Bool, toAddress: Address, changeAddress: Address?) throws -> FullTransaction { if !senderPay { - guard selectedOutputsInfo.fee < value else { + guard fee < value else { throw BuildError.feeMoreThanValue } } @@ -71,20 +47,21 @@ extension TransactionBuilder: ITransactionBuilder { var outputs = [Output]() // Add inputs without unlocking scripts - for output in selectedOutputsInfo.unspentOutputs { + for output in unspentOutputs { inputsToSign.append(try input(fromUnspentOutput: output)) } // Calculate fee - let receivedValue = senderPay ? value : value - selectedOutputsInfo.fee - let sentValue = senderPay ? value + selectedOutputsInfo.fee : value + let receivedValue = senderPay ? value : value - fee + let sentValue = senderPay ? value + fee : value // Add :to output outputs.append(try output(withIndex: 0, address: toAddress, value: receivedValue)) // Add :change output if needed - if selectedOutputsInfo.addChangeOutput { - outputs.append(try output(withIndex: 1, address: changeAddress, value: selectedOutputsInfo.totalValue - sentValue)) + if let changeAddress = changeAddress { + let totalValue = unspentOutputs.reduce(0) { $0 + $1.output.value } + outputs.append(try output(withIndex: 1, address: changeAddress, value: totalValue - sentValue)) } // Build transaction @@ -92,19 +69,21 @@ extension TransactionBuilder: ITransactionBuilder { // Sign inputs for i in 0.. Data) throws -> FullTransaction { - // Calculate fee - let emptySignature = Data(repeating: 0, count: TransactionSizeCalculator.signatureLength) - let emptyPublicKey = Data(repeating: 0, count: TransactionSizeCalculator.pubKeyLength) - - let transactionSize = transactionSizeCalculator.transactionSize(inputs: [unspentOutput.output.scriptType], outputScriptTypes: [address.scriptType]) + - signatureScriptFunction(emptySignature, emptyPublicKey).count - let fee = transactionSize * feeRate + func buildTransaction(from unspentOutput: UnspentOutput, to address: Address, fee: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { + guard unspentOutput.output.scriptType == .p2sh else { + throw BuildError.notSupportedScriptType + } guard fee < unspentOutput.output.value else { throw BuildError.feeMoreThanValue diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index 26c8dd26..3137a3b5 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -6,16 +6,18 @@ class TransactionCreator { private let transactionBuilder: ITransactionBuilder private let transactionProcessor: ITransactionProcessor private let transactionSender: ITransactionSender + private let transactionFeeCalculator: ITransactionFeeCalculator private let bloomFilterManager: IBloomFilterManager private let addressConverter: IAddressConverter private let publicKeyManager: IPublicKeyManager private let bip: Bip - init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender, bloomFilterManager: IBloomFilterManager, - addressConverter: IAddressConverter, publicKeyManager: IPublicKeyManager, bip: Bip) { + init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender, transactionFeeCalculator: ITransactionFeeCalculator, + bloomFilterManager: IBloomFilterManager, addressConverter: IAddressConverter, publicKeyManager: IPublicKeyManager, bip: Bip) { self.transactionBuilder = transactionBuilder self.transactionProcessor = transactionProcessor self.transactionSender = transactionSender + self.transactionFeeCalculator = transactionFeeCalculator self.bloomFilterManager = bloomFilterManager self.addressConverter = addressConverter self.publicKeyManager = publicKeyManager @@ -35,10 +37,18 @@ class TransactionCreator { } private func create(to toAddress: Address, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { - let changePubKey = try publicKeyManager.changePublicKey() - let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) + let feeWithUnspentOutputs = try transactionFeeCalculator.feeWithUnspentOutputs(value: value, feeRate: feeRate, toScriptType: toAddress.scriptType, changeScriptType: bip.scriptType, senderPay: senderPay) - let transaction = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) + var changeAddress: Address? = nil + if feeWithUnspentOutputs.addChangeOutput { + let changePubKey = try publicKeyManager.changePublicKey() + changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) + } + + let transaction = try transactionBuilder.buildTransaction( + value: value, unspentOutputs: feeWithUnspentOutputs.unspentOutputs, fee: feeWithUnspentOutputs.fee, senderPay: senderPay, + toAddress: toAddress, changeAddress: changeAddress + ) try processAndSend(transaction: transaction) return transaction @@ -48,14 +58,6 @@ class TransactionCreator { extension TransactionCreator: ITransactionCreator { - func fee(for value: Int, feeRate: Int, senderPay: Bool, address: String?) throws -> Int { - let toAddress = try address.map { try addressConverter.convert(address: $0) } - let changePubKey = try publicKeyManager.changePublicKey() - let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) - - return try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) - } - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction { let toAddress = try addressConverter.convert(address: address) return try create(to: toAddress, value: value, feeRate: feeRate, senderPay: senderPay) @@ -68,7 +70,8 @@ extension TransactionCreator: ITransactionCreator { func create(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { let toAddress = try addressConverter.convert(address: address) - let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddress, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + let fee = transactionFeeCalculator.fee(inputScriptType: unspentOutput.output.scriptType, outputScriptType: toAddress.scriptType, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddress, fee: fee, signatureScriptFunction: signatureScriptFunction) try processAndSend(transaction: transaction) return transaction diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift new file mode 100644 index 00000000..60d78024 --- /dev/null +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift @@ -0,0 +1,48 @@ +class TransactionFeeCalculator { + + private let unspentOutputSelector: IUnspentOutputSelector + private let transactionSizeCalculator: ITransactionSizeCalculator + private let transactionBuilder: ITransactionBuilder + + init(unspentOutputSelector: IUnspentOutputSelector, transactionSizeCalculator: ITransactionSizeCalculator, transactionBuilder: ITransactionBuilder) { + self.unspentOutputSelector = unspentOutputSelector + self.transactionSizeCalculator = transactionSizeCalculator + self.transactionBuilder = transactionBuilder + } + +} + +extension TransactionFeeCalculator: ITransactionFeeCalculator { + + // :fee method returns the fee for the given amount + // If address given and it's valid, it returns the actual fee + // Otherwise, it returns the estimated fee + func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int { + if let address = toAddress { + // Actual fee + let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) + let transaction = try transactionBuilder.buildTransaction(value: value, unspentOutputs: selectedOutputsInfo.unspentOutputs, fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: address, changeAddress: changeAddress) + return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate + } else { + // Estimated fee + // Default to .p2pkh address + let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: changeAddress.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) + return selectedOutputsInfo.fee + } + } + + func feeWithUnspentOutputs(value: Int, feeRate: Int, toScriptType: ScriptType, changeScriptType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo { + return try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: toScriptType, changeType: changeScriptType, senderPay: senderPay) + } + + func fee(inputScriptType: ScriptType, outputScriptType: ScriptType, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) -> Int { + let emptySignature = Data(repeating: 0, count: TransactionSizeCalculator.signatureLength) + let emptyPublicKey = Data(repeating: 0, count: TransactionSizeCalculator.pubKeyLength) + + let transactionSize = transactionSizeCalculator.transactionSize(inputs: [inputScriptType], outputScriptTypes: [outputScriptType]) + + signatureScriptFunction(emptySignature, emptyPublicKey).count + + return transactionSize * feeRate + } + +} diff --git a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift index ffa7f064..8a68fae6 100644 --- a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift @@ -1,559 +1,559 @@ -import XCTest -import Quick -import Nimble -import Cuckoo -@testable import BitcoinCore - -class BlockSyncerTests: QuickSpec { - override func spec() { - let mockStorage = MockIStorage() - let mockFactory = MockIFactory() - let mockListener = MockISyncStateListener() - let mockTransactionProcessor = MockITransactionProcessor() - let mockBlockchain = MockIBlockchain() - let mockAddressManager = MockIPublicKeyManager() - let mockBloomFilterManager = MockIBloomFilterManager() - let mockState = MockBlockSyncerState() - - let checkpointBlock = TestData.checkpointBlock - var syncer: BlockSyncer! - - beforeEach { - stub(mockStorage) { mock in - when(mock.blocksCount.get).thenReturn(1) - when(mock.lastBlock.get).thenReturn(nil) - when(mock.deleteBlockchainBlockHashes()).thenDoNothing() - } - stub(mockListener) { mock in - when(mock.initialBestBlockHeightUpdated(height: equal(to: 0))).thenDoNothing() - } - stub(mockBlockchain) { mock in - when(mock.handleFork()).thenDoNothing() - } - stub(mockAddressManager) { mock in - when(mock.fillGap()).thenDoNothing() - } - stub(mockBloomFilterManager) { mock in - when(mock.regenerateBloomFilter()).thenDoNothing() - } - stub(mockState) { mock in - when(mock.iteration(hasPartialBlocks: any())).thenDoNothing() - } - } - - afterEach { - reset(mockStorage, mockListener, mockTransactionProcessor, mockBlockchain, mockAddressManager, mockBloomFilterManager, mockState) - - syncer = nil - } - - context("static methods") { - describe("#instance") { - it("triggers #initialBestBlockHeightUpdated event on listener") { - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(checkpointBlock) - } - stub(mockListener) { mock in - when(mock.initialBestBlockHeightUpdated(height: any())).thenDoNothing() - } - - - let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, publicKeyManager: mockAddressManager, hashCheckpointThreshold: 100) - - verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) - verifyNoMoreInteractions(mockListener) - } - } - - describe("#checkpointBlock") { - let bip44CheckpointBlock = TestData.checkpointBlock - let lastCheckpointBlock = TestData.firstBlock - let mockNetwork = MockINetwork() - - beforeEach { - stub(mockNetwork) { mock in - when(mock.bip44CheckpointBlock.get).thenReturn(bip44CheckpointBlock) - when(mock.lastCheckpointBlock.get).thenReturn(lastCheckpointBlock) - } - } - - afterEach { - reset(mockNetwork) - } - - context("when there are some blocks in storage") { - let lastBlock = TestData.secondBlock - - beforeEach { - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(lastBlock) - } - } - - it("doesn't save checkpointBlock to storage") { - _ = BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) - - verify(mockStorage, never()).save(block: any()) - verify(mockStorage).lastBlock.get() - verifyNoMoreInteractions(mockStorage) - } - - context("when syncMode is .full") { - it("returns bip44CheckpointBlock") { - expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) - } - } - - context("when syncMode is not .full") { - context("when lastBlock's height is more than lastCheckpointBlock") { - it("returns lastCheckpointBlock") { - lastBlock.height = lastCheckpointBlock.height + 1 - expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) - } - } - - context("when lastBlock's height is less than lastCheckpointBlock") { - it("returns bip44CheckpointBlock") { - lastBlock.height = lastCheckpointBlock.height - 1 - expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(bip44CheckpointBlock)) - } - } - } - } - - context("when there's no block in storage") { - beforeEach { - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(nil) - when(mock.save(block: any())).thenDoNothing() - } - } - - context("when syncMode is .full") { - it("returns bip44CheckpointBlock") { - expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) - } - - it("saves bip44CheckpointBlock to storage") { - BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage) - verify(mockStorage).save(block: sameInstance(as: bip44CheckpointBlock)) - } - } - - context("when syncMode is not .full") { - it("returns lastCheckpointBlock") { - expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) - } - - it("saves lastCheckpointBlock to storage") { - BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) - verify(mockStorage).save(block: sameInstance(as: lastCheckpointBlock)) - } - } - } - } - } - - context("instance methods") { - beforeEach { - syncer = BlockSyncer(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, - blockchain: mockBlockchain, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, - hashCheckpointThreshold: 100, logger: nil, state: mockState) - } - - describe("#localDownloadedBestBlockHeight") { - context("when there are some blocks in storage") { - it("returns the height of the last block") { - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(checkpointBlock) - } - expect(syncer.localDownloadedBestBlockHeight).to(equal(Int32(checkpointBlock.height))) - } - } - - context("when there's no block in storage") { - it("returns 0") { - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(nil) - } - expect(syncer.localDownloadedBestBlockHeight).to(equal(0)) - } - } - } - - describe("#localKnownBestBlockHeight") { - let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 32), height: 0, order: 0) - - context("when no blockHashes") { - beforeEach { - stub(mockStorage) { mock in - when(mock.blockchainBlockHashes.get).thenReturn([]) - when(mock.blocksCount(headerHashes: equal(to: []))).thenReturn(0) - } - } - - context("when no blocks") { - it("returns 0") { - expect(syncer.localKnownBestBlockHeight).to(equal(0)) - } - } - - context("when there are some blocks") { - it("returns last block's height + blocks count") { - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(checkpointBlock) - } - expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) - } - } - } - - context("when there are some blockHashes which haven't downloaded blocks") { - beforeEach { - stub(mockStorage) { mock in - when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) - when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(0) - } - } - - it("returns lastBlock + blockHashes count") { - expect(syncer.localKnownBestBlockHeight).to(equal(1)) - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(checkpointBlock) - } - expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height + 1))) - } - } - - context("when there are some blockHashes which have downloaded blocks") { - beforeEach { - stub(mockStorage) { mock in - when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) - when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(1) - } - } - - it("returns lastBlock + count of blockHashes without downloaded blocks") { - expect(syncer.localKnownBestBlockHeight).to(equal(0)) - stub(mockStorage) { mock in - when(mock.lastBlock.get).thenReturn(checkpointBlock) - } - expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) - } - } - } - - describe("#prepareForDownload") { - let emptyBlocks = [Block]() - - beforeEach { - stub(mockStorage) { mock in - when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) - when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) - } - stub(mockBlockchain) { mock in - when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() - } - - syncer.prepareForDownload() - } - - it("handles partial blocks") { - verify(mockAddressManager).fillGap() - verify(mockBloomFilterManager).regenerateBloomFilter() - verify(mockState).iteration(hasPartialBlocks: equal(to: false)) - } - - it("clears BlockHashes") { - verify(mockStorage).deleteBlockchainBlockHashes() - } - - it("clears partialBlock blocks") { - verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) - verify(mockStorage).blocks(byHexes: equal(to: [])) - verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) - } - - it("handles fork") { - verify(mockBlockchain).handleFork() - } - } - - describe("#downloadIterationCompleted") { - context("when iteration has partial blocks") { - it("handles partial blocks") { - stub(mockState) { mock in - when(mock.iterationHasPartialBlocks.get).thenReturn(true) - } - syncer.downloadIterationCompleted() - - verify(mockAddressManager).fillGap() - verify(mockBloomFilterManager).regenerateBloomFilter() - verify(mockState).iteration(hasPartialBlocks: equal(to: false)) - } - } - - context("when iteration has not partial blocks") { - it("does not handle partial blocks") { - stub(mockState) { mock in - when(mock.iterationHasPartialBlocks.get).thenReturn(false) - } - syncer.downloadIterationCompleted() - - verify(mockAddressManager, never()).fillGap() - verify(mockBloomFilterManager, never()).regenerateBloomFilter() - verify(mockState, never()).iteration(hasPartialBlocks: any()) - } - } - } - - describe("#downloadCompleted") { - it("handles fork") { - syncer.downloadCompleted() - verify(mockBlockchain).handleFork() - } - } - - describe("#downloadFailed") { - let emptyBlocks = [Block]() - - beforeEach { - stub(mockStorage) { mock in - when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) - when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) - } - stub(mockBlockchain) { mock in - when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() - } - - syncer.downloadFailed() - } - - it("handles partial blocks") { - verify(mockAddressManager).fillGap() - verify(mockBloomFilterManager).regenerateBloomFilter() - verify(mockState).iteration(hasPartialBlocks: equal(to: false)) - } - - it("clears BlockHashes") { - verify(mockStorage).deleteBlockchainBlockHashes() - } - - it("clears partialBlock blocks") { - verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) - verify(mockStorage).blocks(byHexes: equal(to: [])) - verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) - } - - it("handles fork") { - verify(mockBlockchain).handleFork() - } - } - - describe("#getBlockHashes") { - it("returns first 500 blockhashes") { - let blockHashes = [BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0)] - stub(mockStorage) { mock in - when(mock.blockHashesSortedBySequenceAndHeight(limit: equal(to: 500))).thenReturn(blockHashes) - } - - expect(syncer.getBlockHashes()).to(equal(blockHashes)) - verify(mockStorage).blockHashesSortedBySequenceAndHeight(limit: equal(to: 500)) - } - } - - describe("#getBlockLocatorHashes(peerLastBlockHeight:)") { - let peerLastBlockHeight: Int32 = 10 - let firstBlock = TestData.firstBlock - let secondBlock = TestData.secondBlock - - beforeEach { - stub(mockStorage) { mock in - when(mock.lastBlockchainBlockHash.get).thenReturn(nil) - when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([Block]()) - when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(nil) - } - } - - context("when there's no blocks or blockhashes") { - it("returns checkpointBlock's header hash") { - expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([checkpointBlock.headerHash])) - } - } - - context("when there are blockchain blockhashes") { - it("returns last blockchain blockhash") { - let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0) - stub(mockStorage) { mock in - when(mock.lastBlockchainBlockHash.get).thenReturn(blockHash) - when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([firstBlock, secondBlock]) - } - - expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ - blockHash.headerHash, checkpointBlock.headerHash - ])) - } - } - - context("when there's no blockhashes but there are blocks") { - it("returns last 10 blocks' header hashes") { - stub(mockStorage) { mock in - when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([secondBlock, firstBlock]) - } - - expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ - secondBlock.headerHash, firstBlock.headerHash, checkpointBlock.headerHash - ])) - } - } - - context("when the peers last block is already in storage") { - it("returns peers last block's headerHash instead of checkpointBlocks'") { - stub(mockStorage) { mock in - when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(firstBlock) - } - - expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([firstBlock.headerHash])) - } - } - } - - describe("#add(blockHashes:)") { - let existingBlockHash = Data(repeating: 0, count: 32) - let newBlockHash = Data(repeating: 1, count: 32) - let blockHash = BlockHash(headerHash: existingBlockHash, height: 0, order: 10) - - beforeEach { - stub(mockStorage) { mock in - when(mock.blockHashHeaderHashes.get).thenReturn([existingBlockHash]) - when(mock.add(blockHashes: any())).thenDoNothing() - } - stub(mockFactory) { mock in - when(mock.blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: any())).thenReturn(blockHash) - } - } - - context("when there's a blockHash in storage") { - it("sets order of given blockhashes starting from last blockhashes order") { - let lastBlockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 10) - stub(mockStorage) { mock in - when(mock.lastBlockHash.get).thenReturn(lastBlockHash) - } - - syncer.add(blockHashes: [existingBlockHash, newBlockHash]) - - verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: lastBlockHash.sequence + 1)) - verify(mockStorage).add(blockHashes: equal(to: [blockHash])) - } - } - - context("when there's no blockhashes") { - it("sets order of given blockhashes starting from 0") { - stub(mockStorage) { mock in - when(mock.lastBlockHash.get).thenReturn(nil) - } - - syncer.add(blockHashes: [existingBlockHash, newBlockHash]) - - verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: 1)) - verify(mockStorage).add(blockHashes: equal(to: [blockHash])) - } - } - } - - describe("#handle(merkleBlock:,maxBlockHeight:)") { - let block = TestData.firstBlock - let merkleBlock = MerkleBlock(header: block.header, transactionHashes: [], transactions: []) - let maxBlockHeight: Int32 = Int32(block.height + 100) - - beforeEach { - stub(mockBlockchain) { mock in - when(mock.forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height))).thenReturn(block) - when(mock.connect(merkleBlock: equal(to: merkleBlock))).thenReturn(block) - } - stub(mockTransactionProcessor) { mock in - when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() - } - stub(mockState) { mock in - when(mock.iterationHasPartialBlocks.get).thenReturn(false) - } - stub(mockStorage) { mock in - when(mock.deleteBlockHash(byHash: equal(to: block.headerHash))).thenDoNothing() - } - stub(mockListener) { mock in - when(mock.currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight))).thenDoNothing() - } - } - - it("handles merkleBlock") { - try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) - - verify(mockBlockchain).connect(merkleBlock: equal(to: merkleBlock)) - verify(mockTransactionProcessor).processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false)) - verify(mockStorage).deleteBlockHash(byHash: equal(to: block.headerHash)) - verify(mockListener).currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight)) - } - - context("when merklBlocks's height is null") { - it("force adds the block to blockchain") { - merkleBlock.height = block.height - try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) - - verify(mockBlockchain).forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height)) - verifyNoMoreInteractions(mockBlockchain) - } - } - - context("when bloom filter is expired while processing transactions") { - it("sets iteration state to hasPartialBlocks") { - stub(mockTransactionProcessor) { mock in - when(mock.processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false))).thenThrow(BloomFilterManager.BloomFilterExpired()) - } - try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) - - verify(mockState).iteration(hasPartialBlocks: equal(to: true)) - } - } - - context("when iteration has partial blocks") { - it("doesn't delete block hash") { - stub(mockState) { mock in - when(mock.iterationHasPartialBlocks.get).thenReturn(true) - } - stub(mockTransactionProcessor) { mock in - when(mock.processReceived(transactions: equal(to: []), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: true))).thenDoNothing() - } - try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) - - verify(mockStorage, never()).deleteBlockHash(byHash: equal(to: block.headerHash)) - } - } - } - - describe("#shouldRequestBlock(withHash:)") { - let hash = Data(repeating: 0, count: 32) - - context("when the given block is in storage") { - it("returns false") { - stub(mockStorage) { mock in - when(mock.block(byHash: equal(to: hash))).thenReturn(TestData.firstBlock) - } - - expect(syncer.shouldRequestBlock(withHash: hash)).to(beFalsy()) - } - } - - context("when the given block is not in storage") { - it("returns true") { - stub(mockStorage) { mock in - when(mock.block(byHash: equal(to: hash))).thenReturn(nil) - } - - expect(syncer.shouldRequestBlock(withHash: hash)).to(beTruthy()) - } - } - } - } - } -} +//import XCTest +//import Quick +//import Nimble +//import Cuckoo +//@testable import BitcoinCore +// +//class BlockSyncerTests: QuickSpec { +// override func spec() { +// let mockStorage = MockIStorage() +// let mockFactory = MockIFactory() +// let mockListener = MockISyncStateListener() +// let mockTransactionProcessor = MockITransactionProcessor() +// let mockBlockchain = MockIBlockchain() +// let mockAddressManager = MockIPublicKeyManager() +// let mockBloomFilterManager = MockIBloomFilterManager() +// let mockState = MockBlockSyncerState() +// +// let checkpointBlock = TestData.checkpointBlock +// var syncer: BlockSyncer! +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blocksCount.get).thenReturn(1) +// when(mock.lastBlock.get).thenReturn(nil) +// when(mock.deleteBlockchainBlockHashes()).thenDoNothing() +// } +// stub(mockListener) { mock in +// when(mock.initialBestBlockHeightUpdated(height: equal(to: 0))).thenDoNothing() +// } +// stub(mockBlockchain) { mock in +// when(mock.handleFork()).thenDoNothing() +// } +// stub(mockAddressManager) { mock in +// when(mock.fillGap()).thenDoNothing() +// } +// stub(mockBloomFilterManager) { mock in +// when(mock.regenerateBloomFilter()).thenDoNothing() +// } +// stub(mockState) { mock in +// when(mock.iteration(hasPartialBlocks: any())).thenDoNothing() +// } +// } +// +// afterEach { +// reset(mockStorage, mockListener, mockTransactionProcessor, mockBlockchain, mockAddressManager, mockBloomFilterManager, mockState) +// +// syncer = nil +// } +// +// context("static methods") { +// describe("#instance") { +// it("triggers #initialBestBlockHeightUpdated event on listener") { +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(checkpointBlock) +// } +// stub(mockListener) { mock in +// when(mock.initialBestBlockHeightUpdated(height: any())).thenDoNothing() +// } +// +// +// let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, +// blockchain: mockBlockchain, publicKeyManager: mockAddressManager, hashCheckpointThreshold: 100) +// +// verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) +// verifyNoMoreInteractions(mockListener) +// } +// } +// +// describe("#checkpointBlock") { +// let bip44CheckpointBlock = TestData.checkpointBlock +// let lastCheckpointBlock = TestData.firstBlock +// let mockNetwork = MockINetwork() +// +// beforeEach { +// stub(mockNetwork) { mock in +// when(mock.bip44CheckpointBlock.get).thenReturn(bip44CheckpointBlock) +// when(mock.lastCheckpointBlock.get).thenReturn(lastCheckpointBlock) +// } +// } +// +// afterEach { +// reset(mockNetwork) +// } +// +// context("when there are some blocks in storage") { +// let lastBlock = TestData.secondBlock +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(lastBlock) +// } +// } +// +// it("doesn't save checkpointBlock to storage") { +// _ = BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) +// +// verify(mockStorage, never()).save(block: any()) +// verify(mockStorage).lastBlock.get() +// verifyNoMoreInteractions(mockStorage) +// } +// +// context("when syncMode is .full") { +// it("returns bip44CheckpointBlock") { +// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) +// } +// } +// +// context("when syncMode is not .full") { +// context("when lastBlock's height is more than lastCheckpointBlock") { +// it("returns lastCheckpointBlock") { +// lastBlock.height = lastCheckpointBlock.height + 1 +// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) +// } +// } +// +// context("when lastBlock's height is less than lastCheckpointBlock") { +// it("returns bip44CheckpointBlock") { +// lastBlock.height = lastCheckpointBlock.height - 1 +// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(bip44CheckpointBlock)) +// } +// } +// } +// } +// +// context("when there's no block in storage") { +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(nil) +// when(mock.save(block: any())).thenDoNothing() +// } +// } +// +// context("when syncMode is .full") { +// it("returns bip44CheckpointBlock") { +// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) +// } +// +// it("saves bip44CheckpointBlock to storage") { +// BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage) +// verify(mockStorage).save(block: sameInstance(as: bip44CheckpointBlock)) +// } +// } +// +// context("when syncMode is not .full") { +// it("returns lastCheckpointBlock") { +// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) +// } +// +// it("saves lastCheckpointBlock to storage") { +// BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) +// verify(mockStorage).save(block: sameInstance(as: lastCheckpointBlock)) +// } +// } +// } +// } +// } +// +// context("instance methods") { +// beforeEach { +// syncer = BlockSyncer(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, +// blockchain: mockBlockchain, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, +// hashCheckpointThreshold: 100, logger: nil, state: mockState) +// } +// +// describe("#localDownloadedBestBlockHeight") { +// context("when there are some blocks in storage") { +// it("returns the height of the last block") { +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(checkpointBlock) +// } +// expect(syncer.localDownloadedBestBlockHeight).to(equal(Int32(checkpointBlock.height))) +// } +// } +// +// context("when there's no block in storage") { +// it("returns 0") { +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(nil) +// } +// expect(syncer.localDownloadedBestBlockHeight).to(equal(0)) +// } +// } +// } +// +// describe("#localKnownBestBlockHeight") { +// let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 32), height: 0, order: 0) +// +// context("when no blockHashes") { +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blockchainBlockHashes.get).thenReturn([]) +// when(mock.blocksCount(headerHashes: equal(to: []))).thenReturn(0) +// } +// } +// +// context("when no blocks") { +// it("returns 0") { +// expect(syncer.localKnownBestBlockHeight).to(equal(0)) +// } +// } +// +// context("when there are some blocks") { +// it("returns last block's height + blocks count") { +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(checkpointBlock) +// } +// expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) +// } +// } +// } +// +// context("when there are some blockHashes which haven't downloaded blocks") { +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) +// when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(0) +// } +// } +// +// it("returns lastBlock + blockHashes count") { +// expect(syncer.localKnownBestBlockHeight).to(equal(1)) +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(checkpointBlock) +// } +// expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height + 1))) +// } +// } +// +// context("when there are some blockHashes which have downloaded blocks") { +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) +// when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(1) +// } +// } +// +// it("returns lastBlock + count of blockHashes without downloaded blocks") { +// expect(syncer.localKnownBestBlockHeight).to(equal(0)) +// stub(mockStorage) { mock in +// when(mock.lastBlock.get).thenReturn(checkpointBlock) +// } +// expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) +// } +// } +// } +// +// describe("#prepareForDownload") { +// let emptyBlocks = [Block]() +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) +// when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) +// } +// stub(mockBlockchain) { mock in +// when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() +// } +// +// syncer.prepareForDownload() +// } +// +// it("handles partial blocks") { +// verify(mockAddressManager).fillGap() +// verify(mockBloomFilterManager).regenerateBloomFilter() +// verify(mockState).iteration(hasPartialBlocks: equal(to: false)) +// } +// +// it("clears BlockHashes") { +// verify(mockStorage).deleteBlockchainBlockHashes() +// } +// +// it("clears partialBlock blocks") { +// verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) +// verify(mockStorage).blocks(byHexes: equal(to: [])) +// verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) +// } +// +// it("handles fork") { +// verify(mockBlockchain).handleFork() +// } +// } +// +// describe("#downloadIterationCompleted") { +// context("when iteration has partial blocks") { +// it("handles partial blocks") { +// stub(mockState) { mock in +// when(mock.iterationHasPartialBlocks.get).thenReturn(true) +// } +// syncer.downloadIterationCompleted() +// +// verify(mockAddressManager).fillGap() +// verify(mockBloomFilterManager).regenerateBloomFilter() +// verify(mockState).iteration(hasPartialBlocks: equal(to: false)) +// } +// } +// +// context("when iteration has not partial blocks") { +// it("does not handle partial blocks") { +// stub(mockState) { mock in +// when(mock.iterationHasPartialBlocks.get).thenReturn(false) +// } +// syncer.downloadIterationCompleted() +// +// verify(mockAddressManager, never()).fillGap() +// verify(mockBloomFilterManager, never()).regenerateBloomFilter() +// verify(mockState, never()).iteration(hasPartialBlocks: any()) +// } +// } +// } +// +// describe("#downloadCompleted") { +// it("handles fork") { +// syncer.downloadCompleted() +// verify(mockBlockchain).handleFork() +// } +// } +// +// describe("#downloadFailed") { +// let emptyBlocks = [Block]() +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) +// when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) +// } +// stub(mockBlockchain) { mock in +// when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() +// } +// +// syncer.downloadFailed() +// } +// +// it("handles partial blocks") { +// verify(mockAddressManager).fillGap() +// verify(mockBloomFilterManager).regenerateBloomFilter() +// verify(mockState).iteration(hasPartialBlocks: equal(to: false)) +// } +// +// it("clears BlockHashes") { +// verify(mockStorage).deleteBlockchainBlockHashes() +// } +// +// it("clears partialBlock blocks") { +// verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) +// verify(mockStorage).blocks(byHexes: equal(to: [])) +// verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) +// } +// +// it("handles fork") { +// verify(mockBlockchain).handleFork() +// } +// } +// +// describe("#getBlockHashes") { +// it("returns first 500 blockhashes") { +// let blockHashes = [BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0)] +// stub(mockStorage) { mock in +// when(mock.blockHashesSortedBySequenceAndHeight(limit: equal(to: 500))).thenReturn(blockHashes) +// } +// +// expect(syncer.getBlockHashes()).to(equal(blockHashes)) +// verify(mockStorage).blockHashesSortedBySequenceAndHeight(limit: equal(to: 500)) +// } +// } +// +// describe("#getBlockLocatorHashes(peerLastBlockHeight:)") { +// let peerLastBlockHeight: Int32 = 10 +// let firstBlock = TestData.firstBlock +// let secondBlock = TestData.secondBlock +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.lastBlockchainBlockHash.get).thenReturn(nil) +// when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([Block]()) +// when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(nil) +// } +// } +// +// context("when there's no blocks or blockhashes") { +// it("returns checkpointBlock's header hash") { +// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([checkpointBlock.headerHash])) +// } +// } +// +// context("when there are blockchain blockhashes") { +// it("returns last blockchain blockhash") { +// let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0) +// stub(mockStorage) { mock in +// when(mock.lastBlockchainBlockHash.get).thenReturn(blockHash) +// when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([firstBlock, secondBlock]) +// } +// +// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ +// blockHash.headerHash, checkpointBlock.headerHash +// ])) +// } +// } +// +// context("when there's no blockhashes but there are blocks") { +// it("returns last 10 blocks' header hashes") { +// stub(mockStorage) { mock in +// when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([secondBlock, firstBlock]) +// } +// +// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ +// secondBlock.headerHash, firstBlock.headerHash, checkpointBlock.headerHash +// ])) +// } +// } +// +// context("when the peers last block is already in storage") { +// it("returns peers last block's headerHash instead of checkpointBlocks'") { +// stub(mockStorage) { mock in +// when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(firstBlock) +// } +// +// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([firstBlock.headerHash])) +// } +// } +// } +// +// describe("#add(blockHashes:)") { +// let existingBlockHash = Data(repeating: 0, count: 32) +// let newBlockHash = Data(repeating: 1, count: 32) +// let blockHash = BlockHash(headerHash: existingBlockHash, height: 0, order: 10) +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.blockHashHeaderHashes.get).thenReturn([existingBlockHash]) +// when(mock.add(blockHashes: any())).thenDoNothing() +// } +// stub(mockFactory) { mock in +// when(mock.blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: any())).thenReturn(blockHash) +// } +// } +// +// context("when there's a blockHash in storage") { +// it("sets order of given blockhashes starting from last blockhashes order") { +// let lastBlockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 10) +// stub(mockStorage) { mock in +// when(mock.lastBlockHash.get).thenReturn(lastBlockHash) +// } +// +// syncer.add(blockHashes: [existingBlockHash, newBlockHash]) +// +// verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: lastBlockHash.sequence + 1)) +// verify(mockStorage).add(blockHashes: equal(to: [blockHash])) +// } +// } +// +// context("when there's no blockhashes") { +// it("sets order of given blockhashes starting from 0") { +// stub(mockStorage) { mock in +// when(mock.lastBlockHash.get).thenReturn(nil) +// } +// +// syncer.add(blockHashes: [existingBlockHash, newBlockHash]) +// +// verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: 1)) +// verify(mockStorage).add(blockHashes: equal(to: [blockHash])) +// } +// } +// } +// +// describe("#handle(merkleBlock:,maxBlockHeight:)") { +// let block = TestData.firstBlock +// let merkleBlock = MerkleBlock(header: block.header, transactionHashes: [], transactions: []) +// let maxBlockHeight: Int32 = Int32(block.height + 100) +// +// beforeEach { +// stub(mockBlockchain) { mock in +// when(mock.forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height))).thenReturn(block) +// when(mock.connect(merkleBlock: equal(to: merkleBlock))).thenReturn(block) +// } +// stub(mockTransactionProcessor) { mock in +// when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() +// } +// stub(mockState) { mock in +// when(mock.iterationHasPartialBlocks.get).thenReturn(false) +// } +// stub(mockStorage) { mock in +// when(mock.deleteBlockHash(byHash: equal(to: block.headerHash))).thenDoNothing() +// } +// stub(mockListener) { mock in +// when(mock.currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight))).thenDoNothing() +// } +// } +// +// it("handles merkleBlock") { +// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) +// +// verify(mockBlockchain).connect(merkleBlock: equal(to: merkleBlock)) +// verify(mockTransactionProcessor).processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false)) +// verify(mockStorage).deleteBlockHash(byHash: equal(to: block.headerHash)) +// verify(mockListener).currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight)) +// } +// +// context("when merklBlocks's height is null") { +// it("force adds the block to blockchain") { +// merkleBlock.height = block.height +// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) +// +// verify(mockBlockchain).forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height)) +// verifyNoMoreInteractions(mockBlockchain) +// } +// } +// +// context("when bloom filter is expired while processing transactions") { +// it("sets iteration state to hasPartialBlocks") { +// stub(mockTransactionProcessor) { mock in +// when(mock.processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false))).thenThrow(BloomFilterManager.BloomFilterExpired()) +// } +// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) +// +// verify(mockState).iteration(hasPartialBlocks: equal(to: true)) +// } +// } +// +// context("when iteration has partial blocks") { +// it("doesn't delete block hash") { +// stub(mockState) { mock in +// when(mock.iterationHasPartialBlocks.get).thenReturn(true) +// } +// stub(mockTransactionProcessor) { mock in +// when(mock.processReceived(transactions: equal(to: []), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: true))).thenDoNothing() +// } +// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) +// +// verify(mockStorage, never()).deleteBlockHash(byHash: equal(to: block.headerHash)) +// } +// } +// } +// +// describe("#shouldRequestBlock(withHash:)") { +// let hash = Data(repeating: 0, count: 32) +// +// context("when the given block is in storage") { +// it("returns false") { +// stub(mockStorage) { mock in +// when(mock.block(byHash: equal(to: hash))).thenReturn(TestData.firstBlock) +// } +// +// expect(syncer.shouldRequestBlock(withHash: hash)).to(beFalsy()) +// } +// } +// +// context("when the given block is not in storage") { +// it("returns true") { +// stub(mockStorage) { mock in +// when(mock.block(byHash: equal(to: hash))).thenReturn(nil) +// } +// +// expect(syncer.shouldRequestBlock(withHash: hash)).to(beTruthy()) +// } +// } +// } +// } +// } +//} diff --git a/BitcoinCore/BitcoinCoreTests/Extensions.swift b/BitcoinCore/BitcoinCoreTests/Extensions.swift index 04181814..62ab6e62 100644 --- a/BitcoinCore/BitcoinCoreTests/Extensions.swift +++ b/BitcoinCore/BitcoinCoreTests/Extensions.swift @@ -128,6 +128,14 @@ extension Input: Equatable { } +extension Output: Equatable { + + public static func ==(lhs: Output, rhs: Output) -> Bool { + return lhs.keyHash == rhs.keyHash && lhs.scriptType == rhs.scriptType && lhs.value == rhs.value && lhs.index == rhs.index + } + +} + extension BlockHeader: Equatable { public static func ==(lhs: BlockHeader, rhs: BlockHeader) -> Bool { @@ -151,3 +159,11 @@ extension UnspentOutput: Equatable { } } + +extension InputToSign: Equatable { + + public static func ==(lhs: InputToSign, rhs: InputToSign) -> Bool { + return lhs.input == rhs.input && lhs.previousOutputPublicKey == rhs.previousOutputPublicKey + } + +} diff --git a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift index 25a33469..5e192151 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift @@ -1,385 +1,385 @@ -import XCTest -import Cuckoo -import HSHDWalletKit -@testable import BitcoinCore - -class PublicKeyManagerTests: XCTestCase { - - private var mockStorage: MockIStorage! - private var mockHDWallet: MockIHDWallet! - private var mockAddressConverter: MockIAddressConverter! - - private var hdWallet: IHDWallet! - private var manager: PublicKeyManager! - - override func setUp() { - super.setUp() - - mockStorage = MockIStorage() - mockHDWallet = MockIHDWallet() - mockAddressConverter = MockIAddressConverter() - - stub(mockStorage) { mock in - when(mock.add(publicKeys: any())).thenDoNothing() - } - - hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) - manager = PublicKeyManager(storage: mockStorage, hdWallet: mockHDWallet, addressConverter: mockAddressConverter) - } - - override func tearDown() { - mockStorage = nil - mockHDWallet = nil - mockAddressConverter = nil - - hdWallet = nil - manager = nil - - super.tearDown() - } - - func testChangePublicKey() { - let publicKeys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false) - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) - } - - let changePublicKey = try! manager.changePublicKey() - XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) - } - - func testChangePublicKey_NoUnusedPublicKey() { - let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true) - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) - } - - do { - let _ = try manager.changePublicKey() - XCTFail("Should throw exception") - } catch let error as PublicKeyManager.PublicKeyManagerError { - XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) - } catch { - XCTFail("Unexpected exception thrown") - } - } - - func testReceivePublicKey() { - let publicKeys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false) - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) - } - - let changePublicKey = try! manager.receivePublicKey() - XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) - } - - func testReceivePublicKey_NoUnusedPublicKey() { - let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true) - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) - } - - do { - let _ = try manager.receivePublicKey() - XCTFail("Should throw exception") - } catch let error as PublicKeyManager.PublicKeyManagerError { - XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) - } catch { - XCTFail("Unexpected exception thrown") - } - } - - func testFillGap() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) - } - - try! manager.fillGap() - verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: false)) - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: true)) - - verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey])) - verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) - verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey, keys[6].publicKey])) - verify(mockStorage).add(publicKeys: equal(to: [keys[7].publicKey, keys[8].publicKey])) - } - - func testFillGap_NoUsedKey() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[0].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[2].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[3].publicKey) - } - - try! manager.fillGap() - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) - verify(mockHDWallet, never()).publicKey(account: equal(to: 1), index: any(), external: any()) - - verify(mockStorage).add(publicKeys: equal(to: [keys[0].publicKey, keys[1].publicKey])) - verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey, keys[3].publicKey])) - } - - func testFillGap_NoUnusedKeys() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) - } - - try! manager.fillGap() - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) - verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) - - verify(mockStorage).add(publicKeys: equal(to: [keys[1].publicKey, keys[2].publicKey])) - verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) - } - - func testFillGap_NonSequentiallyUsedKeys() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(1) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 3), external: equal(to: false))).thenReturn(keys[5].publicKey) - when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: true))).thenReturn(keys[6].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[7].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[8].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[9].publicKey) - when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[10].publicKey) - } - - try! manager.fillGap() - verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) - verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) - - verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey])) - verify(mockStorage).add(publicKeys: equal(to: [keys[6].publicKey])) - } - - func testAddKeys() { - let keys = [ - getPublicKey(withAccount: 0, index: 0, chain: .internal), - getPublicKey(withAccount: 0, index: 1, chain: .internal), - getPublicKey(withAccount: 0, index: 0, chain: .external), - ] - - try! manager.addKeys(keys: keys) - verify(mockStorage).add(publicKeys: equal(to: keys)) - } - - func testGapShifts() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - } - - XCTAssertEqual(manager.gapShifts(), true) - verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) - } - - func testGapShifts_NoUnusedKeys() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - } - - XCTAssertEqual(manager.gapShifts(), true) - verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) - } - - func testGapShifts_NonSequentiallyUsedKeys() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - } - - XCTAssertEqual(manager.gapShifts(), true) - verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) - } - - func testGapShifts_NoShift() { - let keys = [ - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), - PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), - ] - - stub(mockStorage) { mock in - when(mock.publicKeysWithUsedState()).thenReturn(keys) - } - stub(mockHDWallet) { mock in - when(mock.gapLimit.get).thenReturn(2) - } - - XCTAssertEqual(manager.gapShifts(), false) - } - - func testPublicKey_byPath_ExistsInStorage() { - let key = getPublicKey(withAccount: 10, index: 20, chain: .internal) - - stub(mockStorage) { mock in - when(mock.publicKey(byPath: equal(to: "10/0/20"))).thenReturn(key) - } - - XCTAssertEqual(try! manager.publicKey(byPath: "10/0/20"), key) - verify(mockStorage).publicKey(byPath: equal(to: "10/0/20")) - verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) - } - - func testPublicKey_byPath_DoesNotExistsInStorage() { - let key = getPublicKey(withAccount: 10, index: 20, chain: .external) - - stub(mockStorage) { mock in - when(mock.publicKey(byPath: equal(to: "10/1/20"))).thenReturn(nil) - } - stub(mockHDWallet) { mock in - when(mock.publicKey(account: 10, index: 20, external: true)).thenReturn(key) - } - - XCTAssertEqual(try! manager.publicKey(byPath: "10/1/20"), key) - verify(mockStorage).publicKey(byPath: equal(to: "10/1/20")) - verify(mockHDWallet).publicKey(account: 10, index: 20, external: true) - } - - func testPublicKey_byPath_InvalidPath() { - do { - _ = try manager.publicKey(byPath: "0/0") - XCTFail("Expected exception") - } catch let error as PublicKeyManager.PublicKeyManagerError { - XCTAssertEqual(error, .invalidPath) - } catch { - XCTFail("Unexpected exception") - } - } - - private func getPublicKey(withAccount account: Int, index: Int, chain: HDWallet.Chain) -> PublicKey { - let hdPrivKeyData = try! hdWallet.privateKeyData(account: account, index: index, external: chain == .external) - return PublicKey(withAccount: account, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) - } - -} +//import XCTest +//import Cuckoo +//import HSHDWalletKit +//@testable import BitcoinCore +// +//class PublicKeyManagerTests: XCTestCase { +// +// private var mockStorage: MockIStorage! +// private var mockHDWallet: MockIHDWallet! +// private var mockAddressConverter: MockIAddressConverter! +// +// private var hdWallet: IHDWallet! +// private var manager: PublicKeyManager! +// +// override func setUp() { +// super.setUp() +// +// mockStorage = MockIStorage() +// mockHDWallet = MockIHDWallet() +// mockAddressConverter = MockIAddressConverter() +// +// stub(mockStorage) { mock in +// when(mock.add(publicKeys: any())).thenDoNothing() +// } +// +// hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) +// manager = PublicKeyManager(storage: mockStorage, hdWallet: mockHDWallet, addressConverter: mockAddressConverter) +// } +// +// override func tearDown() { +// mockStorage = nil +// mockHDWallet = nil +// mockAddressConverter = nil +// +// hdWallet = nil +// manager = nil +// +// super.tearDown() +// } +// +// func testChangePublicKey() { +// let publicKeys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false) +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) +// } +// +// let changePublicKey = try! manager.changePublicKey() +// XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) +// } +// +// func testChangePublicKey_NoUnusedPublicKey() { +// let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true) +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) +// } +// +// do { +// let _ = try manager.changePublicKey() +// XCTFail("Should throw exception") +// } catch let error as PublicKeyManager.PublicKeyManagerError { +// XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) +// } catch { +// XCTFail("Unexpected exception thrown") +// } +// } +// +// func testReceivePublicKey() { +// let publicKeys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false) +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) +// } +// +// let changePublicKey = try! manager.receivePublicKey() +// XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) +// } +// +// func testReceivePublicKey_NoUnusedPublicKey() { +// let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true) +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) +// } +// +// do { +// let _ = try manager.receivePublicKey() +// XCTFail("Should throw exception") +// } catch let error as PublicKeyManager.PublicKeyManagerError { +// XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) +// } catch { +// XCTFail("Unexpected exception thrown") +// } +// } +// +// func testFillGap() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) +// } +// +// try! manager.fillGap() +// verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: false)) +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: true)) +// +// verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey])) +// verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) +// verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey, keys[6].publicKey])) +// verify(mockStorage).add(publicKeys: equal(to: [keys[7].publicKey, keys[8].publicKey])) +// } +// +// func testFillGap_NoUsedKey() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[0].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[2].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[3].publicKey) +// } +// +// try! manager.fillGap() +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) +// verify(mockHDWallet, never()).publicKey(account: equal(to: 1), index: any(), external: any()) +// +// verify(mockStorage).add(publicKeys: equal(to: [keys[0].publicKey, keys[1].publicKey])) +// verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey, keys[3].publicKey])) +// } +// +// func testFillGap_NoUnusedKeys() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) +// } +// +// try! manager.fillGap() +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) +// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) +// +// verify(mockStorage).add(publicKeys: equal(to: [keys[1].publicKey, keys[2].publicKey])) +// verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) +// } +// +// func testFillGap_NonSequentiallyUsedKeys() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(1) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 3), external: equal(to: false))).thenReturn(keys[5].publicKey) +// when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: true))).thenReturn(keys[6].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[7].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[8].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[9].publicKey) +// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[10].publicKey) +// } +// +// try! manager.fillGap() +// verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) +// verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) +// +// verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey])) +// verify(mockStorage).add(publicKeys: equal(to: [keys[6].publicKey])) +// } +// +// func testAddKeys() { +// let keys = [ +// getPublicKey(withAccount: 0, index: 0, chain: .internal), +// getPublicKey(withAccount: 0, index: 1, chain: .internal), +// getPublicKey(withAccount: 0, index: 0, chain: .external), +// ] +// +// try! manager.addKeys(keys: keys) +// verify(mockStorage).add(publicKeys: equal(to: keys)) +// } +// +// func testGapShifts() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// } +// +// XCTAssertEqual(manager.gapShifts(), true) +// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) +// } +// +// func testGapShifts_NoUnusedKeys() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// } +// +// XCTAssertEqual(manager.gapShifts(), true) +// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) +// } +// +// func testGapShifts_NonSequentiallyUsedKeys() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// } +// +// XCTAssertEqual(manager.gapShifts(), true) +// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) +// } +// +// func testGapShifts_NoShift() { +// let keys = [ +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), +// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), +// ] +// +// stub(mockStorage) { mock in +// when(mock.publicKeysWithUsedState()).thenReturn(keys) +// } +// stub(mockHDWallet) { mock in +// when(mock.gapLimit.get).thenReturn(2) +// } +// +// XCTAssertEqual(manager.gapShifts(), false) +// } +// +// func testPublicKey_byPath_ExistsInStorage() { +// let key = getPublicKey(withAccount: 10, index: 20, chain: .internal) +// +// stub(mockStorage) { mock in +// when(mock.publicKey(byPath: equal(to: "10/0/20"))).thenReturn(key) +// } +// +// XCTAssertEqual(try! manager.publicKey(byPath: "10/0/20"), key) +// verify(mockStorage).publicKey(byPath: equal(to: "10/0/20")) +// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) +// } +// +// func testPublicKey_byPath_DoesNotExistsInStorage() { +// let key = getPublicKey(withAccount: 10, index: 20, chain: .external) +// +// stub(mockStorage) { mock in +// when(mock.publicKey(byPath: equal(to: "10/1/20"))).thenReturn(nil) +// } +// stub(mockHDWallet) { mock in +// when(mock.publicKey(account: 10, index: 20, external: true)).thenReturn(key) +// } +// +// XCTAssertEqual(try! manager.publicKey(byPath: "10/1/20"), key) +// verify(mockStorage).publicKey(byPath: equal(to: "10/1/20")) +// verify(mockHDWallet).publicKey(account: 10, index: 20, external: true) +// } +// +// func testPublicKey_byPath_InvalidPath() { +// do { +// _ = try manager.publicKey(byPath: "0/0") +// XCTFail("Expected exception") +// } catch let error as PublicKeyManager.PublicKeyManagerError { +// XCTAssertEqual(error, .invalidPath) +// } catch { +// XCTFail("Unexpected exception") +// } +// } +// +// private func getPublicKey(withAccount account: Int, index: Int, chain: HDWallet.Chain) -> PublicKey { +// let hdPrivKeyData = try! hdWallet.privateKeyData(account: account, index: index, external: chain == .external) +// return PublicKey(withAccount: account, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) +// } +// +//} diff --git a/BitcoinCore/BitcoinCoreTests/TestData.swift b/BitcoinCore/BitcoinCoreTests/TestData.swift index 5beeac41..dc9975f4 100644 --- a/BitcoinCore/BitcoinCoreTests/TestData.swift +++ b/BitcoinCore/BitcoinCoreTests/TestData.swift @@ -212,3 +212,12 @@ class TestData { } } + +func randomBytes(length: Int) -> Data { + var bytes = Data(count: length) + let _ = bytes.withUnsafeMutableBytes { mutableBytes -> Int32 in + SecRandomCopyBytes(kSecRandomDefault, length, mutableBytes.baseAddress!.assumingMemoryBound(to: UInt8.self)) + } + + return bytes +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift new file mode 100644 index 00000000..3e8a8051 --- /dev/null +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift @@ -0,0 +1,27 @@ +public class SegWitAddress: Address, Equatable { + public let type: AddressType + public let keyHash: Data + public let stringValue: String + public let version: UInt8 + + public var scriptType: ScriptType { + switch type { + case .pubKeyHash: return .p2wpkh + case .scriptHash: return .p2wsh + } + } + + public init(type: AddressType, keyHash: Data, bech32: String, version: UInt8) { + self.type = type + self.keyHash = keyHash + self.stringValue = bech32 + self.version = version + } + + static public func ==(lhs: SegWitAddress, rhs: T) -> Bool { + guard let rhs = rhs as? SegWitAddress else { + return false + } + return lhs.type == rhs.type && lhs.keyHash == rhs.keyHash && lhs.version == rhs.version + } +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index fadd4ad5..d4f11dda 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -1,432 +1,386 @@ import XCTest import Cuckoo +import Nimble +import Quick @testable import BitcoinCore -public class SegWitAddress: Address, Equatable { - public let type: AddressType - public let keyHash: Data - public let stringValue: String - public let version: UInt8 - public var scriptType: ScriptType { - switch type { - case .pubKeyHash: return .p2wpkh - case .scriptHash: return .p2wsh - } - } - - public init(type: AddressType, keyHash: Data, bech32: String, version: UInt8) { - self.type = type - self.keyHash = keyHash - self.stringValue = bech32 - self.version = version - } - - static public func ==(lhs: SegWitAddress, rhs: T) -> Bool { - guard let rhs = rhs as? SegWitAddress else { - return false - } - return lhs.type == rhs.type && lhs.keyHash == rhs.keyHash && lhs.version == rhs.version - } -} - -class TransactionBuilderTests: XCTestCase { - - private var mockUnspentOutputSelector: MockIUnspentOutputSelector! - private var mockUnspentOutputProvider: MockIUnspentOutputProvider! - private var mockAddressManager: MockIPublicKeyManager! - private var mockAddressConverter: MockIAddressConverter! - private var mockInputSigner: MockIInputSigner! - private var mockScriptBuilder: MockIScriptBuilder! - private var mockFactory: MockIFactory! - private var mockTransactionSizeCalculator: MockITransactionSizeCalculator! - - private var transactionBuilder: TransactionBuilder! - - private var unspentOutputs: SelectedUnspentOutputInfo! - private var previousTransaction: FullTransaction! - private var transaction: Transaction! - private var toOutputPKH: Output! - private var toOutputWPKH: Output! - private var toOutputSH: Output! - private var changeOutput: Output! - private var inputToSign: InputToSign! - private var totalInputValue: Int! - private var value: Int! - private var feeRate: Int! - private var fee: Int! - private var changePubKey: PublicKey! - private var changePubKeyAddress: String! - private var toAddressPKH: String! - private var toAddressSH: String! - private var toAddressWPKH: String! - private var signature = Data(hex: "0000000000000000000111111111111222222222222")! - private var signatureScript = Data(hex: "150000000000000000000111111111111222222222222")! - - override func setUp() { - super.setUp() - - mockUnspentOutputSelector = MockIUnspentOutputSelector() - mockUnspentOutputProvider = MockIUnspentOutputProvider() - mockAddressManager = MockIPublicKeyManager() - mockAddressConverter = MockIAddressConverter() - mockInputSigner = MockIInputSigner() - mockScriptBuilder = MockIScriptBuilder() - mockFactory = MockIFactory() - mockTransactionSizeCalculator = MockITransactionSizeCalculator() - - transactionBuilder = TransactionBuilder(unspentOutputSelector: mockUnspentOutputSelector, unspentOutputProvider: mockUnspentOutputProvider, publicKeyManager: mockAddressManager, addressConverter: mockAddressConverter, inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory, transactionSizeCalculator: mockTransactionSizeCalculator, bip: .bip44) - - changePubKey = TestData.pubKey() - changePubKeyAddress = "Rsfz3aRmCwTe2J8pSWSYRNYmweJ" - - toAddressPKH = "mzwSXvtPs7MFbW2ysNA4Gw3P2KjrcEWaE5" - toAddressSH = "2MyQWMrsLsqAMSUeusduAzN6pWuH2V27ykE" - toAddressWPKH = "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2" - - previousTransaction = TestData.p2pkhTransaction - - unspentOutputs = SelectedUnspentOutputInfo( - unspentOutputs: [UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: 1000)], - totalValue: previousTransaction.outputs[0].value, fee: 1008, addChangeOutput: true - ) - totalInputValue = unspentOutputs.unspentOutputs[0].output.value - value = 10782000 - feeRate = 6 - fee = 1008 - - transaction = Transaction(version: 1, timestamp: 0) - inputToSign = InputToSign( - input: Input(withPreviousOutputTxHash: previousTransaction.header.dataHash, previousOutputIndex: unspentOutputs.unspentOutputs[0].output.index, script: Data(), sequence: 0), - previousOutput: previousTransaction.outputs[0], previousOutputPublicKey: TestData.pubKey() - ) - toOutputPKH = Output(withValue: value - fee, index: 0, lockingScript: Data(), type: .p2pkh, address: toAddressPKH, keyHash: nil) - toOutputWPKH = Output(withValue: value - fee, index: 0, lockingScript: Data(), type: .p2wpkh, address: toAddressWPKH, keyHash: nil) - toOutputSH = Output(withValue: value - fee, index: 0, lockingScript: Data(), type: .p2sh, address: toAddressSH, keyHash: nil) - changeOutput = Output(withValue: totalInputValue - value, index: 1, lockingScript: Data(), type: .p2pkh, keyHash: changePubKey.keyHash) - - stub(mockUnspentOutputSelector) { mock in - when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) - } - - stub(mockUnspentOutputProvider) { mock in - when(mock.allUnspentOutputs.get).thenReturn(unspentOutputs.unspentOutputs) - } - - stub(mockAddressManager) { mock in - when(mock.changePublicKey()).thenReturn(changePubKey) - } - - stub(mockInputSigner) { mock in - when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn([signature]) - } +class TransactionBuilderTests: QuickSpec { + override func spec() { + let mockInputSigner = MockIInputSigner() + let mockScriptBuilder = MockIScriptBuilder() + let mockFactory = MockIFactory() - stub(mockAddressConverter) { mock in - when(mock.convert(address: toAddressPKH)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: Data(hex: "d50bf226c9ff3bcf06f13d8ca129f24bedeef594")!, base58: "mzwSXvtPs7MFbW2ysNA4Gw3P2KjrcEWaE5")) - when(mock.convert(address: toAddressSH)).thenReturn(LegacyAddress(type: .scriptHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, base58: toAddressSH)) - when(mock.convert(address: toAddressWPKH)).thenReturn(SegWitAddress(type: .pubKeyHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, bech32: "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2", version: 0)) - when(mock.convert(address: changePubKeyAddress)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) - when(mock.convert(publicKey: equal(to: changePubKey), type: equal(to: .p2pkh))).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) - // when(mock.convert(address: any())).thenReturn(Address(type: .pubKeyHash, keyHash: Data(), base58: "")) - } - - stub(mockScriptBuilder) { mock in - when(mock.lockingScript(for: any())).thenReturn(Data()) - } + let toAddressPKH = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "toAddressPKH") + let toAddressSH = LegacyAddress(type: .scriptHash, keyHash: randomBytes(length: 32), base58: "toAddressSH") + let changeAddressPKH = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "changeAddressPKH") + let changeAddressWPKH = SegWitAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), bech32: "changeAddressWPKH", version: 0) - stub(mockFactory) { mock in - when(mock.transaction(version: any(), lockTime: any())).thenReturn(transaction) - when(mock.inputToSign(withPreviousOutput: any(), script: any(), sequence: any())).thenReturn(inputToSign) - when(mock.output(withValue: any(), index: any(), lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any())).thenReturn(toOutputPKH) - when(mock.output(withValue: any(), index: any(), lockingScript: any(), type: equal(to: ScriptType.p2sh), address: equal(to: toAddressSH), keyHash: any(), publicKey: any())).thenReturn(toOutputSH) - when(mock.output(withValue: any(), index: any(), lockingScript: any(), type: equal(to: ScriptType.p2wpkh), address: equal(to: toAddressWPKH), keyHash: any(), publicKey: any())).thenReturn(toOutputWPKH) - when(mock.output(withValue: any(), index: any(), lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any())).thenReturn(changeOutput) - } - } + let signatureData = [randomBytes(length: 72), randomBytes(length: 64)] + let sendingValue = 100_000_000 + let fee = 1000 - override func tearDown() { - unspentOutputs = nil - mockUnspentOutputSelector = nil - mockUnspentOutputProvider = nil - mockAddressConverter = nil - mockInputSigner = nil - mockFactory = nil - mockTransactionSizeCalculator = nil - transactionBuilder = nil - changePubKey = nil - toAddressPKH = nil - toAddressSH = nil - value = nil - feeRate = nil - fee = nil - - super.tearDown() - } + var builder: TransactionBuilder! - func testFee_AddressGiven() { - let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH) - XCTAssertEqual(resultFee, 546) - } + beforeEach { + stub(mockFactory) { mock in + when(mock).transaction(version: 1, lockTime: 0).thenReturn(Transaction(version: 1, lockTime: 0)) + when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: toAddressPKH.keyHash), publicKey: isNil()).thenReturn(self.output(from: toAddressPKH)) + when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: changeAddressPKH.keyHash), publicKey: isNil()).thenReturn(self.output(from: changeAddressPKH)) + when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: toAddressSH.keyHash), publicKey: isNil()).thenReturn(self.output(from: toAddressSH)) + when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: changeAddressWPKH.keyHash), publicKey: isNil()).thenReturn(self.output(from: changeAddressWPKH)) + } - func testFee_AddressGiven_Error() { - stub(mockAddressConverter) { mock in - when(mock.convert(address: toAddressPKH)).thenThrow(BitcoinCoreErrors.AddressConversion.invalidAddressLength) + builder = TransactionBuilder(inputSigner: mockInputSigner, scriptBuilder: mockScriptBuilder, factory: mockFactory) } - do { - let _ = try transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false, address: toAddressPKH) - } catch let error as BitcoinCoreErrors.AddressConversion { - XCTAssertEqual(error, BitcoinCoreErrors.AddressConversion.invalidAddressLength) - } catch let error { - XCTFail(error.localizedDescription) + afterEach { + reset(mockInputSigner, mockScriptBuilder, mockFactory) } - } - func testFee_AddressNotGiven_Error() { - let resultFee = try! transactionBuilder.fee(for: value, feeRate: feeRate, senderPay: false) - XCTAssertEqual(resultFee, fee) - } + describe("#buildTransaction") { + var unspentOutput: UnspentOutput! + var inputToSign: InputToSign! + var fullTransaction: FullTransaction! + + beforeEach { + unspentOutput = UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2pkh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + inputToSign = InputToSign( + input: Input(withPreviousOutputTxHash: randomBytes(length: 32), previousOutputIndex: 0, script: Data(), sequence: 0), + previousOutput: unspentOutput.output, previousOutputPublicKey: unspentOutput.publicKey + ) + + stub(mockFactory) { mock in + when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) + } + stub(mockScriptBuilder) { mock in + when(mock).lockingScript(for: any()).thenReturn(Data()) + } + stub(mockInputSigner) { mock in + when(mock).sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any()).thenReturn(signatureData) + } + } - func testBuildTransaction_P2PKH() { - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) - - XCTAssertNotEqual(resultTx.header.dataHash, Data()) - XCTAssertEqual(resultTx.header.status, .new) - XCTAssertEqual(resultTx.header.isMine, true) - XCTAssertEqual(resultTx.header.segWit, false) - XCTAssertEqual(resultTx.inputs.count, 1) - XCTAssertEqual(resultTx.inputs[0].signatureScript, signatureScript) - XCTAssertEqual(resultTx.inputs[0].witnessData.count, 0) - XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) - XCTAssertEqual(resultTx.inputs[0].previousOutputIndex, unspentOutputs.unspentOutputs[0].output.index) - XCTAssertEqual(resultTx.outputs.count, 2) - XCTAssertEqual(resultTx.outputs[0].address, toAddressPKH) - XCTAssertEqual(resultTx.outputs[0].value, value - fee) - XCTAssertEqual(resultTx.outputs[1].keyHash, changePubKey.keyHash) - XCTAssertEqual(resultTx.outputs[1].value, unspentOutputs.unspentOutputs[0].output.value - value) - - verify(mockFactory).output(withValue: value - fee, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) - verify(mockFactory).output(withValue: unspentOutputs.unspentOutputs[0].output.value - value, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any()) - } + afterEach { + unspentOutput = nil + inputToSign = nil + } -// todo: Extrect build witness transaction data to bitcoinKit chain -// func testBuildTransaction_P2WPKH() { -// let previousTransaction = TestData.p2wpkhTransaction -// -// unspentOutputs = SelectedUnspentOutputInfo( -// unspentOutputs: [UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil)], -// totalValue: previousTransaction.outputs[0].value, fee: 1008, addChangeOutput: true -// ) -// totalInputValue = unspentOutputs.unspentOutputs[0].output.value -// value = 10782000 -// feeRate = 6 -// fee = 1008 -// inputToSign = InputToSign( -// input: Input(withPreviousOutputTxReversedHex: previousTransaction.header.dataHashReversedHex, previousOutputIndex: unspentOutputs.unspentOutputs[0].output.index, script: Data(), sequence: 0), -// previousOutput: previousTransaction.outputs[0], previousOutputPublicKey: TestData.pubKey() -// ) -// -// stub(mockFactory) { mock in -// when(mock.inputToSign(withPreviousOutput: any(), script: any(), sequence: any())).thenReturn(inputToSign) -// } -// stub(mockUnspentOutputSelector) { mock in -// when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any(), unspentOutputs: any())).thenReturn(unspentOutputs) -// } -// -// stub(mockUnspentOutputProvider) { mock in -// when(mock.allUnspentOutputs.get).thenReturn(unspentOutputs.unspentOutputs) -// } -// -// let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressWPKH) -// -// XCTAssertNotEqual(resultTx.header.dataHashReversedHex, "") -// XCTAssertEqual(resultTx.header.status, .new) -// XCTAssertEqual(resultTx.header.isMine, true) -// XCTAssertEqual(resultTx.header.segWit, true) -// XCTAssertEqual(resultTx.inputs.count, 1) -// XCTAssertEqual(resultTx.inputs[0].signatureScript.count, 0) -// XCTAssertEqual(resultTx.inputs[0].witnessData.count, 1) -// XCTAssertEqual(resultTx.inputs[0].witnessData[0], signature) -// XCTAssertEqual(resultTx.inputs[0].previousOutputTxReversedHex, unspentOutputs.unspentOutputs[0].output.transactionHash) -// XCTAssertEqual(resultTx.inputs[0].previousOutputIndex, unspentOutputs.unspentOutputs[0].output.index) -// XCTAssertEqual(resultTx.outputs.count, 2) -// XCTAssertEqual(resultTx.outputs[0].address, toAddressWPKH) -// XCTAssertEqual(resultTx.outputs[1].keyHash, changePubKey.keyHash) -// -// verify(mockFactory).output(withValue: value - fee, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2wpkh), address: equal(to: toAddressWPKH), keyHash: any(), publicKey: any()) -// verify(mockFactory).output(withValue: unspentOutputs.unspentOutputs[0].output.value - value, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any()) -// -// } - - func testBuildTransaction_P2SH() { - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressSH) - - XCTAssertNotEqual(resultTx.header.dataHash, Data()) - XCTAssertEqual(resultTx.header.status, .new) - XCTAssertEqual(resultTx.header.isMine, true) - XCTAssertEqual(resultTx.inputs.count, 1) - XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) - XCTAssertEqual(resultTx.inputs[0].previousOutputIndex, unspentOutputs.unspentOutputs[0].output.index) - XCTAssertEqual(resultTx.outputs.count, 2) - XCTAssertEqual(resultTx.outputs[0].address, toAddressSH) - XCTAssertEqual(resultTx.outputs[0].value, value - fee) - XCTAssertEqual(resultTx.outputs[1].keyHash, changePubKey.keyHash) - - verify(mockFactory).output(withValue: value - fee, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2sh), address: equal(to: toAddressSH), keyHash: any(), publicKey: any()) - verify(mockFactory).output(withValue: unspentOutputs.unspentOutputs[0].output.value - value, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any()) - } + context("when unspentOutput is P2PKH, senderPay is true, addChangeOutput is true") { + beforeEach { + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + } + + it("adds input from unspentOutput") { + verify(mockFactory).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()) + expect(fullTransaction.inputs.count).to(equal(1)) + expect(fullTransaction.inputs[0].previousOutputTxHash).to(equal(inputToSign.input.previousOutputTxHash)) + expect(fullTransaction.inputs[0].previousOutputIndex).to(equal(inputToSign.input.previousOutputIndex)) + } + + it("adds 1 output for toAddress") { + verify(mockFactory).output(withValue: sendingValue, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: toAddressPKH.stringValue, keyHash: equal(to: toAddressPKH.keyHash), publicKey: isNil()) + expect(fullTransaction.outputs.count).to(equal(2)) + + let toOutput = self.output(from: toAddressPKH) + expect(fullTransaction.outputs[0].keyHash).to(equal(toOutput.keyHash)) + expect(fullTransaction.outputs[0].value).to(equal(toOutput.value)) + } + + it("adds 1 output for changeAddress") { + let changeValue = unspentOutput.output.value - sendingValue - fee + verify(mockFactory).output(withValue: changeValue, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: changeAddressPKH.stringValue, keyHash: equal(to: changeAddressPKH.keyHash), publicKey: isNil()) + + let changeOutput = self.output(from: changeAddressPKH) + expect(fullTransaction.outputs[1].keyHash).to(equal(changeOutput.keyHash)) + expect(fullTransaction.outputs[1].value).to(equal(changeOutput.value)) + } + + it("signs the input") { + verify(mockInputSigner).sigScriptData(transaction: any(), inputsToSign: equal(to: [inputToSign]), outputs: equal(to: [self.output(from: toAddressPKH), self.output(from: changeAddressPKH)]), index: 0) + expect(fullTransaction.inputs[0].signatureScript).to(equal(OpCode.push(signatureData[0]) + OpCode.push(signatureData[1]))) + } + + it("sets transaction properties") { + expect(fullTransaction.header.status).to(equal(TransactionStatus.new)) + expect(fullTransaction.header.isMine).to(beTrue()) + expect(fullTransaction.header.isOutgoing).to(beTrue()) + expect(fullTransaction.header.segWit).to(beFalse()) + } + } - func testBuildTransactionSenderPay() { - _ = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: true, toAddress: toAddressPKH) + context("when changeAddress is nil") { + beforeEach { + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: nil) + } - verify(mockFactory).output(withValue: value, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) - verify(mockFactory).output(withValue: unspentOutputs.unspentOutputs[0].output.value - value - fee, index: 1, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: changePubKeyAddress), keyHash: any(), publicKey: any()) - } + it("adds 1 output for toAddress") { + verify(mockFactory).output(withValue: sendingValue, index: 0, lockingScript: any(), type: equal(to: toAddressPKH.scriptType), address: toAddressPKH.stringValue, keyHash: equal(to: toAddressPKH.keyHash), publicKey: isNil()) + } - func testBuildTransaction_WithoutChangeOutput() { - value = totalInputValue - unspentOutputs = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs.unspentOutputs, totalValue: unspentOutputs.totalValue, fee: unspentOutputs.fee, addChangeOutput: false) - stub(mockUnspentOutputSelector) { mock in - when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) - } - - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + it("doesn't add 1 output for changeAddress") { + let changeValue = unspentOutput.output.value - sendingValue - fee + verify(mockFactory, never()).output(withValue: changeValue, index: 1, lockingScript: any(), type: equal(to: changeAddressPKH.scriptType), address: changeAddressPKH.stringValue, keyHash: equal(to: changeAddressPKH.keyHash), publicKey: isNil()) + expect(fullTransaction.outputs.count).to(equal(1)) + } + } - XCTAssertEqual(resultTx.inputs.count, 1) - XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) - XCTAssertEqual(resultTx.inputs[0].previousOutputIndex, unspentOutputs.unspentOutputs[0].output.index) - XCTAssertEqual(resultTx.outputs.count, 1) - XCTAssertEqual(resultTx.outputs[0].address, toAddressPKH) - verify(mockFactory).output(withValue: Int(value - fee), index: 0, lockingScript: equal(to: Data()), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) - } + context("when senderPay is false") { + context("value is valid") { + beforeEach { + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: false, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + } + + it("subtracts fee from value in receiver output") { + let receivedValue = sendingValue - fee + verify(mockFactory).output(withValue: receivedValue, index: 0, lockingScript: any(), type: equal(to: toAddressPKH.scriptType), address: toAddressPKH.stringValue, keyHash: equal(to: toAddressPKH.keyHash), publicKey: isNil()) + } + + it("puts the remained value in change output") { + let changeValue = unspentOutput.output.value - sendingValue + verify(mockFactory).output(withValue: changeValue, index: 1, lockingScript: any(), type: equal(to: changeAddressPKH.scriptType), address: changeAddressPKH.stringValue, keyHash: equal(to: changeAddressPKH.keyHash), publicKey: isNil()) + } + } + + context("value less than fee") { + it("throws feeMoreThanValue exception") { + do { + fullTransaction = try builder.buildTransaction(value: fee - 1, unspentOutputs: [unspentOutput], fee: fee, senderPay: false, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fail("Expecting an exception") + } catch let error as TransactionBuilder.BuildError { + expect(error).to(equal(TransactionBuilder.BuildError.feeMoreThanValue)) + } catch { + fail("Unexpected exception") + } + } + } + } - func testBuildTransaction_ChangeNotAddedForDust() { - value = totalInputValue - TransactionSizeCalculator().outputSize(type: .p2pkh) * feeRate - unspentOutputs = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs.unspentOutputs, totalValue: unspentOutputs.totalValue, fee: unspentOutputs.fee, addChangeOutput: false) - stub(mockUnspentOutputSelector) { mock in - when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(unspentOutputs) - } + context("when toAddress and/or changeAddress types are P2SH or P2WPKH") { + beforeEach { + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressSH, changeAddress: changeAddressWPKH) + } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) + it("generates outputs considering address types") { + verify(mockFactory).output(withValue: any(), index: 0, lockingScript: any(), type: equal(to: ScriptType.p2sh), address: toAddressSH.stringValue, keyHash: equal(to: toAddressSH.keyHash), publicKey: isNil()) + verify(mockFactory).output(withValue: any(), index: 1, lockingScript: any(), type: equal(to: ScriptType.p2wpkh), address: changeAddressWPKH.stringValue, keyHash: equal(to: changeAddressWPKH.keyHash), publicKey: isNil()) + } + } - XCTAssertEqual(resultTx.inputs.count, 1) - XCTAssertEqual(resultTx.inputs[0].previousOutputTxHash, unspentOutputs.unspentOutputs[0].output.transactionHash) - XCTAssertEqual(resultTx.inputs[0].previousOutputIndex, unspentOutputs.unspentOutputs[0].output.index) - XCTAssertEqual(resultTx.outputs.count, 1) - XCTAssertEqual(resultTx.outputs[0].address, toAddressPKH) - verify(mockFactory).output(withValue: Int(value - fee), index: 0, lockingScript: equal(to: Data()), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) - } + context("when unspent output is P2WPKH") { + beforeEach { + unspentOutput = UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2wpkh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + inputToSign = InputToSign( + input: Input(withPreviousOutputTxHash: randomBytes(length: 32), previousOutputIndex: 0, script: Data(), sequence: 0), + previousOutput: unspentOutput.output, previousOutputPublicKey: unspentOutput.publicKey + ) + stub(mockFactory) { mock in + when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) + } + + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + } + + it("sets P2WPKH unlocking script to witnessData") { + expect(fullTransaction.inputs[0].witnessData).to(equal(signatureData)) + } + + it("sets empty data to signatureScript") { + expect(fullTransaction.inputs[0].signatureScript).to(equal(Data())) + } + + it("sets segWit flag to true") { + expect(fullTransaction.header.segWit).to(beTrue()) + } + } - func testBuildTransaction_InputsSigned() { - let sigData = [Data(hex: "000001")!, Data(hex: "000002")!] - let sigScript = Data(hex: "0300000103000002")! + context("when unspent output is P2WPKH(SH") { + beforeEach { + unspentOutput = UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2wpkhSh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + inputToSign = InputToSign( + input: Input(withPreviousOutputTxHash: randomBytes(length: 32), previousOutputIndex: 0, script: Data(), sequence: 0), + previousOutput: unspentOutput.output, previousOutputPublicKey: unspentOutput.publicKey + ) + stub(mockFactory) { mock in + when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) + } + + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + } + + it("sets P2WPKH unlocking script to witnessData") { + expect(fullTransaction.inputs[0].witnessData).to(equal(signatureData)) + } + + it("sets P2SH unlocking script to signatureScript") { + let script = OpCode.push(OpCode.scriptWPKH(unspentOutput.publicKey.keyHash)) + expect(fullTransaction.inputs[0].signatureScript).to(equal(script)) + } + + it("sets segWit flag to true") { + expect(fullTransaction.header.segWit).to(beTrue()) + } + } - stub(mockInputSigner) { mock in - when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) + context("when unspent output is not supported") { + it("throws notSupportedScriptType exception") { + unspentOutput = UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2sh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + inputToSign = InputToSign( + input: Input(withPreviousOutputTxHash: randomBytes(length: 32), previousOutputIndex: 0, script: Data(), sequence: 0), + previousOutput: unspentOutput.output, previousOutputPublicKey: unspentOutput.publicKey + ) + stub(mockFactory) { mock in + when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) + } + + do { + fullTransaction = try builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fail("Expecting an exception") + } catch let error as TransactionBuilder.BuildError { + expect(error).to(equal(TransactionBuilder.BuildError.notSupportedScriptType)) + } catch { + fail("Unexpected exception") + } + } + } } - let resultTx = try! transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) - XCTAssertEqual(resultTx.inputs[0].signatureScript, sigScript) - } - - func testBuildTransaction_feeMoreThanValue() { - unspentOutputs = SelectedUnspentOutputInfo( - unspentOutputs: [UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil)], - totalValue: previousTransaction.outputs[0].value, fee: value, addChangeOutput: true - ) - - do { - let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) - } catch let error as TransactionBuilder.BuildError { - XCTAssertEqual(error, TransactionBuilder.BuildError.feeMoreThanValue) - } catch let error { - XCTFail(error.localizedDescription) - } - } + describe("#buildTransaction(P2SH)") { + let signatureScript = randomBytes(length: 100) + var signatureScriptFunctionCalled = false + let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in + XCTAssertEqual(signature, signatureData[0]) + XCTAssertEqual(publicKey, signatureData[1]) + signatureScriptFunctionCalled = true + return signatureScript + } - func testBuildTransaction_noChangeAddress() { - stub(mockAddressManager) { mock in - when(mock.changePublicKey()).thenThrow(PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) - } + var unspentOutput: UnspentOutput! + var inputToSign: InputToSign! + var fullTransaction: FullTransaction! + + beforeEach { + unspentOutput = UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2sh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + inputToSign = InputToSign( + input: Input(withPreviousOutputTxHash: randomBytes(length: 32), previousOutputIndex: 0, script: Data(), sequence: 0), + previousOutput: unspentOutput.output, previousOutputPublicKey: unspentOutput.publicKey + ) + + stub(mockFactory) { mock in + when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) + } + stub(mockScriptBuilder) { mock in + when(mock).lockingScript(for: any()).thenReturn(Data()) + } + stub(mockInputSigner) { mock in + when(mock).sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any()).thenReturn(signatureData) + } + } - do { - let _ = try transactionBuilder.buildTransaction(value: value, feeRate: feeRate, senderPay: false, toAddress: toAddressPKH) - XCTFail("No exception!") - } catch let error as TransactionBuilder.BuildError { - XCTAssertEqual(error, TransactionBuilder.BuildError.noChangeAddress) - } catch { - XCTFail("Unexpected exception!") - } - } + afterEach { + unspentOutput = nil + inputToSign = nil + signatureScriptFunctionCalled = false + } - func testBuildTransaction_FromUnspentOutput_P2SH() { - let sigData = [Data(hex: "000001")!, Data(hex: "000002")!] - let value = 1000000 + context("when fee is valid, unspent output type is P2SH") { + beforeEach { + fullTransaction = try! builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: fee, signatureScriptFunction: signatureScriptFunction) + } + + + it("adds input from unspentOutput") { + verify(mockFactory).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()) + expect(fullTransaction.inputs.count).to(equal(1)) + expect(fullTransaction.inputs[0].previousOutputTxHash).to(equal(inputToSign.input.previousOutputTxHash)) + expect(fullTransaction.inputs[0].previousOutputIndex).to(equal(inputToSign.input.previousOutputIndex)) + } + + it("adds 1 output for toAddress") { + verify(mockFactory).output(withValue: unspentOutput.output.value - fee, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: toAddressPKH.stringValue, keyHash: equal(to: toAddressPKH.keyHash), publicKey: isNil()) + expect(fullTransaction.outputs.count).to(equal(1)) + + let toOutput = self.output(from: toAddressPKH) + expect(fullTransaction.outputs[0].keyHash).to(equal(toOutput.keyHash)) + expect(fullTransaction.outputs[0].value).to(equal(toOutput.value)) + } + + it("signs the input") { + verify(mockInputSigner).sigScriptData(transaction: any(), inputsToSign: equal(to: [inputToSign]), outputs: equal(to: [self.output(from: toAddressPKH)]), index: 0) + expect(fullTransaction.inputs[0].signatureScript).to(equal(signatureScript)) + expect(signatureScriptFunctionCalled).to(beTrue()) + } + + it("sets transaction properties") { + expect(fullTransaction.header.status).to(equal(TransactionStatus.new)) + expect(fullTransaction.header.isMine).to(beTrue()) + expect(fullTransaction.header.isOutgoing).to(beFalse()) + expect(fullTransaction.header.segWit).to(beFalse()) + } + } - stub(mockInputSigner) { mock in - when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) - } - stub(mockTransactionSizeCalculator) { mock in - when(mock.transactionSize(inputs: equal(to: [ScriptType.p2sh]), outputScriptTypes: equal(to: [ScriptType.p2pkh]))).thenReturn(90) - } + context("when fee is less than value") { + it("throws feeMoreThanValue exception") { + do { + fullTransaction = try builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: unspentOutput.output.value + 1, signatureScriptFunction: signatureScriptFunction) + fail("Expecting an exception") + } catch let error as TransactionBuilder.BuildError { + expect(error).to(equal(TransactionBuilder.BuildError.feeMoreThanValue)) + } catch { + fail("Unexpected exception") + } + } + } - let previousTransaction = TestData.p2shTransaction - previousTransaction.outputs[0].value = value - let unspentOutput = UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil) - let signatureScript = Data(repeating: 0, count: 10) - var calledWithSignatureAndPublicKey = false - let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in - if signature == sigData[0] { - XCTAssertEqual(signature, sigData[0]) - XCTAssertEqual(publicKey, sigData[1]) - calledWithSignatureAndPublicKey = true + context("when unspent output type is not P2SH") { + it("throws feeMoreThanValue exception") { + unspentOutput = UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2wsh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + + do { + fullTransaction = try builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: fee, signatureScriptFunction: signatureScriptFunction) + fail("Expecting an exception") + } catch let error as TransactionBuilder.BuildError { + expect(error).to(equal(TransactionBuilder.BuildError.notSupportedScriptType)) + } catch { + fail("Unexpected exception") + } + } } - return signatureScript } - let fee = (10 + 90) * feeRate - - let resultTx = try! transactionBuilder.buildTransaction(from: unspentOutput, to: toAddressPKH, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) - - XCTAssertNotEqual(resultTx.header.dataHash, Data()) - XCTAssertEqual(resultTx.header.status, .new) - XCTAssertEqual(resultTx.header.isMine, true) - XCTAssertEqual(resultTx.header.segWit, false) - XCTAssertEqual(resultTx.header.isOutgoing, false) - XCTAssertEqual(resultTx.inputs.count, 1) - XCTAssertEqual(resultTx.outputs.count, 1) - XCTAssertEqual(resultTx.inputs[0].signatureScript, signatureScript) - - verify(mockFactory).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: equal(to: Data()), sequence: 0xFFFFFFFF) - verify(mockFactory).output(withValue: value - fee, index: 0, lockingScript: any(), type: equal(to: ScriptType.p2pkh), address: equal(to: toAddressPKH), keyHash: any(), publicKey: any()) - XCTAssertTrue(calledWithSignatureAndPublicKey) } - func testBuildTransaction_FromUnspentOutput_FeeMoreThanValue() { - let sigData = [Data(hex: "000001")!, Data(hex: "000002")!] - stub(mockInputSigner) { mock in - when(mock.sigScriptData(transaction: any(), inputsToSign: any(), outputs: any(), index: any())).thenReturn(sigData) - } - stub(mockTransactionSizeCalculator) { mock in - when(mock.transactionSize(inputs: equal(to: [ScriptType.p2sh]), outputScriptTypes: equal(to: [ScriptType.p2pkh]))).thenReturn(90) - } - - let previousTransaction = TestData.p2shTransaction - let unspentOutput = UnspentOutput(output: previousTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: previousTransaction.header, blockHeight: nil) - let signatureScript = Data(repeating: 0, count: 10) - let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in - return signatureScript - } - let fee = (10 + 90) * feeRate - previousTransaction.outputs[0].value = fee - 1 - - do { - _ = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddressPKH, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) - } catch let error as TransactionBuilder.BuildError { - XCTAssertEqual(error, TransactionBuilder.BuildError.feeMoreThanValue) - } catch let error { - XCTFail(error.localizedDescription) - } - + func output(from address: Address) -> Output { + return Output(withValue: 0, index: 0, lockingScript: Data(), keyHash: address.keyHash) } } diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index ec1fd617..76df0a73 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -1,188 +1,233 @@ -import XCTest -import Cuckoo -import Nimble -import Quick -@testable import BitcoinCore - -class TransactionCreatorTests: QuickSpec { - override func spec() { - let mockTransactionBuilder = MockITransactionBuilder() - let mockTransactionProcessor = MockITransactionProcessor() - let mockTransactionSender = MockITransactionSender() - let mockBloomFilterManager = MockIBloomFilterManager() - - var transactionCreator: TransactionCreator! - let transaction = TestData.p2pkhTransaction - - afterEach { - reset(mockTransactionBuilder, mockTransactionProcessor, mockTransactionSender, mockBloomFilterManager) - transactionCreator = nil - } - - describe("#create(to:value:feeRate:senderPay:)") { - beforeEach { - stub(mockTransactionBuilder) { mock in - when(mock.buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any())).thenReturn(transaction) - } - stub(mockTransactionProcessor) { mock in - when(mock.processCreated(transaction: any())).thenDoNothing() - } - stub(mockTransactionSender) { mock in - when(mock.send(pendingTransaction: any())).thenDoNothing() - } - stub(mockBloomFilterManager) { mock in - when(mock.regenerateBloomFilter()).thenDoNothing() - } - - transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, bloomFilterManager: mockBloomFilterManager) - } - - context("when BloomFilterManager.BloomFilterExpired error") { - beforeEach { - stub(mockTransactionProcessor) { mock in - when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) - } - stub(mockTransactionSender) { mock in - when(mock.verifyCanSend()).thenDoNothing() - } - - _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) - } - - it("does create transaction") { - verify(mockTransactionBuilder).buildTransaction(value: any(), feeRate: any(), senderPay: any(), toAddress: any()) - verify(mockTransactionProcessor).processCreated(transaction: any()) - } - - it("does send transaction") { - verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) - } - - it("regenerates bloomfilter") { - verify(mockBloomFilterManager).regenerateBloomFilter() - } - } - - context("when other error") { - beforeEach { - stub(mockTransactionSender) { mock in - when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) - } - - _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) - } - - it("doesn't create transaction") { - verify(mockTransactionProcessor, never()).processCreated(transaction: any()) - } - - it("doesn't regenerate bloomfilter") { - verify(mockBloomFilterManager, never()).regenerateBloomFilter() - } - } - - context("when success") { - beforeEach { - stub(mockTransactionSender) { mock in - when(mock.verifyCanSend()).thenDoNothing() - } - _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) - } - - it("creates transaction") { - verify(mockTransactionBuilder).buildTransaction(value: 0, feeRate: 0, senderPay: false, toAddress: "") - verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) - } - - it("sends transaction") { - verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) - } - } - } - - describe("#create(from:to:feeRate:signatureScriptFunction:)") { - let unspentOutput = UnspentOutput(output: TestData.p2shTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: Transaction(), blockHeight: nil) - let signatureScriptFunction: (Data, Data) -> Data = { return $0 + $1 } - - beforeEach { - stub(mockTransactionBuilder) { mock in - when(mock.buildTransaction(from: any(), to: any(), feeRate: any(), signatureScriptFunction: any())).thenReturn(transaction) - } - stub(mockTransactionProcessor) { mock in - when(mock.processCreated(transaction: any())).thenDoNothing() - } - stub(mockTransactionSender) { mock in - when(mock.send(pendingTransaction: any())).thenDoNothing() - } - stub(mockBloomFilterManager) { mock in - when(mock.regenerateBloomFilter()).thenDoNothing() - } - - transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, bloomFilterManager: mockBloomFilterManager) - } - - context("when BloomFilterManager.BloomFilterExpired error") { - beforeEach { - stub(mockTransactionProcessor) { mock in - when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) - } - stub(mockTransactionSender) { mock in - when(mock.verifyCanSend()).thenDoNothing() - } - - _ = try? transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) - } - - it("does create transaction") { - verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: "", feeRate: 0, signatureScriptFunction: any()) - verify(mockTransactionProcessor).processCreated(transaction: any()) - } - - it("does send transaction") { - verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) - } - - it("regenerates bloomfilter") { - verify(mockBloomFilterManager).regenerateBloomFilter() - } - } - - context("when other error") { - beforeEach { - stub(mockTransactionSender) { mock in - when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) - } - - _ = try? transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) - } - - it("doesn't create transaction") { - verify(mockTransactionProcessor, never()).processCreated(transaction: any()) - } - - it("doesn't regenerate bloomfilter") { - verify(mockBloomFilterManager, never()).regenerateBloomFilter() - } - } - - context("when success") { - beforeEach { - stub(mockTransactionSender) { mock in - when(mock.verifyCanSend()).thenDoNothing() - } - - _ = try! transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) - } - - it("creates transaction") { - verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: "", feeRate: 0, signatureScriptFunction: any()) - verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) - } - - it("sends transaction") { - verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) - } - } - } - } -} +//import XCTest +//import Cuckoo +//import Nimble +//import Quick +//@testable import BitcoinCore +// +//class TransactionCreatorTests: QuickSpec { +// override func spec() { +// let mockTransactionBuilder = MockITransactionBuilder() +// let mockTransactionProcessor = MockITransactionProcessor() +// let mockTransactionFeeCalculator = MockITransactionFeeCalculator() +// let mockTransactionSender = MockITransactionSender() +// let mockBloomFilterManager = MockIBloomFilterManager() +// let mockAddressConverter = MockIAddressConverter() +// let mockPublicKeyManager = MockIPublicKeyManager() +// +// let transaction = TestData.p2pkhTransaction +// let changePubKey = TestData.pubKey() +// +// let toAddressPKH = LegacyAddress(type: .pubKeyHash, keyHash: Data(hex: "d50bf226c9ff3bcf06f13d8ca129f24bedeef594")!, base58: "mzwSXvtPs7MFbW2ysNA4Gw3P2KjrcEWaE5") +// let toAddressSH = LegacyAddress(type: .scriptHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, base58: toAddressSH) +// let toAddressWPKH = SegWitAddress(type: .pubKeyHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, bech32: "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2", version: 0) +// let changeAddressPKH = LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changeAddressPKH) +// +// +// let sendingValue = 100_000_000 +// +// var unspentOutputs: [UnspentOutput]! +// var selectedOutputsInfo: SelectedUnspentOutputInfo! +// +// var transactionCreator: TransactionCreator! +// +// beforeEach { +// unspentOutputs = [ +// UnspentOutput( +// output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2pkh), +// publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), +// transaction: Transaction(), +// blockHeight: 1000 +// ) +// ] +// selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: 1000, addChangeOutput: true) +// +// stub(mockTransactionFeeCalculator) { mock in +// when(mock).feeWithUnspentOutputs(value: sendingValue, feeRate: any(), toScriptType: any(), changeScriptType: any(), senderPay: any()).thenReturn(selectedOutputsInfo) +// } +// +// stub(mockAddressConverter) { mock in +// when(mock.convert(address: toAddressPKH)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: Data(hex: "d50bf226c9ff3bcf06f13d8ca129f24bedeef594")!, base58: "mzwSXvtPs7MFbW2ysNA4Gw3P2KjrcEWaE5")) +// when(mock.convert(address: toAddressSH)).thenReturn(LegacyAddress(type: .scriptHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, base58: toAddressSH)) +// when(mock.convert(address: toAddressWPKH)).thenReturn(SegWitAddress(type: .pubKeyHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, bech32: "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2", version: 0)) +// when(mock.convert(address: changePubKeyAddress)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) +// when(mock.convert(publicKey: equal(to: changePubKey), type: equal(to: .p2pkh))).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) +// } +// +// +// } +// +// afterEach { +// reset(mockTransactionBuilder, mockTransactionProcessor, mockTransactionFeeCalculator, mockTransactionSender, mockBloomFilterManager, mockAddressConverter, mockPublicKeyManager) +// transactionCreator = nil +// } +// +// describe("#create(to:value:feeRate:senderPay:)") { +// beforeEach { +// stub(mockTransactionBuilder) { mock in +// when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any())).thenReturn(transaction) +// } +// stub(mockTransactionProcessor) { mock in +// when(mock.processCreated(transaction: any())).thenDoNothing() +// } +// stub(mockTransactionSender) { mock in +// when(mock.send(pendingTransaction: any())).thenDoNothing() +// } +// stub(mockBloomFilterManager) { mock in +// when(mock.regenerateBloomFilter()).thenDoNothing() +// } +// +// transactionCreator = TransactionCreator( +// transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, transactionFeeCalculator: mockTransactionFeeCalculator, +// bloomFilterManager: mockBloomFilterManager, addressConverter: mockAddressConverter, publicKeyManager: mockPublicKeyManager, bip: .bip44) +// } +// +// context("when BloomFilterManager.BloomFilterExpired error") { +// beforeEach { +// stub(mockTransactionProcessor) { mock in +// when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) +// } +// stub(mockTransactionSender) { mock in +// when(mock.verifyCanSend()).thenDoNothing() +// } +// +// _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) +// } +// +// it("does create transaction") { +// verify(mockTransactionBuilder).buildTransaction(value: 0, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: any(), toAddress: any(), changeAddress: nil) +// verify(mockTransactionProcessor).processCreated(transaction: any()) +// } +// +// it("does send transaction") { +// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) +// } +// +// it("regenerates bloomfilter") { +// verify(mockBloomFilterManager).regenerateBloomFilter() +// } +// } +// +// context("when other error") { +// beforeEach { +// stub(mockTransactionSender) { mock in +// when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) +// } +// +// _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) +// } +// +// it("doesn't create transaction") { +// verify(mockTransactionProcessor, never()).processCreated(transaction: any()) +// } +// +// it("doesn't regenerate bloomfilter") { +// verify(mockBloomFilterManager, never()).regenerateBloomFilter() +// } +// } +// +// context("when success") { +// beforeEach { +// stub(mockTransactionSender) { mock in +// when(mock.verifyCanSend()).thenDoNothing() +// } +// _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) +// } +// +// it("creates transaction") { +// verify(mockTransactionBuilder).buildTransaction(value: 0, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: any(), toAddress: any(), changeAddress: nil) +// verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) +// } +// +// it("sends transaction") { +// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) +// } +// } +// } +// +// describe("#create(from:to:feeRate:signatureScriptFunction:)") { +// let unspentOutput = UnspentOutput(output: TestData.p2shTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: Transaction(), blockHeight: nil) +// let signatureScriptFunction: (Data, Data) -> Data = { return $0 + $1 } +// let fee = 1000 +// +// beforeEach { +// stub(mockTransactionBuilder) { mock in +// when(mock.buildTransaction(from: any(), to: any(), fee: any(), signatureScriptFunction: any())).thenReturn(transaction) +// } +// stub(mockTransactionProcessor) { mock in +// when(mock.processCreated(transaction: any())).thenDoNothing() +// } +// stub(mockTransactionSender) { mock in +// when(mock.send(pendingTransaction: any())).thenDoNothing() +// } +// stub(mockBloomFilterManager) { mock in +// when(mock.regenerateBloomFilter()).thenDoNothing() +// } +// +// transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, bloomFilterManager: mockBloomFilterManager) +// } +// +// context("when BloomFilterManager.BloomFilterExpired error") { +// beforeEach { +// stub(mockTransactionProcessor) { mock in +// when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) +// } +// stub(mockTransactionSender) { mock in +// when(mock.verifyCanSend()).thenDoNothing() +// } +// +// _ = try? transactionCreator.create(from: unspentOutput, to: toAddressPKH, feeRate: 0, signatureScriptFunction: signatureScriptFunction) +// } +// +// it("does create transaction") { +// verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: , fee: fee, signatureScriptFunction: any()) +// verify(mockTransactionProcessor).processCreated(transaction: any()) +// } +// +// it("does send transaction") { +// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) +// } +// +// it("regenerates bloomfilter") { +// verify(mockBloomFilterManager).regenerateBloomFilter() +// } +// } +// +// context("when other error") { +// beforeEach { +// stub(mockTransactionSender) { mock in +// when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) +// } +// +// _ = try? transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) +// } +// +// it("doesn't create transaction") { +// verify(mockTransactionProcessor, never()).processCreated(transaction: any()) +// } +// +// it("doesn't regenerate bloomfilter") { +// verify(mockBloomFilterManager, never()).regenerateBloomFilter() +// } +// } +// +// context("when success") { +// beforeEach { +// stub(mockTransactionSender) { mock in +// when(mock.verifyCanSend()).thenDoNothing() +// } +// +// _ = try! transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) +// } +// +// it("creates transaction") { +// verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: "", feeRate: 0, signatureScriptFunction: any()) +// verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) +// } +// +// it("sends transaction") { +// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) +// } +// } +// } +// } +//} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift index f0310374..beb9b053 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift @@ -1,534 +1,534 @@ -import XCTest -import Cuckoo -@testable import BitcoinCore - -class TransactionProcessorTests: XCTestCase { - private var mockStorage: MockIStorage! - private var mockOutputExtractor: MockITransactionExtractor! - private var mockOutputAddressExtractor: MockITransactionOutputAddressExtractor! - private var mockInputExtractor: MockITransactionExtractor! - private var mockOutputsCache: MockIOutputsCache! - private var mockAddressManager: MockIPublicKeyManager! - private var mockBlockchainDataListener: MockIBlockchainDataListener! - private var mockTransactionListener: MockITransactionListener! - - private var generatedDate: Date! - private var dateGenerator: (() -> Date)! - - private var transactionProcessor: TransactionProcessor! - - override func setUp() { - super.setUp() - - generatedDate = Date() - dateGenerator = { - return self.generatedDate - } - - mockStorage = MockIStorage() - mockOutputExtractor = MockITransactionExtractor() - mockOutputAddressExtractor = MockITransactionOutputAddressExtractor() - mockInputExtractor = MockITransactionExtractor() - mockOutputsCache = MockIOutputsCache() - mockAddressManager = MockIPublicKeyManager() - mockBlockchainDataListener = MockIBlockchainDataListener() - mockTransactionListener = MockITransactionListener() - - stub(mockStorage) { mock in - when(mock.transaction(byHash: any())).thenReturn(nil) - when(mock.add(transaction: any())).thenDoNothing() - when(mock.update(transaction: any())).thenDoNothing() - when(mock.update(block: any())).thenDoNothing() - } - stub(mockOutputsCache) { mock in - when(mock.add(fromOutputs: any())).thenDoNothing() - when(mock.hasOutputs(forInputs: any())).thenReturn(false) - } - stub(mockOutputExtractor) { mock in - when(mock.extract(transaction: any())).thenDoNothing() - } - stub(mockOutputAddressExtractor) { mock in - when(mock.extractOutputAddresses(transaction: any())).thenDoNothing() - } - stub(mockInputExtractor) { mock in - when(mock.extract(transaction: any())).thenDoNothing() - } - stub(mockAddressManager) { mock in - when(mock.gapShifts()).thenReturn(false) - } - stub(mockBlockchainDataListener) { mock in - when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() - when(mock.onDelete(transactionHashes: any())).thenDoNothing() - when(mock.onInsert(block: any())).thenDoNothing() - } - stub(mockTransactionListener) { mock in - when(mock.onReceive(transaction: any())).thenDoNothing() - } - - transactionProcessor = TransactionProcessor(storage: mockStorage, outputExtractor: mockOutputExtractor, inputExtractor: mockInputExtractor, outputsCache: mockOutputsCache, outputAddressExtractor: mockOutputAddressExtractor, addressManager: mockAddressManager, listener: mockBlockchainDataListener, dateGenerator: dateGenerator) - transactionProcessor.transactionListener = mockTransactionListener - } - - override func tearDown() { - mockStorage = nil - mockOutputExtractor = nil - mockInputExtractor = nil - mockOutputsCache = nil - transactionProcessor = nil - mockBlockchainDataListener = nil - - generatedDate = nil - dateGenerator = nil - - super.tearDown() - } - - func testProcessCreated() { - let transaction = TestData.p2pkhTransaction - - try! transactionProcessor.processCreated(transaction: transaction) - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [Transaction]()), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) - verify(mockStorage).add(transaction: equal(to: transaction)) - verifyNoMoreInteractions(mockOutputAddressExtractor) - verifyNoMoreInteractions(mockInputExtractor) - } - - func testProcessCreated_isMine() { - let transaction = TestData.p2pkhTransaction - transaction.header.isMine = true - - try! transactionProcessor.processCreated(transaction: transaction) - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) - verify(mockStorage).add(transaction: equal(to: transaction)) - verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) - verify(mockInputExtractor).extract(transaction: equal(to: transaction)) - } - - func testProcessCreated_TransactionExists() { - let transaction = TestData.p2pkhTransaction - transaction.header.isMine = true - - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) - } - - do { - try transactionProcessor.processCreated(transaction: transaction) - XCTFail("Expecting error") - } catch let error as TransactionCreator.CreationError { - XCTAssertEqual(error, TransactionCreator.CreationError.transactionAlreadyExists) - } catch { - XCTFail("Unexpected error") - } - - verify(mockOutputExtractor, never()).extract(transaction: any()) - verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) - verify(mockBlockchainDataListener, never()).onUpdate(updated: any(), inserted: any(), inBlock: any()) - verify(mockStorage, never()).add(transaction: any()) - verify(mockOutputAddressExtractor, never()).extractOutputAddresses(transaction: any()) - verify(mockInputExtractor, never()).extract(transaction: any()) - } - - func testProcessCreated_OutputsExpireBloomFiler() { - let transaction = TestData.p2wpkhTransaction - transaction.header.isMine = true - transaction.outputs[0].publicKeyPath = TestData.pubKey().path - - do { - try transactionProcessor.processCreated(transaction: transaction) - XCTFail("Expecting error") - } catch _ as BloomFilterManager.BloomFilterExpired { - } catch { - XCTFail("Unexpected error") - } - } - - func testProcessReceived_TransactionExists() { - let transaction = TestData.p2pkhTransaction - transaction.header.status = .new - - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) - } - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - - verify(mockOutputExtractor, never()).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) - verify(mockStorage).update(transaction: equal(to: transaction.header)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, nil) - XCTAssertEqual(transaction.header.order, 0) - } - - func testProcessReceived_SeveralMempoolTransactions() { - let transactions = self.transactions() - for transaction in transactions { - transaction.header.isMine = true - transaction.header.timestamp = 0 - transaction.header.order = 0 - } - transactions[1].header.status = .new - - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) - when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) - } - - try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: nil, skipCheckBloomFilter: false) - - verify(mockStorage).add(transaction: equal(to: transactions[0])) - verify(mockStorage).update(transaction: equal(to: transactions[1].header)) - verify(mockStorage).add(transaction: equal(to: transactions[2])) - verify(mockStorage).update(transaction: equal(to: transactions[3].header)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: nil)) - - for (i, transaction) in transactions.enumerated() { - XCTAssertEqual(transaction.header.blockHash, nil) - XCTAssertEqual(transaction.header.status, .relayed) - XCTAssertEqual(transaction.header.order, i) - XCTAssertEqual(transaction.header.timestamp, Int(generatedDate.timeIntervalSince1970)) - } - } - - func testProcessReceivedMempool_After_Block_TransactionExists() { - let transaction = TestData.p2pkhTransaction - let block = TestData.firstBlock - transaction.header.status = .new - - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) - } - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) - verify(mockStorage).update(transaction: equal(to: transaction.header)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) - - reset(mockStorage, mockBlockchainDataListener) - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) - } - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - - verify(mockStorage, never()).update(transaction: equal(to: transaction.header)) - verify(mockBlockchainDataListener, never()).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, block.headerHash) - XCTAssertEqual(transaction.header.timestamp, block.timestamp) - XCTAssertEqual(transaction.header.order, 0) - - } - - func testProcessReceivedBlock_After_Block_TransactionExists() { - let transaction = TestData.p2pkhTransaction - let block = TestData.firstBlock - let nextBlock = TestData.secondBlock - transaction.header.status = .new - - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) - } - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) - verify(mockStorage).update(transaction: equal(to: transaction.header)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) - - reset(mockStorage, mockBlockchainDataListener) - stub(mockStorage) { mock in - when(mock.update(transaction: any())).thenDoNothing() - when(mock.update(block: any())).thenDoNothing() - when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) - } - stub(mockBlockchainDataListener) { mock in - when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() - } - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nextBlock, skipCheckBloomFilter: false) - - verify(mockStorage).update(transaction: equal(to: transaction.header)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nextBlock)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, nextBlock.headerHash) - XCTAssertEqual(transaction.header.timestamp, nextBlock.timestamp) - XCTAssertEqual(transaction.header.order, 0) - } - - - func testProcessReceived_SeveralTransactionsInBlock() { - let transactions = self.transactions() - let block = TestData.firstBlock - - for transaction in transactions { - transaction.header.isMine = true - transaction.header.timestamp = 0 - transaction.header.order = 0 - } - transactions[1].header.status = .new - - stub(mockStorage) { mock in - when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) - when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) - } - - try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: block, skipCheckBloomFilter: false) - - verify(mockStorage).add(transaction: equal(to: transactions[0])) - verify(mockStorage).update(transaction: equal(to: transactions[1].header)) - verify(mockStorage).add(transaction: equal(to: transactions[2])) - verify(mockStorage).update(transaction: equal(to: transactions[3].header)) - verify(mockStorage).update(block: equal(to: block)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: block)) - - for (i, transaction) in transactions.enumerated() { - XCTAssertEqual(transaction.header.blockHash, block.headerHash) - XCTAssertEqual(transaction.header.status, .relayed) - XCTAssertEqual(transaction.header.order, i) - XCTAssertEqual(transaction.header.timestamp, block.header.timestamp) - } - } - - func testProcessReceived_TransactionNotExists_Mine() { - let transaction = TestData.p2pkhTransaction - transaction.header.isMine = true - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) - verify(mockStorage).add(transaction: equal(to: transaction)) - verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) - verify(mockInputExtractor).extract(transaction: equal(to: transaction)) - verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, nil) - } - - func testProcessReceived_TransactionNotExists_NotMine() { - let transaction = TestData.p2pkhTransaction - transaction.header.isMine = false - - try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockStorage, never()).add(transaction: any()) - verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) - verifyNoMoreInteractions(mockBlockchainDataListener) - verifyNoMoreInteractions(mockOutputAddressExtractor) - verifyNoMoreInteractions(mockInputExtractor) - } - - func testProcessReceived_TransactionNotExists_Mine_GapShifts() { - let transaction = TestData.p2pkhTransaction - transaction.header.isMine = true - - stub(mockAddressManager) { mock in - when(mock.gapShifts()).thenReturn(true) - } - - do { - try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - XCTFail("Should throw exception") - } catch _ as BloomFilterManager.BloomFilterExpired { - } catch { - XCTFail("Unknown error thrown") - } - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) - verify(mockStorage).add(transaction: equal(to: transaction)) - verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) - verify(mockInputExtractor).extract(transaction: equal(to: transaction)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, nil) - } - - func testProcessReceived_TransactionNotExists_Mine_OutputsExpireBloomFiler() { - let transaction = TestData.p2wpkhTransaction - transaction.header.isMine = true - transaction.outputs[0].publicKeyPath = TestData.pubKey().path - - do { - try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - XCTFail("Should throw exception") - } catch _ as BloomFilterManager.BloomFilterExpired { - } catch { - XCTFail("Unknown error thrown") - } - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) - verify(mockStorage).add(transaction: equal(to: transaction)) - verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) - verify(mockInputExtractor).extract(transaction: equal(to: transaction)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, nil) - } - - func testProcessReceived_TransactionNotExists_Mine_GapShifts_CheckBloomFilterFalse() { - let transaction = TestData.p2wpkhTransaction - transaction.header.isMine = true - transaction.outputs[0].publicKeyPath = TestData.pubKey().path - - do { - try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: true) - } catch { - XCTFail("Unknown error thrown") - } - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) - verify(mockStorage).add(transaction: equal(to: transaction)) - verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) - verify(mockInputExtractor).extract(transaction: equal(to: transaction)) - - XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) - XCTAssertEqual(transaction.header.blockHash, nil) - } - - func testProcessReceived_TransactionNotExists_NotMine_GapShifts() { - let transaction = TestData.p2pkhTransaction - transaction.header.isMine = false - - stub(mockAddressManager) { mock in - when(mock.gapShifts()).thenReturn(true) - } - - do { - try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) - } catch { - XCTFail("Shouldn't throw exception") - } - - verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) - verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) - verify(mockStorage, never()).add(transaction: any()) - verifyNoMoreInteractions(mockBlockchainDataListener) - verifyNoMoreInteractions(mockOutputAddressExtractor) - verifyNoMoreInteractions(mockInputExtractor) - } - - func testProcessReceived_TransactionNotInTopologicalOrder() { - let transactions = self.transactions() - var calledTransactions = [FullTransaction]() - - stub(mockOutputExtractor) { mock in - when(mock.extract(transaction: any())).then { transaction in - calledTransactions.append(transaction) - } - } - - for i in 0..<4 { - for j in 0..<4 { - for k in 0..<4 { - for l in 0..<4 { - if [0, 1, 2, 3].contains(where: { $0 != i && $0 != j && $0 != k && $0 != l }) { - continue - } - - calledTransactions = [] - - try! transactionProcessor.processReceived(transactions: [transactions[i], transactions[j], transactions[k], transactions[l]], inBlock: nil, skipCheckBloomFilter: false) - - verifyNoMoreInteractions(mockBlockchainDataListener) - - for (m, transaction) in calledTransactions.enumerated() { - XCTAssertEqual(transaction.header.dataHash, transactions[m].header.dataHash) - } - } - } - } - } - } - - - private func transactions() -> [FullTransaction] { - let transaction = FullTransaction( - header: Transaction(version: 0, lockTime: 0), - inputs: [ - Input( - withPreviousOutputTxHash: Data(from: 1), - previousOutputIndex: 0, - script: Data(from: 999999999999), - sequence: 0 - ) - ], - outputs: [ - Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) - ] - ) - - let transaction2 = FullTransaction( - header: Transaction(version: 0, lockTime: 0), - inputs: [ - Input( - withPreviousOutputTxHash: transaction.header.dataHash, - previousOutputIndex: 0, - script: Data(from: 999999999999), - sequence: 0 - ) - ], - outputs: [ - Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), - Output(withValue: 0, index: 1, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) - ] - ) - - let transaction3 = FullTransaction( - header: Transaction(version: 0, lockTime: 0), - inputs: [ - Input( - withPreviousOutputTxHash: transaction2.header.dataHash, - previousOutputIndex: 0, - script: Data(from: 999999999999), - sequence: 0 - ) - ], - outputs: [ - Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), - ] - ) - - let transaction4 = FullTransaction( - header: Transaction(version: 0, lockTime: 0), - inputs: [ - Input( - withPreviousOutputTxHash: transaction2.header.dataHash, - previousOutputIndex: 1, - script: Data(from: 999999999999), - sequence: 0 - ), - Input( - withPreviousOutputTxHash: transaction3.header.dataHash, - previousOutputIndex: 0, - script: Data(from: 999999999999), - sequence: 0 - ) - ], - outputs: [ - Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) - ] - ) - - return [transaction, transaction2, transaction3, transaction4] - } - -} +//import XCTest +//import Cuckoo +//@testable import BitcoinCore +// +//class TransactionProcessorTests: XCTestCase { +// private var mockStorage: MockIStorage! +// private var mockOutputExtractor: MockITransactionExtractor! +// private var mockOutputAddressExtractor: MockITransactionOutputAddressExtractor! +// private var mockInputExtractor: MockITransactionExtractor! +// private var mockOutputsCache: MockIOutputsCache! +// private var mockAddressManager: MockIPublicKeyManager! +// private var mockBlockchainDataListener: MockIBlockchainDataListener! +// private var mockTransactionListener: MockITransactionListener! +// +// private var generatedDate: Date! +// private var dateGenerator: (() -> Date)! +// +// private var transactionProcessor: TransactionProcessor! +// +// override func setUp() { +// super.setUp() +// +// generatedDate = Date() +// dateGenerator = { +// return self.generatedDate +// } +// +// mockStorage = MockIStorage() +// mockOutputExtractor = MockITransactionExtractor() +// mockOutputAddressExtractor = MockITransactionOutputAddressExtractor() +// mockInputExtractor = MockITransactionExtractor() +// mockOutputsCache = MockIOutputsCache() +// mockAddressManager = MockIPublicKeyManager() +// mockBlockchainDataListener = MockIBlockchainDataListener() +// mockTransactionListener = MockITransactionListener() +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: any())).thenReturn(nil) +// when(mock.add(transaction: any())).thenDoNothing() +// when(mock.update(transaction: any())).thenDoNothing() +// when(mock.update(block: any())).thenDoNothing() +// } +// stub(mockOutputsCache) { mock in +// when(mock.add(fromOutputs: any())).thenDoNothing() +// when(mock.hasOutputs(forInputs: any())).thenReturn(false) +// } +// stub(mockOutputExtractor) { mock in +// when(mock.extract(transaction: any())).thenDoNothing() +// } +// stub(mockOutputAddressExtractor) { mock in +// when(mock.extractOutputAddresses(transaction: any())).thenDoNothing() +// } +// stub(mockInputExtractor) { mock in +// when(mock.extract(transaction: any())).thenDoNothing() +// } +// stub(mockAddressManager) { mock in +// when(mock.gapShifts()).thenReturn(false) +// } +// stub(mockBlockchainDataListener) { mock in +// when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() +// when(mock.onDelete(transactionHashes: any())).thenDoNothing() +// when(mock.onInsert(block: any())).thenDoNothing() +// } +// stub(mockTransactionListener) { mock in +// when(mock.onReceive(transaction: any())).thenDoNothing() +// } +// +// transactionProcessor = TransactionProcessor(storage: mockStorage, outputExtractor: mockOutputExtractor, inputExtractor: mockInputExtractor, outputsCache: mockOutputsCache, outputAddressExtractor: mockOutputAddressExtractor, addressManager: mockAddressManager, listener: mockBlockchainDataListener, dateGenerator: dateGenerator) +// transactionProcessor.transactionListener = mockTransactionListener +// } +// +// override func tearDown() { +// mockStorage = nil +// mockOutputExtractor = nil +// mockInputExtractor = nil +// mockOutputsCache = nil +// transactionProcessor = nil +// mockBlockchainDataListener = nil +// +// generatedDate = nil +// dateGenerator = nil +// +// super.tearDown() +// } +// +// func testProcessCreated() { +// let transaction = TestData.p2pkhTransaction +// +// try! transactionProcessor.processCreated(transaction: transaction) +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [Transaction]()), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) +// verify(mockStorage).add(transaction: equal(to: transaction)) +// verifyNoMoreInteractions(mockOutputAddressExtractor) +// verifyNoMoreInteractions(mockInputExtractor) +// } +// +// func testProcessCreated_isMine() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.isMine = true +// +// try! transactionProcessor.processCreated(transaction: transaction) +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) +// verify(mockStorage).add(transaction: equal(to: transaction)) +// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) +// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) +// } +// +// func testProcessCreated_TransactionExists() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.isMine = true +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) +// } +// +// do { +// try transactionProcessor.processCreated(transaction: transaction) +// XCTFail("Expecting error") +// } catch let error as TransactionCreator.CreationError { +// XCTAssertEqual(error, TransactionCreator.CreationError.transactionAlreadyExists) +// } catch { +// XCTFail("Unexpected error") +// } +// +// verify(mockOutputExtractor, never()).extract(transaction: any()) +// verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) +// verify(mockBlockchainDataListener, never()).onUpdate(updated: any(), inserted: any(), inBlock: any()) +// verify(mockStorage, never()).add(transaction: any()) +// verify(mockOutputAddressExtractor, never()).extractOutputAddresses(transaction: any()) +// verify(mockInputExtractor, never()).extract(transaction: any()) +// } +// +// func testProcessCreated_OutputsExpireBloomFiler() { +// let transaction = TestData.p2wpkhTransaction +// transaction.header.isMine = true +// transaction.outputs[0].publicKeyPath = TestData.pubKey().path +// +// do { +// try transactionProcessor.processCreated(transaction: transaction) +// XCTFail("Expecting error") +// } catch _ as BloomFilterManager.BloomFilterExpired { +// } catch { +// XCTFail("Unexpected error") +// } +// } +// +// func testProcessReceived_TransactionExists() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.status = .new +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) +// } +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// +// verify(mockOutputExtractor, never()).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) +// verify(mockStorage).update(transaction: equal(to: transaction.header)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, nil) +// XCTAssertEqual(transaction.header.order, 0) +// } +// +// func testProcessReceived_SeveralMempoolTransactions() { +// let transactions = self.transactions() +// for transaction in transactions { +// transaction.header.isMine = true +// transaction.header.timestamp = 0 +// transaction.header.order = 0 +// } +// transactions[1].header.status = .new +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) +// when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) +// } +// +// try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: nil, skipCheckBloomFilter: false) +// +// verify(mockStorage).add(transaction: equal(to: transactions[0])) +// verify(mockStorage).update(transaction: equal(to: transactions[1].header)) +// verify(mockStorage).add(transaction: equal(to: transactions[2])) +// verify(mockStorage).update(transaction: equal(to: transactions[3].header)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: nil)) +// +// for (i, transaction) in transactions.enumerated() { +// XCTAssertEqual(transaction.header.blockHash, nil) +// XCTAssertEqual(transaction.header.status, .relayed) +// XCTAssertEqual(transaction.header.order, i) +// XCTAssertEqual(transaction.header.timestamp, Int(generatedDate.timeIntervalSince1970)) +// } +// } +// +// func testProcessReceivedMempool_After_Block_TransactionExists() { +// let transaction = TestData.p2pkhTransaction +// let block = TestData.firstBlock +// transaction.header.status = .new +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) +// } +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) +// verify(mockStorage).update(transaction: equal(to: transaction.header)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) +// +// reset(mockStorage, mockBlockchainDataListener) +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) +// } +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// +// verify(mockStorage, never()).update(transaction: equal(to: transaction.header)) +// verify(mockBlockchainDataListener, never()).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, block.headerHash) +// XCTAssertEqual(transaction.header.timestamp, block.timestamp) +// XCTAssertEqual(transaction.header.order, 0) +// +// } +// +// func testProcessReceivedBlock_After_Block_TransactionExists() { +// let transaction = TestData.p2pkhTransaction +// let block = TestData.firstBlock +// let nextBlock = TestData.secondBlock +// transaction.header.status = .new +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) +// } +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) +// verify(mockStorage).update(transaction: equal(to: transaction.header)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) +// +// reset(mockStorage, mockBlockchainDataListener) +// stub(mockStorage) { mock in +// when(mock.update(transaction: any())).thenDoNothing() +// when(mock.update(block: any())).thenDoNothing() +// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) +// } +// stub(mockBlockchainDataListener) { mock in +// when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() +// } +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nextBlock, skipCheckBloomFilter: false) +// +// verify(mockStorage).update(transaction: equal(to: transaction.header)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nextBlock)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, nextBlock.headerHash) +// XCTAssertEqual(transaction.header.timestamp, nextBlock.timestamp) +// XCTAssertEqual(transaction.header.order, 0) +// } +// +// +// func testProcessReceived_SeveralTransactionsInBlock() { +// let transactions = self.transactions() +// let block = TestData.firstBlock +// +// for transaction in transactions { +// transaction.header.isMine = true +// transaction.header.timestamp = 0 +// transaction.header.order = 0 +// } +// transactions[1].header.status = .new +// +// stub(mockStorage) { mock in +// when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) +// when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) +// } +// +// try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: block, skipCheckBloomFilter: false) +// +// verify(mockStorage).add(transaction: equal(to: transactions[0])) +// verify(mockStorage).update(transaction: equal(to: transactions[1].header)) +// verify(mockStorage).add(transaction: equal(to: transactions[2])) +// verify(mockStorage).update(transaction: equal(to: transactions[3].header)) +// verify(mockStorage).update(block: equal(to: block)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: block)) +// +// for (i, transaction) in transactions.enumerated() { +// XCTAssertEqual(transaction.header.blockHash, block.headerHash) +// XCTAssertEqual(transaction.header.status, .relayed) +// XCTAssertEqual(transaction.header.order, i) +// XCTAssertEqual(transaction.header.timestamp, block.header.timestamp) +// } +// } +// +// func testProcessReceived_TransactionNotExists_Mine() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.isMine = true +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) +// verify(mockStorage).add(transaction: equal(to: transaction)) +// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) +// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, nil) +// } +// +// func testProcessReceived_TransactionNotExists_NotMine() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.isMine = false +// +// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockStorage, never()).add(transaction: any()) +// verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) +// verifyNoMoreInteractions(mockBlockchainDataListener) +// verifyNoMoreInteractions(mockOutputAddressExtractor) +// verifyNoMoreInteractions(mockInputExtractor) +// } +// +// func testProcessReceived_TransactionNotExists_Mine_GapShifts() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.isMine = true +// +// stub(mockAddressManager) { mock in +// when(mock.gapShifts()).thenReturn(true) +// } +// +// do { +// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// XCTFail("Should throw exception") +// } catch _ as BloomFilterManager.BloomFilterExpired { +// } catch { +// XCTFail("Unknown error thrown") +// } +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) +// verify(mockStorage).add(transaction: equal(to: transaction)) +// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) +// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, nil) +// } +// +// func testProcessReceived_TransactionNotExists_Mine_OutputsExpireBloomFiler() { +// let transaction = TestData.p2wpkhTransaction +// transaction.header.isMine = true +// transaction.outputs[0].publicKeyPath = TestData.pubKey().path +// +// do { +// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// XCTFail("Should throw exception") +// } catch _ as BloomFilterManager.BloomFilterExpired { +// } catch { +// XCTFail("Unknown error thrown") +// } +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) +// verify(mockStorage).add(transaction: equal(to: transaction)) +// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) +// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, nil) +// } +// +// func testProcessReceived_TransactionNotExists_Mine_GapShifts_CheckBloomFilterFalse() { +// let transaction = TestData.p2wpkhTransaction +// transaction.header.isMine = true +// transaction.outputs[0].publicKeyPath = TestData.pubKey().path +// +// do { +// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: true) +// } catch { +// XCTFail("Unknown error thrown") +// } +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) +// verify(mockStorage).add(transaction: equal(to: transaction)) +// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) +// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) +// +// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) +// XCTAssertEqual(transaction.header.blockHash, nil) +// } +// +// func testProcessReceived_TransactionNotExists_NotMine_GapShifts() { +// let transaction = TestData.p2pkhTransaction +// transaction.header.isMine = false +// +// stub(mockAddressManager) { mock in +// when(mock.gapShifts()).thenReturn(true) +// } +// +// do { +// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) +// } catch { +// XCTFail("Shouldn't throw exception") +// } +// +// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) +// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) +// verify(mockStorage, never()).add(transaction: any()) +// verifyNoMoreInteractions(mockBlockchainDataListener) +// verifyNoMoreInteractions(mockOutputAddressExtractor) +// verifyNoMoreInteractions(mockInputExtractor) +// } +// +// func testProcessReceived_TransactionNotInTopologicalOrder() { +// let transactions = self.transactions() +// var calledTransactions = [FullTransaction]() +// +// stub(mockOutputExtractor) { mock in +// when(mock.extract(transaction: any())).then { transaction in +// calledTransactions.append(transaction) +// } +// } +// +// for i in 0..<4 { +// for j in 0..<4 { +// for k in 0..<4 { +// for l in 0..<4 { +// if [0, 1, 2, 3].contains(where: { $0 != i && $0 != j && $0 != k && $0 != l }) { +// continue +// } +// +// calledTransactions = [] +// +// try! transactionProcessor.processReceived(transactions: [transactions[i], transactions[j], transactions[k], transactions[l]], inBlock: nil, skipCheckBloomFilter: false) +// +// verifyNoMoreInteractions(mockBlockchainDataListener) +// +// for (m, transaction) in calledTransactions.enumerated() { +// XCTAssertEqual(transaction.header.dataHash, transactions[m].header.dataHash) +// } +// } +// } +// } +// } +// } +// +// +// private func transactions() -> [FullTransaction] { +// let transaction = FullTransaction( +// header: Transaction(version: 0, lockTime: 0), +// inputs: [ +// Input( +// withPreviousOutputTxHash: Data(from: 1), +// previousOutputIndex: 0, +// script: Data(from: 999999999999), +// sequence: 0 +// ) +// ], +// outputs: [ +// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) +// ] +// ) +// +// let transaction2 = FullTransaction( +// header: Transaction(version: 0, lockTime: 0), +// inputs: [ +// Input( +// withPreviousOutputTxHash: transaction.header.dataHash, +// previousOutputIndex: 0, +// script: Data(from: 999999999999), +// sequence: 0 +// ) +// ], +// outputs: [ +// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), +// Output(withValue: 0, index: 1, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) +// ] +// ) +// +// let transaction3 = FullTransaction( +// header: Transaction(version: 0, lockTime: 0), +// inputs: [ +// Input( +// withPreviousOutputTxHash: transaction2.header.dataHash, +// previousOutputIndex: 0, +// script: Data(from: 999999999999), +// sequence: 0 +// ) +// ], +// outputs: [ +// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), +// ] +// ) +// +// let transaction4 = FullTransaction( +// header: Transaction(version: 0, lockTime: 0), +// inputs: [ +// Input( +// withPreviousOutputTxHash: transaction2.header.dataHash, +// previousOutputIndex: 1, +// script: Data(from: 999999999999), +// sequence: 0 +// ), +// Input( +// withPreviousOutputTxHash: transaction3.header.dataHash, +// previousOutputIndex: 0, +// script: Data(from: 999999999999), +// sequence: 0 +// ) +// ], +// outputs: [ +// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) +// ] +// ) +// +// return [transaction, transaction2, transaction3, transaction4] +// } +// +//} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift index c99dd357..0afd26c5 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift @@ -1,247 +1,247 @@ -import XCTest -import Quick -import Nimble -import Cuckoo -@testable import BitcoinCore - -class TransactionSyncerTests: QuickSpec { - override func spec() { - let mockStorage = MockIStorage() - let mockTransactionProcessor = MockITransactionProcessor() - let mockAddressManager = MockIPublicKeyManager() - let mockBloomFilterManager = MockIBloomFilterManager() - let maxRetriesCount = 3 - let retriesPeriod: Double = 60 - let totalRetriesPeriod: Double = 60 * 60 * 24 - - var syncer: TransactionSyncer! - - beforeEach { - stub(mockStorage) { mock in - when(mock.add(sentTransaction: any())).thenDoNothing() - when(mock.update(sentTransaction: any())).thenDoNothing() - } - stub(mockTransactionProcessor) { mock in - when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() - } - stub(mockAddressManager) { mock in - when(mock.fillGap()).thenDoNothing() - } - stub(mockBloomFilterManager) { mock in - when(mock.regenerateBloomFilter()).thenDoNothing() - } - - syncer = TransactionSyncer( - storage: mockStorage, processor: mockTransactionProcessor, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, - maxRetriesCount: maxRetriesCount, retriesPeriod: retriesPeriod, totalRetriesPeriod: totalRetriesPeriod) - } - - afterEach { - reset(mockStorage, mockTransactionProcessor, mockAddressManager, mockBloomFilterManager) - - syncer = nil - } - - describe("#pendingTransactions") { - let fullTransaction = TestData.p2pkTransaction - - context("when transaction is .new") { - beforeEach { - stub(mockStorage) { mock in - when(mock.newTransactions()).thenReturn([fullTransaction.header]) - } - } - - context("when it wasn't sent") { - it("returns transaction") { - stub(mockStorage) { mock in - when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) - when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) - when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) - } - let transactions = syncer.pendingTransactions() - - expect(transactions.count).to(equal(1)) - expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) - } - } - - context("when it was sent") { - let sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) - sentTransaction.lastSendTime = CACurrentMediaTime() - retriesPeriod - 1 - sentTransaction.retriesCount = 0 - beforeEach { - stub(mockStorage) { mock in - when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) - } - } - - context("when sent not too many times or too frequently") { - it("returns transaction") { - stub(mockStorage) { mock in - when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) - when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) - } - - let transactions = syncer.pendingTransactions() - expect(transactions.count).to(equal(1)) - expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) - } - } - - context("when sent too many times") { - it("doesn't return transaction") { - sentTransaction.retriesCount = maxRetriesCount - expect(syncer.pendingTransactions()).to(beEmpty()) - } - } - - context("when sent too often") { - it("doesn't return transaction") { - sentTransaction.lastSendTime = CACurrentMediaTime() - expect(syncer.pendingTransactions()).to(beEmpty()) - } - } - - context("when sent too often in totalRetriesPeriod period") { - it("doesn't return transaction") { - sentTransaction.firstSendTime = CACurrentMediaTime() - totalRetriesPeriod - 1 - expect(syncer.pendingTransactions()).to(beEmpty()) - } - } - } - } - - context("when transaction is not new") { - it("doesn't return transaction") { - stub(mockStorage) { mock in - when(mock.newTransactions()).thenReturn([]) - } - expect(syncer.pendingTransactions()).to(beEmpty()) - } - } - } - - describe("#handle(sentTransaction:)") { - let fullTransaction = TestData.p2pkhTransaction - - context("when SentTransaction does not exist") { - it("adds new SentTransaction object") { - stub(mockStorage) { mock in - when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) - when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) - } - - syncer.handle(sentTransaction: fullTransaction) - - let argumentCaptor = ArgumentCaptor() - verify(mockStorage).add(sentTransaction: argumentCaptor.capture()) - let sentTransaction = argumentCaptor.value! - - expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) - } - } - - context("when SentTransaction exists") { - var sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) - sentTransaction.firstSendTime = sentTransaction.firstSendTime - 100 - sentTransaction.lastSendTime = sentTransaction.lastSendTime - 100 - - it("updates existing SentTransaction object") { - stub(mockStorage) { mock in - when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) - when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) - } - - syncer.handle(sentTransaction: fullTransaction) - - let argumentCaptor = ArgumentCaptor() - verify(mockStorage).update(sentTransaction: argumentCaptor.capture()) - sentTransaction = argumentCaptor.value! - - expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) - } - } - - context("when Transaction doesn't exist") { - it("neither adds new nor updates existing") { - stub(mockStorage) { mock in - when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) - } - - syncer.handle(sentTransaction: fullTransaction) - - verify(mockStorage, never()).add(sentTransaction: any()) - verify(mockStorage, never()).update(sentTransaction: any()) - } - } - } - - describe("#handle(transactions:)") { - context("when empty array is given") { - it("doesn't do anything") { - syncer.handle(transactions: []) - - verify(mockTransactionProcessor, never()).processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any()) - verify(mockAddressManager, never()).fillGap() - verify(mockBloomFilterManager, never()).regenerateBloomFilter() - } - } - - context("when not empty array is given") { - let transactions = [TestData.p2pkhTransaction] - - context("when need to update bloom filter") { - it("fills addresses gap and regenerates bloom filter") { - stub(mockTransactionProcessor) { mock in - when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) - } - - syncer.handle(transactions: transactions) - verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) - verify(mockAddressManager).fillGap() - verify(mockBloomFilterManager).regenerateBloomFilter() - } - } - - context("when don't need to update bloom filter") { - it("doesn't run address fillGap and doesn't regenerate bloom filter") { - stub(mockTransactionProcessor) { mock in - when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: equal(to: false))).thenDoNothing() - } - - syncer.handle(transactions: transactions) - verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) - verify(mockAddressManager, never()).fillGap() - verify(mockBloomFilterManager, never()).regenerateBloomFilter() - } - } - - } - } - - describe("#shouldRequestTransaction") { - let fullTransaction = TestData.p2wpkhTransaction - - context("when relayed transaction exists") { - it("returns false") { - stub(mockStorage) { mock in - when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(true) - } - - XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), false) - } - } - - context("when relayed transaction doesn't exist") { - it("returns true") { - stub(mockStorage) { mock in - when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(false) - } - - XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), true) - } - } - } - } -} +//import XCTest +//import Quick +//import Nimble +//import Cuckoo +//@testable import BitcoinCore +// +//class TransactionSyncerTests: QuickSpec { +// override func spec() { +// let mockStorage = MockIStorage() +// let mockTransactionProcessor = MockITransactionProcessor() +// let mockAddressManager = MockIPublicKeyManager() +// let mockBloomFilterManager = MockIBloomFilterManager() +// let maxRetriesCount = 3 +// let retriesPeriod: Double = 60 +// let totalRetriesPeriod: Double = 60 * 60 * 24 +// +// var syncer: TransactionSyncer! +// +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.add(sentTransaction: any())).thenDoNothing() +// when(mock.update(sentTransaction: any())).thenDoNothing() +// } +// stub(mockTransactionProcessor) { mock in +// when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() +// } +// stub(mockAddressManager) { mock in +// when(mock.fillGap()).thenDoNothing() +// } +// stub(mockBloomFilterManager) { mock in +// when(mock.regenerateBloomFilter()).thenDoNothing() +// } +// +// syncer = TransactionSyncer( +// storage: mockStorage, processor: mockTransactionProcessor, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, +// maxRetriesCount: maxRetriesCount, retriesPeriod: retriesPeriod, totalRetriesPeriod: totalRetriesPeriod) +// } +// +// afterEach { +// reset(mockStorage, mockTransactionProcessor, mockAddressManager, mockBloomFilterManager) +// +// syncer = nil +// } +// +// describe("#pendingTransactions") { +// let fullTransaction = TestData.p2pkTransaction +// +// context("when transaction is .new") { +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.newTransactions()).thenReturn([fullTransaction.header]) +// } +// } +// +// context("when it wasn't sent") { +// it("returns transaction") { +// stub(mockStorage) { mock in +// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) +// when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) +// when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) +// } +// let transactions = syncer.pendingTransactions() +// +// expect(transactions.count).to(equal(1)) +// expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) +// } +// } +// +// context("when it was sent") { +// let sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) +// sentTransaction.lastSendTime = CACurrentMediaTime() - retriesPeriod - 1 +// sentTransaction.retriesCount = 0 +// beforeEach { +// stub(mockStorage) { mock in +// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) +// } +// } +// +// context("when sent not too many times or too frequently") { +// it("returns transaction") { +// stub(mockStorage) { mock in +// when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) +// when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) +// } +// +// let transactions = syncer.pendingTransactions() +// expect(transactions.count).to(equal(1)) +// expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) +// } +// } +// +// context("when sent too many times") { +// it("doesn't return transaction") { +// sentTransaction.retriesCount = maxRetriesCount +// expect(syncer.pendingTransactions()).to(beEmpty()) +// } +// } +// +// context("when sent too often") { +// it("doesn't return transaction") { +// sentTransaction.lastSendTime = CACurrentMediaTime() +// expect(syncer.pendingTransactions()).to(beEmpty()) +// } +// } +// +// context("when sent too often in totalRetriesPeriod period") { +// it("doesn't return transaction") { +// sentTransaction.firstSendTime = CACurrentMediaTime() - totalRetriesPeriod - 1 +// expect(syncer.pendingTransactions()).to(beEmpty()) +// } +// } +// } +// } +// +// context("when transaction is not new") { +// it("doesn't return transaction") { +// stub(mockStorage) { mock in +// when(mock.newTransactions()).thenReturn([]) +// } +// expect(syncer.pendingTransactions()).to(beEmpty()) +// } +// } +// } +// +// describe("#handle(sentTransaction:)") { +// let fullTransaction = TestData.p2pkhTransaction +// +// context("when SentTransaction does not exist") { +// it("adds new SentTransaction object") { +// stub(mockStorage) { mock in +// when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) +// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) +// } +// +// syncer.handle(sentTransaction: fullTransaction) +// +// let argumentCaptor = ArgumentCaptor() +// verify(mockStorage).add(sentTransaction: argumentCaptor.capture()) +// let sentTransaction = argumentCaptor.value! +// +// expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) +// } +// } +// +// context("when SentTransaction exists") { +// var sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) +// sentTransaction.firstSendTime = sentTransaction.firstSendTime - 100 +// sentTransaction.lastSendTime = sentTransaction.lastSendTime - 100 +// +// it("updates existing SentTransaction object") { +// stub(mockStorage) { mock in +// when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) +// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) +// } +// +// syncer.handle(sentTransaction: fullTransaction) +// +// let argumentCaptor = ArgumentCaptor() +// verify(mockStorage).update(sentTransaction: argumentCaptor.capture()) +// sentTransaction = argumentCaptor.value! +// +// expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) +// } +// } +// +// context("when Transaction doesn't exist") { +// it("neither adds new nor updates existing") { +// stub(mockStorage) { mock in +// when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) +// } +// +// syncer.handle(sentTransaction: fullTransaction) +// +// verify(mockStorage, never()).add(sentTransaction: any()) +// verify(mockStorage, never()).update(sentTransaction: any()) +// } +// } +// } +// +// describe("#handle(transactions:)") { +// context("when empty array is given") { +// it("doesn't do anything") { +// syncer.handle(transactions: []) +// +// verify(mockTransactionProcessor, never()).processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any()) +// verify(mockAddressManager, never()).fillGap() +// verify(mockBloomFilterManager, never()).regenerateBloomFilter() +// } +// } +// +// context("when not empty array is given") { +// let transactions = [TestData.p2pkhTransaction] +// +// context("when need to update bloom filter") { +// it("fills addresses gap and regenerates bloom filter") { +// stub(mockTransactionProcessor) { mock in +// when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) +// } +// +// syncer.handle(transactions: transactions) +// verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) +// verify(mockAddressManager).fillGap() +// verify(mockBloomFilterManager).regenerateBloomFilter() +// } +// } +// +// context("when don't need to update bloom filter") { +// it("doesn't run address fillGap and doesn't regenerate bloom filter") { +// stub(mockTransactionProcessor) { mock in +// when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: equal(to: false))).thenDoNothing() +// } +// +// syncer.handle(transactions: transactions) +// verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) +// verify(mockAddressManager, never()).fillGap() +// verify(mockBloomFilterManager, never()).regenerateBloomFilter() +// } +// } +// +// } +// } +// +// describe("#shouldRequestTransaction") { +// let fullTransaction = TestData.p2wpkhTransaction +// +// context("when relayed transaction exists") { +// it("returns false") { +// stub(mockStorage) { mock in +// when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(true) +// } +// +// XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), false) +// } +// } +// +// context("when relayed transaction doesn't exist") { +// it("returns true") { +// stub(mockStorage) { mock in +// when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(false) +// } +// +// XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), true) +// } +// } +// } +// } +//} From 3824173b8cd5f887d625534a3cd9f4119c0fc0c3 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Wed, 11 Sep 2019 18:28:29 +0600 Subject: [PATCH 028/234] Bugfix and Tests - Bugfix: TransactionFeeCalculator shouldn't pass changeAddress to TransactionBuilder if UnspentOutputSelector returns addChangeOutput: false - Add/Fix tests --- .../BitcoinCore.xcodeproj/project.pbxproj | 12 +- .../TransactionFeeCalculator.swift | 5 +- .../Blocks/BlockSyncerTests.swift | 1110 ++++++++--------- BitcoinCore/BitcoinCoreTests/Extensions.swift | 15 + .../Managers/BloomFilterManagerTests.swift | 129 -- .../Managers/IrregularOutputFinderTests.swift | 122 ++ .../Managers/PublicKeyManagerTests.swift | 809 ++++++------ .../Transactions/Builder/SegWitAddress.swift | 2 + .../TransactionCreatorTests.swift | 523 ++++---- .../TransactionFeeCalculatorTests.swift | 131 ++ .../TransactionProcessorTests.swift | 1084 ++++++++-------- .../Transactions/TransactionSyncerTests.swift | 487 ++++---- 12 files changed, 2339 insertions(+), 2090 deletions(-) create mode 100644 BitcoinCore/BitcoinCoreTests/Managers/IrregularOutputFinderTests.swift create mode 100644 BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index da85a628..4e9a9718 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -54,7 +54,6 @@ 2FA5D2239DF0C24C4CC6366B /* PeerHostManagerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D632A9EFB1F0CF2523DF /* PeerHostManagerDelegateTests.swift */; }; 2FA5D26905936BB701998750 /* GetBlockHashesTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D65667DA8BFD91E91C8B /* GetBlockHashesTask.swift */; }; 2FA5D2E6BA34E1A932C9C16B /* PublicKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5A9656117A6FD41E814 /* PublicKeyManager.swift */; }; - 2FA5D2E6CAD495129575C402 /* SegWitAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF8081FB47483ABBA87E /* SegWitAddress.swift */; }; 2FA5D371584B554068CCAD8E /* IPeerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D99586B15F4477D695F7 /* IPeerTests.swift */; }; 2FA5D37E8B4A6D9B89410173 /* SendTransactionTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D2DAB8485F5F9E2E23B6 /* SendTransactionTaskTests.swift */; }; 2FA5D39FBC802B5EF03AD53A /* TransactionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D11405173CD80A6B9904 /* TransactionBuilder.swift */; }; @@ -85,6 +84,7 @@ 2FA5D8B8A3A3F14EDCE2EB25 /* SignatureScriptSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D79D58E62B0A4FA3952A /* SignatureScriptSerializer.swift */; }; 2FA5D91DCF7F320369162C5A /* ReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D6676E3CB6028E1B6018 /* ReachabilityManager.swift */; }; 2FA5D97434F2504E5B4AC072 /* RestoreKeyConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D706F48D5B7F0629BCB6 /* RestoreKeyConverter.swift */; }; + 2FA5D982A831627B8852689D /* TransactionFeeCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D291FC37E00FD5B35016 /* TransactionFeeCalculatorTests.swift */; }; 2FA5D9D3DA2DF9BD402CE5DD /* SentTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF037E951950A0F7FF03 /* SentTransaction.swift */; }; 2FA5D9ED521660FA5EEF360E /* MerkleBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D5C9BB0E185AF72B9174 /* MerkleBlock.swift */; }; 2FA5DA409AB91D54163250D7 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D9929AB239235F9B6895 /* Logger.swift */; }; @@ -102,6 +102,8 @@ 2FA5DD904B10D404DF9C6E04 /* RequestTransactionTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D4A92FB8C2376A9A0090 /* RequestTransactionTaskTests.swift */; }; 2FA5DDCC995A7F27AFB3DE18 /* PeerDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DCB87581859B2E083377 /* PeerDelegateTests.swift */; }; 2FA5DDD47BF94625B499ECC2 /* TransactionOutputSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DEC34722F594F85688C0 /* TransactionOutputSerializer.swift */; }; + 2FA5DDE3E1C73A21996BE9B0 /* IrregularOutputFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D95BB38C4F12EC7DBB80 /* IrregularOutputFinderTests.swift */; }; + 2FA5DE48BC996CBA1AD1187E /* SegWitAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF8081FB47483ABBA87E /* SegWitAddress.swift */; }; 2FA5DEB1D802341D26456814 /* TransactionProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D0B8032597D7392B0637 /* TransactionProcessorTests.swift */; }; 2FA5DEDC4F93EAB928423720 /* IPeerGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DF41CAF034B375E58E7A /* IPeerGroupTests.swift */; }; 2FA5DF1A340A352EEDBF41E6 /* BlockchainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5DEC678BF41F0D38095DD /* BlockchainTests.swift */; }; @@ -279,6 +281,7 @@ 2FA5D23F2B71A6AC790350FF /* TransactionFeeCalculator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFeeCalculator.swift; sourceTree = ""; }; 2FA5D27387B0CF46C2B0A22A /* PeerManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerManagerTests.swift; sourceTree = ""; }; 2FA5D2907674257196C4F586 /* GetMerkleBlocksTaskTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetMerkleBlocksTaskTests.swift; sourceTree = ""; }; + 2FA5D291FC37E00FD5B35016 /* TransactionFeeCalculatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionFeeCalculatorTests.swift; sourceTree = ""; }; 2FA5D2DAB8485F5F9E2E23B6 /* SendTransactionTaskTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendTransactionTaskTests.swift; sourceTree = ""; }; 2FA5D2DDF60A9C5DA3776D8A /* BlockSyncerState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockSyncerState.swift; sourceTree = ""; }; 2FA5D307095385625960B0A0 /* TransactionMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionMessage.swift; sourceTree = ""; }; @@ -302,6 +305,7 @@ 2FA5D859C070F9718CACF08F /* KitStateProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KitStateProvider.swift; sourceTree = ""; }; 2FA5D880471F191DBCDAE574 /* TransactionSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionSerializer.swift; sourceTree = ""; }; 2FA5D8E67003F981BCB89CE3 /* WatchedTransactionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchedTransactionManager.swift; sourceTree = ""; }; + 2FA5D95BB38C4F12EC7DBB80 /* IrregularOutputFinderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IrregularOutputFinderTests.swift; sourceTree = ""; }; 2FA5D9929AB239235F9B6895 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 2FA5D99586B15F4477D695F7 /* IPeerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPeerTests.swift; sourceTree = ""; }; 2FA5D9C845F4D930457A5C6F /* WatchedTransactionManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchedTransactionManagerTests.swift; sourceTree = ""; }; @@ -518,6 +522,7 @@ 58AAAECE6429C4CBD7FD2394 /* UnspentOutputProviderTests.swift */, 58AAAA81938DAE46B8074C9D /* UnspentOutputSelectorSingleNoChangeTests.swift */, 2FA5D9C845F4D930457A5C6F /* WatchedTransactionManagerTests.swift */, + 2FA5D95BB38C4F12EC7DBB80 /* IrregularOutputFinderTests.swift */, ); path = Managers; sourceTree = ""; @@ -588,6 +593,7 @@ 58AAA97B8114B3CE27A6D32C /* TransactionPublicKeySetterTests.swift */, 58AAAED5E10009C0720D949E /* TransactionSizeCalculatorTests.swift */, 2FA5D316DD8A722A5300002B /* TransactionSyncerTests.swift */, + 2FA5D291FC37E00FD5B35016 /* TransactionFeeCalculatorTests.swift */, ); path = Transactions; sourceTree = ""; @@ -1315,6 +1321,9 @@ 58AAAA0E73B45A6262D8272F /* MerkleBranchTests.swift in Sources */, 58AAAE80BE4074DB3B1F0B1C /* UnspentOutputSelectorSingleNoChangeTests.swift in Sources */, 2FA5DA939D3783E4377AF546 /* WatchedTransactionManagerTests.swift in Sources */, + 2FA5DE48BC996CBA1AD1187E /* SegWitAddress.swift in Sources */, + 2FA5D982A831627B8852689D /* TransactionFeeCalculatorTests.swift in Sources */, + 2FA5DDE3E1C73A21996BE9B0 /* IrregularOutputFinderTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1464,7 +1473,6 @@ 2FA5D87B923C2B78B5257957 /* Bip.swift in Sources */, 2FA5D97434F2504E5B4AC072 /* RestoreKeyConverter.swift in Sources */, 2FA5D761883BB2CCC770B790 /* IrregularOutputFinder.swift in Sources */, - 2FA5D2E6CAD495129575C402 /* SegWitAddress.swift in Sources */, 2FA5D653017C871A9F2E387D /* TransactionFeeCalculator.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift index 60d78024..ae4cfde7 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift @@ -21,7 +21,10 @@ extension TransactionFeeCalculator: ITransactionFeeCalculator { if let address = toAddress { // Actual fee let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) - let transaction = try transactionBuilder.buildTransaction(value: value, unspentOutputs: selectedOutputsInfo.unspentOutputs, fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: address, changeAddress: changeAddress) + let transaction = try transactionBuilder.buildTransaction( + value: value, unspentOutputs: selectedOutputsInfo.unspentOutputs, fee: selectedOutputsInfo.fee, senderPay: senderPay, + toAddress: address, changeAddress: selectedOutputsInfo.addChangeOutput ? changeAddress : nil + ) return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate } else { // Estimated fee diff --git a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift index 8a68fae6..5485f92e 100644 --- a/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Blocks/BlockSyncerTests.swift @@ -1,559 +1,551 @@ -//import XCTest -//import Quick -//import Nimble -//import Cuckoo -//@testable import BitcoinCore -// -//class BlockSyncerTests: QuickSpec { -// override func spec() { -// let mockStorage = MockIStorage() -// let mockFactory = MockIFactory() -// let mockListener = MockISyncStateListener() -// let mockTransactionProcessor = MockITransactionProcessor() -// let mockBlockchain = MockIBlockchain() -// let mockAddressManager = MockIPublicKeyManager() -// let mockBloomFilterManager = MockIBloomFilterManager() -// let mockState = MockBlockSyncerState() -// -// let checkpointBlock = TestData.checkpointBlock -// var syncer: BlockSyncer! -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blocksCount.get).thenReturn(1) -// when(mock.lastBlock.get).thenReturn(nil) -// when(mock.deleteBlockchainBlockHashes()).thenDoNothing() -// } -// stub(mockListener) { mock in -// when(mock.initialBestBlockHeightUpdated(height: equal(to: 0))).thenDoNothing() -// } -// stub(mockBlockchain) { mock in -// when(mock.handleFork()).thenDoNothing() -// } -// stub(mockAddressManager) { mock in -// when(mock.fillGap()).thenDoNothing() -// } -// stub(mockBloomFilterManager) { mock in -// when(mock.regenerateBloomFilter()).thenDoNothing() -// } -// stub(mockState) { mock in -// when(mock.iteration(hasPartialBlocks: any())).thenDoNothing() -// } -// } -// -// afterEach { -// reset(mockStorage, mockListener, mockTransactionProcessor, mockBlockchain, mockAddressManager, mockBloomFilterManager, mockState) -// -// syncer = nil -// } -// -// context("static methods") { -// describe("#instance") { -// it("triggers #initialBestBlockHeightUpdated event on listener") { -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(checkpointBlock) -// } -// stub(mockListener) { mock in -// when(mock.initialBestBlockHeightUpdated(height: any())).thenDoNothing() -// } -// -// -// let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, -// blockchain: mockBlockchain, publicKeyManager: mockAddressManager, hashCheckpointThreshold: 100) -// -// verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) -// verifyNoMoreInteractions(mockListener) -// } -// } -// -// describe("#checkpointBlock") { -// let bip44CheckpointBlock = TestData.checkpointBlock -// let lastCheckpointBlock = TestData.firstBlock -// let mockNetwork = MockINetwork() -// -// beforeEach { -// stub(mockNetwork) { mock in -// when(mock.bip44CheckpointBlock.get).thenReturn(bip44CheckpointBlock) -// when(mock.lastCheckpointBlock.get).thenReturn(lastCheckpointBlock) -// } -// } -// -// afterEach { -// reset(mockNetwork) -// } -// -// context("when there are some blocks in storage") { -// let lastBlock = TestData.secondBlock -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(lastBlock) -// } -// } -// -// it("doesn't save checkpointBlock to storage") { -// _ = BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) -// -// verify(mockStorage, never()).save(block: any()) -// verify(mockStorage).lastBlock.get() -// verifyNoMoreInteractions(mockStorage) -// } -// -// context("when syncMode is .full") { -// it("returns bip44CheckpointBlock") { -// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) -// } -// } -// -// context("when syncMode is not .full") { -// context("when lastBlock's height is more than lastCheckpointBlock") { -// it("returns lastCheckpointBlock") { -// lastBlock.height = lastCheckpointBlock.height + 1 -// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) -// } -// } -// -// context("when lastBlock's height is less than lastCheckpointBlock") { -// it("returns bip44CheckpointBlock") { -// lastBlock.height = lastCheckpointBlock.height - 1 -// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(bip44CheckpointBlock)) -// } -// } -// } -// } -// -// context("when there's no block in storage") { -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(nil) -// when(mock.save(block: any())).thenDoNothing() -// } -// } -// -// context("when syncMode is .full") { -// it("returns bip44CheckpointBlock") { -// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) -// } -// -// it("saves bip44CheckpointBlock to storage") { -// BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage) -// verify(mockStorage).save(block: sameInstance(as: bip44CheckpointBlock)) -// } -// } -// -// context("when syncMode is not .full") { -// it("returns lastCheckpointBlock") { -// expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) -// } -// -// it("saves lastCheckpointBlock to storage") { -// BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) -// verify(mockStorage).save(block: sameInstance(as: lastCheckpointBlock)) -// } -// } -// } -// } -// } -// -// context("instance methods") { -// beforeEach { -// syncer = BlockSyncer(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, -// blockchain: mockBlockchain, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, -// hashCheckpointThreshold: 100, logger: nil, state: mockState) -// } -// -// describe("#localDownloadedBestBlockHeight") { -// context("when there are some blocks in storage") { -// it("returns the height of the last block") { -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(checkpointBlock) -// } -// expect(syncer.localDownloadedBestBlockHeight).to(equal(Int32(checkpointBlock.height))) -// } -// } -// -// context("when there's no block in storage") { -// it("returns 0") { -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(nil) -// } -// expect(syncer.localDownloadedBestBlockHeight).to(equal(0)) -// } -// } -// } -// -// describe("#localKnownBestBlockHeight") { -// let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 32), height: 0, order: 0) -// -// context("when no blockHashes") { -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blockchainBlockHashes.get).thenReturn([]) -// when(mock.blocksCount(headerHashes: equal(to: []))).thenReturn(0) -// } -// } -// -// context("when no blocks") { -// it("returns 0") { -// expect(syncer.localKnownBestBlockHeight).to(equal(0)) -// } -// } -// -// context("when there are some blocks") { -// it("returns last block's height + blocks count") { -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(checkpointBlock) -// } -// expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) -// } -// } -// } -// -// context("when there are some blockHashes which haven't downloaded blocks") { -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) -// when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(0) -// } -// } -// -// it("returns lastBlock + blockHashes count") { -// expect(syncer.localKnownBestBlockHeight).to(equal(1)) -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(checkpointBlock) -// } -// expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height + 1))) -// } -// } -// -// context("when there are some blockHashes which have downloaded blocks") { -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) -// when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(1) -// } -// } -// -// it("returns lastBlock + count of blockHashes without downloaded blocks") { -// expect(syncer.localKnownBestBlockHeight).to(equal(0)) -// stub(mockStorage) { mock in -// when(mock.lastBlock.get).thenReturn(checkpointBlock) -// } -// expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) -// } -// } -// } -// -// describe("#prepareForDownload") { -// let emptyBlocks = [Block]() -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) -// when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) -// } -// stub(mockBlockchain) { mock in -// when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() -// } -// -// syncer.prepareForDownload() -// } -// -// it("handles partial blocks") { -// verify(mockAddressManager).fillGap() -// verify(mockBloomFilterManager).regenerateBloomFilter() -// verify(mockState).iteration(hasPartialBlocks: equal(to: false)) -// } -// -// it("clears BlockHashes") { -// verify(mockStorage).deleteBlockchainBlockHashes() -// } -// -// it("clears partialBlock blocks") { -// verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) -// verify(mockStorage).blocks(byHexes: equal(to: [])) -// verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) -// } -// -// it("handles fork") { -// verify(mockBlockchain).handleFork() -// } -// } -// -// describe("#downloadIterationCompleted") { -// context("when iteration has partial blocks") { -// it("handles partial blocks") { -// stub(mockState) { mock in -// when(mock.iterationHasPartialBlocks.get).thenReturn(true) -// } -// syncer.downloadIterationCompleted() -// -// verify(mockAddressManager).fillGap() -// verify(mockBloomFilterManager).regenerateBloomFilter() -// verify(mockState).iteration(hasPartialBlocks: equal(to: false)) -// } -// } -// -// context("when iteration has not partial blocks") { -// it("does not handle partial blocks") { -// stub(mockState) { mock in -// when(mock.iterationHasPartialBlocks.get).thenReturn(false) -// } -// syncer.downloadIterationCompleted() -// -// verify(mockAddressManager, never()).fillGap() -// verify(mockBloomFilterManager, never()).regenerateBloomFilter() -// verify(mockState, never()).iteration(hasPartialBlocks: any()) -// } -// } -// } -// -// describe("#downloadCompleted") { -// it("handles fork") { -// syncer.downloadCompleted() -// verify(mockBlockchain).handleFork() -// } -// } -// -// describe("#downloadFailed") { -// let emptyBlocks = [Block]() -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) -// when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) -// } -// stub(mockBlockchain) { mock in -// when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() -// } -// -// syncer.downloadFailed() -// } -// -// it("handles partial blocks") { -// verify(mockAddressManager).fillGap() -// verify(mockBloomFilterManager).regenerateBloomFilter() -// verify(mockState).iteration(hasPartialBlocks: equal(to: false)) -// } -// -// it("clears BlockHashes") { -// verify(mockStorage).deleteBlockchainBlockHashes() -// } -// -// it("clears partialBlock blocks") { -// verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) -// verify(mockStorage).blocks(byHexes: equal(to: [])) -// verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) -// } -// -// it("handles fork") { -// verify(mockBlockchain).handleFork() -// } -// } -// -// describe("#getBlockHashes") { -// it("returns first 500 blockhashes") { -// let blockHashes = [BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0)] -// stub(mockStorage) { mock in -// when(mock.blockHashesSortedBySequenceAndHeight(limit: equal(to: 500))).thenReturn(blockHashes) -// } -// -// expect(syncer.getBlockHashes()).to(equal(blockHashes)) -// verify(mockStorage).blockHashesSortedBySequenceAndHeight(limit: equal(to: 500)) -// } -// } -// -// describe("#getBlockLocatorHashes(peerLastBlockHeight:)") { -// let peerLastBlockHeight: Int32 = 10 -// let firstBlock = TestData.firstBlock -// let secondBlock = TestData.secondBlock -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.lastBlockchainBlockHash.get).thenReturn(nil) -// when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([Block]()) -// when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(nil) -// } -// } -// -// context("when there's no blocks or blockhashes") { -// it("returns checkpointBlock's header hash") { -// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([checkpointBlock.headerHash])) -// } -// } -// -// context("when there are blockchain blockhashes") { -// it("returns last blockchain blockhash") { -// let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0) -// stub(mockStorage) { mock in -// when(mock.lastBlockchainBlockHash.get).thenReturn(blockHash) -// when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([firstBlock, secondBlock]) -// } -// -// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ -// blockHash.headerHash, checkpointBlock.headerHash -// ])) -// } -// } -// -// context("when there's no blockhashes but there are blocks") { -// it("returns last 10 blocks' header hashes") { -// stub(mockStorage) { mock in -// when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([secondBlock, firstBlock]) -// } -// -// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ -// secondBlock.headerHash, firstBlock.headerHash, checkpointBlock.headerHash -// ])) -// } -// } -// -// context("when the peers last block is already in storage") { -// it("returns peers last block's headerHash instead of checkpointBlocks'") { -// stub(mockStorage) { mock in -// when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(firstBlock) -// } -// -// expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([firstBlock.headerHash])) -// } -// } -// } -// -// describe("#add(blockHashes:)") { -// let existingBlockHash = Data(repeating: 0, count: 32) -// let newBlockHash = Data(repeating: 1, count: 32) -// let blockHash = BlockHash(headerHash: existingBlockHash, height: 0, order: 10) -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.blockHashHeaderHashes.get).thenReturn([existingBlockHash]) -// when(mock.add(blockHashes: any())).thenDoNothing() -// } -// stub(mockFactory) { mock in -// when(mock.blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: any())).thenReturn(blockHash) -// } -// } -// -// context("when there's a blockHash in storage") { -// it("sets order of given blockhashes starting from last blockhashes order") { -// let lastBlockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 10) -// stub(mockStorage) { mock in -// when(mock.lastBlockHash.get).thenReturn(lastBlockHash) -// } -// -// syncer.add(blockHashes: [existingBlockHash, newBlockHash]) -// -// verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: lastBlockHash.sequence + 1)) -// verify(mockStorage).add(blockHashes: equal(to: [blockHash])) -// } -// } -// -// context("when there's no blockhashes") { -// it("sets order of given blockhashes starting from 0") { -// stub(mockStorage) { mock in -// when(mock.lastBlockHash.get).thenReturn(nil) -// } -// -// syncer.add(blockHashes: [existingBlockHash, newBlockHash]) -// -// verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: 1)) -// verify(mockStorage).add(blockHashes: equal(to: [blockHash])) -// } -// } -// } -// -// describe("#handle(merkleBlock:,maxBlockHeight:)") { -// let block = TestData.firstBlock -// let merkleBlock = MerkleBlock(header: block.header, transactionHashes: [], transactions: []) -// let maxBlockHeight: Int32 = Int32(block.height + 100) -// -// beforeEach { -// stub(mockBlockchain) { mock in -// when(mock.forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height))).thenReturn(block) -// when(mock.connect(merkleBlock: equal(to: merkleBlock))).thenReturn(block) -// } -// stub(mockTransactionProcessor) { mock in -// when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() -// } -// stub(mockState) { mock in -// when(mock.iterationHasPartialBlocks.get).thenReturn(false) -// } -// stub(mockStorage) { mock in -// when(mock.deleteBlockHash(byHash: equal(to: block.headerHash))).thenDoNothing() -// } -// stub(mockListener) { mock in -// when(mock.currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight))).thenDoNothing() -// } -// } -// -// it("handles merkleBlock") { -// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) -// -// verify(mockBlockchain).connect(merkleBlock: equal(to: merkleBlock)) -// verify(mockTransactionProcessor).processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false)) -// verify(mockStorage).deleteBlockHash(byHash: equal(to: block.headerHash)) -// verify(mockListener).currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight)) -// } -// -// context("when merklBlocks's height is null") { -// it("force adds the block to blockchain") { -// merkleBlock.height = block.height -// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) -// -// verify(mockBlockchain).forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height)) -// verifyNoMoreInteractions(mockBlockchain) -// } -// } -// -// context("when bloom filter is expired while processing transactions") { -// it("sets iteration state to hasPartialBlocks") { -// stub(mockTransactionProcessor) { mock in -// when(mock.processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false))).thenThrow(BloomFilterManager.BloomFilterExpired()) -// } -// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) -// -// verify(mockState).iteration(hasPartialBlocks: equal(to: true)) -// } -// } -// -// context("when iteration has partial blocks") { -// it("doesn't delete block hash") { -// stub(mockState) { mock in -// when(mock.iterationHasPartialBlocks.get).thenReturn(true) -// } -// stub(mockTransactionProcessor) { mock in -// when(mock.processReceived(transactions: equal(to: []), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: true))).thenDoNothing() -// } -// try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) -// -// verify(mockStorage, never()).deleteBlockHash(byHash: equal(to: block.headerHash)) -// } -// } -// } -// -// describe("#shouldRequestBlock(withHash:)") { -// let hash = Data(repeating: 0, count: 32) -// -// context("when the given block is in storage") { -// it("returns false") { -// stub(mockStorage) { mock in -// when(mock.block(byHash: equal(to: hash))).thenReturn(TestData.firstBlock) -// } -// -// expect(syncer.shouldRequestBlock(withHash: hash)).to(beFalsy()) -// } -// } -// -// context("when the given block is not in storage") { -// it("returns true") { -// stub(mockStorage) { mock in -// when(mock.block(byHash: equal(to: hash))).thenReturn(nil) -// } -// -// expect(syncer.shouldRequestBlock(withHash: hash)).to(beTruthy()) -// } -// } -// } -// } -// } -//} +import XCTest +import Quick +import Nimble +import Cuckoo +@testable import BitcoinCore + +class BlockSyncerTests: QuickSpec { + override func spec() { + let mockStorage = MockIStorage() + let mockFactory = MockIFactory() + let mockListener = MockISyncStateListener() + let mockTransactionProcessor = MockITransactionProcessor() + let mockBlockchain = MockIBlockchain() + let mockAddressManager = MockIPublicKeyManager() + let mockState = MockBlockSyncerState() + + let checkpointBlock = TestData.checkpointBlock + var syncer: BlockSyncer! + + beforeEach { + stub(mockStorage) { mock in + when(mock.blocksCount.get).thenReturn(1) + when(mock.lastBlock.get).thenReturn(nil) + when(mock.deleteBlockchainBlockHashes()).thenDoNothing() + } + stub(mockListener) { mock in + when(mock.initialBestBlockHeightUpdated(height: equal(to: 0))).thenDoNothing() + } + stub(mockBlockchain) { mock in + when(mock.handleFork()).thenDoNothing() + } + stub(mockAddressManager) { mock in + when(mock.fillGap()).thenDoNothing() + } + stub(mockState) { mock in + when(mock.iteration(hasPartialBlocks: any())).thenDoNothing() + } + } + + afterEach { + reset(mockStorage, mockListener, mockTransactionProcessor, mockBlockchain, mockAddressManager, mockState) + + syncer = nil + } + + context("static methods") { + describe("#instance") { + it("triggers #initialBestBlockHeightUpdated event on listener") { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(checkpointBlock) + } + stub(mockListener) { mock in + when(mock.initialBestBlockHeightUpdated(height: any())).thenDoNothing() + } + + + let _ = BlockSyncer.instance(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, + blockchain: mockBlockchain, publicKeyManager: mockAddressManager, hashCheckpointThreshold: 100) + + verify(mockListener).initialBestBlockHeightUpdated(height: equal(to: Int32(checkpointBlock.height))) + verifyNoMoreInteractions(mockListener) + } + } + + describe("#checkpointBlock") { + let bip44CheckpointBlock = TestData.checkpointBlock + let lastCheckpointBlock = TestData.firstBlock + let mockNetwork = MockINetwork() + + beforeEach { + stub(mockNetwork) { mock in + when(mock.bip44CheckpointBlock.get).thenReturn(bip44CheckpointBlock) + when(mock.lastCheckpointBlock.get).thenReturn(lastCheckpointBlock) + } + } + + afterEach { + reset(mockNetwork) + } + + context("when there are some blocks in storage") { + let lastBlock = TestData.secondBlock + + beforeEach { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(lastBlock) + } + } + + it("doesn't save checkpointBlock to storage") { + _ = BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) + + verify(mockStorage, never()).save(block: any()) + verify(mockStorage).lastBlock.get() + verifyNoMoreInteractions(mockStorage) + } + + context("when syncMode is .full") { + it("returns bip44CheckpointBlock") { + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) + } + } + + context("when syncMode is not .full") { + context("when lastBlock's height is more than lastCheckpointBlock") { + it("returns lastCheckpointBlock") { + lastBlock.height = lastCheckpointBlock.height + 1 + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) + } + } + + context("when lastBlock's height is less than lastCheckpointBlock") { + it("returns bip44CheckpointBlock") { + lastBlock.height = lastCheckpointBlock.height - 1 + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(bip44CheckpointBlock)) + } + } + } + } + + context("when there's no block in storage") { + beforeEach { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(nil) + when(mock.save(block: any())).thenDoNothing() + } + } + + context("when syncMode is .full") { + it("returns bip44CheckpointBlock") { + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage)).to(equal(bip44CheckpointBlock)) + } + + it("saves bip44CheckpointBlock to storage") { + BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .full, storage: mockStorage) + verify(mockStorage).save(block: sameInstance(as: bip44CheckpointBlock)) + } + } + + context("when syncMode is not .full") { + it("returns lastCheckpointBlock") { + expect(BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage)).to(equal(lastCheckpointBlock)) + } + + it("saves lastCheckpointBlock to storage") { + BlockSyncer.checkpointBlock(network: mockNetwork, syncMode: .api, storage: mockStorage) + verify(mockStorage).save(block: sameInstance(as: lastCheckpointBlock)) + } + } + } + } + } + + context("instance methods") { + beforeEach { + syncer = BlockSyncer(storage: mockStorage, checkpointBlock: checkpointBlock, factory: mockFactory, listener: mockListener, transactionProcessor: mockTransactionProcessor, + blockchain: mockBlockchain, publicKeyManager: mockAddressManager, + hashCheckpointThreshold: 100, logger: nil, state: mockState) + } + + describe("#localDownloadedBestBlockHeight") { + context("when there are some blocks in storage") { + it("returns the height of the last block") { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(checkpointBlock) + } + expect(syncer.localDownloadedBestBlockHeight).to(equal(Int32(checkpointBlock.height))) + } + } + + context("when there's no block in storage") { + it("returns 0") { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(nil) + } + expect(syncer.localDownloadedBestBlockHeight).to(equal(0)) + } + } + } + + describe("#localKnownBestBlockHeight") { + let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 32), height: 0, order: 0) + + context("when no blockHashes") { + beforeEach { + stub(mockStorage) { mock in + when(mock.blockchainBlockHashes.get).thenReturn([]) + when(mock.blocksCount(headerHashes: equal(to: []))).thenReturn(0) + } + } + + context("when no blocks") { + it("returns 0") { + expect(syncer.localKnownBestBlockHeight).to(equal(0)) + } + } + + context("when there are some blocks") { + it("returns last block's height + blocks count") { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(checkpointBlock) + } + expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) + } + } + } + + context("when there are some blockHashes which haven't downloaded blocks") { + beforeEach { + stub(mockStorage) { mock in + when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) + when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(0) + } + } + + it("returns lastBlock + blockHashes count") { + expect(syncer.localKnownBestBlockHeight).to(equal(1)) + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(checkpointBlock) + } + expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height + 1))) + } + } + + context("when there are some blockHashes which have downloaded blocks") { + beforeEach { + stub(mockStorage) { mock in + when(mock.blockchainBlockHashes.get).thenReturn([blockHash]) + when(mock.blocksCount(headerHashes: equal(to: [blockHash.headerHash]))).thenReturn(1) + } + } + + it("returns lastBlock + count of blockHashes without downloaded blocks") { + expect(syncer.localKnownBestBlockHeight).to(equal(0)) + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(checkpointBlock) + } + expect(syncer.localKnownBestBlockHeight).to(equal(Int32(checkpointBlock.height))) + } + } + } + + describe("#prepareForDownload") { + let emptyBlocks = [Block]() + + beforeEach { + stub(mockStorage) { mock in + when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) + when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) + } + stub(mockBlockchain) { mock in + when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() + } + + syncer.prepareForDownload() + } + + it("handles partial blocks") { + verify(mockAddressManager).fillGap() + verify(mockState).iteration(hasPartialBlocks: equal(to: false)) + } + + it("clears BlockHashes") { + verify(mockStorage).deleteBlockchainBlockHashes() + } + + it("clears partialBlock blocks") { + verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) + verify(mockStorage).blocks(byHexes: equal(to: [])) + verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) + } + + it("handles fork") { + verify(mockBlockchain).handleFork() + } + } + + describe("#downloadIterationCompleted") { + context("when iteration has partial blocks") { + it("handles partial blocks") { + stub(mockState) { mock in + when(mock.iterationHasPartialBlocks.get).thenReturn(true) + } + syncer.downloadIterationCompleted() + + verify(mockAddressManager).fillGap() + verify(mockState).iteration(hasPartialBlocks: equal(to: false)) + } + } + + context("when iteration has not partial blocks") { + it("does not handle partial blocks") { + stub(mockState) { mock in + when(mock.iterationHasPartialBlocks.get).thenReturn(false) + } + syncer.downloadIterationCompleted() + + verify(mockAddressManager, never()).fillGap() + verify(mockState, never()).iteration(hasPartialBlocks: any()) + } + } + } + + describe("#downloadCompleted") { + it("handles fork") { + syncer.downloadCompleted() + verify(mockBlockchain).handleFork() + } + } + + describe("#downloadFailed") { + let emptyBlocks = [Block]() + + beforeEach { + stub(mockStorage) { mock in + when(mock.blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash))).thenReturn([]) + when(mock.blocks(byHexes: equal(to: []))).thenReturn(emptyBlocks) + } + stub(mockBlockchain) { mock in + when(mock.deleteBlocks(blocks: equal(to: emptyBlocks))).thenDoNothing() + } + + syncer.downloadFailed() + } + + it("handles partial blocks") { + verify(mockAddressManager).fillGap() + verify(mockState).iteration(hasPartialBlocks: equal(to: false)) + } + + it("clears BlockHashes") { + verify(mockStorage).deleteBlockchainBlockHashes() + } + + it("clears partialBlock blocks") { + verify(mockStorage).blockHashHeaderHashes(except: equal(to: checkpointBlock.headerHash)) + verify(mockStorage).blocks(byHexes: equal(to: [])) + verify(mockBlockchain).deleteBlocks(blocks: equal(to: emptyBlocks)) + } + + it("handles fork") { + verify(mockBlockchain).handleFork() + } + } + + describe("#getBlockHashes") { + it("returns first 500 blockhashes") { + let blockHashes = [BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0)] + stub(mockStorage) { mock in + when(mock.blockHashesSortedBySequenceAndHeight(limit: equal(to: 500))).thenReturn(blockHashes) + } + + expect(syncer.getBlockHashes()).to(equal(blockHashes)) + verify(mockStorage).blockHashesSortedBySequenceAndHeight(limit: equal(to: 500)) + } + } + + describe("#getBlockLocatorHashes(peerLastBlockHeight:)") { + let peerLastBlockHeight: Int32 = 10 + let firstBlock = TestData.firstBlock + let secondBlock = TestData.secondBlock + + beforeEach { + stub(mockStorage) { mock in + when(mock.lastBlockchainBlockHash.get).thenReturn(nil) + when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([Block]()) + when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(nil) + } + } + + context("when there's no blocks or blockhashes") { + it("returns checkpointBlock's header hash") { + expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([checkpointBlock.headerHash])) + } + } + + context("when there are blockchain blockhashes") { + it("returns last blockchain blockhash") { + let blockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 0) + stub(mockStorage) { mock in + when(mock.lastBlockchainBlockHash.get).thenReturn(blockHash) + when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([firstBlock, secondBlock]) + } + + expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ + blockHash.headerHash, checkpointBlock.headerHash + ])) + } + } + + context("when there's no blockhashes but there are blocks") { + it("returns last 10 blocks' header hashes") { + stub(mockStorage) { mock in + when(mock.blocks(heightGreaterThan: equal(to: checkpointBlock.height), sortedBy: equal(to: Block.Columns.height), limit: equal(to: 10))).thenReturn([secondBlock, firstBlock]) + } + + expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([ + secondBlock.headerHash, firstBlock.headerHash, checkpointBlock.headerHash + ])) + } + } + + context("when the peers last block is already in storage") { + it("returns peers last block's headerHash instead of checkpointBlocks'") { + stub(mockStorage) { mock in + when(mock.block(byHeight: Int(peerLastBlockHeight))).thenReturn(firstBlock) + } + + expect(syncer.getBlockLocatorHashes(peerLastBlockHeight: peerLastBlockHeight)).to(equal([firstBlock.headerHash])) + } + } + } + + describe("#add(blockHashes:)") { + let existingBlockHash = Data(repeating: 0, count: 32) + let newBlockHash = Data(repeating: 1, count: 32) + let blockHash = BlockHash(headerHash: existingBlockHash, height: 0, order: 10) + + beforeEach { + stub(mockStorage) { mock in + when(mock.blockHashHeaderHashes.get).thenReturn([existingBlockHash]) + when(mock.add(blockHashes: any())).thenDoNothing() + } + stub(mockFactory) { mock in + when(mock.blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: any())).thenReturn(blockHash) + } + } + + context("when there's a blockHash in storage") { + it("sets order of given blockhashes starting from last blockhashes order") { + let lastBlockHash = BlockHash(headerHash: Data(repeating: 0, count: 0), height: 0, order: 10) + stub(mockStorage) { mock in + when(mock.lastBlockHash.get).thenReturn(lastBlockHash) + } + + syncer.add(blockHashes: [existingBlockHash, newBlockHash]) + + verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: lastBlockHash.sequence + 1)) + verify(mockStorage).add(blockHashes: equal(to: [blockHash])) + } + } + + context("when there's no blockhashes") { + it("sets order of given blockhashes starting from 0") { + stub(mockStorage) { mock in + when(mock.lastBlockHash.get).thenReturn(nil) + } + + syncer.add(blockHashes: [existingBlockHash, newBlockHash]) + + verify(mockFactory).blockHash(withHeaderHash: equal(to: newBlockHash), height: equal(to: 0), order: equal(to: 1)) + verify(mockStorage).add(blockHashes: equal(to: [blockHash])) + } + } + } + + describe("#handle(merkleBlock:,maxBlockHeight:)") { + let block = TestData.firstBlock + let merkleBlock = MerkleBlock(header: block.header, transactionHashes: [], transactions: []) + let maxBlockHeight: Int32 = Int32(block.height + 100) + + beforeEach { + stub(mockBlockchain) { mock in + when(mock.forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height))).thenReturn(block) + when(mock.connect(merkleBlock: equal(to: merkleBlock))).thenReturn(block) + } + stub(mockTransactionProcessor) { mock in + when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() + } + stub(mockState) { mock in + when(mock.iterationHasPartialBlocks.get).thenReturn(false) + } + stub(mockStorage) { mock in + when(mock.deleteBlockHash(byHash: equal(to: block.headerHash))).thenDoNothing() + } + stub(mockListener) { mock in + when(mock.currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight))).thenDoNothing() + } + } + + it("handles merkleBlock") { + try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) + + verify(mockBlockchain).connect(merkleBlock: equal(to: merkleBlock)) + verify(mockTransactionProcessor).processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false)) + verify(mockStorage).deleteBlockHash(byHash: equal(to: block.headerHash)) + verify(mockListener).currentBestBlockHeightUpdated(height: equal(to: Int32(block.height)), maxBlockHeight: equal(to: maxBlockHeight)) + } + + context("when merklBlocks's height is null") { + it("force adds the block to blockchain") { + merkleBlock.height = block.height + try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) + + verify(mockBlockchain).forceAdd(merkleBlock: equal(to: merkleBlock), height: equal(to: block.height)) + verifyNoMoreInteractions(mockBlockchain) + } + } + + context("when bloom filter is expired while processing transactions") { + it("sets iteration state to hasPartialBlocks") { + stub(mockTransactionProcessor) { mock in + when(mock.processReceived(transactions: equal(to: [FullTransaction]()), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: false))).thenThrow(BloomFilterManager.BloomFilterExpired()) + } + try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) + + verify(mockState).iteration(hasPartialBlocks: equal(to: true)) + } + } + + context("when iteration has partial blocks") { + it("doesn't delete block hash") { + stub(mockState) { mock in + when(mock.iterationHasPartialBlocks.get).thenReturn(true) + } + stub(mockTransactionProcessor) { mock in + when(mock.processReceived(transactions: equal(to: []), inBlock: equal(to: block), skipCheckBloomFilter: equal(to: true))).thenDoNothing() + } + try! syncer.handle(merkleBlock: merkleBlock, maxBlockHeight: maxBlockHeight) + + verify(mockStorage, never()).deleteBlockHash(byHash: equal(to: block.headerHash)) + } + } + } + + describe("#shouldRequestBlock(withHash:)") { + let hash = Data(repeating: 0, count: 32) + + context("when the given block is in storage") { + it("returns false") { + stub(mockStorage) { mock in + when(mock.block(byHash: equal(to: hash))).thenReturn(TestData.firstBlock) + } + + expect(syncer.shouldRequestBlock(withHash: hash)).to(beFalsy()) + } + } + + context("when the given block is not in storage") { + it("returns true") { + stub(mockStorage) { mock in + when(mock.block(byHash: equal(to: hash))).thenReturn(nil) + } + + expect(syncer.shouldRequestBlock(withHash: hash)).to(beTruthy()) + } + } + } + } + } +} diff --git a/BitcoinCore/BitcoinCoreTests/Extensions.swift b/BitcoinCore/BitcoinCoreTests/Extensions.swift index 62ab6e62..d7ffdf14 100644 --- a/BitcoinCore/BitcoinCoreTests/Extensions.swift +++ b/BitcoinCore/BitcoinCoreTests/Extensions.swift @@ -1,4 +1,5 @@ import XCTest +import Cuckoo @testable import BitcoinCore extension XCTestCase { @@ -167,3 +168,17 @@ extension InputToSign: Equatable { } } + +func addressMatcher(_ address: Address) -> ParameterMatcher
{ + return ParameterMatcher
{ address.stringValue == $0.stringValue } +} + +func addressMatcher(_ address: Address?) -> ParameterMatcher { + return ParameterMatcher { tested in + if let a1 = address, let a2 = tested { + return addressMatcher(a1).matches(a2) + } else { + return address == nil && tested == nil + } + } +} diff --git a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift index 057f9b81..65db065c 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/BloomFilterManagerTests.swift @@ -39,120 +39,6 @@ class BloomFilterManagerTests: QuickSpec { } describe("#regenerateBloomFilter") { - context("when has publicKeys") { - let keys = [ - getPublicKey(withIndex: 0, chain: .external), - getPublicKey(withIndex: 0, chain: .internal), - getPublicKey(withIndex: 1, chain: .external), - ] - - beforeEach { - stub(mockStorage) { mock in - when(mock.publicKeys()).thenReturn(keys) - when(mock.outputsWithPublicKeys()).thenReturn([]) - } - } - - it("adds keys to bloom filter") { - manager.regenerateBloomFilter() - - var expectedElements: [Data] = [] - for key in keys { - expectedElements.append(key.keyHash) - expectedElements.append(key.raw) - expectedElements.append(key.scriptHashForP2WPKH) - } - - verify(mockFactory).bloomFilter(withElements: equal(to: expectedElements)) - verify(mockBloomFilterManagerDelegate).bloomFilterUpdated(bloomFilter: equal(to: bloomFilter, equalWhen: { $0.filter == $1.filter })) - } - } - - context("when has outputs with publicKeys") { - let output1 = TestData.p2wpkhTransaction.outputs[0] - output1.transactionHash = Data(repeating: 0, count: 32) - - beforeEach { - stub(mockStorage) { mock in - when(mock.publicKeys()).thenReturn([]) - } - } - - context("when output is not spent") { - beforeEach { - stub(mockStorage) { mock in - when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: nil, spendingBlockHeight: nil)]) - } - } - - it("adds outputs to bloom filter") { - manager.regenerateBloomFilter() - - let expectedElements = [output1.transactionHash + self.byteArrayLittleEndian(int: output1.index)] - - verify(mockFactory).bloomFilter(withElements: equal(to: expectedElements)) - verify(mockBloomFilterManagerDelegate).bloomFilterUpdated(bloomFilter: equal(to: bloomFilter, equalWhen: { $0.filter == $1.filter })) - } - } - - context("when output is spent") { - let input = TestData.p2pkhTransaction.inputs[0] - input.previousOutputTxHash = output1.transactionHash - input.previousOutputIndex = output1.index - - context("when spending transaction is in mempool") { - beforeEach { - stub(mockStorage) { mock in - when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: input, spendingBlockHeight: nil)]) - } - } - - it("adds output to bloom filter") { - manager.regenerateBloomFilter() - - let expectedElements = [output1.transactionHash + self.byteArrayLittleEndian(int: output1.index)] - - verify(mockFactory).bloomFilter(withElements: equal(to: expectedElements)) - verify(mockBloomFilterManagerDelegate).bloomFilterUpdated(bloomFilter: equal(to: bloomFilter, equalWhen: { $0.filter == $1.filter })) - } - } - - context("when spending transaction is in block") { - context("when block is not far enough in history") { - beforeEach { - stub(mockStorage) { mock in - when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: input, spendingBlockHeight: lastBlock.height - 98)]) - } - } - - it("adds output to bloom filter") { - manager.regenerateBloomFilter() - - let expectedElements = [output1.transactionHash + self.byteArrayLittleEndian(int: output1.index)] - - verify(mockFactory).bloomFilter(withElements: equal(to: expectedElements)) - verify(mockBloomFilterManagerDelegate).bloomFilterUpdated(bloomFilter: equal(to: bloomFilter, equalWhen: { $0.filter == $1.filter })) - } - } - - context("when block is far enough") { - beforeEach { - stub(mockStorage) { mock in - when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: input, spendingBlockHeight: lastBlock.height - 100)]) - } - } - - it("doesn't add output to bloom filter") { - manager.regenerateBloomFilter() - - verify(mockFactory, never()).bloomFilter(withElements: any()) - verify(mockBloomFilterManagerDelegate, never()).bloomFilterUpdated(bloomFilter: any()) - } - } - } - } - } - context("when has providers") { let elements = [Data(repeating: 0, count: 32), Data(repeating: 1, count: 20)] @@ -196,19 +82,4 @@ class BloomFilterManagerTests: QuickSpec { } } - private func getPublicKey(withIndex index: Int, chain: HDWallet.Chain) -> PublicKey { - let hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) - let hdPrivKeyData = try! hdWallet.privateKeyData(account: 0, index: index, external: chain == .external) - return PublicKey(withAccount: 0, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) - } - - private func byteArrayLittleEndian(int: Int) -> [UInt8] { - return [ - UInt8(int & 0x000000FF), - UInt8((int & 0x0000FF00) >> 8), - UInt8((int & 0x00FF0000) >> 16), - UInt8((int & 0xFF000000) >> 24) - ] - } - } diff --git a/BitcoinCore/BitcoinCoreTests/Managers/IrregularOutputFinderTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/IrregularOutputFinderTests.swift new file mode 100644 index 00000000..4e9dce27 --- /dev/null +++ b/BitcoinCore/BitcoinCoreTests/Managers/IrregularOutputFinderTests.swift @@ -0,0 +1,122 @@ +import Quick +import Nimble +import XCTest +import Cuckoo +import HSHDWalletKit +@testable import BitcoinCore + +class IrregularOutputFinderTests: QuickSpec { + override func spec() { + let mockStorage = MockIStorage() + let lastBlock = TestData.checkpointBlock + + var finder: IrregularOutputFinder! + var elements: [Data]! + + beforeEach { + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(lastBlock) + } + + finder = IrregularOutputFinder(storage: mockStorage) + } + + afterEach { + reset(mockStorage) + finder = nil + } + + describe("#filterElements") { + let output1 = TestData.p2wpkhTransaction.outputs[0] + output1.transactionHash = Data(repeating: 0, count: 32) + + beforeEach { + stub(mockStorage) { mock in + when(mock.publicKeys()).thenReturn([]) + } + } + + context("when output is not spent") { + beforeEach { + stub(mockStorage) { mock in + when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: nil, spendingBlockHeight: nil)]) + } + } + + it("returns outputs") { + elements = finder.filterElements() + + let expectedElements = [output1.transactionHash + self.byteArrayLittleEndian(int: output1.index)] + expect(elements).to(equal(expectedElements)) + } + } + + context("when output is spent") { + let input = TestData.p2pkhTransaction.inputs[0] + input.previousOutputTxHash = output1.transactionHash + input.previousOutputIndex = output1.index + + context("when spending transaction is in mempool") { + beforeEach { + stub(mockStorage) { mock in + when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: input, spendingBlockHeight: nil)]) + } + } + + it("returns output") { + elements = finder.filterElements() + + let expectedElements = [output1.transactionHash + self.byteArrayLittleEndian(int: output1.index)] + expect(elements).to(equal(expectedElements)) + } + } + + context("when spending transaction is in block") { + context("when block is not far enough in history") { + beforeEach { + stub(mockStorage) { mock in + when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: input, spendingBlockHeight: lastBlock.height - 98)]) + } + } + + it("returns output") { + elements = finder.filterElements() + + let expectedElements = [output1.transactionHash + self.byteArrayLittleEndian(int: output1.index)] + expect(elements).to(equal(expectedElements)) + } + } + + context("when block is far enough") { + beforeEach { + stub(mockStorage) { mock in + when(mock.outputsWithPublicKeys()).thenReturn([OutputWithPublicKey(output: output1, publicKey: TestData.pubKey(), spendingInput: input, spendingBlockHeight: lastBlock.height - 100)]) + } + } + + it("doesn't return output") { + elements = finder.filterElements() + expect(elements).to(equal([Data]())) + } + } + } + } + } + } + + private func getPublicKey(withIndex index: Int, chain: HDWallet.Chain) -> PublicKey { + let hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) + let hdPrivKeyData = try! hdWallet.privateKeyData(account: 0, index: index, external: chain == .external) + return PublicKey(withAccount: 0, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) + } + + private func byteArrayLittleEndian(int: Int) -> [UInt8] { + return [ + UInt8(int & 0x000000FF), + UInt8((int & 0x0000FF00) >> 8), + UInt8((int & 0x00FF0000) >> 16), + UInt8((int & 0xFF000000) >> 24) + ] + } + +} diff --git a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift index 5e192151..0e20211f 100644 --- a/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Managers/PublicKeyManagerTests.swift @@ -1,385 +1,424 @@ -//import XCTest -//import Cuckoo -//import HSHDWalletKit -//@testable import BitcoinCore -// -//class PublicKeyManagerTests: XCTestCase { -// -// private var mockStorage: MockIStorage! -// private var mockHDWallet: MockIHDWallet! -// private var mockAddressConverter: MockIAddressConverter! -// -// private var hdWallet: IHDWallet! -// private var manager: PublicKeyManager! -// -// override func setUp() { -// super.setUp() -// -// mockStorage = MockIStorage() -// mockHDWallet = MockIHDWallet() -// mockAddressConverter = MockIAddressConverter() -// -// stub(mockStorage) { mock in -// when(mock.add(publicKeys: any())).thenDoNothing() -// } -// -// hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) -// manager = PublicKeyManager(storage: mockStorage, hdWallet: mockHDWallet, addressConverter: mockAddressConverter) -// } -// -// override func tearDown() { -// mockStorage = nil -// mockHDWallet = nil -// mockAddressConverter = nil -// -// hdWallet = nil -// manager = nil -// -// super.tearDown() -// } -// -// func testChangePublicKey() { -// let publicKeys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false) -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) -// } -// -// let changePublicKey = try! manager.changePublicKey() -// XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) -// } -// -// func testChangePublicKey_NoUnusedPublicKey() { -// let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true) -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) -// } -// -// do { -// let _ = try manager.changePublicKey() -// XCTFail("Should throw exception") -// } catch let error as PublicKeyManager.PublicKeyManagerError { -// XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) -// } catch { -// XCTFail("Unexpected exception thrown") -// } -// } -// -// func testReceivePublicKey() { -// let publicKeys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false) -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) -// } -// -// let changePublicKey = try! manager.receivePublicKey() -// XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) -// } -// -// func testReceivePublicKey_NoUnusedPublicKey() { -// let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true) -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) -// } -// -// do { -// let _ = try manager.receivePublicKey() -// XCTFail("Should throw exception") -// } catch let error as PublicKeyManager.PublicKeyManagerError { -// XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) -// } catch { -// XCTFail("Unexpected exception thrown") -// } -// } -// -// func testFillGap() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) -// } -// -// try! manager.fillGap() -// verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: false)) -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: true)) -// -// verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey])) -// verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) -// verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey, keys[6].publicKey])) -// verify(mockStorage).add(publicKeys: equal(to: [keys[7].publicKey, keys[8].publicKey])) -// } -// -// func testFillGap_NoUsedKey() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[0].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[2].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[3].publicKey) -// } -// -// try! manager.fillGap() -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) -// verify(mockHDWallet, never()).publicKey(account: equal(to: 1), index: any(), external: any()) -// -// verify(mockStorage).add(publicKeys: equal(to: [keys[0].publicKey, keys[1].publicKey])) -// verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey, keys[3].publicKey])) -// } -// -// func testFillGap_NoUnusedKeys() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) -// } -// -// try! manager.fillGap() -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) -// verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) -// -// verify(mockStorage).add(publicKeys: equal(to: [keys[1].publicKey, keys[2].publicKey])) -// verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) -// } -// -// func testFillGap_NonSequentiallyUsedKeys() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(1) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 3), external: equal(to: false))).thenReturn(keys[5].publicKey) -// when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: true))).thenReturn(keys[6].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[7].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[8].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[9].publicKey) -// when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[10].publicKey) -// } -// -// try! manager.fillGap() -// verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) -// verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) -// -// verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey])) -// verify(mockStorage).add(publicKeys: equal(to: [keys[6].publicKey])) -// } -// -// func testAddKeys() { -// let keys = [ -// getPublicKey(withAccount: 0, index: 0, chain: .internal), -// getPublicKey(withAccount: 0, index: 1, chain: .internal), -// getPublicKey(withAccount: 0, index: 0, chain: .external), -// ] -// -// try! manager.addKeys(keys: keys) -// verify(mockStorage).add(publicKeys: equal(to: keys)) -// } -// -// func testGapShifts() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// } -// -// XCTAssertEqual(manager.gapShifts(), true) -// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) -// } -// -// func testGapShifts_NoUnusedKeys() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// } -// -// XCTAssertEqual(manager.gapShifts(), true) -// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) -// } -// -// func testGapShifts_NonSequentiallyUsedKeys() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// } -// -// XCTAssertEqual(manager.gapShifts(), true) -// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) -// } -// -// func testGapShifts_NoShift() { -// let keys = [ -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), -// PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), -// ] -// -// stub(mockStorage) { mock in -// when(mock.publicKeysWithUsedState()).thenReturn(keys) -// } -// stub(mockHDWallet) { mock in -// when(mock.gapLimit.get).thenReturn(2) -// } -// -// XCTAssertEqual(manager.gapShifts(), false) -// } -// -// func testPublicKey_byPath_ExistsInStorage() { -// let key = getPublicKey(withAccount: 10, index: 20, chain: .internal) -// -// stub(mockStorage) { mock in -// when(mock.publicKey(byPath: equal(to: "10/0/20"))).thenReturn(key) -// } -// -// XCTAssertEqual(try! manager.publicKey(byPath: "10/0/20"), key) -// verify(mockStorage).publicKey(byPath: equal(to: "10/0/20")) -// verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) -// } -// -// func testPublicKey_byPath_DoesNotExistsInStorage() { -// let key = getPublicKey(withAccount: 10, index: 20, chain: .external) -// -// stub(mockStorage) { mock in -// when(mock.publicKey(byPath: equal(to: "10/1/20"))).thenReturn(nil) -// } -// stub(mockHDWallet) { mock in -// when(mock.publicKey(account: 10, index: 20, external: true)).thenReturn(key) -// } -// -// XCTAssertEqual(try! manager.publicKey(byPath: "10/1/20"), key) -// verify(mockStorage).publicKey(byPath: equal(to: "10/1/20")) -// verify(mockHDWallet).publicKey(account: 10, index: 20, external: true) -// } -// -// func testPublicKey_byPath_InvalidPath() { -// do { -// _ = try manager.publicKey(byPath: "0/0") -// XCTFail("Expected exception") -// } catch let error as PublicKeyManager.PublicKeyManagerError { -// XCTAssertEqual(error, .invalidPath) -// } catch { -// XCTFail("Unexpected exception") -// } -// } -// -// private func getPublicKey(withAccount account: Int, index: Int, chain: HDWallet.Chain) -> PublicKey { -// let hdPrivKeyData = try! hdWallet.privateKeyData(account: account, index: index, external: chain == .external) -// return PublicKey(withAccount: account, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) -// } -// -//} +import XCTest +import Cuckoo +import HSHDWalletKit +@testable import BitcoinCore + +class PublicKeyManagerTests: XCTestCase { + + private var mockStorage: MockIStorage! + private var mockHDWallet: MockIHDWallet! + private var mockAddressConverter: MockIAddressConverter! + private var mockRestoreKeyConverter: MockIRestoreKeyConverter! + private var mockBloomFilterManager: MockIBloomFilterManager! + + private var hdWallet: IHDWallet! + private var manager: PublicKeyManager! + + override func setUp() { + super.setUp() + + mockStorage = MockIStorage() + mockHDWallet = MockIHDWallet() + mockAddressConverter = MockIAddressConverter() + mockRestoreKeyConverter = MockIRestoreKeyConverter() + mockBloomFilterManager = MockIBloomFilterManager() + + stub(mockStorage) { mock in + when(mock.add(publicKeys: any())).thenDoNothing() + } + stub(mockBloomFilterManager) { mock in + when(mock.regenerateBloomFilter()).thenDoNothing() + } + + hdWallet = HDWallet(seed: Data(), coinType: UInt32(1), xPrivKey: UInt32(0x04358394), xPubKey: UInt32(0x043587cf)) + manager = PublicKeyManager(storage: mockStorage, hdWallet: mockHDWallet, restoreKeyConverter: mockRestoreKeyConverter) + manager.bloomFilterManager = mockBloomFilterManager + } + + override func tearDown() { + mockStorage = nil + mockHDWallet = nil + mockAddressConverter = nil + mockRestoreKeyConverter = nil + mockBloomFilterManager = nil + + hdWallet = nil + manager = nil + + super.tearDown() + } + + func testChangePublicKey() { + let publicKeys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false) + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) + } + + let changePublicKey = try! manager.changePublicKey() + XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) + } + + func testChangePublicKey_NoUnusedPublicKey() { + let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true) + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) + } + + do { + let _ = try manager.changePublicKey() + XCTFail("Should throw exception") + } catch let error as PublicKeyManager.PublicKeyManagerError { + XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) + } catch { + XCTFail("Unexpected exception thrown") + } + } + + func testReceivePublicKey() { + let publicKeys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false) + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn(publicKeys) + } + + let changePublicKey = try! manager.receivePublicKey() + XCTAssertEqual(changePublicKey.keyHash, publicKeys[3].publicKey.keyHash) + } + + func testReceivePublicKey_NoUnusedPublicKey() { + let publicKey = PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true) + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([publicKey]) + } + + do { + let _ = try manager.receivePublicKey() + XCTFail("Should throw exception") + } catch let error as PublicKeyManager.PublicKeyManagerError { + XCTAssertEqual(error, PublicKeyManager.PublicKeyManagerError.noUnusedPublicKey) + } catch { + XCTFail("Unexpected exception thrown") + } + } + + func testFillGap() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) + } + + try! manager.fillGap() + verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: false)) + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 1), index: any(), external: equal(to: true)) + + verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey])) + verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) + verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey, keys[6].publicKey])) + verify(mockStorage).add(publicKeys: equal(to: [keys[7].publicKey, keys[8].publicKey])) + + verify(mockBloomFilterManager).regenerateBloomFilter() + + } + + func testFillGap_NoUsedKey() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[0].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[2].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[3].publicKey) + } + + try! manager.fillGap() + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) + verify(mockHDWallet, never()).publicKey(account: equal(to: 1), index: any(), external: any()) + + verify(mockStorage).add(publicKeys: equal(to: [keys[0].publicKey, keys[1].publicKey])) + verify(mockStorage).add(publicKeys: equal(to: [keys[2].publicKey, keys[3].publicKey])) + } + + func testFillGap_NoUnusedKeys() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[1].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: false))).thenReturn(keys[2].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[3].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[4].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[5].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[6].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[7].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[8].publicKey) + } + + try! manager.fillGap() + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) + verify(mockHDWallet, times(2)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) + + verify(mockStorage).add(publicKeys: equal(to: [keys[1].publicKey, keys[2].publicKey])) + verify(mockStorage).add(publicKeys: equal(to: [keys[3].publicKey, keys[4].publicKey])) + } + + func testFillGap_NonSequentiallyUsedKeys() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 1, index: 1, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(1) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 3), external: equal(to: false))).thenReturn(keys[5].publicKey) + when(mock.publicKey(account: equal(to: 0), index: equal(to: 2), external: equal(to: true))).thenReturn(keys[6].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: false))).thenReturn(keys[7].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: false))).thenReturn(keys[8].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 0), external: equal(to: true))).thenReturn(keys[9].publicKey) + when(mock.publicKey(account: equal(to: 1), index: equal(to: 1), external: equal(to: true))).thenReturn(keys[10].publicKey) + } + + try! manager.fillGap() + verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: false)) + verify(mockHDWallet, times(1)).publicKey(account: equal(to: 0), index: any(), external: equal(to: true)) + + verify(mockStorage).add(publicKeys: equal(to: [keys[5].publicKey])) + verify(mockStorage).add(publicKeys: equal(to: [keys[6].publicKey])) + } + + func testAddKeys() { + let keys = [ + getPublicKey(withAccount: 0, index: 0, chain: .internal), + getPublicKey(withAccount: 0, index: 1, chain: .internal), + getPublicKey(withAccount: 0, index: 0, chain: .external), + ] + + try! manager.addKeys(keys: keys) + verify(mockStorage).add(publicKeys: equal(to: keys)) + } + + func testGapShifts() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1]]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + } + + XCTAssertEqual(manager.gapShifts(), true) + verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) + } + + func testGapShifts_NoUnusedKeys() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([keys[0]]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + } + + XCTAssertEqual(manager.gapShifts(), true) + verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) + } + + func testGapShifts_NonSequentiallyUsedKeys() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .internal), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 3, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn([keys[0], keys[1], keys[2], keys[3], keys[4]]) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + } + + XCTAssertEqual(manager.gapShifts(), true) + verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) + } + + func testGapShifts_NoShift() { + let keys = [ + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .internal), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 0, chain: .external), used: true), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 1, chain: .external), used: false), + PublicKeyWithUsedState(publicKey: getPublicKey(withAccount: 0, index: 2, chain: .external), used: false), + ] + + stub(mockStorage) { mock in + when(mock.publicKeysWithUsedState()).thenReturn(keys) + } + stub(mockHDWallet) { mock in + when(mock.gapLimit.get).thenReturn(2) + } + + XCTAssertEqual(manager.gapShifts(), false) + } + + func testPublicKey_byPath_ExistsInStorage() { + let key = getPublicKey(withAccount: 10, index: 20, chain: .internal) + + stub(mockStorage) { mock in + when(mock.publicKey(byPath: equal(to: "10/0/20"))).thenReturn(key) + } + + XCTAssertEqual(try! manager.publicKey(byPath: "10/0/20"), key) + verify(mockStorage).publicKey(byPath: equal(to: "10/0/20")) + verify(mockHDWallet, never()).publicKey(account: any(), index: any(), external: any()) + } + + func testPublicKey_byPath_DoesNotExistsInStorage() { + let key = getPublicKey(withAccount: 10, index: 20, chain: .external) + + stub(mockStorage) { mock in + when(mock.publicKey(byPath: equal(to: "10/1/20"))).thenReturn(nil) + } + stub(mockHDWallet) { mock in + when(mock.publicKey(account: 10, index: 20, external: true)).thenReturn(key) + } + + XCTAssertEqual(try! manager.publicKey(byPath: "10/1/20"), key) + verify(mockStorage).publicKey(byPath: equal(to: "10/1/20")) + verify(mockHDWallet).publicKey(account: 10, index: 20, external: true) + } + + func testPublicKey_byPath_InvalidPath() { + do { + _ = try manager.publicKey(byPath: "0/0") + XCTFail("Expected exception") + } catch let error as PublicKeyManager.PublicKeyManagerError { + XCTAssertEqual(error, .invalidPath) + } catch { + XCTFail("Unexpected exception") + } + } + + func testFilterElements() { + let publicKeys = [ + getPublicKey(withAccount: 0, index: 0, chain: .internal), + getPublicKey(withAccount: 0, index: 0, chain: .external), + getPublicKey(withAccount: 0, index: 3, chain: .internal), + getPublicKey(withAccount: 0, index: 1, chain: .internal), + getPublicKey(withAccount: 0, index: 2, chain: .internal), + getPublicKey(withAccount: 0, index: 1, chain: .external) + ] + + stub(mockStorage) { mock in + when(mock.publicKeys()).thenReturn(publicKeys) + } + + var elements = [Data]() + stub(mockRestoreKeyConverter) { mock in + for publicKey in publicKeys { + let newElements = [publicKey.keyHash] + elements.append(contentsOf: newElements) + when(mock).bloomFilterElements(publicKey: equal(to: publicKey)).thenReturn(newElements) + } + } + + XCTAssertEqual(manager.filterElements(), elements) + } + + private func getPublicKey(withAccount account: Int, index: Int, chain: HDWallet.Chain) -> PublicKey { + let hdPrivKeyData = try! hdWallet.privateKeyData(account: account, index: index, external: chain == .external) + return PublicKey(withAccount: account, index: index, external: chain == .external, hdPublicKeyData: hdPrivKeyData) + } + +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift index 3e8a8051..a3317c73 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/SegWitAddress.swift @@ -1,3 +1,5 @@ +@testable import BitcoinCore + public class SegWitAddress: Address, Equatable { public let type: AddressType public let keyHash: Data diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index 76df0a73..a7875e7c 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -1,233 +1,290 @@ -//import XCTest -//import Cuckoo -//import Nimble -//import Quick -//@testable import BitcoinCore -// -//class TransactionCreatorTests: QuickSpec { -// override func spec() { -// let mockTransactionBuilder = MockITransactionBuilder() -// let mockTransactionProcessor = MockITransactionProcessor() -// let mockTransactionFeeCalculator = MockITransactionFeeCalculator() -// let mockTransactionSender = MockITransactionSender() -// let mockBloomFilterManager = MockIBloomFilterManager() -// let mockAddressConverter = MockIAddressConverter() -// let mockPublicKeyManager = MockIPublicKeyManager() -// -// let transaction = TestData.p2pkhTransaction -// let changePubKey = TestData.pubKey() -// -// let toAddressPKH = LegacyAddress(type: .pubKeyHash, keyHash: Data(hex: "d50bf226c9ff3bcf06f13d8ca129f24bedeef594")!, base58: "mzwSXvtPs7MFbW2ysNA4Gw3P2KjrcEWaE5") -// let toAddressSH = LegacyAddress(type: .scriptHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, base58: toAddressSH) -// let toAddressWPKH = SegWitAddress(type: .pubKeyHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, bech32: "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2", version: 0) -// let changeAddressPKH = LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changeAddressPKH) -// -// -// let sendingValue = 100_000_000 -// -// var unspentOutputs: [UnspentOutput]! -// var selectedOutputsInfo: SelectedUnspentOutputInfo! -// -// var transactionCreator: TransactionCreator! -// -// beforeEach { -// unspentOutputs = [ -// UnspentOutput( -// output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2pkh), -// publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), -// transaction: Transaction(), -// blockHeight: 1000 -// ) -// ] -// selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: 1000, addChangeOutput: true) -// -// stub(mockTransactionFeeCalculator) { mock in -// when(mock).feeWithUnspentOutputs(value: sendingValue, feeRate: any(), toScriptType: any(), changeScriptType: any(), senderPay: any()).thenReturn(selectedOutputsInfo) -// } -// -// stub(mockAddressConverter) { mock in -// when(mock.convert(address: toAddressPKH)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: Data(hex: "d50bf226c9ff3bcf06f13d8ca129f24bedeef594")!, base58: "mzwSXvtPs7MFbW2ysNA4Gw3P2KjrcEWaE5")) -// when(mock.convert(address: toAddressSH)).thenReturn(LegacyAddress(type: .scriptHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, base58: toAddressSH)) -// when(mock.convert(address: toAddressWPKH)).thenReturn(SegWitAddress(type: .pubKeyHash, keyHash: Data(hex: "43922a3f1dc4569f9eccce9a71549d5acabbc0ca")!, bech32: "bcrt1qsay3z5rn44v6du6c0u0eu352mm0sz3el0f0cs2", version: 0)) -// when(mock.convert(address: changePubKeyAddress)).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) -// when(mock.convert(publicKey: equal(to: changePubKey), type: equal(to: .p2pkh))).thenReturn(LegacyAddress(type: .pubKeyHash, keyHash: changePubKey.keyHash, base58: changePubKeyAddress)) -// } -// -// -// } -// -// afterEach { -// reset(mockTransactionBuilder, mockTransactionProcessor, mockTransactionFeeCalculator, mockTransactionSender, mockBloomFilterManager, mockAddressConverter, mockPublicKeyManager) -// transactionCreator = nil -// } -// -// describe("#create(to:value:feeRate:senderPay:)") { -// beforeEach { -// stub(mockTransactionBuilder) { mock in -// when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any())).thenReturn(transaction) -// } -// stub(mockTransactionProcessor) { mock in -// when(mock.processCreated(transaction: any())).thenDoNothing() -// } -// stub(mockTransactionSender) { mock in -// when(mock.send(pendingTransaction: any())).thenDoNothing() -// } -// stub(mockBloomFilterManager) { mock in -// when(mock.regenerateBloomFilter()).thenDoNothing() -// } -// -// transactionCreator = TransactionCreator( -// transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, transactionFeeCalculator: mockTransactionFeeCalculator, -// bloomFilterManager: mockBloomFilterManager, addressConverter: mockAddressConverter, publicKeyManager: mockPublicKeyManager, bip: .bip44) -// } -// -// context("when BloomFilterManager.BloomFilterExpired error") { -// beforeEach { -// stub(mockTransactionProcessor) { mock in -// when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) -// } -// stub(mockTransactionSender) { mock in -// when(mock.verifyCanSend()).thenDoNothing() -// } -// -// _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) -// } -// -// it("does create transaction") { -// verify(mockTransactionBuilder).buildTransaction(value: 0, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: any(), toAddress: any(), changeAddress: nil) -// verify(mockTransactionProcessor).processCreated(transaction: any()) -// } -// -// it("does send transaction") { -// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) -// } -// -// it("regenerates bloomfilter") { -// verify(mockBloomFilterManager).regenerateBloomFilter() -// } -// } -// -// context("when other error") { -// beforeEach { -// stub(mockTransactionSender) { mock in -// when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) -// } -// -// _ = try? transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) -// } -// -// it("doesn't create transaction") { -// verify(mockTransactionProcessor, never()).processCreated(transaction: any()) -// } -// -// it("doesn't regenerate bloomfilter") { -// verify(mockBloomFilterManager, never()).regenerateBloomFilter() -// } -// } -// -// context("when success") { -// beforeEach { -// stub(mockTransactionSender) { mock in -// when(mock.verifyCanSend()).thenDoNothing() -// } -// _ = try! transactionCreator.create(to: "", value: 0, feeRate: 0, senderPay: false) -// } -// -// it("creates transaction") { -// verify(mockTransactionBuilder).buildTransaction(value: 0, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: any(), toAddress: any(), changeAddress: nil) -// verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) -// } -// -// it("sends transaction") { -// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) -// } -// } -// } -// -// describe("#create(from:to:feeRate:signatureScriptFunction:)") { -// let unspentOutput = UnspentOutput(output: TestData.p2shTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: Transaction(), blockHeight: nil) -// let signatureScriptFunction: (Data, Data) -> Data = { return $0 + $1 } -// let fee = 1000 -// -// beforeEach { -// stub(mockTransactionBuilder) { mock in -// when(mock.buildTransaction(from: any(), to: any(), fee: any(), signatureScriptFunction: any())).thenReturn(transaction) -// } -// stub(mockTransactionProcessor) { mock in -// when(mock.processCreated(transaction: any())).thenDoNothing() -// } -// stub(mockTransactionSender) { mock in -// when(mock.send(pendingTransaction: any())).thenDoNothing() -// } -// stub(mockBloomFilterManager) { mock in -// when(mock.regenerateBloomFilter()).thenDoNothing() -// } -// -// transactionCreator = TransactionCreator(transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, bloomFilterManager: mockBloomFilterManager) -// } -// -// context("when BloomFilterManager.BloomFilterExpired error") { -// beforeEach { -// stub(mockTransactionProcessor) { mock in -// when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) -// } -// stub(mockTransactionSender) { mock in -// when(mock.verifyCanSend()).thenDoNothing() -// } -// -// _ = try? transactionCreator.create(from: unspentOutput, to: toAddressPKH, feeRate: 0, signatureScriptFunction: signatureScriptFunction) -// } -// -// it("does create transaction") { -// verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: , fee: fee, signatureScriptFunction: any()) -// verify(mockTransactionProcessor).processCreated(transaction: any()) -// } -// -// it("does send transaction") { -// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) -// } -// -// it("regenerates bloomfilter") { -// verify(mockBloomFilterManager).regenerateBloomFilter() -// } -// } -// -// context("when other error") { -// beforeEach { -// stub(mockTransactionSender) { mock in -// when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) -// } -// -// _ = try? transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) -// } -// -// it("doesn't create transaction") { -// verify(mockTransactionProcessor, never()).processCreated(transaction: any()) -// } -// -// it("doesn't regenerate bloomfilter") { -// verify(mockBloomFilterManager, never()).regenerateBloomFilter() -// } -// } -// -// context("when success") { -// beforeEach { -// stub(mockTransactionSender) { mock in -// when(mock.verifyCanSend()).thenDoNothing() -// } -// -// _ = try! transactionCreator.create(from: unspentOutput, to: "", feeRate: 0, signatureScriptFunction: signatureScriptFunction) -// } -// -// it("creates transaction") { -// verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: "", feeRate: 0, signatureScriptFunction: any()) -// verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) -// } -// -// it("sends transaction") { -// verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) -// } -// } -// } -// } -//} +import XCTest +import Cuckoo +import Nimble +import Quick +@testable import BitcoinCore + +class TransactionCreatorTests: QuickSpec { + override func spec() { + let mockTransactionBuilder = MockITransactionBuilder() + let mockTransactionProcessor = MockITransactionProcessor() + let mockTransactionFeeCalculator = MockITransactionFeeCalculator() + let mockTransactionSender = MockITransactionSender() + let mockBloomFilterManager = MockIBloomFilterManager() + let mockAddressConverter = MockIAddressConverter() + let mockPublicKeyManager = MockIPublicKeyManager() + + let transaction = TestData.p2pkhTransaction + let changePublicKey = TestData.pubKey() + + let toAddress = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "toAddressPKH") + let changeAddress = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "changeAddressPKH") + + let sendingValue = 100_000_000 + let feeRate = 1000 + let senderPay = true + let bip = Bip.bip44 + + var unspentOutputs: [UnspentOutput]! + var selectedOutputsInfo: SelectedUnspentOutputInfo! + + var creator: TransactionCreator! + + beforeEach { + unspentOutputs = [ + UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2pkh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + ] + selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: 1000, addChangeOutput: true) + + stub(mockTransactionFeeCalculator) { mock in + when(mock).feeWithUnspentOutputs(value: sendingValue, feeRate: any(), toScriptType: any(), changeScriptType: any(), senderPay: any()).thenReturn(selectedOutputsInfo) + } + stub(mockAddressConverter) { mock in + when(mock.convert(address: equal(to: toAddress.stringValue))).thenReturn(toAddress) + when(mock.convert(address: equal(to: changeAddress.stringValue))).thenReturn(changeAddress) + when(mock.convert(keyHash: equal(to: toAddress.keyHash), type: equal(to: toAddress.scriptType))).thenReturn(toAddress) + when(mock.convert(publicKey: equal(to: changePublicKey), type: equal(to: changeAddress.scriptType))).thenReturn(changeAddress) + } + stub(mockPublicKeyManager) { mock in + when(mock).changePublicKey().thenReturn(changePublicKey) + } + stub(mockTransactionBuilder) { mock in + when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any())).thenReturn(transaction) + } + stub(mockTransactionProcessor) { mock in + when(mock.processCreated(transaction: any())).thenDoNothing() + } + stub(mockTransactionSender) { mock in + when(mock.send(pendingTransaction: any())).thenDoNothing() + } + stub(mockBloomFilterManager) { mock in + when(mock.regenerateBloomFilter()).thenDoNothing() + } + + creator = TransactionCreator( + transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, transactionFeeCalculator: mockTransactionFeeCalculator, + bloomFilterManager: mockBloomFilterManager, addressConverter: mockAddressConverter, publicKeyManager: mockPublicKeyManager, bip: bip + ) + } + + afterEach { + reset(mockTransactionBuilder, mockTransactionProcessor, mockTransactionFeeCalculator, mockTransactionSender, mockBloomFilterManager, mockAddressConverter, mockPublicKeyManager) + creator = nil + } + + describe("#create(to:value:feeRate:senderPay:)") { + context("when all valid, addChangeOutput is true") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + _ = try! creator.create(to: toAddress.stringValue, value: sendingValue, feeRate: feeRate, senderPay: senderPay) + } + + it("parses address string") { + verify(mockAddressConverter).convert(address: equal(to: toAddress.stringValue)) + } + + it("calculates fee and selects unspent outputs") { + verify(mockTransactionFeeCalculator).feeWithUnspentOutputs(value: sendingValue, feeRate: feeRate, toScriptType: equal(to: toAddress.scriptType), changeScriptType: equal(to: bip.scriptType), senderPay: senderPay) + } + + it("generates change address") { + verify(mockPublicKeyManager).changePublicKey() + verify(mockAddressConverter).convert(publicKey: equal(to: changePublicKey), type: equal(to: bip.scriptType)) + } + + it("creates transaction") { + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + } + + it("sends transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + } + + context("when addChangeOutput is false") { + beforeEach { + selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: 1000, addChangeOutput: false) + + stub(mockTransactionFeeCalculator) { mock in + when(mock).feeWithUnspentOutputs(value: sendingValue, feeRate: any(), toScriptType: any(), changeScriptType: any(), senderPay: any()).thenReturn(selectedOutputsInfo) + } + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + _ = try! creator.create(to: toAddress.stringValue, value: sendingValue, feeRate: feeRate, senderPay: senderPay) + } + + it("creates transaction without change address") { + verify(mockPublicKeyManager, never()).changePublicKey() + verify(mockAddressConverter, never()).convert(publicKey: equal(to: changePublicKey), type: equal(to: bip.scriptType)) + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: any(), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(nil)) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + } + } + + context("when BloomFilterManager.BloomFilterExpired error") { + beforeEach { + stub(mockTransactionProcessor) { mock in + when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) + } + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + + _ = try? creator.create(to: toAddress.stringValue, value: sendingValue, feeRate: feeRate, senderPay: senderPay) + } + + it("does create transaction") { + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: any(), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + verify(mockTransactionProcessor).processCreated(transaction: any()) + } + + it("does send transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + + it("regenerates bloomfilter") { + verify(mockBloomFilterManager).regenerateBloomFilter() + } + } + + context("when other error") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) + } + + _ = try? creator.create(to: toAddress.stringValue, value: sendingValue, feeRate: feeRate, senderPay: senderPay) + } + + it("doesn't create transaction") { + verify(mockTransactionProcessor, never()).processCreated(transaction: any()) + } + + it("doesn't regenerate bloomfilter") { + verify(mockBloomFilterManager, never()).regenerateBloomFilter() + } + } + } + + describe("#create(to:scriptType:value:feeRate:senderPay:)") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + _ = try! creator.create(to: toAddress.keyHash, scriptType: toAddress.scriptType, value: sendingValue, feeRate: feeRate, senderPay: senderPay) + } + + it("parses hash and script type to Address") { + verify(mockAddressConverter).convert(keyHash: equal(to: toAddress.keyHash), type: equal(to: toAddress.scriptType)) + } + + it("creates and sends a transaction") { + verify(mockTransactionFeeCalculator).feeWithUnspentOutputs(value: sendingValue, feeRate: feeRate, toScriptType: equal(to: toAddress.scriptType), changeScriptType: equal(to: bip.scriptType), senderPay: senderPay) + verify(mockPublicKeyManager).changePublicKey() + verify(mockAddressConverter).convert(publicKey: equal(to: changePublicKey), type: equal(to: bip.scriptType)) + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + } + + describe("#create(from:to:feeRate:signatureScriptFunction:)") { + let unspentOutput = UnspentOutput(output: TestData.p2shTransaction.outputs[0], publicKey: TestData.pubKey(), transaction: Transaction(), blockHeight: nil) + let signatureScriptFunction: (Data, Data) -> Data = { return $0 + $1 } + let fee = 1000 + + beforeEach { + stub(mockTransactionFeeCalculator) { mock in + when(mock).fee(inputScriptType: any(), outputScriptType: any(), feeRate: any(), signatureScriptFunction: any()).thenReturn(fee) + } + stub(mockTransactionBuilder) { mock in + when(mock).buildTransaction(from: any(), to: any(), fee: any(), signatureScriptFunction: any()).thenReturn(transaction) + } + } + + context("when success") { + beforeEach { + stub(mockTransactionFeeCalculator) { mock in + when(mock).fee(inputScriptType: any(), outputScriptType: any(), feeRate: any(), signatureScriptFunction: any()).thenReturn(fee) + } + + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + + _ = try! creator.create(from: unspentOutput, to: toAddress.stringValue, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + } + + it("parses 'to' string to Address") { + verify(mockAddressConverter).convert(address: toAddress.stringValue) + } + + it("calculates fee") { + verify(mockTransactionFeeCalculator).fee(inputScriptType: equal(to: unspentOutput.output.scriptType), outputScriptType: equal(to: toAddress.scriptType), feeRate: feeRate, signatureScriptFunction: any()) + } + + it("creates transaction") { + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: addressMatcher(toAddress), fee: fee, signatureScriptFunction: any()) + verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) + } + + it("sends transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + } + + context("when BloomFilterManager.BloomFilterExpired error") { + beforeEach { + stub(mockTransactionProcessor) { mock in + when(mock.processCreated(transaction: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) + } + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + + _ = try? creator.create(from: unspentOutput, to: toAddress.stringValue, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + } + + it("does create transaction") { + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: any(), fee: fee, signatureScriptFunction: any()) + verify(mockTransactionProcessor).processCreated(transaction: any()) + } + + it("does send transaction") { + verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) + } + + it("regenerates bloomfilter") { + verify(mockBloomFilterManager).regenerateBloomFilter() + } + } + + context("when other error") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenThrow(BitcoinCoreErrors.TransactionSendError.noConnectedPeers) + } + + _ = try? creator.create(from: unspentOutput, to: toAddress.stringValue, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + } + + it("doesn't create transaction") { + verify(mockTransactionProcessor, never()).processCreated(transaction: any()) + } + + it("doesn't regenerate bloomfilter") { + verify(mockBloomFilterManager, never()).regenerateBloomFilter() + } + } + } + } +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift new file mode 100644 index 00000000..0c6736e7 --- /dev/null +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift @@ -0,0 +1,131 @@ +import XCTest +import Cuckoo +import Nimble +import Quick +@testable import BitcoinCore + +class TransactionFeeCalculatorTests: QuickSpec { + override func spec() { + let mockUnspentOutputSelector = MockIUnspentOutputSelector() + let mockTransactionSizeCalculator = MockITransactionSizeCalculator() + let mockTransactionBuilder = MockITransactionBuilder() + + let toAddress = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "toAddress") + let changeAddress = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "changeAddress") + let value = 100_000_000 + let feeRate = 10 + let senderPay = true + let fee = 1000 + let transaction = TestData.p2pkhTransaction + let unspentOutputs = [ + UnspentOutput( + output: Output(withValue: 200_000_000, index: 0, lockingScript: randomBytes(length: 32), type: .p2pkh), + publicKey: PublicKey(withAccount: 0, index: 0, external: false, hdPublicKeyData: randomBytes(length: 32)), + transaction: Transaction(), + blockHeight: 1000 + ) + ] + + var selectedOutputsInfo: SelectedUnspentOutputInfo! + var resultFee: Int! + + var calculator: TransactionFeeCalculator! + + beforeEach() { + selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: fee, addChangeOutput: true) + + stub(mockUnspentOutputSelector) { mock in + when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(selectedOutputsInfo) + } + stub(mockTransactionBuilder) { mock in + when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any())).thenReturn(transaction) + } + + calculator = TransactionFeeCalculator(unspentOutputSelector: mockUnspentOutputSelector, transactionSizeCalculator: mockTransactionSizeCalculator, transactionBuilder: mockTransactionBuilder) + } + + afterEach() { + reset(mockUnspentOutputSelector, mockTransactionBuilder, mockTransactionSizeCalculator) + calculator = nil + resultFee = nil + selectedOutputsInfo = nil + } + + describe("fee(for:feeRate:senderPay:toAddress:changeAddress:)") { + context("when toAddress exists") { + context("when addChangeOutput is true") { + beforeEach() { + resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) + } + + it("selects unspent outputs with given parameters") { + verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: toAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) + } + + it("builds actual transaction and returns fee") { + verify(mockTransactionBuilder).buildTransaction(value: value, unspentOutputs: equal(to: unspentOutputs), fee: fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + expect(resultFee).to(equal(TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate)) + } + } + + context("when addChangeOutput is false") { + beforeEach() { + selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: fee, addChangeOutput: false) + stub(mockUnspentOutputSelector) { mock in + when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(selectedOutputsInfo) + } + + resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) + } + + it("builds actual transaction without changeAddress") { + verify(mockTransactionBuilder).buildTransaction(value: value, unspentOutputs: equal(to: unspentOutputs), fee: fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(nil)) + } + } + } + + context("when toAddress is nil") { + it("selects unspent outputs with given parameters and returns fee") { + resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: nil, changeAddress: changeAddress) + verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: toAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) + verify(mockTransactionBuilder, never()).buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any()) + expect(resultFee).to(equal(fee)) + } + } + } + + describe("feeWithUnspentOutputs") { + it("selects unspent outputs with given parameters and returns it") { + let feeWithUnspentOutputs = try! calculator.feeWithUnspentOutputs(value: value, feeRate: feeRate, toScriptType: toAddress.scriptType, changeScriptType: changeAddress.scriptType, senderPay: senderPay) + verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: toAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) + expect(feeWithUnspentOutputs.fee).to(equal(selectedOutputsInfo.fee)) + } + } + + describe("fee(inputScriptType:outputScriptType:feeRate:signatureScriptFunction:)") { + let signatureData = [randomBytes(length: TransactionSizeCalculator.signatureLength), randomBytes(length: TransactionSizeCalculator.pubKeyLength)] + let signatureScript = randomBytes(length: 100) + var signatureScriptFunctionCalled = false + let signatureScriptFunction: ((Data, Data) -> Data) = { (signature: Data, publicKey: Data) in + XCTAssertEqual(signature.count, signatureData[0].count) + XCTAssertEqual(publicKey.count, signatureData[1].count) + signatureScriptFunctionCalled = true + return signatureScript + } + let size = 500 + + it("calculates fee from transaction size returns it") { + stub(mockTransactionSizeCalculator) { mock in + when(mock).transactionSize(inputs: any(), outputScriptTypes: any()).thenReturn(size) + } + + resultFee = calculator.fee(inputScriptType: .p2pkh, outputScriptType: .p2pkh, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + let expectedFee = (size + signatureScript.count) * feeRate + + verify(mockTransactionSizeCalculator).transactionSize(inputs: equal(to: [.p2pkh]), outputScriptTypes: equal(to: [.p2pkh])) + expect(resultFee).to(equal(expectedFee)) + expect(signatureScriptFunctionCalled).to(beTrue()) + } + } + } +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift index beb9b053..921919fc 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionProcessorTests.swift @@ -1,534 +1,550 @@ -//import XCTest -//import Cuckoo -//@testable import BitcoinCore -// -//class TransactionProcessorTests: XCTestCase { -// private var mockStorage: MockIStorage! -// private var mockOutputExtractor: MockITransactionExtractor! -// private var mockOutputAddressExtractor: MockITransactionOutputAddressExtractor! -// private var mockInputExtractor: MockITransactionExtractor! -// private var mockOutputsCache: MockIOutputsCache! -// private var mockAddressManager: MockIPublicKeyManager! -// private var mockBlockchainDataListener: MockIBlockchainDataListener! -// private var mockTransactionListener: MockITransactionListener! -// -// private var generatedDate: Date! -// private var dateGenerator: (() -> Date)! -// -// private var transactionProcessor: TransactionProcessor! -// -// override func setUp() { -// super.setUp() -// -// generatedDate = Date() -// dateGenerator = { -// return self.generatedDate -// } -// -// mockStorage = MockIStorage() -// mockOutputExtractor = MockITransactionExtractor() -// mockOutputAddressExtractor = MockITransactionOutputAddressExtractor() -// mockInputExtractor = MockITransactionExtractor() -// mockOutputsCache = MockIOutputsCache() -// mockAddressManager = MockIPublicKeyManager() -// mockBlockchainDataListener = MockIBlockchainDataListener() -// mockTransactionListener = MockITransactionListener() -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: any())).thenReturn(nil) -// when(mock.add(transaction: any())).thenDoNothing() -// when(mock.update(transaction: any())).thenDoNothing() -// when(mock.update(block: any())).thenDoNothing() -// } -// stub(mockOutputsCache) { mock in -// when(mock.add(fromOutputs: any())).thenDoNothing() -// when(mock.hasOutputs(forInputs: any())).thenReturn(false) -// } -// stub(mockOutputExtractor) { mock in -// when(mock.extract(transaction: any())).thenDoNothing() -// } -// stub(mockOutputAddressExtractor) { mock in -// when(mock.extractOutputAddresses(transaction: any())).thenDoNothing() -// } -// stub(mockInputExtractor) { mock in -// when(mock.extract(transaction: any())).thenDoNothing() -// } -// stub(mockAddressManager) { mock in -// when(mock.gapShifts()).thenReturn(false) -// } -// stub(mockBlockchainDataListener) { mock in -// when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() -// when(mock.onDelete(transactionHashes: any())).thenDoNothing() -// when(mock.onInsert(block: any())).thenDoNothing() -// } -// stub(mockTransactionListener) { mock in -// when(mock.onReceive(transaction: any())).thenDoNothing() -// } -// -// transactionProcessor = TransactionProcessor(storage: mockStorage, outputExtractor: mockOutputExtractor, inputExtractor: mockInputExtractor, outputsCache: mockOutputsCache, outputAddressExtractor: mockOutputAddressExtractor, addressManager: mockAddressManager, listener: mockBlockchainDataListener, dateGenerator: dateGenerator) -// transactionProcessor.transactionListener = mockTransactionListener -// } -// -// override func tearDown() { -// mockStorage = nil -// mockOutputExtractor = nil -// mockInputExtractor = nil -// mockOutputsCache = nil -// transactionProcessor = nil -// mockBlockchainDataListener = nil -// -// generatedDate = nil -// dateGenerator = nil -// -// super.tearDown() -// } -// -// func testProcessCreated() { -// let transaction = TestData.p2pkhTransaction -// -// try! transactionProcessor.processCreated(transaction: transaction) -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [Transaction]()), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) -// verify(mockStorage).add(transaction: equal(to: transaction)) -// verifyNoMoreInteractions(mockOutputAddressExtractor) -// verifyNoMoreInteractions(mockInputExtractor) -// } -// -// func testProcessCreated_isMine() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.isMine = true -// -// try! transactionProcessor.processCreated(transaction: transaction) -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) -// verify(mockStorage).add(transaction: equal(to: transaction)) -// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) -// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) -// } -// -// func testProcessCreated_TransactionExists() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.isMine = true -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) -// } -// -// do { -// try transactionProcessor.processCreated(transaction: transaction) -// XCTFail("Expecting error") -// } catch let error as TransactionCreator.CreationError { -// XCTAssertEqual(error, TransactionCreator.CreationError.transactionAlreadyExists) -// } catch { -// XCTFail("Unexpected error") -// } -// -// verify(mockOutputExtractor, never()).extract(transaction: any()) -// verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) -// verify(mockBlockchainDataListener, never()).onUpdate(updated: any(), inserted: any(), inBlock: any()) -// verify(mockStorage, never()).add(transaction: any()) -// verify(mockOutputAddressExtractor, never()).extractOutputAddresses(transaction: any()) -// verify(mockInputExtractor, never()).extract(transaction: any()) -// } -// -// func testProcessCreated_OutputsExpireBloomFiler() { -// let transaction = TestData.p2wpkhTransaction -// transaction.header.isMine = true -// transaction.outputs[0].publicKeyPath = TestData.pubKey().path -// -// do { -// try transactionProcessor.processCreated(transaction: transaction) -// XCTFail("Expecting error") -// } catch _ as BloomFilterManager.BloomFilterExpired { -// } catch { -// XCTFail("Unexpected error") -// } -// } -// -// func testProcessReceived_TransactionExists() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.status = .new -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) -// } -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// -// verify(mockOutputExtractor, never()).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) -// verify(mockStorage).update(transaction: equal(to: transaction.header)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, nil) -// XCTAssertEqual(transaction.header.order, 0) -// } -// -// func testProcessReceived_SeveralMempoolTransactions() { -// let transactions = self.transactions() -// for transaction in transactions { -// transaction.header.isMine = true -// transaction.header.timestamp = 0 -// transaction.header.order = 0 -// } -// transactions[1].header.status = .new -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) -// when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) -// } -// -// try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: nil, skipCheckBloomFilter: false) -// -// verify(mockStorage).add(transaction: equal(to: transactions[0])) -// verify(mockStorage).update(transaction: equal(to: transactions[1].header)) -// verify(mockStorage).add(transaction: equal(to: transactions[2])) -// verify(mockStorage).update(transaction: equal(to: transactions[3].header)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: nil)) -// -// for (i, transaction) in transactions.enumerated() { -// XCTAssertEqual(transaction.header.blockHash, nil) -// XCTAssertEqual(transaction.header.status, .relayed) -// XCTAssertEqual(transaction.header.order, i) -// XCTAssertEqual(transaction.header.timestamp, Int(generatedDate.timeIntervalSince1970)) -// } -// } -// -// func testProcessReceivedMempool_After_Block_TransactionExists() { -// let transaction = TestData.p2pkhTransaction -// let block = TestData.firstBlock -// transaction.header.status = .new -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) -// } -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) -// verify(mockStorage).update(transaction: equal(to: transaction.header)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) -// -// reset(mockStorage, mockBlockchainDataListener) -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) -// } -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// -// verify(mockStorage, never()).update(transaction: equal(to: transaction.header)) -// verify(mockBlockchainDataListener, never()).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, block.headerHash) -// XCTAssertEqual(transaction.header.timestamp, block.timestamp) -// XCTAssertEqual(transaction.header.order, 0) -// -// } -// -// func testProcessReceivedBlock_After_Block_TransactionExists() { -// let transaction = TestData.p2pkhTransaction -// let block = TestData.firstBlock -// let nextBlock = TestData.secondBlock -// transaction.header.status = .new -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) -// } -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) -// verify(mockStorage).update(transaction: equal(to: transaction.header)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) -// -// reset(mockStorage, mockBlockchainDataListener) -// stub(mockStorage) { mock in -// when(mock.update(transaction: any())).thenDoNothing() -// when(mock.update(block: any())).thenDoNothing() -// when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) -// } -// stub(mockBlockchainDataListener) { mock in -// when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() -// } -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nextBlock, skipCheckBloomFilter: false) -// -// verify(mockStorage).update(transaction: equal(to: transaction.header)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nextBlock)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, nextBlock.headerHash) -// XCTAssertEqual(transaction.header.timestamp, nextBlock.timestamp) -// XCTAssertEqual(transaction.header.order, 0) -// } -// -// -// func testProcessReceived_SeveralTransactionsInBlock() { -// let transactions = self.transactions() -// let block = TestData.firstBlock -// -// for transaction in transactions { -// transaction.header.isMine = true -// transaction.header.timestamp = 0 -// transaction.header.order = 0 -// } -// transactions[1].header.status = .new -// -// stub(mockStorage) { mock in -// when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) -// when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) -// } -// -// try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: block, skipCheckBloomFilter: false) -// -// verify(mockStorage).add(transaction: equal(to: transactions[0])) -// verify(mockStorage).update(transaction: equal(to: transactions[1].header)) -// verify(mockStorage).add(transaction: equal(to: transactions[2])) -// verify(mockStorage).update(transaction: equal(to: transactions[3].header)) -// verify(mockStorage).update(block: equal(to: block)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: block)) -// -// for (i, transaction) in transactions.enumerated() { -// XCTAssertEqual(transaction.header.blockHash, block.headerHash) -// XCTAssertEqual(transaction.header.status, .relayed) -// XCTAssertEqual(transaction.header.order, i) -// XCTAssertEqual(transaction.header.timestamp, block.header.timestamp) -// } -// } -// -// func testProcessReceived_TransactionNotExists_Mine() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.isMine = true -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) -// verify(mockStorage).add(transaction: equal(to: transaction)) -// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) -// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, nil) -// } -// -// func testProcessReceived_TransactionNotExists_NotMine() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.isMine = false -// -// try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockStorage, never()).add(transaction: any()) -// verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) -// verifyNoMoreInteractions(mockBlockchainDataListener) -// verifyNoMoreInteractions(mockOutputAddressExtractor) -// verifyNoMoreInteractions(mockInputExtractor) -// } -// -// func testProcessReceived_TransactionNotExists_Mine_GapShifts() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.isMine = true -// -// stub(mockAddressManager) { mock in -// when(mock.gapShifts()).thenReturn(true) -// } -// -// do { -// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// XCTFail("Should throw exception") -// } catch _ as BloomFilterManager.BloomFilterExpired { -// } catch { -// XCTFail("Unknown error thrown") -// } -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) -// verify(mockStorage).add(transaction: equal(to: transaction)) -// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) -// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, nil) -// } -// -// func testProcessReceived_TransactionNotExists_Mine_OutputsExpireBloomFiler() { -// let transaction = TestData.p2wpkhTransaction -// transaction.header.isMine = true -// transaction.outputs[0].publicKeyPath = TestData.pubKey().path -// -// do { -// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// XCTFail("Should throw exception") -// } catch _ as BloomFilterManager.BloomFilterExpired { -// } catch { -// XCTFail("Unknown error thrown") -// } -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) -// verify(mockStorage).add(transaction: equal(to: transaction)) -// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) -// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, nil) -// } -// -// func testProcessReceived_TransactionNotExists_Mine_GapShifts_CheckBloomFilterFalse() { -// let transaction = TestData.p2wpkhTransaction -// transaction.header.isMine = true -// transaction.outputs[0].publicKeyPath = TestData.pubKey().path -// -// do { -// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: true) -// } catch { -// XCTFail("Unknown error thrown") -// } -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) -// verify(mockStorage).add(transaction: equal(to: transaction)) -// verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) -// verify(mockInputExtractor).extract(transaction: equal(to: transaction)) -// -// XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) -// XCTAssertEqual(transaction.header.blockHash, nil) -// } -// -// func testProcessReceived_TransactionNotExists_NotMine_GapShifts() { -// let transaction = TestData.p2pkhTransaction -// transaction.header.isMine = false -// -// stub(mockAddressManager) { mock in -// when(mock.gapShifts()).thenReturn(true) -// } -// -// do { -// try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) -// } catch { -// XCTFail("Shouldn't throw exception") -// } -// -// verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) -// verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) -// verify(mockStorage, never()).add(transaction: any()) -// verifyNoMoreInteractions(mockBlockchainDataListener) -// verifyNoMoreInteractions(mockOutputAddressExtractor) -// verifyNoMoreInteractions(mockInputExtractor) -// } -// -// func testProcessReceived_TransactionNotInTopologicalOrder() { -// let transactions = self.transactions() -// var calledTransactions = [FullTransaction]() -// -// stub(mockOutputExtractor) { mock in -// when(mock.extract(transaction: any())).then { transaction in -// calledTransactions.append(transaction) -// } -// } -// -// for i in 0..<4 { -// for j in 0..<4 { -// for k in 0..<4 { -// for l in 0..<4 { -// if [0, 1, 2, 3].contains(where: { $0 != i && $0 != j && $0 != k && $0 != l }) { -// continue -// } -// -// calledTransactions = [] -// -// try! transactionProcessor.processReceived(transactions: [transactions[i], transactions[j], transactions[k], transactions[l]], inBlock: nil, skipCheckBloomFilter: false) -// -// verifyNoMoreInteractions(mockBlockchainDataListener) -// -// for (m, transaction) in calledTransactions.enumerated() { -// XCTAssertEqual(transaction.header.dataHash, transactions[m].header.dataHash) -// } -// } -// } -// } -// } -// } -// -// -// private func transactions() -> [FullTransaction] { -// let transaction = FullTransaction( -// header: Transaction(version: 0, lockTime: 0), -// inputs: [ -// Input( -// withPreviousOutputTxHash: Data(from: 1), -// previousOutputIndex: 0, -// script: Data(from: 999999999999), -// sequence: 0 -// ) -// ], -// outputs: [ -// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) -// ] -// ) -// -// let transaction2 = FullTransaction( -// header: Transaction(version: 0, lockTime: 0), -// inputs: [ -// Input( -// withPreviousOutputTxHash: transaction.header.dataHash, -// previousOutputIndex: 0, -// script: Data(from: 999999999999), -// sequence: 0 -// ) -// ], -// outputs: [ -// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), -// Output(withValue: 0, index: 1, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) -// ] -// ) -// -// let transaction3 = FullTransaction( -// header: Transaction(version: 0, lockTime: 0), -// inputs: [ -// Input( -// withPreviousOutputTxHash: transaction2.header.dataHash, -// previousOutputIndex: 0, -// script: Data(from: 999999999999), -// sequence: 0 -// ) -// ], -// outputs: [ -// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), -// ] -// ) -// -// let transaction4 = FullTransaction( -// header: Transaction(version: 0, lockTime: 0), -// inputs: [ -// Input( -// withPreviousOutputTxHash: transaction2.header.dataHash, -// previousOutputIndex: 1, -// script: Data(from: 999999999999), -// sequence: 0 -// ), -// Input( -// withPreviousOutputTxHash: transaction3.header.dataHash, -// previousOutputIndex: 0, -// script: Data(from: 999999999999), -// sequence: 0 -// ) -// ], -// outputs: [ -// Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) -// ] -// ) -// -// return [transaction, transaction2, transaction3, transaction4] -// } -// -//} +import XCTest +import Cuckoo +@testable import BitcoinCore + +class TransactionProcessorTests: XCTestCase { + private var mockStorage: MockIStorage! + private var mockOutputExtractor: MockITransactionExtractor! + private var mockOutputAddressExtractor: MockITransactionOutputAddressExtractor! + private var mockInputExtractor: MockITransactionExtractor! + private var mockOutputsCache: MockIOutputsCache! + private var mockAddressManager: MockIPublicKeyManager! + private var mockBlockchainDataListener: MockIBlockchainDataListener! + private var mockTransactionListener: MockITransactionListener! + private var mockIrregularOutputFinder: MockIIrregularOutputFinder! + + private var generatedDate: Date! + private var dateGenerator: (() -> Date)! + + private var transactionProcessor: TransactionProcessor! + + override func setUp() { + super.setUp() + + generatedDate = Date() + dateGenerator = { + return self.generatedDate + } + + mockStorage = MockIStorage() + mockOutputExtractor = MockITransactionExtractor() + mockOutputAddressExtractor = MockITransactionOutputAddressExtractor() + mockInputExtractor = MockITransactionExtractor() + mockOutputsCache = MockIOutputsCache() + mockAddressManager = MockIPublicKeyManager() + mockBlockchainDataListener = MockIBlockchainDataListener() + mockTransactionListener = MockITransactionListener() + mockIrregularOutputFinder = MockIIrregularOutputFinder() + + stub(mockStorage) { mock in + when(mock.transaction(byHash: any())).thenReturn(nil) + when(mock.add(transaction: any())).thenDoNothing() + when(mock.update(transaction: any())).thenDoNothing() + when(mock.update(block: any())).thenDoNothing() + } + stub(mockOutputsCache) { mock in + when(mock.add(fromOutputs: any())).thenDoNothing() + when(mock.hasOutputs(forInputs: any())).thenReturn(false) + } + stub(mockOutputExtractor) { mock in + when(mock.extract(transaction: any())).thenDoNothing() + } + stub(mockOutputAddressExtractor) { mock in + when(mock.extractOutputAddresses(transaction: any())).thenDoNothing() + } + stub(mockInputExtractor) { mock in + when(mock.extract(transaction: any())).thenDoNothing() + } + stub(mockAddressManager) { mock in + when(mock.gapShifts()).thenReturn(false) + } + stub(mockBlockchainDataListener) { mock in + when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() + when(mock.onDelete(transactionHashes: any())).thenDoNothing() + when(mock.onInsert(block: any())).thenDoNothing() + } + stub(mockTransactionListener) { mock in + when(mock.onReceive(transaction: any())).thenDoNothing() + } + stub(mockIrregularOutputFinder) { mock in + when(mock.hasIrregularOutput(outputs: any())).thenReturn(false) + } + + transactionProcessor = TransactionProcessor( + storage: mockStorage, outputExtractor: mockOutputExtractor, inputExtractor: mockInputExtractor, outputsCache: mockOutputsCache, + outputAddressExtractor: mockOutputAddressExtractor, addressManager: mockAddressManager, irregularOutputFinder: mockIrregularOutputFinder, + listener: mockBlockchainDataListener, dateGenerator: dateGenerator + ) + transactionProcessor.transactionListener = mockTransactionListener + } + + override func tearDown() { + mockStorage = nil + mockOutputExtractor = nil + mockInputExtractor = nil + mockOutputsCache = nil + transactionProcessor = nil + mockBlockchainDataListener = nil + mockIrregularOutputFinder = nil + + generatedDate = nil + dateGenerator = nil + + super.tearDown() + } + + func testProcessCreated() { + let transaction = TestData.p2pkhTransaction + + try! transactionProcessor.processCreated(transaction: transaction) + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [Transaction]()), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) + verify(mockStorage).add(transaction: equal(to: transaction)) + verifyNoMoreInteractions(mockOutputAddressExtractor) + verifyNoMoreInteractions(mockInputExtractor) + } + + func testProcessCreated_isMine() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = true + + try! transactionProcessor.processCreated(transaction: transaction) + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) + verify(mockStorage).add(transaction: equal(to: transaction)) + verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) + verify(mockInputExtractor).extract(transaction: equal(to: transaction)) + } + + func testProcessCreated_TransactionExists() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = true + + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + + do { + try transactionProcessor.processCreated(transaction: transaction) + XCTFail("Expecting error") + } catch let error as TransactionCreator.CreationError { + XCTAssertEqual(error, TransactionCreator.CreationError.transactionAlreadyExists) + } catch { + XCTFail("Unexpected error") + } + + verify(mockOutputExtractor, never()).extract(transaction: any()) + verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) + verify(mockBlockchainDataListener, never()).onUpdate(updated: any(), inserted: any(), inBlock: any()) + verify(mockStorage, never()).add(transaction: any()) + verify(mockOutputAddressExtractor, never()).extractOutputAddresses(transaction: any()) + verify(mockInputExtractor, never()).extract(transaction: any()) + } + + func testProcessCreated_HasIrregularOutput() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = true + + stub(mockIrregularOutputFinder) { mock in + when(mock.hasIrregularOutput(outputs: equal(to: transaction.outputs))).thenReturn(true) + } + + do { + try transactionProcessor.processCreated(transaction: transaction) + XCTFail("Expecting error") + } catch _ as BloomFilterManager.BloomFilterExpired { + } catch { + XCTFail("Unexpected error") + } + } + + func testProcessReceived_TransactionExists() { + let transaction = TestData.p2pkhTransaction + transaction.header.status = .new + + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + + verify(mockOutputExtractor, never()).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache, never()).hasOutputs(forInputs: any()) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) + verify(mockStorage).update(transaction: equal(to: transaction.header)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, nil) + XCTAssertEqual(transaction.header.order, 0) + } + + func testProcessReceived_SeveralMempoolTransactions() { + let transactions = self.transactions() + for transaction in transactions { + transaction.header.isMine = true + transaction.header.timestamp = 0 + transaction.header.order = 0 + } + transactions[1].header.status = .new + + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) + when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) + } + + try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: nil, skipCheckBloomFilter: false) + + verify(mockStorage).add(transaction: equal(to: transactions[0])) + verify(mockStorage).update(transaction: equal(to: transactions[1].header)) + verify(mockStorage).add(transaction: equal(to: transactions[2])) + verify(mockStorage).update(transaction: equal(to: transactions[3].header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: nil)) + + for (i, transaction) in transactions.enumerated() { + XCTAssertEqual(transaction.header.blockHash, nil) + XCTAssertEqual(transaction.header.status, .relayed) + XCTAssertEqual(transaction.header.order, i) + XCTAssertEqual(transaction.header.timestamp, Int(generatedDate.timeIntervalSince1970)) + } + } + + func testProcessReceivedMempool_After_Block_TransactionExists() { + let transaction = TestData.p2pkhTransaction + let block = TestData.firstBlock + transaction.header.status = .new + + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) + verify(mockStorage).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) + + reset(mockStorage, mockBlockchainDataListener) + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + + verify(mockStorage, never()).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener, never()).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nil)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, block.headerHash) + XCTAssertEqual(transaction.header.timestamp, block.timestamp) + XCTAssertEqual(transaction.header.order, 0) + + } + + func testProcessReceivedBlock_After_Block_TransactionExists() { + let transaction = TestData.p2pkhTransaction + let block = TestData.firstBlock + let nextBlock = TestData.secondBlock + transaction.header.status = .new + + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: block, skipCheckBloomFilter: false) + verify(mockStorage).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: block)) + + reset(mockStorage, mockBlockchainDataListener) + stub(mockStorage) { mock in + when(mock.update(transaction: any())).thenDoNothing() + when(mock.update(block: any())).thenDoNothing() + when(mock.transaction(byHash: equal(to: transaction.header.dataHash))).thenReturn(transaction.header) + } + stub(mockBlockchainDataListener) { mock in + when(mock.onUpdate(updated: any(), inserted: any(), inBlock: any())).thenDoNothing() + } + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nextBlock, skipCheckBloomFilter: false) + + verify(mockStorage).update(transaction: equal(to: transaction.header)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transaction.header]), inserted: equal(to: []), inBlock: equal(to: nextBlock)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, nextBlock.headerHash) + XCTAssertEqual(transaction.header.timestamp, nextBlock.timestamp) + XCTAssertEqual(transaction.header.order, 0) + } + + + func testProcessReceived_SeveralTransactionsInBlock() { + let transactions = self.transactions() + let block = TestData.firstBlock + + for transaction in transactions { + transaction.header.isMine = true + transaction.header.timestamp = 0 + transaction.header.order = 0 + } + transactions[1].header.status = .new + + stub(mockStorage) { mock in + when(mock.transaction(byHash: equal(to: transactions[1].header.dataHash))).thenReturn(transactions[1].header) + when(mock.transaction(byHash: equal(to: transactions[3].header.dataHash))).thenReturn(transactions[3].header) + } + + try! transactionProcessor.processReceived(transactions: [transactions[3], transactions[1], transactions[2], transactions[0]], inBlock: block, skipCheckBloomFilter: false) + + verify(mockStorage).add(transaction: equal(to: transactions[0])) + verify(mockStorage).update(transaction: equal(to: transactions[1].header)) + verify(mockStorage).add(transaction: equal(to: transactions[2])) + verify(mockStorage).update(transaction: equal(to: transactions[3].header)) + verify(mockStorage).update(block: equal(to: block)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: [transactions[1].header, transactions[3].header]), inserted: equal(to: [transactions[0].header, transactions[2].header]), inBlock: equal(to: block)) + + for (i, transaction) in transactions.enumerated() { + XCTAssertEqual(transaction.header.blockHash, block.headerHash) + XCTAssertEqual(transaction.header.status, .relayed) + XCTAssertEqual(transaction.header.order, i) + XCTAssertEqual(transaction.header.timestamp, block.header.timestamp) + } + } + + func testProcessReceived_TransactionNotExists_Mine() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = true + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) + verify(mockStorage).add(transaction: equal(to: transaction)) + verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) + verify(mockInputExtractor).extract(transaction: equal(to: transaction)) + verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, nil) + } + + func testProcessReceived_TransactionNotExists_NotMine() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = false + + try! transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockStorage, never()).add(transaction: any()) + verify(mockTransactionListener).onReceive(transaction: equal(to: transaction)) + verifyNoMoreInteractions(mockBlockchainDataListener) + verifyNoMoreInteractions(mockOutputAddressExtractor) + verifyNoMoreInteractions(mockInputExtractor) + } + + func testProcessReceived_TransactionNotExists_Mine_GapShifts() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = true + + stub(mockAddressManager) { mock in + when(mock.gapShifts()).thenReturn(true) + } + + do { + try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + XCTFail("Should throw exception") + } catch _ as BloomFilterManager.BloomFilterExpired { + } catch { + XCTFail("Unknown error thrown") + } + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) + verify(mockStorage).add(transaction: equal(to: transaction)) + verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) + verify(mockInputExtractor).extract(transaction: equal(to: transaction)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, nil) + } + + func testProcessReceived_TransactionNotExists_Mine_HasIrregularOutputs() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = true + + stub(mockIrregularOutputFinder) { mock in + when(mock.hasIrregularOutput(outputs: equal(to: transaction.outputs))).thenReturn(true) + } + + do { + try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + XCTFail("Should throw exception") + } catch _ as BloomFilterManager.BloomFilterExpired { + } catch { + XCTFail("Unknown error thrown") + } + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) + verify(mockStorage).add(transaction: equal(to: transaction)) + verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) + verify(mockInputExtractor).extract(transaction: equal(to: transaction)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, nil) + } + + func testProcessReceived_TransactionNotExists_Mine_GapShifts_CheckBloomFilterFalse() { + let transaction = TestData.p2wpkhTransaction + transaction.header.isMine = true + transaction.outputs[0].publicKeyPath = TestData.pubKey().path + + do { + try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: true) + } catch { + XCTFail("Unknown error thrown") + } + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockBlockchainDataListener).onUpdate(updated: equal(to: []), inserted: equal(to: [transaction.header]), inBlock: equal(to: nil)) + verify(mockStorage).add(transaction: equal(to: transaction)) + verify(mockOutputAddressExtractor).extractOutputAddresses(transaction: equal(to: transaction)) + verify(mockInputExtractor).extract(transaction: equal(to: transaction)) + + XCTAssertEqual(transaction.header.status, TransactionStatus.relayed) + XCTAssertEqual(transaction.header.blockHash, nil) + } + + func testProcessReceived_TransactionNotExists_NotMine_GapShifts() { + let transaction = TestData.p2pkhTransaction + transaction.header.isMine = false + + stub(mockAddressManager) { mock in + when(mock.gapShifts()).thenReturn(true) + } + + do { + try transactionProcessor.processReceived(transactions: [transaction], inBlock: nil, skipCheckBloomFilter: false) + } catch { + XCTFail("Shouldn't throw exception") + } + + verify(mockOutputExtractor).extract(transaction: equal(to: transaction)) + verify(mockOutputsCache).hasOutputs(forInputs: equal(to: transaction.inputs)) + verify(mockStorage, never()).add(transaction: any()) + verifyNoMoreInteractions(mockBlockchainDataListener) + verifyNoMoreInteractions(mockOutputAddressExtractor) + verifyNoMoreInteractions(mockInputExtractor) + } + + func testProcessReceived_TransactionNotInTopologicalOrder() { + let transactions = self.transactions() + var calledTransactions = [FullTransaction]() + + stub(mockOutputExtractor) { mock in + when(mock.extract(transaction: any())).then { transaction in + calledTransactions.append(transaction) + } + } + + for i in 0..<4 { + for j in 0..<4 { + for k in 0..<4 { + for l in 0..<4 { + if [0, 1, 2, 3].contains(where: { $0 != i && $0 != j && $0 != k && $0 != l }) { + continue + } + + calledTransactions = [] + + try! transactionProcessor.processReceived(transactions: [transactions[i], transactions[j], transactions[k], transactions[l]], inBlock: nil, skipCheckBloomFilter: false) + + verifyNoMoreInteractions(mockBlockchainDataListener) + + for (m, transaction) in calledTransactions.enumerated() { + XCTAssertEqual(transaction.header.dataHash, transactions[m].header.dataHash) + } + } + } + } + } + } + + + private func transactions() -> [FullTransaction] { + let transaction = FullTransaction( + header: Transaction(version: 0, lockTime: 0), + inputs: [ + Input( + withPreviousOutputTxHash: Data(from: 1), + previousOutputIndex: 0, + script: Data(from: 999999999999), + sequence: 0 + ) + ], + outputs: [ + Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) + ] + ) + + let transaction2 = FullTransaction( + header: Transaction(version: 0, lockTime: 0), + inputs: [ + Input( + withPreviousOutputTxHash: transaction.header.dataHash, + previousOutputIndex: 0, + script: Data(from: 999999999999), + sequence: 0 + ) + ], + outputs: [ + Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), + Output(withValue: 0, index: 1, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) + ] + ) + + let transaction3 = FullTransaction( + header: Transaction(version: 0, lockTime: 0), + inputs: [ + Input( + withPreviousOutputTxHash: transaction2.header.dataHash, + previousOutputIndex: 0, + script: Data(from: 999999999999), + sequence: 0 + ) + ], + outputs: [ + Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()), + ] + ) + + let transaction4 = FullTransaction( + header: Transaction(version: 0, lockTime: 0), + inputs: [ + Input( + withPreviousOutputTxHash: transaction2.header.dataHash, + previousOutputIndex: 1, + script: Data(from: 999999999999), + sequence: 0 + ), + Input( + withPreviousOutputTxHash: transaction3.header.dataHash, + previousOutputIndex: 0, + script: Data(from: 999999999999), + sequence: 0 + ) + ], + outputs: [ + Output(withValue: 0, index: 0, lockingScript: Data(hex: "9999999999")!, type: .p2pk, keyHash: Data()) + ] + ) + + return [transaction, transaction2, transaction3, transaction4] + } + +} diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift index 0afd26c5..2cc5e3f0 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionSyncerTests.swift @@ -1,247 +1,240 @@ -//import XCTest -//import Quick -//import Nimble -//import Cuckoo -//@testable import BitcoinCore -// -//class TransactionSyncerTests: QuickSpec { -// override func spec() { -// let mockStorage = MockIStorage() -// let mockTransactionProcessor = MockITransactionProcessor() -// let mockAddressManager = MockIPublicKeyManager() -// let mockBloomFilterManager = MockIBloomFilterManager() -// let maxRetriesCount = 3 -// let retriesPeriod: Double = 60 -// let totalRetriesPeriod: Double = 60 * 60 * 24 -// -// var syncer: TransactionSyncer! -// -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.add(sentTransaction: any())).thenDoNothing() -// when(mock.update(sentTransaction: any())).thenDoNothing() -// } -// stub(mockTransactionProcessor) { mock in -// when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() -// } -// stub(mockAddressManager) { mock in -// when(mock.fillGap()).thenDoNothing() -// } -// stub(mockBloomFilterManager) { mock in -// when(mock.regenerateBloomFilter()).thenDoNothing() -// } -// -// syncer = TransactionSyncer( -// storage: mockStorage, processor: mockTransactionProcessor, publicKeyManager: mockAddressManager, bloomFilterManager: mockBloomFilterManager, -// maxRetriesCount: maxRetriesCount, retriesPeriod: retriesPeriod, totalRetriesPeriod: totalRetriesPeriod) -// } -// -// afterEach { -// reset(mockStorage, mockTransactionProcessor, mockAddressManager, mockBloomFilterManager) -// -// syncer = nil -// } -// -// describe("#pendingTransactions") { -// let fullTransaction = TestData.p2pkTransaction -// -// context("when transaction is .new") { -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.newTransactions()).thenReturn([fullTransaction.header]) -// } -// } -// -// context("when it wasn't sent") { -// it("returns transaction") { -// stub(mockStorage) { mock in -// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) -// when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) -// when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) -// } -// let transactions = syncer.pendingTransactions() -// -// expect(transactions.count).to(equal(1)) -// expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) -// } -// } -// -// context("when it was sent") { -// let sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) -// sentTransaction.lastSendTime = CACurrentMediaTime() - retriesPeriod - 1 -// sentTransaction.retriesCount = 0 -// beforeEach { -// stub(mockStorage) { mock in -// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) -// } -// } -// -// context("when sent not too many times or too frequently") { -// it("returns transaction") { -// stub(mockStorage) { mock in -// when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) -// when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) -// } -// -// let transactions = syncer.pendingTransactions() -// expect(transactions.count).to(equal(1)) -// expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) -// } -// } -// -// context("when sent too many times") { -// it("doesn't return transaction") { -// sentTransaction.retriesCount = maxRetriesCount -// expect(syncer.pendingTransactions()).to(beEmpty()) -// } -// } -// -// context("when sent too often") { -// it("doesn't return transaction") { -// sentTransaction.lastSendTime = CACurrentMediaTime() -// expect(syncer.pendingTransactions()).to(beEmpty()) -// } -// } -// -// context("when sent too often in totalRetriesPeriod period") { -// it("doesn't return transaction") { -// sentTransaction.firstSendTime = CACurrentMediaTime() - totalRetriesPeriod - 1 -// expect(syncer.pendingTransactions()).to(beEmpty()) -// } -// } -// } -// } -// -// context("when transaction is not new") { -// it("doesn't return transaction") { -// stub(mockStorage) { mock in -// when(mock.newTransactions()).thenReturn([]) -// } -// expect(syncer.pendingTransactions()).to(beEmpty()) -// } -// } -// } -// -// describe("#handle(sentTransaction:)") { -// let fullTransaction = TestData.p2pkhTransaction -// -// context("when SentTransaction does not exist") { -// it("adds new SentTransaction object") { -// stub(mockStorage) { mock in -// when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) -// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) -// } -// -// syncer.handle(sentTransaction: fullTransaction) -// -// let argumentCaptor = ArgumentCaptor() -// verify(mockStorage).add(sentTransaction: argumentCaptor.capture()) -// let sentTransaction = argumentCaptor.value! -// -// expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) -// } -// } -// -// context("when SentTransaction exists") { -// var sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) -// sentTransaction.firstSendTime = sentTransaction.firstSendTime - 100 -// sentTransaction.lastSendTime = sentTransaction.lastSendTime - 100 -// -// it("updates existing SentTransaction object") { -// stub(mockStorage) { mock in -// when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) -// when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) -// } -// -// syncer.handle(sentTransaction: fullTransaction) -// -// let argumentCaptor = ArgumentCaptor() -// verify(mockStorage).update(sentTransaction: argumentCaptor.capture()) -// sentTransaction = argumentCaptor.value! -// -// expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) -// } -// } -// -// context("when Transaction doesn't exist") { -// it("neither adds new nor updates existing") { -// stub(mockStorage) { mock in -// when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) -// } -// -// syncer.handle(sentTransaction: fullTransaction) -// -// verify(mockStorage, never()).add(sentTransaction: any()) -// verify(mockStorage, never()).update(sentTransaction: any()) -// } -// } -// } -// -// describe("#handle(transactions:)") { -// context("when empty array is given") { -// it("doesn't do anything") { -// syncer.handle(transactions: []) -// -// verify(mockTransactionProcessor, never()).processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any()) -// verify(mockAddressManager, never()).fillGap() -// verify(mockBloomFilterManager, never()).regenerateBloomFilter() -// } -// } -// -// context("when not empty array is given") { -// let transactions = [TestData.p2pkhTransaction] -// -// context("when need to update bloom filter") { -// it("fills addresses gap and regenerates bloom filter") { -// stub(mockTransactionProcessor) { mock in -// when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) -// } -// -// syncer.handle(transactions: transactions) -// verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) -// verify(mockAddressManager).fillGap() -// verify(mockBloomFilterManager).regenerateBloomFilter() -// } -// } -// -// context("when don't need to update bloom filter") { -// it("doesn't run address fillGap and doesn't regenerate bloom filter") { -// stub(mockTransactionProcessor) { mock in -// when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: equal(to: false))).thenDoNothing() -// } -// -// syncer.handle(transactions: transactions) -// verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) -// verify(mockAddressManager, never()).fillGap() -// verify(mockBloomFilterManager, never()).regenerateBloomFilter() -// } -// } -// -// } -// } -// -// describe("#shouldRequestTransaction") { -// let fullTransaction = TestData.p2wpkhTransaction -// -// context("when relayed transaction exists") { -// it("returns false") { -// stub(mockStorage) { mock in -// when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(true) -// } -// -// XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), false) -// } -// } -// -// context("when relayed transaction doesn't exist") { -// it("returns true") { -// stub(mockStorage) { mock in -// when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(false) -// } -// -// XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), true) -// } -// } -// } -// } -//} +import XCTest +import Quick +import Nimble +import Cuckoo +@testable import BitcoinCore + +class TransactionSyncerTests: QuickSpec { + override func spec() { + let mockStorage = MockIStorage() + let mockTransactionProcessor = MockITransactionProcessor() + let mockAddressManager = MockIPublicKeyManager() + let maxRetriesCount = 3 + let retriesPeriod: Double = 60 + let totalRetriesPeriod: Double = 60 * 60 * 24 + + var syncer: TransactionSyncer! + + beforeEach { + stub(mockStorage) { mock in + when(mock.add(sentTransaction: any())).thenDoNothing() + when(mock.update(sentTransaction: any())).thenDoNothing() + } + stub(mockTransactionProcessor) { mock in + when(mock.processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any())).thenDoNothing() + } + stub(mockAddressManager) { mock in + when(mock.fillGap()).thenDoNothing() + } + + syncer = TransactionSyncer( + storage: mockStorage, processor: mockTransactionProcessor, publicKeyManager: mockAddressManager, + maxRetriesCount: maxRetriesCount, retriesPeriod: retriesPeriod, totalRetriesPeriod: totalRetriesPeriod) + } + + afterEach { + reset(mockStorage, mockTransactionProcessor, mockAddressManager) + + syncer = nil + } + + describe("#pendingTransactions") { + let fullTransaction = TestData.p2pkTransaction + + context("when transaction is .new") { + beforeEach { + stub(mockStorage) { mock in + when(mock.newTransactions()).thenReturn([fullTransaction.header]) + } + } + + context("when it wasn't sent") { + it("returns transaction") { + stub(mockStorage) { mock in + when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) + when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) + when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) + } + let transactions = syncer.pendingTransactions() + + expect(transactions.count).to(equal(1)) + expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) + } + } + + context("when it was sent") { + let sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) + sentTransaction.lastSendTime = CACurrentMediaTime() - retriesPeriod - 1 + sentTransaction.retriesCount = 0 + beforeEach { + stub(mockStorage) { mock in + when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) + } + } + + context("when sent not too many times or too frequently") { + it("returns transaction") { + stub(mockStorage) { mock in + when(mock.inputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.inputs) + when(mock.outputs(transactionHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.outputs) + } + + let transactions = syncer.pendingTransactions() + expect(transactions.count).to(equal(1)) + expect(transactions.first!.header.dataHash).to(equal(fullTransaction.header.dataHash)) + } + } + + context("when sent too many times") { + it("doesn't return transaction") { + sentTransaction.retriesCount = maxRetriesCount + expect(syncer.pendingTransactions()).to(beEmpty()) + } + } + + context("when sent too often") { + it("doesn't return transaction") { + sentTransaction.lastSendTime = CACurrentMediaTime() + expect(syncer.pendingTransactions()).to(beEmpty()) + } + } + + context("when sent too often in totalRetriesPeriod period") { + it("doesn't return transaction") { + sentTransaction.firstSendTime = CACurrentMediaTime() - totalRetriesPeriod - 1 + expect(syncer.pendingTransactions()).to(beEmpty()) + } + } + } + } + + context("when transaction is not new") { + it("doesn't return transaction") { + stub(mockStorage) { mock in + when(mock.newTransactions()).thenReturn([]) + } + expect(syncer.pendingTransactions()).to(beEmpty()) + } + } + } + + describe("#handle(sentTransaction:)") { + let fullTransaction = TestData.p2pkhTransaction + + context("when SentTransaction does not exist") { + it("adds new SentTransaction object") { + stub(mockStorage) { mock in + when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) + when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) + } + + syncer.handle(sentTransaction: fullTransaction) + + let argumentCaptor = ArgumentCaptor() + verify(mockStorage).add(sentTransaction: argumentCaptor.capture()) + let sentTransaction = argumentCaptor.value! + + expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) + } + } + + context("when SentTransaction exists") { + var sentTransaction = SentTransaction(dataHash: fullTransaction.header.dataHash) + sentTransaction.firstSendTime = sentTransaction.firstSendTime - 100 + sentTransaction.lastSendTime = sentTransaction.lastSendTime - 100 + + it("updates existing SentTransaction object") { + stub(mockStorage) { mock in + when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(fullTransaction.header) + when(mock.sentTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(sentTransaction) + } + + syncer.handle(sentTransaction: fullTransaction) + + let argumentCaptor = ArgumentCaptor() + verify(mockStorage).update(sentTransaction: argumentCaptor.capture()) + sentTransaction = argumentCaptor.value! + + expect(sentTransaction.dataHash).to(equal(fullTransaction.header.dataHash)) + } + } + + context("when Transaction doesn't exist") { + it("neither adds new nor updates existing") { + stub(mockStorage) { mock in + when(mock.newTransaction(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(nil) + } + + syncer.handle(sentTransaction: fullTransaction) + + verify(mockStorage, never()).add(sentTransaction: any()) + verify(mockStorage, never()).update(sentTransaction: any()) + } + } + } + + describe("#handle(transactions:)") { + context("when empty array is given") { + it("doesn't do anything") { + syncer.handle(transactions: []) + + verify(mockTransactionProcessor, never()).processReceived(transactions: any(), inBlock: any(), skipCheckBloomFilter: any()) + verify(mockAddressManager, never()).fillGap() + } + } + + context("when not empty array is given") { + let transactions = [TestData.p2pkhTransaction] + + context("when need to update bloom filter") { + it("fills addresses gap and regenerates bloom filter") { + stub(mockTransactionProcessor) { mock in + when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: any())).thenThrow(BloomFilterManager.BloomFilterExpired()) + } + + syncer.handle(transactions: transactions) + verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) + verify(mockAddressManager).fillGap() + } + } + + context("when don't need to update bloom filter") { + it("doesn't run address fillGap and doesn't regenerate bloom filter") { + stub(mockTransactionProcessor) { mock in + when(mock.processReceived(transactions: equal(to: transactions), inBlock: any(), skipCheckBloomFilter: equal(to: false))).thenDoNothing() + } + + syncer.handle(transactions: transactions) + verify(mockTransactionProcessor).processReceived(transactions: equal(to: transactions), inBlock: equal(to: nil), skipCheckBloomFilter: equal(to: false)) + verify(mockAddressManager, never()).fillGap() + } + } + + } + } + + describe("#shouldRequestTransaction") { + let fullTransaction = TestData.p2wpkhTransaction + + context("when relayed transaction exists") { + it("returns false") { + stub(mockStorage) { mock in + when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(true) + } + + XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), false) + } + } + + context("when relayed transaction doesn't exist") { + it("returns true") { + stub(mockStorage) { mock in + when(mock.relayedTransactionExists(byHash: equal(to: fullTransaction.header.dataHash))).thenReturn(false) + } + + XCTAssertEqual(syncer.shouldRequestTransaction(hash: fullTransaction.header.dataHash), true) + } + } + } + } +} From 9ae9dc2a9938de08937f04b9c40a5ee8d6009457 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 12 Sep 2019 10:50:06 +0600 Subject: [PATCH 029/234] Increase pod version --- BitcoinCashKit.swift.podspec | 4 ++-- BitcoinCore.swift.podspec | 4 ++-- BitcoinKit.swift.podspec | 4 ++-- DashKit.swift.podspec | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/BitcoinCashKit.swift.podspec b/BitcoinCashKit.swift.podspec index 51aba3e1..6a318979 100644 --- a/BitcoinCashKit.swift.podspec +++ b/BitcoinCashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCashKit.swift' spec.module_name = "BitcoinCashKit" - spec.version = '0.7.1' + spec.version = '0.8.0' spec.summary = 'BitcoinCash library for Swift' spec.description = <<-DESC BitcoinCashKit implements BitcoinCash protocol in Swift. It is an implementation of the BitcoinCash SPV protocol written (almost) entirely in swift. @@ -20,7 +20,7 @@ Pod::Spec.new do |spec| spec.dependency 'BitcoinCore.swift', '~> 0.7.0' spec.dependency 'HSCryptoKit', '~> 1.4' - spec.dependency 'HSHDWalletKit', '~> 1.1' + spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'Alamofire', '~> 4.0' spec.dependency 'ObjectMapper', '~> 3.0' spec.dependency 'RxSwift', '~> 5.0' diff --git a/BitcoinCore.swift.podspec b/BitcoinCore.swift.podspec index 5d195f18..3cff1084 100644 --- a/BitcoinCore.swift.podspec +++ b/BitcoinCore.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinCore.swift' spec.module_name = "BitcoinCore" - spec.version = '0.7.1' + spec.version = '0.8.0' spec.summary = 'Core library Bitcoin derived wallets for Swift' spec.description = <<-DESC BitcoinCore implements Bitcoin core protocol in Swift. It is an implementation of the Bitcoin SPV protocol written (almost) entirely in swift. @@ -19,7 +19,7 @@ Pod::Spec.new do |spec| spec.swift_version = '5' spec.dependency 'HSCryptoKit', '~> 1.4' - spec.dependency 'HSHDWalletKit', '~> 1.1' + spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'Alamofire', '~> 4.0' spec.dependency 'ObjectMapper', '~> 3.0' spec.dependency 'RxSwift', '~> 5.0' diff --git a/BitcoinKit.swift.podspec b/BitcoinKit.swift.podspec index eb147e9d..284612ef 100644 --- a/BitcoinKit.swift.podspec +++ b/BitcoinKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'BitcoinKit.swift' spec.module_name = 'BitcoinKit' - spec.version = '0.7.1' + spec.version = '0.8.0' spec.summary = 'Bitcoin library for Swift' spec.description = <<-DESC BitcoinKit implements Bitcoin protocol in Swift. @@ -20,7 +20,7 @@ Pod::Spec.new do |spec| spec.dependency 'BitcoinCore.swift', '~> 0.7.0' spec.dependency 'HSCryptoKit', '~> 1.4' - spec.dependency 'HSHDWalletKit', '~> 1.1' + spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'Alamofire', '~> 4.0' spec.dependency 'ObjectMapper', '~> 3.0' spec.dependency 'RxSwift', '~> 5.0' diff --git a/DashKit.swift.podspec b/DashKit.swift.podspec index 07f9e420..375b4cd1 100644 --- a/DashKit.swift.podspec +++ b/DashKit.swift.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = 'DashKit.swift' spec.module_name = 'DashKit' - spec.version = '0.7.1' + spec.version = '0.8.0' spec.summary = 'Dash library for Swift' spec.description = <<-DESC DashKit implements Dash protocol in Swift. @@ -20,7 +20,7 @@ Pod::Spec.new do |spec| spec.dependency 'BitcoinCore.swift', '~> 0.7.0' spec.dependency 'HSCryptoKit', '~> 1.4' - spec.dependency 'HSHDWalletKit', '~> 1.1' + spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'CryptoBLS.swift', '~> 1.1' spec.dependency 'CryptoX11.swift', '~> 1.1' spec.dependency 'Alamofire', '~> 4.0' From d889d70c1b31f8a9f04cefb7f257404c3d075c58 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 12 Sep 2019 14:46:56 +0600 Subject: [PATCH 030/234] Update kits' BitcoinCore dependency version --- BitcoinCashKit.swift.podspec | 2 +- BitcoinKit.swift.podspec | 2 +- DashKit.swift.podspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BitcoinCashKit.swift.podspec b/BitcoinCashKit.swift.podspec index 6a318979..2eb391ca 100644 --- a/BitcoinCashKit.swift.podspec +++ b/BitcoinCashKit.swift.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.7.0' + spec.dependency 'BitcoinCore.swift', '~> 0.8.0' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'Alamofire', '~> 4.0' diff --git a/BitcoinKit.swift.podspec b/BitcoinKit.swift.podspec index 284612ef..27ed6865 100644 --- a/BitcoinKit.swift.podspec +++ b/BitcoinKit.swift.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.7.0' + spec.dependency 'BitcoinCore.swift', '~> 0.8.0' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'Alamofire', '~> 4.0' diff --git a/DashKit.swift.podspec b/DashKit.swift.podspec index 375b4cd1..11300d09 100644 --- a/DashKit.swift.podspec +++ b/DashKit.swift.podspec @@ -18,7 +18,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' spec.swift_version = '5' - spec.dependency 'BitcoinCore.swift', '~> 0.7.0' + spec.dependency 'BitcoinCore.swift', '~> 0.8.0' spec.dependency 'HSCryptoKit', '~> 1.4' spec.dependency 'HSHDWalletKit', '~> 1.2' spec.dependency 'CryptoBLS.swift', '~> 1.1' From 565f9fc4f0f386cff6b3955e41249fabd6109a91 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 12 Sep 2019 18:32:29 +0600 Subject: [PATCH 031/234] Fee calculator returns fee calculated by UnspentOutputSelector --- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- .../TransactionFeeCalculator.swift | 24 +++------ .../TransactionFeeCalculatorTests.swift | 49 +++++-------------- 3 files changed, 18 insertions(+), 57 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index cdd11a3e..075279ca 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -195,7 +195,7 @@ public class BitcoinCoreBuilder { let scriptBuilder = ScriptBuilderChain() let transactionSizeCalculator = TransactionSizeCalculator() let transactionBuilder = TransactionBuilder(inputSigner: inputSigner, scriptBuilder: scriptBuilder, factory: factory) - let transactionFeeCalculator = TransactionFeeCalculator(unspentOutputSelector: unspentOutputSelector, transactionSizeCalculator: transactionSizeCalculator, transactionBuilder: transactionBuilder) + let transactionFeeCalculator = TransactionFeeCalculator(unspentOutputSelector: unspentOutputSelector, transactionSizeCalculator: transactionSizeCalculator) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, transactionFeeCalculator: transactionFeeCalculator, bloomFilterManager: bloomFilterManager, addressConverter: addressConverter, publicKeyManager: publicKeyManager, bip: bip) diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift index ae4cfde7..606f5dab 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift @@ -2,36 +2,24 @@ class TransactionFeeCalculator { private let unspentOutputSelector: IUnspentOutputSelector private let transactionSizeCalculator: ITransactionSizeCalculator - private let transactionBuilder: ITransactionBuilder - init(unspentOutputSelector: IUnspentOutputSelector, transactionSizeCalculator: ITransactionSizeCalculator, transactionBuilder: ITransactionBuilder) { + init(unspentOutputSelector: IUnspentOutputSelector, transactionSizeCalculator: ITransactionSizeCalculator) { self.unspentOutputSelector = unspentOutputSelector self.transactionSizeCalculator = transactionSizeCalculator - self.transactionBuilder = transactionBuilder } } extension TransactionFeeCalculator: ITransactionFeeCalculator { - // :fee method returns the fee for the given amount - // If address given and it's valid, it returns the actual fee - // Otherwise, it returns the estimated fee func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int { + var outputScriptType = changeAddress.scriptType if let address = toAddress { - // Actual fee - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: address.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) - let transaction = try transactionBuilder.buildTransaction( - value: value, unspentOutputs: selectedOutputsInfo.unspentOutputs, fee: selectedOutputsInfo.fee, senderPay: senderPay, - toAddress: address, changeAddress: selectedOutputsInfo.addChangeOutput ? changeAddress : nil - ) - return TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate - } else { - // Estimated fee - // Default to .p2pkh address - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: changeAddress.scriptType, changeType: changeAddress.scriptType, senderPay: senderPay) - return selectedOutputsInfo.fee + outputScriptType = address.scriptType } + + let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: outputScriptType, changeType: changeAddress.scriptType, senderPay: senderPay) + return selectedOutputsInfo.fee } func feeWithUnspentOutputs(value: Int, feeRate: Int, toScriptType: ScriptType, changeScriptType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo { diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift index 0c6736e7..f8a8555e 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionFeeCalculatorTests.swift @@ -8,9 +8,8 @@ class TransactionFeeCalculatorTests: QuickSpec { override func spec() { let mockUnspentOutputSelector = MockIUnspentOutputSelector() let mockTransactionSizeCalculator = MockITransactionSizeCalculator() - let mockTransactionBuilder = MockITransactionBuilder() - let toAddress = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "toAddress") + let toAddress = SegWitAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), bech32: "toAddress", version: 0) let changeAddress = LegacyAddress(type: .pubKeyHash, keyHash: randomBytes(length: 32), base58: "changeAddress") let value = 100_000_000 let feeRate = 10 @@ -37,15 +36,12 @@ class TransactionFeeCalculatorTests: QuickSpec { stub(mockUnspentOutputSelector) { mock in when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(selectedOutputsInfo) } - stub(mockTransactionBuilder) { mock in - when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any())).thenReturn(transaction) - } - calculator = TransactionFeeCalculator(unspentOutputSelector: mockUnspentOutputSelector, transactionSizeCalculator: mockTransactionSizeCalculator, transactionBuilder: mockTransactionBuilder) + calculator = TransactionFeeCalculator(unspentOutputSelector: mockUnspentOutputSelector, transactionSizeCalculator: mockTransactionSizeCalculator) } afterEach() { - reset(mockUnspentOutputSelector, mockTransactionBuilder, mockTransactionSizeCalculator) + reset(mockUnspentOutputSelector, mockTransactionSizeCalculator) calculator = nil resultFee = nil selectedOutputsInfo = nil @@ -53,43 +49,20 @@ class TransactionFeeCalculatorTests: QuickSpec { describe("fee(for:feeRate:senderPay:toAddress:changeAddress:)") { context("when toAddress exists") { - context("when addChangeOutput is true") { - beforeEach() { - resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) - } - - it("selects unspent outputs with given parameters") { - verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: toAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) - } - - it("builds actual transaction and returns fee") { - verify(mockTransactionBuilder).buildTransaction(value: value, unspentOutputs: equal(to: unspentOutputs), fee: fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) - expect(resultFee).to(equal(TransactionSerializer.serialize(transaction: transaction, withoutWitness: true).count * feeRate)) - } - } - - context("when addChangeOutput is false") { - beforeEach() { - selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: fee, addChangeOutput: false) - stub(mockUnspentOutputSelector) { mock in - when(mock.select(value: any(), feeRate: any(), outputScriptType: any(), changeType: any(), senderPay: any())).thenReturn(selectedOutputsInfo) - } - - resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) - } + it("selects unspent outputs with toAddress scriptType") { + resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) - it("builds actual transaction without changeAddress") { - verify(mockTransactionBuilder).buildTransaction(value: value, unspentOutputs: equal(to: unspentOutputs), fee: fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(nil)) - } + verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: toAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) + expect(resultFee).to(equal(selectedOutputsInfo.fee)) } } context("when toAddress is nil") { - it("selects unspent outputs with given parameters and returns fee") { + it("selects unspent outputs with changeAddress scriptType") { resultFee = try! calculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: nil, changeAddress: changeAddress) - verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: toAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) - verify(mockTransactionBuilder, never()).buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any()) - expect(resultFee).to(equal(fee)) + + verify(mockUnspentOutputSelector).select(value: value, feeRate: feeRate, outputScriptType: equal(to: changeAddress.scriptType), changeType: equal(to: changeAddress.scriptType), senderPay: senderPay) + expect(resultFee).to(equal(selectedOutputsInfo.fee)) } } } From cba82bd1d304994f1bd90c9977da6a6289f9275e Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 16 Sep 2019 16:52:18 +0600 Subject: [PATCH 032/234] Don't call ISyncStateListener#syncFinished if peer hasn't sent all of it's blocks --- BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift b/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift index 49ef154a..935e4f79 100644 --- a/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift +++ b/BitcoinCore/BitcoinCore/Blocks/InitialBlockDownload.swift @@ -125,7 +125,13 @@ public class InitialBlockDownload { syncedPeers.append(peer) subject.onNext(.onPeerSynced(peer: peer)) - syncStateListener.syncFinished() + + if blockSyncer.localDownloadedBestBlockHeight >= peer.announcedLastBlockHeight { + // Some peers fail to send InventoryMessage within expected time + // and become 'synced' in InitialBlockDownload without sending all of their blocks. + // In such case, we shouldn't call 'syncFinished' + syncStateListener.syncFinished() + } } private func setPeerNotSynced(_ peer: IPeer) { From 9ae84639972114000fa942db22cd653c8c28e9e6 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 16 Sep 2019 18:11:36 +0600 Subject: [PATCH 033/234] Set last blocks height to transaction nLockTime field --- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 4 +- .../Builder/TransactionBuilder.swift | 8 +-- .../Transactions/TransactionCreator.swift | 8 ++- .../Builder/TransactionBuilderTests.swift | 25 ++++---- .../TransactionCreatorTests.swift | 60 ++++++++++++++++--- 6 files changed, 76 insertions(+), 31 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 075279ca..6c516e9b 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -198,7 +198,7 @@ public class BitcoinCoreBuilder { let transactionFeeCalculator = TransactionFeeCalculator(unspentOutputSelector: unspentOutputSelector, transactionSizeCalculator: transactionSizeCalculator) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, transactionFeeCalculator: transactionFeeCalculator, - bloomFilterManager: bloomFilterManager, addressConverter: addressConverter, publicKeyManager: publicKeyManager, bip: bip) + bloomFilterManager: bloomFilterManager, addressConverter: addressConverter, publicKeyManager: publicKeyManager, storage: storage, bip: bip) let syncManager = SyncManager(reachabilityManager: reachabilityManager, initialSyncer: initialSyncer, peerGroup: peerGroup) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index c866bbd0..44e8596e 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -346,8 +346,8 @@ public protocol ITransactionCreator { } protocol ITransactionBuilder { - func buildTransaction(value: Int, unspentOutputs: [UnspentOutput], fee: Int, senderPay: Bool, toAddress: Address, changeAddress: Address?) throws -> FullTransaction - func buildTransaction(from: UnspentOutput, to: Address, fee: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction + func buildTransaction(value: Int, unspentOutputs: [UnspentOutput], fee: Int, senderPay: Bool, toAddress: Address, changeAddress: Address?, lastBlockHeight: Int) throws -> FullTransaction + func buildTransaction(from: UnspentOutput, to: Address, fee: Int, lastBlockHeight: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionFeeCalculator { diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 5228dd33..502727f7 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -36,7 +36,7 @@ class TransactionBuilder { extension TransactionBuilder: ITransactionBuilder { - func buildTransaction(value: Int, unspentOutputs: [UnspentOutput], fee: Int, senderPay: Bool, toAddress: Address, changeAddress: Address?) throws -> FullTransaction { + func buildTransaction(value: Int, unspentOutputs: [UnspentOutput], fee: Int, senderPay: Bool, toAddress: Address, changeAddress: Address?, lastBlockHeight: Int) throws -> FullTransaction { if !senderPay { guard fee < value else { throw BuildError.feeMoreThanValue @@ -65,7 +65,7 @@ extension TransactionBuilder: ITransactionBuilder { } // Build transaction - let transaction = factory.transaction(version: 1, lockTime: 0) + let transaction = factory.transaction(version: 1, lockTime: lastBlockHeight) // Sign inputs for i in 0.. Data) throws -> FullTransaction { + func buildTransaction(from unspentOutput: UnspentOutput, to address: Address, fee: Int, lastBlockHeight: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { guard unspentOutput.output.scriptType == .p2sh else { throw BuildError.notSupportedScriptType } @@ -115,7 +115,7 @@ extension TransactionBuilder: ITransactionBuilder { let output = try self.output(withIndex: 0, address: address, value: receivedValue) // Build transaction - let transaction = factory.transaction(version: 1, lockTime: 0) + let transaction = factory.transaction(version: 1, lockTime: lastBlockHeight) // Sign inputs let sigScriptData = try inputSigner.sigScriptData(transaction: transaction, inputsToSign: [inputToSign], outputs: [output], index: 0) diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index 3137a3b5..a0d030b0 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -10,10 +10,11 @@ class TransactionCreator { private let bloomFilterManager: IBloomFilterManager private let addressConverter: IAddressConverter private let publicKeyManager: IPublicKeyManager + private let storage: IStorage private let bip: Bip init(transactionBuilder: ITransactionBuilder, transactionProcessor: ITransactionProcessor, transactionSender: ITransactionSender, transactionFeeCalculator: ITransactionFeeCalculator, - bloomFilterManager: IBloomFilterManager, addressConverter: IAddressConverter, publicKeyManager: IPublicKeyManager, bip: Bip) { + bloomFilterManager: IBloomFilterManager, addressConverter: IAddressConverter, publicKeyManager: IPublicKeyManager, storage: IStorage, bip: Bip) { self.transactionBuilder = transactionBuilder self.transactionProcessor = transactionProcessor self.transactionSender = transactionSender @@ -21,6 +22,7 @@ class TransactionCreator { self.bloomFilterManager = bloomFilterManager self.addressConverter = addressConverter self.publicKeyManager = publicKeyManager + self.storage = storage self.bip = bip } @@ -47,7 +49,7 @@ class TransactionCreator { let transaction = try transactionBuilder.buildTransaction( value: value, unspentOutputs: feeWithUnspentOutputs.unspentOutputs, fee: feeWithUnspentOutputs.fee, senderPay: senderPay, - toAddress: toAddress, changeAddress: changeAddress + toAddress: toAddress, changeAddress: changeAddress, lastBlockHeight: storage.lastBlock?.height ?? 0 ) try processAndSend(transaction: transaction) @@ -71,7 +73,7 @@ extension TransactionCreator: ITransactionCreator { func create(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { let toAddress = try addressConverter.convert(address: address) let fee = transactionFeeCalculator.fee(inputScriptType: unspentOutput.output.scriptType, outputScriptType: toAddress.scriptType, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) - let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddress, fee: fee, signatureScriptFunction: signatureScriptFunction) + let transaction = try transactionBuilder.buildTransaction(from: unspentOutput, to: toAddress, fee: fee, lastBlockHeight: storage.lastBlock?.height ?? 0, signatureScriptFunction: signatureScriptFunction) try processAndSend(transaction: transaction) return transaction diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift index d4f11dda..4b4b4aeb 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/Builder/TransactionBuilderTests.swift @@ -19,12 +19,13 @@ class TransactionBuilderTests: QuickSpec { let signatureData = [randomBytes(length: 72), randomBytes(length: 64)] let sendingValue = 100_000_000 let fee = 1000 + let lastBlockHeight = 1000 var builder: TransactionBuilder! beforeEach { stub(mockFactory) { mock in - when(mock).transaction(version: 1, lockTime: 0).thenReturn(Transaction(version: 1, lockTime: 0)) + when(mock).transaction(version: 1, lockTime: lastBlockHeight).thenReturn(Transaction(version: 1, lockTime: lastBlockHeight)) when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: toAddressPKH.keyHash), publicKey: isNil()).thenReturn(self.output(from: toAddressPKH)) when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: changeAddressPKH.keyHash), publicKey: isNil()).thenReturn(self.output(from: changeAddressPKH)) when(mock).output(withValue: any(), index: any(), lockingScript: any(), type: any(), address: any(), keyHash: equal(to: toAddressSH.keyHash), publicKey: isNil()).thenReturn(self.output(from: toAddressSH)) @@ -73,7 +74,7 @@ class TransactionBuilderTests: QuickSpec { context("when unspentOutput is P2PKH, senderPay is true, addChangeOutput is true") { beforeEach { - fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH, lastBlockHeight: lastBlockHeight) } it("adds input from unspentOutput") { @@ -116,7 +117,7 @@ class TransactionBuilderTests: QuickSpec { context("when changeAddress is nil") { beforeEach { - fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: nil) + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: nil, lastBlockHeight: lastBlockHeight) } it("adds 1 output for toAddress") { @@ -133,7 +134,7 @@ class TransactionBuilderTests: QuickSpec { context("when senderPay is false") { context("value is valid") { beforeEach { - fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: false, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: false, toAddress: toAddressPKH, changeAddress: changeAddressPKH, lastBlockHeight: lastBlockHeight) } it("subtracts fee from value in receiver output") { @@ -150,7 +151,7 @@ class TransactionBuilderTests: QuickSpec { context("value less than fee") { it("throws feeMoreThanValue exception") { do { - fullTransaction = try builder.buildTransaction(value: fee - 1, unspentOutputs: [unspentOutput], fee: fee, senderPay: false, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fullTransaction = try builder.buildTransaction(value: fee - 1, unspentOutputs: [unspentOutput], fee: fee, senderPay: false, toAddress: toAddressPKH, changeAddress: changeAddressPKH, lastBlockHeight: lastBlockHeight) fail("Expecting an exception") } catch let error as TransactionBuilder.BuildError { expect(error).to(equal(TransactionBuilder.BuildError.feeMoreThanValue)) @@ -163,7 +164,7 @@ class TransactionBuilderTests: QuickSpec { context("when toAddress and/or changeAddress types are P2SH or P2WPKH") { beforeEach { - fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressSH, changeAddress: changeAddressWPKH) + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressSH, changeAddress: changeAddressWPKH, lastBlockHeight: lastBlockHeight) } it("generates outputs considering address types") { @@ -188,7 +189,7 @@ class TransactionBuilderTests: QuickSpec { when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) } - fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH, lastBlockHeight: lastBlockHeight) } it("sets P2WPKH unlocking script to witnessData") { @@ -220,7 +221,7 @@ class TransactionBuilderTests: QuickSpec { when(mock).inputToSign(withPreviousOutput: equal(to: unspentOutput), script: any(), sequence: any()).thenReturn(inputToSign) } - fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fullTransaction = try! builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH, lastBlockHeight: lastBlockHeight) } it("sets P2WPKH unlocking script to witnessData") { @@ -254,7 +255,7 @@ class TransactionBuilderTests: QuickSpec { } do { - fullTransaction = try builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH) + fullTransaction = try builder.buildTransaction(value: sendingValue, unspentOutputs: [unspentOutput], fee: fee, senderPay: true, toAddress: toAddressPKH, changeAddress: changeAddressPKH, lastBlockHeight: lastBlockHeight) fail("Expecting an exception") } catch let error as TransactionBuilder.BuildError { expect(error).to(equal(TransactionBuilder.BuildError.notSupportedScriptType)) @@ -310,7 +311,7 @@ class TransactionBuilderTests: QuickSpec { context("when fee is valid, unspent output type is P2SH") { beforeEach { - fullTransaction = try! builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: fee, signatureScriptFunction: signatureScriptFunction) + fullTransaction = try! builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: fee, lastBlockHeight: lastBlockHeight, signatureScriptFunction: signatureScriptFunction) } @@ -347,7 +348,7 @@ class TransactionBuilderTests: QuickSpec { context("when fee is less than value") { it("throws feeMoreThanValue exception") { do { - fullTransaction = try builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: unspentOutput.output.value + 1, signatureScriptFunction: signatureScriptFunction) + fullTransaction = try builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: unspentOutput.output.value + 1, lastBlockHeight: lastBlockHeight, signatureScriptFunction: signatureScriptFunction) fail("Expecting an exception") } catch let error as TransactionBuilder.BuildError { expect(error).to(equal(TransactionBuilder.BuildError.feeMoreThanValue)) @@ -367,7 +368,7 @@ class TransactionBuilderTests: QuickSpec { ) do { - fullTransaction = try builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: fee, signatureScriptFunction: signatureScriptFunction) + fullTransaction = try builder.buildTransaction(from: unspentOutput, to: toAddressPKH, fee: fee, lastBlockHeight: lastBlockHeight, signatureScriptFunction: signatureScriptFunction) fail("Expecting an exception") } catch let error as TransactionBuilder.BuildError { expect(error).to(equal(TransactionBuilder.BuildError.notSupportedScriptType)) diff --git a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift index a7875e7c..e77da62b 100644 --- a/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift +++ b/BitcoinCore/BitcoinCoreTests/Transactions/TransactionCreatorTests.swift @@ -13,6 +13,7 @@ class TransactionCreatorTests: QuickSpec { let mockBloomFilterManager = MockIBloomFilterManager() let mockAddressConverter = MockIAddressConverter() let mockPublicKeyManager = MockIPublicKeyManager() + let mockStorage = MockIStorage() let transaction = TestData.p2pkhTransaction let changePublicKey = TestData.pubKey() @@ -24,6 +25,7 @@ class TransactionCreatorTests: QuickSpec { let feeRate = 1000 let senderPay = true let bip = Bip.bip44 + let lastBlock = TestData.firstBlock var unspentOutputs: [UnspentOutput]! var selectedOutputsInfo: SelectedUnspentOutputInfo! @@ -41,6 +43,9 @@ class TransactionCreatorTests: QuickSpec { ] selectedOutputsInfo = SelectedUnspentOutputInfo(unspentOutputs: unspentOutputs, totalValue: 100_000_000, fee: 1000, addChangeOutput: true) + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(lastBlock) + } stub(mockTransactionFeeCalculator) { mock in when(mock).feeWithUnspentOutputs(value: sendingValue, feeRate: any(), toScriptType: any(), changeScriptType: any(), senderPay: any()).thenReturn(selectedOutputsInfo) } @@ -54,7 +59,7 @@ class TransactionCreatorTests: QuickSpec { when(mock).changePublicKey().thenReturn(changePublicKey) } stub(mockTransactionBuilder) { mock in - when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any())).thenReturn(transaction) + when(mock.buildTransaction(value: any(), unspentOutputs: any(), fee: any(), senderPay: any(), toAddress: any(), changeAddress: any(), lastBlockHeight: any())).thenReturn(transaction) } stub(mockTransactionProcessor) { mock in when(mock.processCreated(transaction: any())).thenDoNothing() @@ -68,7 +73,7 @@ class TransactionCreatorTests: QuickSpec { creator = TransactionCreator( transactionBuilder: mockTransactionBuilder, transactionProcessor: mockTransactionProcessor, transactionSender: mockTransactionSender, transactionFeeCalculator: mockTransactionFeeCalculator, - bloomFilterManager: mockBloomFilterManager, addressConverter: mockAddressConverter, publicKeyManager: mockPublicKeyManager, bip: bip + bloomFilterManager: mockBloomFilterManager, addressConverter: mockAddressConverter, publicKeyManager: mockPublicKeyManager, storage: mockStorage, bip: bip ) } @@ -100,7 +105,7 @@ class TransactionCreatorTests: QuickSpec { } it("creates transaction") { - verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress), lastBlockHeight: lastBlock.height) verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) } @@ -125,11 +130,28 @@ class TransactionCreatorTests: QuickSpec { it("creates transaction without change address") { verify(mockPublicKeyManager, never()).changePublicKey() verify(mockAddressConverter, never()).convert(publicKey: equal(to: changePublicKey), type: equal(to: bip.scriptType)) - verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: any(), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(nil)) + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: any(), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(nil), lastBlockHeight: lastBlock.height) verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) } } + context("when lastBlock is nil") { + beforeEach { + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(nil) + } + + _ = try! creator.create(to: toAddress.stringValue, value: sendingValue, feeRate: feeRate, senderPay: senderPay) + } + + it("builds transactions with lastBlockHeight: 0") { + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress), lastBlockHeight: 0) + } + } + context("when BloomFilterManager.BloomFilterExpired error") { beforeEach { stub(mockTransactionProcessor) { mock in @@ -143,7 +165,7 @@ class TransactionCreatorTests: QuickSpec { } it("does create transaction") { - verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: any(), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: any(), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress), lastBlockHeight: lastBlock.height) verify(mockTransactionProcessor).processCreated(transaction: any()) } @@ -191,7 +213,7 @@ class TransactionCreatorTests: QuickSpec { verify(mockTransactionFeeCalculator).feeWithUnspentOutputs(value: sendingValue, feeRate: feeRate, toScriptType: equal(to: toAddress.scriptType), changeScriptType: equal(to: bip.scriptType), senderPay: senderPay) verify(mockPublicKeyManager).changePublicKey() verify(mockAddressConverter).convert(publicKey: equal(to: changePublicKey), type: equal(to: bip.scriptType)) - verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress)) + verify(mockTransactionBuilder).buildTransaction(value: sendingValue, unspentOutputs: equal(to: unspentOutputs), fee: selectedOutputsInfo.fee, senderPay: senderPay, toAddress: addressMatcher(toAddress), changeAddress: addressMatcher(changeAddress), lastBlockHeight: lastBlock.height) verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) verify(mockTransactionSender).send(pendingTransaction: equal(to: transaction)) } @@ -207,7 +229,7 @@ class TransactionCreatorTests: QuickSpec { when(mock).fee(inputScriptType: any(), outputScriptType: any(), feeRate: any(), signatureScriptFunction: any()).thenReturn(fee) } stub(mockTransactionBuilder) { mock in - when(mock).buildTransaction(from: any(), to: any(), fee: any(), signatureScriptFunction: any()).thenReturn(transaction) + when(mock).buildTransaction(from: any(), to: any(), fee: any(), lastBlockHeight: any(), signatureScriptFunction: any()).thenReturn(transaction) } } @@ -233,7 +255,7 @@ class TransactionCreatorTests: QuickSpec { } it("creates transaction") { - verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: addressMatcher(toAddress), fee: fee, signatureScriptFunction: any()) + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: addressMatcher(toAddress), fee: fee, lastBlockHeight: lastBlock.height, signatureScriptFunction: any()) verify(mockTransactionProcessor).processCreated(transaction: equal(to: transaction)) } @@ -242,6 +264,26 @@ class TransactionCreatorTests: QuickSpec { } } + context("when lastBlock is nil") { + beforeEach { + stub(mockTransactionFeeCalculator) { mock in + when(mock).fee(inputScriptType: any(), outputScriptType: any(), feeRate: any(), signatureScriptFunction: any()).thenReturn(fee) + } + stub(mockTransactionSender) { mock in + when(mock.verifyCanSend()).thenDoNothing() + } + stub(mockStorage) { mock in + when(mock.lastBlock.get).thenReturn(nil) + } + + _ = try! creator.create(from: unspentOutput, to: toAddress.stringValue, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + } + + it("builds transactions with lastBlockHeight: 0") { + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: addressMatcher(toAddress), fee: fee, lastBlockHeight: 0, signatureScriptFunction: any()) + } + } + context("when BloomFilterManager.BloomFilterExpired error") { beforeEach { stub(mockTransactionProcessor) { mock in @@ -255,7 +297,7 @@ class TransactionCreatorTests: QuickSpec { } it("does create transaction") { - verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: any(), fee: fee, signatureScriptFunction: any()) + verify(mockTransactionBuilder).buildTransaction(from: equal(to: unspentOutput), to: any(), fee: fee, lastBlockHeight: lastBlock.height, signatureScriptFunction: any()) verify(mockTransactionProcessor).processCreated(transaction: any()) } From fbf7f155dba6ac619b1628f55e0bc22e1a8995ba Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Mon, 23 Sep 2019 17:22:24 +0600 Subject: [PATCH 034/234] Set 0xFFFFFFFE to nSequence to inputs to enable nLockTime and disable Replace-By-Fee --- .../Transactions/Builder/TransactionBuilder.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 502727f7..d0ee2d00 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -24,7 +24,11 @@ class TransactionBuilder { unspentOutput.output.keyHash?.removeFirst(2) } - return factory.inputToSign(withPreviousOutput: unspentOutput, script: Data(), sequence: 0xFFFFFFFF) + // Maximum nSequence value (0xFFFFFFFF) disables nLockTime. + // According to BIP-125, any value less than 0xFFFFFFFE makes a Replace-by-Fee(RBF) opted in. + let sequence = 0xFFFFFFFE + + return factory.inputToSign(withPreviousOutput: unspentOutput, script: Data(), sequence: sequence) } private func output(withIndex index: Int, address: Address, value: Int) throws -> Output { From 5d02ef8ffa653c5ab9876d81bec0b73f1cf8c93e Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 27 Sep 2019 18:10:22 +0600 Subject: [PATCH 035/234] Add seed nodes to BitcoinCash mainnet and testnet --- BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift | 1 + BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift b/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift index f97cd1ac..49f39dc4 100644 --- a/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift +++ b/BitcoinCashKit/BitcoinCashKit/Network/MainNet.swift @@ -19,6 +19,7 @@ class MainNet: INetwork { let dnsSeeds = [ "seed.bitcoinabc.org", + "seed-abc.bitcoinforks.org", ] var bip44CheckpointBlock: Block { diff --git a/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift b/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift index 305c1198..a2fab94d 100644 --- a/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift +++ b/BitcoinCashKit/BitcoinCashKit/Network/TestNet.swift @@ -19,6 +19,7 @@ class TestNet: INetwork { let dnsSeeds = [ "testnet-seed.bitcoinabc.org", + "testnet-seed-abc.bitcoinforks.org" ] var bip44CheckpointBlock: Block { From b394159c89a90b1ddd5d4f80a5f5d2223adff693 Mon Sep 17 00:00:00 2001 From: Dmitry Frishbuter Date: Sat, 5 Oct 2019 22:40:05 +0600 Subject: [PATCH 036/234] Fix typos in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5951ebde..add9d1de 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Bitcoin, BitcoinCash(ABC) and Dash wallet toolkit for Swift. This is a full impl ### BitcoinCashKit.swift - bech32 cashaddr addresses -### DashKit.swfit +### DashKit.swift - Instant send - LLMQ lock, Masternodes validation @@ -56,7 +56,7 @@ let dashKit = DashKit(withWords: words, walletId: "dash-wallet-id", syncMode: .a All 3 *Kits* can be configured to work in `.mainNet` or `.testNet`. -##### `sycMode` paramater +##### `sycMode` parameter *Kits* can restore existing wallet or create a new one. When restoring, it generates addresses for given wallet according to bip44 protocol, then it pulls all historical transactions for each of those addresses. This is done only once on initial sync. `syncMode` parameter defines where it pulls historical transactions from. When they are pulled, it continues to sync according to [SPV](https://en.bitcoinwiki.org/wiki/Simplified_Payment_Verification) protocol no matter which syncMode was used for initial sync. There are 3 modes available: - `.full`: Fully synchronizes from peer-to-peer network starting from the block when bip44 was introduced. This mode is the most private (since it fully complies with [SPV](https://en.bitcoinwiki.org/wiki/Simplified_Payment_Verification) protocol), but it takes approximately 2 hours to sync upto now (June 10, 2019). @@ -304,7 +304,7 @@ All features of the library are used in example project. It can be referred as a ## Dependencies -* [HSHDWalletKit](https://github.com/horizontalsystems/hd-wallet-kit-ios) - HD Wallet related features, mnemonic phrase geneartion. +* [HSHDWalletKit](https://github.com/horizontalsystems/hd-wallet-kit-ios) - HD Wallet related features, mnemonic phrase generation. * [HSCryptoKit](https://github.com/horizontalsystems/crypto-kit-ios) - Crypto functions required for working with blockchain. ### Dash dependencies From 12ca9106885e9ff44f490fc6748bc007b0d9dd06 Mon Sep 17 00:00:00 2001 From: Dmitry Frishbuter Date: Sat, 5 Oct 2019 22:44:38 +0600 Subject: [PATCH 037/234] Fix `syncMode` parameter name in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index add9d1de..fbb204e1 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ let dashKit = DashKit(withWords: words, walletId: "dash-wallet-id", syncMode: .a All 3 *Kits* can be configured to work in `.mainNet` or `.testNet`. -##### `sycMode` parameter +##### `syncMode` parameter *Kits* can restore existing wallet or create a new one. When restoring, it generates addresses for given wallet according to bip44 protocol, then it pulls all historical transactions for each of those addresses. This is done only once on initial sync. `syncMode` parameter defines where it pulls historical transactions from. When they are pulled, it continues to sync according to [SPV](https://en.bitcoinwiki.org/wiki/Simplified_Payment_Verification) protocol no matter which syncMode was used for initial sync. There are 3 modes available: - `.full`: Fully synchronizes from peer-to-peer network starting from the block when bip44 was introduced. This mode is the most private (since it fully complies with [SPV](https://en.bitcoinwiki.org/wiki/Simplified_Payment_Verification) protocol), but it takes approximately 2 hours to sync upto now (June 10, 2019). From 61223d912cec80a86bebecd2f355241c259a109f Mon Sep 17 00:00:00 2001 From: max Date: Thu, 17 Oct 2019 14:19:58 +0600 Subject: [PATCH 038/234] Add status info to BitcoinCore --- .../BitcoinCore/Core/AbstractKit.swift | 32 +++++++++++-------- .../BitcoinCore/Core/BitcoinCore.swift | 20 +++++++++++- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 3 +- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index 3a1268a1..fd5b07ce 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -19,31 +19,31 @@ open class AbstractKit { } open var lastBlockInfo: BlockInfo? { - return bitcoinCore.lastBlockInfo + bitcoinCore.lastBlockInfo } open var balance: Int { - return bitcoinCore.balance + bitcoinCore.balance } open var syncState: BitcoinCore.KitState { - return bitcoinCore.syncState + bitcoinCore.syncState } open func transactions(fromHash: String? = nil, limit: Int? = nil) -> Single<[TransactionInfo]> { - return bitcoinCore.transactions(fromHash: fromHash, limit: limit) + bitcoinCore.transactions(fromHash: fromHash, limit: limit) } open func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { - return try bitcoinCore.send(to: address, value: value, feeRate: feeRate) + try bitcoinCore.send(to: address, value: value, feeRate: feeRate) } public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { - return try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate) + try bitcoinCore.send(to: hash, scriptType: scriptType, value: value, feeRate: feeRate) } public func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { - return try bitcoinCore.redeem(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + try bitcoinCore.redeem(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) } open func validate(address: String) throws { @@ -51,27 +51,27 @@ open class AbstractKit { } open func parse(paymentAddress: String) -> BitcoinPaymentData { - return bitcoinCore.parse(paymentAddress: paymentAddress) + bitcoinCore.parse(paymentAddress: paymentAddress) } open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - return try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate) + try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate) } open func receiveAddress() -> String { - return bitcoinCore.receiveAddress() + bitcoinCore.receiveAddress() } open func changePublicKey() throws -> PublicKey { - return try bitcoinCore.changePublicKey() + try bitcoinCore.changePublicKey() } open func receivePublicKey() throws -> PublicKey { - return try bitcoinCore.receivePublicKey() + try bitcoinCore.receivePublicKey() } public func publicKey(byPath path: String) throws -> PublicKey { - return try bitcoinCore.publicKey(byPath: path) + try bitcoinCore.publicKey(byPath: path) } open func watch(transaction: BitcoinCore.TransactionFilter, delegate: IWatchedTransactionDelegate) { @@ -79,7 +79,11 @@ open class AbstractKit { } open var debugInfo: String { - return bitcoinCore.debugInfo + bitcoinCore.debugInfo + } + + open var statusInfo: [(String, Any)] { + bitcoinCore.statusInfo } } diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 1348fe0a..ce36a2f2 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -33,6 +33,8 @@ public class BitcoinCore { private let bip: Bip + private let peerManager: IPeerManager + // START: Extending public let peerGroup: IPeerGroup @@ -99,7 +101,8 @@ public class BitcoinCore { unspentOutputSelector: UnspentOutputSelectorChain, kitStateProvider: IKitStateProvider & ISyncStateListener, scriptBuilder: ScriptBuilderChain, transactionCreator: ITransactionCreator, transactionFeeCalculator: ITransactionFeeCalculator, paymentAddressParser: IPaymentAddressParser, networkMessageParser: NetworkMessageParser, networkMessageSerializer: NetworkMessageSerializer, - syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, bip: Bip) { + syncManager: SyncManager, watchedTransactionManager: IWatchedTransactionManager, bip: Bip, + peerManager: IPeerManager) { self.storage = storage self.cache = cache self.dataProvider = dataProvider @@ -125,6 +128,8 @@ public class BitcoinCore { self.syncManager = syncManager self.watchedTransactionManager = watchedTransactionManager self.bip = bip + + self.peerManager = peerManager } } @@ -212,6 +217,19 @@ extension BitcoinCore { return dataProvider.debugInfo } + public var statusInfo: [(String, Any)] { + var status = [(String, Any)]() + status.append(("synced until", ((lastBlockInfo?.timestamp.map { Double($0) })?.map { Date(timeIntervalSince1970: $0) }) ?? "n/a")) + + status.append(contentsOf: + peerManager.connected().enumerated().map { (index, peer) in + ("peer \(index + 1)", initialBlockDownload.isSynced(peer: peer) ? "synced" : "not synced") + } + ) + + return status + } + } extension BitcoinCore: IDataProviderDelegate { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 6c516e9b..79a5c5e5 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -224,7 +224,8 @@ public class BitcoinCoreBuilder { networkMessageSerializer: networkMessageSerializer, syncManager: syncManager, watchedTransactionManager: watchedTransactionManager, - bip: bip) + bip: bip, + peerManager: peerManager) initialSyncer.delegate = syncManager bloomFilterManager.delegate = bloomFilterLoader From 15864a6b60851d89e7b90e72eee197b05b3e74a0 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 17 Oct 2019 16:22:43 +0600 Subject: [PATCH 039/234] Add host and best block to peer status --- BitcoinCore/BitcoinCore/Core/BitcoinCore.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index ce36a2f2..4f382565 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -223,7 +223,12 @@ extension BitcoinCore { status.append(contentsOf: peerManager.connected().enumerated().map { (index, peer) in - ("peer \(index + 1)", initialBlockDownload.isSynced(peer: peer) ? "synced" : "not synced") + var peerStatus = [(String, Any)]() + peerStatus.append(("status", initialBlockDownload.isSynced(peer: peer) ? "synced" : "not synced")) + peerStatus.append(("host", peer.host)) + peerStatus.append(("best block", peer.announcedLastBlockHeight)) + + return ("peer \(index + 1)", peerStatus) } ) From 537dbd83f0f8c9bd54cfeee9ee08ab56489cc792 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Tue, 1 Oct 2019 14:30:22 +0600 Subject: [PATCH 040/234] Minor code refactoring - Return bip based addresses from AbstractKit#debugInfo - Remove obselete "return" words --- .../BitcoinCashKit/Core/BitcoinCashKit.swift | 2 +- .../BitcoinCore/Core/AbstractKit.swift | 2 +- .../BitcoinCore/Core/BitcoinCore.swift | 24 +++++++++---------- .../BitcoinCore/Core/DataProvider.swift | 6 ++--- BitcoinCore/BitcoinCore/Core/Protocols.swift | 2 +- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 14 +---------- DashKit/DashKit/Core/DashKit.swift | 8 +++---- Demo/Demo/Adapters/BaseAdapter.swift | 22 ++++++++--------- Demo/Demo/Adapters/BitcoinAdapter.swift | 2 +- Demo/Demo/Controllers/ReceiveController.swift | 4 ++-- Demo/Demo/Controllers/ReceiveController.xib | 17 ++++++------- .../Controllers/TransactionsController.swift | 1 + 12 files changed, 45 insertions(+), 59 deletions(-) diff --git a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift index 8e87649d..fd9f71cf 100644 --- a/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift +++ b/BitcoinCashKit/BitcoinCashKit/Core/BitcoinCashKit.swift @@ -101,7 +101,7 @@ extension BitcoinCashKit { } private static func databaseFileName(walletId: String, networkType: NetworkType) -> String { - return "\(walletId)-\(networkType.rawValue)" + "\(walletId)-\(networkType.rawValue)" } } diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index fd5b07ce..4e94b33a 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -79,7 +79,7 @@ open class AbstractKit { } open var debugInfo: String { - bitcoinCore.debugInfo + bitcoinCore.debugInfo(network: network) } open var statusInfo: [(String, Any)] { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 4f382565..8c43f337 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -149,31 +149,31 @@ extension BitcoinCore { extension BitcoinCore { public var lastBlockInfo: BlockInfo? { - return dataProvider.lastBlockInfo + dataProvider.lastBlockInfo } public var balance: Int { - return dataProvider.balance + dataProvider.balance } public var syncState: BitcoinCore.KitState { - return kitStateProvider.syncState + kitStateProvider.syncState } public func transactions(fromHash: String? = nil, limit: Int? = nil) -> Single<[TransactionInfo]> { - return dataProvider.transactions(fromHash: fromHash, limit: limit) + dataProvider.transactions(fromHash: fromHash, limit: limit) } public func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { - return try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true) + try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true) } public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { - return try transactionCreator.create(to: hash, scriptType: scriptType, value: value, feeRate: feeRate, senderPay: true) + try transactionCreator.create(to: hash, scriptType: scriptType, value: value, feeRate: feeRate, senderPay: true) } func redeem(from unspentOutput: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction { - return try transactionCreator.create(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) + try transactionCreator.create(from: unspentOutput, to: address, feeRate: feeRate, signatureScriptFunction: signatureScriptFunction) } public func validate(address: String) throws { @@ -181,7 +181,7 @@ extension BitcoinCore { } public func parse(paymentAddress: String) -> BitcoinPaymentData { - return paymentAddressParser.parse(paymentAddress: paymentAddress) + paymentAddressParser.parse(paymentAddress: paymentAddress) } public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { @@ -202,19 +202,19 @@ extension BitcoinCore { } public func changePublicKey() throws -> PublicKey { - return try publicKeyManager.changePublicKey() + try publicKeyManager.changePublicKey() } public func receivePublicKey() throws -> PublicKey { - return try publicKeyManager.receivePublicKey() + try publicKeyManager.receivePublicKey() } func watch(transaction: BitcoinCore.TransactionFilter, delegate: IWatchedTransactionDelegate) { watchedTransactionManager.add(transactionFilter: transaction, delegatedTo: delegate) } - public var debugInfo: String { - return dataProvider.debugInfo + public func debugInfo(network: INetwork) -> String { + dataProvider.debugInfo(network: network, scriptType: bip.scriptType, addressConverter: addressConverter) } public var statusInfo: [(String, Any)] { diff --git a/BitcoinCore/BitcoinCore/Core/DataProvider.swift b/BitcoinCore/BitcoinCore/Core/DataProvider.swift index 20aaf588..135e6408 100644 --- a/BitcoinCore/BitcoinCore/Core/DataProvider.swift +++ b/BitcoinCore/BitcoinCore/Core/DataProvider.swift @@ -37,7 +37,7 @@ class DataProvider { } private func blockInfo(fromBlock block: Block) -> BlockInfo { - return BlockInfo( + BlockInfo( headerHash: block.headerHash.reversedHex, height: block.height, timestamp: block.timestamp @@ -100,13 +100,13 @@ extension DataProvider: IDataProvider { } } - var debugInfo: String { + func debugInfo(network: INetwork, scriptType: ScriptType, addressConverter: IAddressConverter) -> String { var lines = [String]() let pubKeys = storage.publicKeys().sorted(by: { $0.index < $1.index }) for pubKey in pubKeys { - lines.append("acc: \(pubKey.account) - inx: \(pubKey.index) - ext: \(pubKey.external) : \((try! Base58AddressConverter(addressVersion: 0x00, addressScriptVersion: 0x05).convert(keyHash: pubKey.keyHash, type: .p2pkh)).stringValue)") + lines.append("acc: \(pubKey.account) - inx: \(pubKey.index) - ext: \(pubKey.external) : \((try! addressConverter.convert(keyHash: pubKey.keyHash, type: scriptType)).stringValue)") } lines.append("PUBLIC KEYS COUNT: \(pubKeys.count)") return lines.joined(separator: "\n") diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 44e8596e..bdc4060e 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -432,7 +432,7 @@ protocol IDataProvider { var lastBlockInfo: BlockInfo? { get } var balance: Int { get } - var debugInfo: String { get } + func debugInfo(network: INetwork, scriptType: ScriptType, addressConverter: IAddressConverter) -> String func transactions(fromHash: String?, limit: Int?) -> Single<[TransactionInfo]> } diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index 0f2f7b81..783db9a9 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -83,18 +83,6 @@ public class BitcoinKit: AbstractKit { } } - override open var debugInfo: String { - var lines = [String](arrayLiteral: bitcoinCore.debugInfo) - let pubKeys = storage.publicKeys().sorted(by: { $0.index < $1.index }) - - lines.append("--------------- Bitcoin Segwit (zero program) addresses --------------------") - for pubKey in pubKeys { - lines.append("acc: \(pubKey.account) - inx: \(pubKey.index) - ext: \(pubKey.external) : \(try! bech32AddressConverter.convert(keyHash: Data([0x00, 0x14]) + pubKey.keyHash, type: .p2wpkh).stringValue)") - } - - return lines.joined(separator: "\n") - } - } extension BitcoinKit { @@ -112,7 +100,7 @@ extension BitcoinKit { } private static func databaseFileName(walletId: String, networkType: NetworkType) -> String { - return "\(walletId)-\(networkType.rawValue)" + "\(walletId)-\(networkType.rawValue)" } } diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index bdee1aa1..099470ab 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -141,15 +141,15 @@ public class DashKit: AbstractKit { } private func cast(transactionInfos:[TransactionInfo]) -> [DashTransactionInfo] { - return transactionInfos.compactMap { $0 as? DashTransactionInfo } + transactionInfos.compactMap { $0 as? DashTransactionInfo } } public override func send(to address: String, value: Int, feeRate: Int) throws -> FullTransaction { - return try super.send(to: address, value: value, feeRate: feeRate) + try super.send(to: address, value: value, feeRate: feeRate) } public func transactions(fromHash: String?, limit: Int?) -> Single<[DashTransactionInfo]> { - return super.transactions(fromHash: fromHash, limit: limit).map { self.cast(transactionInfos: $0) } + super.transactions(fromHash: fromHash, limit: limit).map { self.cast(transactionInfos: $0) } } } @@ -212,7 +212,7 @@ extension DashKit { } private static func databaseFileName(walletId: String, networkType: NetworkType) -> String { - return "\(walletId)-\(networkType.rawValue)" + "\(walletId)-\(networkType.rawValue)" } } diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 569b3f3b..2e2391f0 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -16,7 +16,7 @@ class BaseAdapter { let transactionsSignal = Signal() var debugInfo: String { - return abstractKit.debugInfo + abstractKit.debugInfo } init(name: String, coinCode: String, abstractKit: AbstractKit) { @@ -55,7 +55,7 @@ class BaseAdapter { } func transactionsSingle(fromHash: String?, limit: Int) -> Single<[TransactionRecord]> { - return abstractKit.transactions(fromHash: fromHash, limit: limit) + abstractKit.transactions(fromHash: fromHash, limit: limit) .map { [weak self] transactions -> [TransactionRecord] in return transactions.compactMap { self?.transactionRecord(fromTransaction: $0) @@ -68,19 +68,19 @@ class BaseAdapter { extension BaseAdapter { var lastBlockObservable: Observable { - return lastBlockSignal.asObservable().throttle(DispatchTimeInterval.milliseconds(200), scheduler: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + lastBlockSignal.asObservable().throttle(DispatchTimeInterval.milliseconds(200), scheduler: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) } var syncStateObservable: Observable { - return syncStateSignal.asObservable().throttle(DispatchTimeInterval.milliseconds(200), scheduler: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) + syncStateSignal.asObservable().throttle(DispatchTimeInterval.milliseconds(200), scheduler: ConcurrentDispatchQueueScheduler(qos: .userInitiated)) } var balanceObservable: Observable { - return balanceSignal.asObservable() + balanceSignal.asObservable() } var transactionsObservable: Observable { - return transactionsSignal.asObservable() + transactionsSignal.asObservable() } func start() { @@ -90,19 +90,19 @@ extension BaseAdapter { } var balance: Decimal { - return Decimal(abstractKit.balance) / coinRate + Decimal(abstractKit.balance) / coinRate } var lastBlockInfo: BlockInfo? { - return abstractKit.lastBlockInfo + abstractKit.lastBlockInfo } var syncState: BitcoinCore.KitState { - return abstractKit.syncState + abstractKit.syncState } func receiveAddress() -> String { - return abstractKit.receiveAddress() + abstractKit.receiveAddress() } func validate(address: String) throws { @@ -131,7 +131,7 @@ extension BaseAdapter { } func availableBalance(for address: String?) -> Decimal { - return max(0, balance - fee(for: balance, address: address)) + max(0, balance - fee(for: balance, address: address)) } func fee(for value: Decimal, address: String?) -> Decimal { diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index 3191517d..1b3c3276 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -8,7 +8,7 @@ class BitcoinAdapter: BaseAdapter { init(words: [String], bip: Bip, testMode: Bool, syncMode: BitcoinCore.SyncMode) { let networkType: BitcoinKit.NetworkType = testMode ? .testNet : .mainNet - bitcoinKit = try! BitcoinKit(withWords: words, bip: bip, walletId: "walletId", syncMode: syncMode, networkType: networkType, minLogLevel: Configuration.shared.minLogLevel) + bitcoinKit = try! BitcoinKit(withWords: words, bip: bip, walletId: "walletId", syncMode: syncMode, networkType: networkType, confirmationsThreshold: 1, minLogLevel: Configuration.shared.minLogLevel) super.init(name: "Bitcoin", coinCode: "BTC", abstractKit: bitcoinKit) bitcoinKit.delegate = self diff --git a/Demo/Demo/Controllers/ReceiveController.swift b/Demo/Demo/Controllers/ReceiveController.swift index d41ab269..d2bf879c 100644 --- a/Demo/Demo/Controllers/ReceiveController.swift +++ b/Demo/Demo/Controllers/ReceiveController.swift @@ -18,8 +18,6 @@ class ReceiveController: UIViewController { addressLabel?.layer.cornerRadius = 8 addressLabel?.clipsToBounds = true - segmentedControl.addTarget(self, action: #selector(onSegmentChanged), for: .valueChanged) - Manager.shared.adapterSignal .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)) .observeOn(MainScheduler.instance) @@ -29,6 +27,7 @@ class ReceiveController: UIViewController { .disposed(by: disposeBag) updateAdapters() + segmentedControl.addTarget(self, action: #selector(onSegmentChanged), for: .valueChanged) } private func updateAdapters() { @@ -54,6 +53,7 @@ class ReceiveController: UIViewController { @objc func onSegmentChanged() { updateAddress() + print("segment changed") if let adapter = currentAdapter { print(adapter.debugInfo) diff --git a/Demo/Demo/Controllers/ReceiveController.xib b/Demo/Demo/Controllers/ReceiveController.xib index 9a27aaaf..cda4d9de 100644 --- a/Demo/Demo/Controllers/ReceiveController.xib +++ b/Demo/Demo/Controllers/ReceiveController.xib @@ -1,11 +1,9 @@ - - - - + + - + @@ -22,13 +20,13 @@ - - + + + + diff --git a/Demo/Demo/Controllers/TransactionsController.swift b/Demo/Demo/Controllers/TransactionsController.swift index de069635..142439a5 100644 --- a/Demo/Demo/Controllers/TransactionsController.swift +++ b/Demo/Demo/Controllers/TransactionsController.swift @@ -76,15 +76,15 @@ class TransactionsController: UITableViewController { } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return transactions.count + transactions.count } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 220 + 220 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - return tableView.dequeueReusableCell(withIdentifier: String(describing: TransactionCell.self), for: indexPath) + tableView.dequeueReusableCell(withIdentifier: String(describing: TransactionCell.self), for: indexPath) } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { diff --git a/Hodler/Hodler.xcodeproj/project.pbxproj b/Hodler/Hodler.xcodeproj/project.pbxproj index 544f3010..7bda0a70 100644 --- a/Hodler/Hodler.xcodeproj/project.pbxproj +++ b/Hodler/Hodler.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2FA5DDA7206D8B8B0881996E /* HolderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D842CADC75745AFA6132 /* HolderPlugin.swift */; }; ABFFF88A886209596FE87ABE /* Pods_Hodler.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07C4E7D9B6ADC34E17C2BC97 /* Pods_Hodler.framework */; }; D02C6C9723474D6100F48BD0 /* Hodler.h in Headers */ = {isa = PBXBuildFile; fileRef = D02C6C9523474D6100F48BD0 /* Hodler.h */; settings = {ATTRIBUTES = (Public, ); }; }; D02C6C9F23474DC100F48BD0 /* BitcoinCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D02C6C9E23474DC100F48BD0 /* BitcoinCore.framework */; }; @@ -29,6 +30,7 @@ /* Begin PBXFileReference section */ 07C4E7D9B6ADC34E17C2BC97 /* Pods_Hodler.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Hodler.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FA5D842CADC75745AFA6132 /* HolderPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HolderPlugin.swift; sourceTree = ""; }; CE692A61FBB18EF69F72A4E6 /* Pods-Hodler.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Hodler.debug.xcconfig"; path = "Target Support Files/Pods-Hodler/Pods-Hodler.debug.xcconfig"; sourceTree = ""; }; D02C6C9223474D6100F48BD0 /* Hodler.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Hodler.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D02C6C9523474D6100F48BD0 /* Hodler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Hodler.h; sourceTree = ""; }; @@ -60,6 +62,14 @@ path = ../Pods; sourceTree = ""; }; + 2FA5D7257FBCFEC95241C2CF /* Core */ = { + isa = PBXGroup; + children = ( + 2FA5D842CADC75745AFA6132 /* HolderPlugin.swift */, + ); + path = Core; + sourceTree = ""; + }; D02C6C8823474D6100F48BD0 = { isa = PBXGroup; children = ( @@ -81,6 +91,7 @@ D02C6C9423474D6100F48BD0 /* Hodler */ = { isa = PBXGroup; children = ( + 2FA5D7257FBCFEC95241C2CF /* Core */, D02C6C9523474D6100F48BD0 /* Hodler.h */, D02C6C9623474D6100F48BD0 /* Info.plist */, ); @@ -202,6 +213,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2FA5DDA7206D8B8B0881996E /* HolderPlugin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Hodler/Hodler/Core/HolderPlugin.swift b/Hodler/Hodler/Core/HolderPlugin.swift new file mode 100644 index 00000000..83c3de4b --- /dev/null +++ b/Hodler/Hodler/Core/HolderPlugin.swift @@ -0,0 +1,29 @@ +import BitcoinCore +import HSCryptoKit + +public class HodlerPlugin: IPlugin { + enum HodlerPluginError: Error { + case unsupportedAddress + } + + public init() {} + + public func processOutputs(mutableTransaction: MutableTransaction, extraData: [String: [String: Any]], addressConverter: IAddressConverter) throws { + guard let hodlerData = extraData["hodler"], let timeLockParam = hodlerData["locked_until"], let unlockTime = timeLockParam as? Int else { + return + } + + guard let recipientAddress = mutableTransaction.recipientAddress, recipientAddress.scriptType == .p2pkh else { + throw HodlerPluginError.unsupportedAddress + } + + let unlockTimeData = Data(from: UInt32(unlockTime)) + let redeemScript = OpCode.push(unlockTimeData) + Data([OpCode.checkLockTimeVerify, OpCode.drop]) + recipientAddress.lockingScript + let scriptHash = CryptoKit.sha256ripemd160(redeemScript) + let newAddress = try addressConverter.convert(keyHash: scriptHash, type: .p2sh) + + mutableTransaction.recipientAddress = newAddress + mutableTransaction.add(extraData: OpCode.push(unlockTimeData) + OpCode.push(recipientAddress.keyHash), pluginId: 1) + } + +} From b7ee5f26c3df7080ee400e94f557226663d8b885 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 10 Oct 2019 17:49:54 +0600 Subject: [PATCH 044/234] Parse hodler transactions --- .../BitcoinCore/Core/AbstractKit.swift | 4 +- .../BitcoinCore/Core/BitcoinCore.swift | 4 +- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 6 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 11 ++-- .../BitcoinCore/Managers/PluginManager.swift | 33 ++++++++--- BitcoinCore/BitcoinCore/Models/Output.swift | 11 +++- .../BitcoinCore/Storage/GrdbStorage.swift | 11 +++- .../Transactions/Builder/InputSetter.swift | 2 +- .../Builder/MutableTransaction.swift | 14 ++--- .../Transactions/Builder/OutputSetter.swift | 4 +- .../Builder/TransactionBuilder.swift | 4 +- .../Transactions/TransactionCreator.swift | 4 +- .../TransactionOutputExtractor.swift | 15 ++++- DashKit/DashKit/Core/DashKit.swift | 2 +- Demo/Demo/Adapters/BaseAdapter.swift | 4 +- Demo/Demo/Controllers/SendController.swift | 6 +- Hodler/Hodler.xcodeproj/project.pbxproj | 4 ++ Hodler/Hodler/Core/HodlerData.swift | 32 +++++++++++ Hodler/Hodler/Core/HolderPlugin.swift | 55 ++++++++++++++++--- 19 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 Hodler/Hodler/Core/HodlerData.swift diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index c347333f..c0fc1fb4 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -34,8 +34,8 @@ open class AbstractKit { bitcoinCore.transactions(fromHash: fromHash, limit: limit) } - open func send(to address: String, value: Int, feeRate: Int, extraData: [String: [String: Any]] = [:]) throws -> FullTransaction { - try bitcoinCore.send(to: address, value: value, feeRate: feeRate, extraData: extraData) + open func send(to address: String, value: Int, feeRate: Int, pluginData: [String: [String: Any]] = [:]) throws -> FullTransaction { + try bitcoinCore.send(to: address, value: value, feeRate: feeRate, pluginData: pluginData) } public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index 8d37e20e..b2377206 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -163,8 +163,8 @@ extension BitcoinCore { dataProvider.transactions(fromHash: fromHash, limit: limit) } - public func send(to address: String, value: Int, feeRate: Int, extraData: [String: [String: Any]] = [:]) throws -> FullTransaction { - try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true, extraData: extraData) + public func send(to address: String, value: Int, feeRate: Int, pluginData: [String: [String: Any]] = [:]) throws -> FullTransaction { + try transactionCreator.create(to: address, value: value, feeRate: feeRate, senderPay: true, pluginData: pluginData) } public func send(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int) throws -> FullTransaction { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 830d70ac..056deab0 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -120,7 +120,9 @@ public class BitcoinCoreBuilder { } let addressConverter = AddressConverterChain() + let scriptConverter = ScriptConverter() let restoreKeyConverterChain = RestoreKeyConverterChain() + let pluginManager = PluginManager(addressConverter: addressConverter, scriptConverter: scriptConverter, storage: storage) // let dbName = "bitcoinkit-${network.javaClass}-$walletId" // let database = KitDatabase.getInstance(context, dbName) @@ -148,10 +150,9 @@ public class BitcoinCoreBuilder { let myOutputsCache = OutputsCache.instance(storage: storage) let irregularOutputFinder = IrregularOutputFinder(storage: storage) - let scriptConverter = ScriptConverter() let transactionInputExtractor = TransactionInputExtractor(storage: storage, scriptConverter: scriptConverter, addressConverter: addressConverter, logger: logger) let transactionKeySetter = TransactionPublicKeySetter(storage: storage) - let transactionOutputExtractor = TransactionOutputExtractor(transactionKeySetter: transactionKeySetter, logger: logger) + let transactionOutputExtractor = TransactionOutputExtractor(transactionKeySetter: transactionKeySetter, pluginManager: pluginManager, logger: logger) let transactionAddressExtractor = TransactionOutputAddressExtractor(storage: storage, addressConverter: addressConverter) let transactionProcessor = TransactionProcessor(storage: storage, outputExtractor: transactionOutputExtractor, inputExtractor: transactionInputExtractor, @@ -191,7 +192,6 @@ public class BitcoinCoreBuilder { peerManager: peerManager, logger: logger) let syncedReadyPeerManager = SyncedReadyPeerManager(peerGroup: peerGroup, initialBlockDownload: initialBlockDownload) - let pluginManager = PluginManager(addressConverter: addressConverter, storage: storage) let inputSigner = InputSigner(hdWallet: hdWallet, network: network) let transactionSizeCalculator = TransactionSizeCalculator() let outputSetter = OutputSetter(addressConverter: addressConverter, factory: factory, pluginManager: pluginManager) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index f6d7a40b..a0637345 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -340,13 +340,13 @@ public protocol ITransactionSyncer: class { } public protocol ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, extraData: [String: [String: Any]]) throws -> FullTransaction + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, pluginData: [String: [String: Any]]) throws -> FullTransaction func create(to hash: Data, scriptType: ScriptType, value: Int, feeRate: Int, senderPay: Bool) throws -> FullTransaction func create(from: UnspentOutput, to address: String, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } protocol ITransactionBuilder { - func buildTransaction(toAddress: String, value: Int, feeRate: Int, senderPay: Bool, extraData: [String: [String: Any]]) throws -> FullTransaction + func buildTransaction(toAddress: String, value: Int, feeRate: Int, senderPay: Bool, pluginData: [String: [String: Any]]) throws -> FullTransaction func buildTransaction(from: UnspentOutput, to: Address, fee: Int, lastBlockHeight: Int, signatureScriptFunction: (Data, Data) -> Data) throws -> FullTransaction } @@ -555,10 +555,13 @@ protocol IIrregularOutputFinder { } public protocol IPlugin { - func processOutputs(mutableTransaction: MutableTransaction, extraData: [String: [String: Any]], addressConverter: IAddressConverter) throws + var id: UInt8 { get } + func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]], addressConverter: IAddressConverter) throws + func processTransactionWithNullData(transaction: FullTransaction, nullDataChunks: inout IndexingIterator<[Chunk]>, storage: IStorage, addressConverter: IAddressConverter) throws } protocol IPluginManager { - func processOutputs(mutableTransaction: MutableTransaction, extraData: [String: [String: Any]]) throws func add(plugin: IPlugin) + func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]]) throws + func processTransactionWithNullData(transaction: FullTransaction, nullDataOutput: Output) throws } diff --git a/BitcoinCore/BitcoinCore/Managers/PluginManager.swift b/BitcoinCore/BitcoinCore/Managers/PluginManager.swift index ed5fc21e..df0c7fa7 100644 --- a/BitcoinCore/BitcoinCore/Managers/PluginManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PluginManager.swift @@ -1,10 +1,12 @@ class PluginManager { private let addressConverter: IAddressConverter + private let scriptConverter: IScriptConverter private let storage: IStorage - private var plugins = [IPlugin]() + private var plugins = [UInt8: IPlugin]() - init(addressConverter: IAddressConverter, storage: IStorage) { + init(addressConverter: IAddressConverter, scriptConverter: IScriptConverter, storage: IStorage) { self.addressConverter = addressConverter + self.scriptConverter = scriptConverter self.storage = storage } @@ -12,14 +14,29 @@ class PluginManager { extension PluginManager: IPluginManager { - func processOutputs(mutableTransaction: MutableTransaction, extraData: [String: [String: Any]]) throws { - for plugin in plugins { - try plugin.processOutputs(mutableTransaction: mutableTransaction, extraData: extraData, addressConverter: addressConverter) - } + func add(plugin: IPlugin) { + plugins[plugin.id] = plugin } - func add(plugin: IPlugin) { - plugins.append(plugin) + func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]]) throws { + for (_, plugin) in plugins { + try plugin.processOutputs(mutableTransaction: mutableTransaction, pluginData: pluginData, addressConverter: addressConverter) + } } + func processTransactionWithNullData(transaction: FullTransaction, nullDataOutput: Output) throws { + let script = try scriptConverter.decode(data: nullDataOutput.lockingScript) + var iterator = script.chunks.makeIterator() + + // the first byte OP_RETURN + _ = iterator.next() + + while let pluginId = iterator.next() { + guard let plugin = plugins[pluginId.opCode] else { + break + } + + try plugin.processTransactionWithNullData(transaction: transaction, nullDataChunks: &iterator, storage: storage, addressConverter: addressConverter) + } + } } diff --git a/BitcoinCore/BitcoinCore/Models/Output.swift b/BitcoinCore/BitcoinCore/Models/Output.swift index 1874269b..e58e15b0 100644 --- a/BitcoinCore/BitcoinCore/Models/Output.swift +++ b/BitcoinCore/BitcoinCore/Models/Output.swift @@ -29,12 +29,15 @@ public class Output: Record { public var lockingScript: Data public var index: Int var transactionHash: Data - var publicKeyPath: String? = nil + public var publicKeyPath: String? = nil public var scriptType: ScriptType = .unknown public var redeemScript: Data? = nil public var keyHash: Data? = nil var address: String? = nil + public var pluginId: UInt8? = nil + public var pluginData: String? = nil + public init(withValue value: Int, index: Int, lockingScript script: Data, transactionHash: Data = Data(), type: ScriptType = .unknown, redeemScript: Data? = nil, address: String? = nil, keyHash: Data? = nil, publicKey: PublicKey? = nil) { self.value = value self.lockingScript = script @@ -63,6 +66,8 @@ public class Output: Record { case redeemScript case keyHash case address + case pluginId + case pluginData } required init(row: Row) { @@ -75,6 +80,8 @@ public class Output: Record { redeemScript = row[Columns.redeemScript] keyHash = row[Columns.keyHash] address = row[Columns.address] + pluginId = row[Columns.pluginId] + pluginData = row[Columns.pluginData] super.init(row: row) } @@ -89,6 +96,8 @@ public class Output: Record { container[Columns.redeemScript] = redeemScript container[Columns.keyHash] = keyHash container[Columns.address] = address + container[Columns.pluginId] = pluginId + container[Columns.pluginData] = pluginData } } diff --git a/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift b/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift index fc82e390..a00a6401 100644 --- a/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift +++ b/BitcoinCore/BitcoinCore/Storage/GrdbStorage.swift @@ -164,6 +164,13 @@ open class GrdbStorage { } } + migrator.registerMigration("addPluginInfoToOutput") { db in + try db.alter(table: Output.databaseTableName) { t in + t.add(column: Output.Columns.pluginId.name, .integer) + t.add(column: Output.Columns.pluginData.name, .text) + } + } + return migrator } @@ -511,8 +518,8 @@ extension GrdbStorage: IStorage { } } - var inputsByTransaction: [Data: [InputWithPreviousOutput]] = Dictionary(grouping: inputs, by: { $0.input.transactionHash }) - var outputsByTransaction: [Data: [Output]] = Dictionary(grouping: outputs, by: { $0.transactionHash }) + let inputsByTransaction: [Data: [InputWithPreviousOutput]] = Dictionary(grouping: inputs, by: { $0.input.transactionHash }) + let outputsByTransaction: [Data: [Output]] = Dictionary(grouping: outputs, by: { $0.transactionHash }) var results = [FullTransactionForInfo]() for transactionWithBlock in transactionsWithBlocks { diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift index afec4490..d7a29fb3 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift @@ -16,7 +16,7 @@ class InputSetter { func setInputs(to mutableTransaction: MutableTransaction, feeRate: Int, senderPay: Bool) throws { let value = mutableTransaction.recipientValue - _ = mutableTransaction.extraDataOutputSize + _ = mutableTransaction.pluginDataOutputSize let unspentOutputInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: mutableTransaction.recipientAddress.scriptType, changeType: changeScriptType, senderPay: senderPay) let unspentOutputs = unspentOutputInfo.unspentOutputs diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift index b8369abe..f81081c3 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift @@ -7,7 +7,7 @@ public class MutableTransaction { var changeAddress: Address? = nil var changeValue = 0 - private var extraData = [Int: Data]() + private var pluginData = [UInt8: Data]() var outputs: [Output] { var outputs = [Output]() @@ -24,11 +24,11 @@ public class MutableTransaction { index += 1 } - if !extraData.isEmpty { + if !pluginData.isEmpty { var data = Data([OpCode.op_return]) - extraData.forEach { key, value in - data += OpCode.push(key) + value + pluginData.forEach { key, value in + data += Data([key]) + value } outputs.append(Output(withValue: 0, index: index, lockingScript: data, type: .nullData)) @@ -37,7 +37,7 @@ public class MutableTransaction { return outputs } - var extraDataOutputSize: Int { + var pluginDataOutputSize: Int { 0 } @@ -47,8 +47,8 @@ public class MutableTransaction { transaction.isOutgoing = true } - public func add(extraData: Data, pluginId: Int) { - self.extraData[pluginId] = extraData + public func add(pluginData: Data, pluginId: UInt8) { + self.pluginData[pluginId] = pluginData } func add(inputToSign: InputToSign) { diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/OutputSetter.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/OutputSetter.swift index 1123f126..a4ef8f1a 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/OutputSetter.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/OutputSetter.swift @@ -9,11 +9,11 @@ class OutputSetter { self.pluginManager = pluginManager } - func setOutputs(to mutableTransaction: MutableTransaction, toAddress: String, value: Int, extraData: [String: [String: Any]]) throws { + func setOutputs(to mutableTransaction: MutableTransaction, toAddress: String, value: Int, pluginData: [String: [String: Any]]) throws { mutableTransaction.recipientAddress = try addressConverter.convert(address: toAddress) mutableTransaction.recipientValue = value - try pluginManager.processOutputs(mutableTransaction: mutableTransaction, extraData: extraData) + try pluginManager.processOutputs(mutableTransaction: mutableTransaction, pluginData: pluginData) } } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index d909a197..5f8f29ef 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -43,10 +43,10 @@ class TransactionBuilder { extension TransactionBuilder: ITransactionBuilder { - func buildTransaction(toAddress: String, value: Int, feeRate: Int, senderPay: Bool, extraData: [String: [String: Any]]) throws -> FullTransaction { + func buildTransaction(toAddress: String, value: Int, feeRate: Int, senderPay: Bool, pluginData: [String: [String: Any]]) throws -> FullTransaction { let mutableTransaction = MutableTransaction() - try outputSetter.setOutputs(to: mutableTransaction, toAddress: toAddress, value: value, extraData: extraData) + try outputSetter.setOutputs(to: mutableTransaction, toAddress: toAddress, value: value, pluginData: pluginData) try inputSetter.setInputs(to: mutableTransaction, feeRate: feeRate, senderPay: senderPay) lockTimeSetter.setLockTime(to: mutableTransaction) try signer.sign(mutableTransaction: mutableTransaction) diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift index 6167d764..10f9242b 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionCreator.swift @@ -38,13 +38,13 @@ class TransactionCreator { extension TransactionCreator: ITransactionCreator { - func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, extraData: [String: [String: Any]] = [:]) throws -> FullTransaction { + func create(to address: String, value: Int, feeRate: Int, senderPay: Bool, pluginData: [String: [String: Any]] = [:]) throws -> FullTransaction { let transaction = try transactionBuilder.buildTransaction( toAddress: address, value: value, feeRate: feeRate, senderPay: senderPay, - extraData: extraData + pluginData: pluginData ) try processAndSend(transaction: transaction) diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionOutputExtractor.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionOutputExtractor.swift index 932377ae..87e35988 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionOutputExtractor.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionOutputExtractor.swift @@ -2,10 +2,12 @@ import HSCryptoKit class TransactionOutputExtractor { let transactionKeySetter: ITransactionPublicKeySetter + let pluginManager: IPluginManager let logger: Logger? - init(transactionKeySetter: ITransactionPublicKeySetter, logger: Logger? = nil) { + init(transactionKeySetter: ITransactionPublicKeySetter, pluginManager: IPluginManager, logger: Logger? = nil) { self.transactionKeySetter = transactionKeySetter + self.pluginManager = pluginManager self.logger = logger } @@ -14,6 +16,8 @@ class TransactionOutputExtractor { extension TransactionOutputExtractor: ITransactionExtractor { func extract(transaction: FullTransaction) { + var nullDataOutput: Output? = nil + for output in transaction.outputs { var payload: Data? var validScriptType: ScriptType = .unknown @@ -49,7 +53,11 @@ extension TransactionOutputExtractor: ITransactionExtractor { // parse P2WPKH transaction output payload = lockingScript.subdata(in: 0.. FullTransaction { + public override func send(to address: String, value: Int, feeRate: Int, pluginData: [String: [String: Any]]) throws -> FullTransaction { try super.send(to: address, value: value, feeRate: feeRate) } diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 92109210..2c6aca01 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -115,12 +115,12 @@ extension BaseAdapter { } } - func sendSingle(to address: String, amount: Decimal, extraData: [String: [String: Any]] = [:]) -> Single { + func sendSingle(to address: String, amount: Decimal, pluginData: [String: [String: Any]] = [:]) -> Single { let satoshiAmount = convertToSatoshi(value: amount) return Single.create { [unowned self] observer in do { - _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate, extraData: extraData) + _ = try self.abstractKit.send(to: address, value: satoshiAmount, feeRate: self.feeRate, pluginData: pluginData) observer(.success(())) } catch { observer(.error(error)) diff --git a/Demo/Demo/Controllers/SendController.swift b/Demo/Demo/Controllers/SendController.swift index 039ff845..15c1bee9 100644 --- a/Demo/Demo/Controllers/SendController.swift +++ b/Demo/Demo/Controllers/SendController.swift @@ -70,12 +70,12 @@ class SendController: UIViewController { return } - var extraData = [String: [String: Any]]() + var pluginData = [String: [String: Any]]() if let lockUntil = datePicker?.date { - extraData["hodler"] = ["locked_until": Int(lockUntil.timeIntervalSince1970)] + pluginData["hodler"] = ["locked_until": Int(lockUntil.timeIntervalSince1970)] } - currentAdapter?.sendSingle(to: address, amount: amount, extraData: extraData) + currentAdapter?.sendSingle(to: address, amount: amount, pluginData: pluginData) .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated)) .observeOn(MainScheduler.instance) .subscribe(onSuccess: { [weak self] _ in diff --git a/Hodler/Hodler.xcodeproj/project.pbxproj b/Hodler/Hodler.xcodeproj/project.pbxproj index 7bda0a70..4a9e192e 100644 --- a/Hodler/Hodler.xcodeproj/project.pbxproj +++ b/Hodler/Hodler.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2FA5D676B12FF745A5911E40 /* HodlerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D16242CA2BBD78ABA8F7 /* HodlerData.swift */; }; 2FA5DDA7206D8B8B0881996E /* HolderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA5D842CADC75745AFA6132 /* HolderPlugin.swift */; }; ABFFF88A886209596FE87ABE /* Pods_Hodler.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 07C4E7D9B6ADC34E17C2BC97 /* Pods_Hodler.framework */; }; D02C6C9723474D6100F48BD0 /* Hodler.h in Headers */ = {isa = PBXBuildFile; fileRef = D02C6C9523474D6100F48BD0 /* Hodler.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -30,6 +31,7 @@ /* Begin PBXFileReference section */ 07C4E7D9B6ADC34E17C2BC97 /* Pods_Hodler.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Hodler.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FA5D16242CA2BBD78ABA8F7 /* HodlerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HodlerData.swift; sourceTree = ""; }; 2FA5D842CADC75745AFA6132 /* HolderPlugin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HolderPlugin.swift; sourceTree = ""; }; CE692A61FBB18EF69F72A4E6 /* Pods-Hodler.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Hodler.debug.xcconfig"; path = "Target Support Files/Pods-Hodler/Pods-Hodler.debug.xcconfig"; sourceTree = ""; }; D02C6C9223474D6100F48BD0 /* Hodler.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Hodler.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -66,6 +68,7 @@ isa = PBXGroup; children = ( 2FA5D842CADC75745AFA6132 /* HolderPlugin.swift */, + 2FA5D16242CA2BBD78ABA8F7 /* HodlerData.swift */, ); path = Core; sourceTree = ""; @@ -214,6 +217,7 @@ buildActionMask = 2147483647; files = ( 2FA5DDA7206D8B8B0881996E /* HolderPlugin.swift in Sources */, + 2FA5D676B12FF745A5911E40 /* HodlerData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Hodler/Hodler/Core/HodlerData.swift b/Hodler/Hodler/Core/HodlerData.swift new file mode 100644 index 00000000..9d3e79de --- /dev/null +++ b/Hodler/Hodler/Core/HodlerData.swift @@ -0,0 +1,32 @@ +class HodlerData { + let lockedUntilTimestamp: Int + let addressString: String + + static func parse(serialized: String) throws -> HodlerData { + let parts = serialized.split(separator: "|") + + guard parts.count == 2 else { + throw HodlerPluginError.invalidHodlerData + } + + let lockedUntilTimestampStr = String(parts[0]) + let addressString = String(parts[1]) + + guard let lockedUntilTimestamp = Int(lockedUntilTimestampStr) else { + throw HodlerPluginError.invalidHodlerData + } + + + return HodlerData(lockedUntilTimestamp: lockedUntilTimestamp, addressString: addressString) + } + + init(lockedUntilTimestamp: Int, addressString: String) { + self.lockedUntilTimestamp = lockedUntilTimestamp + self.addressString = addressString + } + + func toString() -> String { + "\(lockedUntilTimestamp)|\(addressString)" + } + +} diff --git a/Hodler/Hodler/Core/HolderPlugin.swift b/Hodler/Hodler/Core/HolderPlugin.swift index 83c3de4b..5b89dd2c 100644 --- a/Hodler/Hodler/Core/HolderPlugin.swift +++ b/Hodler/Hodler/Core/HolderPlugin.swift @@ -1,15 +1,27 @@ +import Foundation import BitcoinCore import HSCryptoKit -public class HodlerPlugin: IPlugin { - enum HodlerPluginError: Error { - case unsupportedAddress - } - +enum HodlerPluginError: Error { + case unsupportedAddress + case invalidHodlerData +} + +public class HodlerPlugin { + + public let id: UInt8 = OpCode.push(1)[0] + public init() {} - public func processOutputs(mutableTransaction: MutableTransaction, extraData: [String: [String: Any]], addressConverter: IAddressConverter) throws { - guard let hodlerData = extraData["hodler"], let timeLockParam = hodlerData["locked_until"], let unlockTime = timeLockParam as? Int else { + private func cltvRedeemScript(lockedUntil: Data, publicKeyHash: Data) -> Data { + OpCode.push(lockedUntil) + Data([OpCode.checkLockTimeVerify, OpCode.drop]) + OpCode.p2pkhStart + OpCode.push(publicKeyHash) + OpCode.p2pkhFinish + } +} + +extension HodlerPlugin: IPlugin { + + public func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]], addressConverter: IAddressConverter) throws { + guard let hodlerData = pluginData["hodler"], let timeLockParam = hodlerData["locked_until"], let unlockTime = timeLockParam as? Int else { return } @@ -18,12 +30,37 @@ public class HodlerPlugin: IPlugin { } let unlockTimeData = Data(from: UInt32(unlockTime)) - let redeemScript = OpCode.push(unlockTimeData) + Data([OpCode.checkLockTimeVerify, OpCode.drop]) + recipientAddress.lockingScript + let redeemScript = cltvRedeemScript(lockedUntil: unlockTimeData, publicKeyHash: recipientAddress.keyHash) let scriptHash = CryptoKit.sha256ripemd160(redeemScript) let newAddress = try addressConverter.convert(keyHash: scriptHash, type: .p2sh) mutableTransaction.recipientAddress = newAddress - mutableTransaction.add(extraData: OpCode.push(unlockTimeData) + OpCode.push(recipientAddress.keyHash), pluginId: 1) + mutableTransaction.add(pluginData: OpCode.push(unlockTimeData) + OpCode.push(recipientAddress.keyHash), pluginId: id) + } + + public func processTransactionWithNullData(transaction: FullTransaction, nullDataChunks: inout IndexingIterator<[Chunk]>, storage: IStorage, addressConverter: IAddressConverter) throws { + guard let lockedUntil = nullDataChunks.next()?.data, let publicKeyHash = nullDataChunks.next()?.data else { + throw HodlerPluginError.invalidHodlerData + } + + let redeemScript = cltvRedeemScript(lockedUntil: lockedUntil, publicKeyHash: publicKeyHash) + let redeemScriptHash = CryptoKit.sha256ripemd160(redeemScript) + + guard let output = transaction.outputs.first(where: { $0.keyHash == redeemScriptHash }) else { + return + } + + let p2pkhAddress = try addressConverter.convert(keyHash: publicKeyHash, type: .p2pkh) + let lockedUntilInt = lockedUntil.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: Int32.self).pointee } + + output.pluginId = id + output.pluginData = HodlerData(lockedUntilTimestamp: Int(lockedUntilInt), addressString: p2pkhAddress.stringValue).toString() + + if let publicKey = storage.publicKey(byRawOrKeyHash: publicKeyHash) { + output.redeemScript = redeemScript + output.publicKeyPath = publicKey.path + transaction.header.isMine = true + } } } From 83ad6663dab358fa30cb7dec12f84277f01bb4f1 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Thu, 10 Oct 2019 18:19:14 +0600 Subject: [PATCH 045/234] Filter currently time-locked transaction out in UnspentOutputProvider --- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 10 +++++++- BitcoinCore/BitcoinCore/Core/Protocols.swift | 3 +++ .../BitcoinCore/Managers/PluginManager.swift | 9 +++++++ .../Managers/UnspentOutputProvider.swift | 24 ++++++++++++------- BitcoinKit/BitcoinKit/Core/BitcoinKit.swift | 2 +- Hodler/Hodler/Core/HolderPlugin.swift | 14 +++++++++++ 6 files changed, 51 insertions(+), 11 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 056deab0..c74d6db4 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -12,6 +12,7 @@ public class BitcoinCoreBuilder { private var paymentAddressParser: IPaymentAddressParser? private var walletId: String? private var initialSyncApi: ISyncTransactionApi? + private var plugins = [IPlugin]() private var logger: Logger private var blockHeaderHasher: IHasher? @@ -90,6 +91,11 @@ public class BitcoinCoreBuilder { return self } + public func add(plugin: IPlugin) -> BitcoinCoreBuilder { + plugins.append(plugin) + return self + } + public init(minLogLevel: Logger.Level = .verbose) { self.logger = Logger(network: network, minLogLevel: minLogLevel) } @@ -124,12 +130,14 @@ public class BitcoinCoreBuilder { let restoreKeyConverterChain = RestoreKeyConverterChain() let pluginManager = PluginManager(addressConverter: addressConverter, scriptConverter: scriptConverter, storage: storage) + plugins.forEach { pluginManager.add(plugin: $0) } + // let dbName = "bitcoinkit-${network.javaClass}-$walletId" // let database = KitDatabase.getInstance(context, dbName) // let realmFactory = RealmFactory(dbName) // let storage = Storage(database, realmFactory) // - let unspentOutputProvider = UnspentOutputProvider(storage: storage, confirmationsThreshold: confirmationsThreshold) + let unspentOutputProvider = UnspentOutputProvider(storage: storage, pluginManager: pluginManager, confirmationsThreshold: confirmationsThreshold) let transactionInfoConverter = self.transactionInfoConverter ?? TransactionInfoConverter(baseTransactionInfoConverter: BaseTransactionInfoConverter()) let dataProvider = DataProvider(storage: storage, unspentOutputProvider: unspentOutputProvider, transactionInfoConverter: transactionInfoConverter) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index a0637345..051d0bf3 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -558,10 +558,13 @@ public protocol IPlugin { var id: UInt8 { get } func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]], addressConverter: IAddressConverter) throws func processTransactionWithNullData(transaction: FullTransaction, nullDataChunks: inout IndexingIterator<[Chunk]>, storage: IStorage, addressConverter: IAddressConverter) throws + func isSpendable(output: Output) throws -> Bool + func transactionLockTime(output: Output) throws -> Int } protocol IPluginManager { func add(plugin: IPlugin) func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]]) throws func processTransactionWithNullData(transaction: FullTransaction, nullDataOutput: Output) throws + func isSpendable(output: Output) -> Bool } diff --git a/BitcoinCore/BitcoinCore/Managers/PluginManager.swift b/BitcoinCore/BitcoinCore/Managers/PluginManager.swift index df0c7fa7..e5b466fe 100644 --- a/BitcoinCore/BitcoinCore/Managers/PluginManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PluginManager.swift @@ -39,4 +39,13 @@ extension PluginManager: IPluginManager { try plugin.processTransactionWithNullData(transaction: transaction, nullDataChunks: &iterator, storage: storage, addressConverter: addressConverter) } } + + func isSpendable(output: Output) -> Bool { + guard let pluginId = output.pluginId, let plugin = plugins[pluginId] else { + return true + } + + return (try? plugin.isSpendable(output: output)) ?? true + } + } diff --git a/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift b/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift index 13f3722e..b2b37cb1 100644 --- a/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift +++ b/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift @@ -1,16 +1,9 @@ class UnspentOutputProvider { let storage: IStorage + let pluginManager: IPluginManager let confirmationsThreshold: Int - init(storage: IStorage, confirmationsThreshold: Int) { - self.storage = storage - self.confirmationsThreshold = confirmationsThreshold - } -} - -extension UnspentOutputProvider: IUnspentOutputProvider { - - var allUnspentOutputs: [UnspentOutput] { + private var confirmedOutputs: [UnspentOutput] { let lastBlockHeight = storage.lastBlock?.height ?? 0 // Output must have a public key, that is, must belong to the user @@ -32,4 +25,17 @@ extension UnspentOutputProvider: IUnspentOutputProvider { }) } + init(storage: IStorage, pluginManager: IPluginManager, confirmationsThreshold: Int) { + self.storage = storage + self.pluginManager = pluginManager + self.confirmationsThreshold = confirmationsThreshold + } +} + +extension UnspentOutputProvider: IUnspentOutputProvider { + + var allUnspentOutputs: [UnspentOutput] { + confirmedOutputs.filter { pluginManager.isSpendable(output: $0.output) } + } + } \ No newline at end of file diff --git a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift index c746c11d..15b9e1c4 100644 --- a/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift +++ b/BitcoinKit/BitcoinKit/Core/BitcoinKit.swift @@ -53,6 +53,7 @@ public class BitcoinKit: AbstractKit { .set(peerSize: 10) .set(syncMode: syncMode) .set(storage: storage) + .add(plugin: HodlerPlugin()) .build() let scriptConverter = ScriptConverter() @@ -77,7 +78,6 @@ public class BitcoinKit: AbstractKit { } bitcoinCore.add(restoreKeyConverterForBip: bip) - bitcoinCore.add(plugin: HodlerPlugin()) if bip == .bip44 { bitcoinCore.add(restoreKeyConverterForBip: .bip49) bitcoinCore.add(restoreKeyConverterForBip: .bip84) diff --git a/Hodler/Hodler/Core/HolderPlugin.swift b/Hodler/Hodler/Core/HolderPlugin.swift index 5b89dd2c..392c26ce 100644 --- a/Hodler/Hodler/Core/HolderPlugin.swift +++ b/Hodler/Hodler/Core/HolderPlugin.swift @@ -63,4 +63,18 @@ extension HodlerPlugin: IPlugin { } } + public func isSpendable(output: Output) throws -> Bool { + let lockedUntilTimestamp = try transactionLockTime(output: output) + + return lockedUntilTimestamp < Int(Date().timeIntervalSince1970) + } + + public func transactionLockTime(output: Output) throws -> Int { + guard let pluginData = output.pluginData else { + throw HodlerPluginError.invalidHodlerData + } + + return try HodlerData.parse(serialized: pluginData).lockedUntilTimestamp + } + } From d6ad7b5ce100037e560bf32ec6f9ab5772fdf736 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 11 Oct 2019 11:22:23 +0600 Subject: [PATCH 046/234] Spend time-locked transactions --- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 1 + .../BitcoinCore/Managers/PluginManager.swift | 10 ++++++++++ .../Transactions/Builder/LockTimeSetter.swift | 9 ++++++--- .../Transactions/Builder/TransactionBuilder.swift | 2 +- .../Transactions/Builder/TransactionSigner.swift | 15 +++++++++++++-- 6 files changed, 32 insertions(+), 7 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index c74d6db4..d1d9261d 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -204,7 +204,7 @@ public class BitcoinCoreBuilder { let transactionSizeCalculator = TransactionSizeCalculator() let outputSetter = OutputSetter(addressConverter: addressConverter, factory: factory, pluginManager: pluginManager) let inputSetter = InputSetter(unspentOutputSelector: unspentOutputSelector, addressConverter: addressConverter, publicKeyManager: publicKeyManager, factory: factory, changeScriptType: bip.scriptType) - let lockTimeSetter = LockTimeSetter(storage: storage) + let lockTimeSetter = LockTimeSetter(storage: storage, pluginManager: pluginManager) let transactionSigner = TransactionSigner(inputSigner: inputSigner) let transactionBuilder = TransactionBuilder(inputSigner: inputSigner, factory: factory, outputSetter: outputSetter, inputSetter: inputSetter, lockTimeSetter: lockTimeSetter, signer: transactionSigner) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index 051d0bf3..cb639657 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -567,4 +567,5 @@ protocol IPluginManager { func processOutputs(mutableTransaction: MutableTransaction, pluginData: [String: [String: Any]]) throws func processTransactionWithNullData(transaction: FullTransaction, nullDataOutput: Output) throws func isSpendable(output: Output) -> Bool + func transactionLockTime(transaction: MutableTransaction) throws -> Int? } diff --git a/BitcoinCore/BitcoinCore/Managers/PluginManager.swift b/BitcoinCore/BitcoinCore/Managers/PluginManager.swift index e5b466fe..dadc990f 100644 --- a/BitcoinCore/BitcoinCore/Managers/PluginManager.swift +++ b/BitcoinCore/BitcoinCore/Managers/PluginManager.swift @@ -48,4 +48,14 @@ extension PluginManager: IPluginManager { return (try? plugin.isSpendable(output: output)) ?? true } + func transactionLockTime(transaction: MutableTransaction) throws -> Int? { + let lockTimes: [Int] = try transaction.inputsToSign.compactMap { inputToSign in + try inputToSign.previousOutput.pluginId.flatMap { pluginId in + try plugins[pluginId]?.transactionLockTime(output: inputToSign.previousOutput) + } + } + + return lockTimes.max() + } + } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/LockTimeSetter.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/LockTimeSetter.swift index 19e15d21..ce08eaab 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/LockTimeSetter.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/LockTimeSetter.swift @@ -1,12 +1,15 @@ class LockTimeSetter { private let storage: IStorage + private let pluginManager: IPluginManager - init(storage: IStorage) { + init(storage: IStorage, pluginManager: IPluginManager) { self.storage = storage + self.pluginManager = pluginManager } - func setLockTime(to mutableTransaction: MutableTransaction) { - mutableTransaction.transaction.lockTime = storage.lastBlock?.height ?? 0 + func setLockTime(to mutableTransaction: MutableTransaction) throws { + let pluginsMaxLockTime = try pluginManager.transactionLockTime(transaction: mutableTransaction) + mutableTransaction.transaction.lockTime = pluginsMaxLockTime ?? storage.lastBlock?.height ?? 0 } } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift index 5f8f29ef..fe941e83 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionBuilder.swift @@ -48,7 +48,7 @@ extension TransactionBuilder: ITransactionBuilder { try outputSetter.setOutputs(to: mutableTransaction, toAddress: toAddress, value: value, pluginData: pluginData) try inputSetter.setInputs(to: mutableTransaction, feeRate: feeRate, senderPay: senderPay) - lockTimeSetter.setLockTime(to: mutableTransaction) + try lockTimeSetter.setLockTime(to: mutableTransaction) try signer.sign(mutableTransaction: mutableTransaction) return mutableTransaction.build() diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionSigner.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionSigner.swift index 3094527d..604785e5 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionSigner.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/TransactionSigner.swift @@ -1,6 +1,7 @@ class TransactionSigner { enum SignError: Error { case notSupportedScriptType + case noRedeemScript } private let inputSigner: IInputSigner @@ -9,12 +10,16 @@ class TransactionSigner { self.inputSigner = inputSigner } + private func signatureScript(from sigScriptData: [Data]) -> Data { + sigScriptData.reduce(Data()) { $0 + OpCode.push($1) } + } + func sign(mutableTransaction: MutableTransaction) throws { for (index, inputToSign) in mutableTransaction.inputsToSign.enumerated() { let previousOutput = inputToSign.previousOutput let publicKey = inputToSign.previousOutputPublicKey - let sigScriptData = try inputSigner.sigScriptData( + var sigScriptData = try inputSigner.sigScriptData( transaction: mutableTransaction.transaction, inputsToSign: mutableTransaction.inputsToSign, outputs: mutableTransaction.outputs, @@ -23,7 +28,7 @@ class TransactionSigner { switch previousOutput.scriptType { case .p2pkh: - inputToSign.input.signatureScript = sigScriptData.reduce(Data()) { $0 + OpCode.push($1) } + inputToSign.input.signatureScript = signatureScript(from: sigScriptData) case .p2wpkh: mutableTransaction.transaction.segWit = true inputToSign.input.witnessData = sigScriptData @@ -31,6 +36,12 @@ class TransactionSigner { mutableTransaction.transaction.segWit = true inputToSign.input.witnessData = sigScriptData inputToSign.input.signatureScript = OpCode.push(OpCode.scriptWPKH(publicKey.keyHash)) + case .p2sh: + guard let redeemScript = previousOutput.redeemScript else { + throw SignError.noRedeemScript + } + sigScriptData.append(redeemScript) + inputToSign.input.signatureScript = signatureScript(from: sigScriptData) default: throw SignError.notSupportedScriptType } } From 119bf0323f7ccecfa34f464f95af91cb38b351db Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Fri, 11 Oct 2019 12:27:47 +0600 Subject: [PATCH 047/234] Addd BalanceInfo with "spendable" and "unspendable" balances --- .../BitcoinCashBlockValidatorHelper.swift | 2 +- .../BitcoinCore.xcodeproj/project.pbxproj | 8 +++---- .../BitcoinCore/Core/AbstractKit.swift | 2 +- .../BitcoinCore/Core/BitcoinCore.swift | 10 ++++---- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 2 +- .../BitcoinCore/Core/DataProvider.swift | 14 +++++------ BitcoinCore/BitcoinCore/Core/Protocols.swift | 10 +++++--- .../Managers/UnspentOutputProvider.swift | 23 +++++++++++++++---- .../Managers/UnspentOutputSelector.swift | 2 +- .../UnspentOutputSelectorSingleNoChange.swift | 2 +- ...ransactionInfo.swift => InfoObjects.swift} | 9 ++++++++ DashKit/DashKit/Core/DashKit.swift | 2 +- DashKit/DashKit/Core/Protocols.swift | 2 +- .../ConfirmedUnspentOutputProvider.swift | 2 +- Demo/Demo/Adapters/BaseAdapter.swift | 10 +++++--- Demo/Demo/Adapters/BitcoinAdapter.swift | 2 +- Demo/Demo/Adapters/BitcoinCashAdapter.swift | 2 +- Demo/Demo/Adapters/DashAdapter.swift | 2 +- Demo/Demo/Controllers/BalanceController.swift | 2 +- Demo/Demo/Controllers/Cells/BalanceCell.swift | 6 +++-- 20 files changed, 74 insertions(+), 40 deletions(-) rename BitcoinCore/BitcoinCore/Models/{TransactionInfo.swift => InfoObjects.swift} (79%) diff --git a/BitcoinCashKit/BitcoinCashKit/Blocks/BitcoinCashBlockValidatorHelper.swift b/BitcoinCashKit/BitcoinCashKit/Blocks/BitcoinCashBlockValidatorHelper.swift index 81e5b0ab..7a149a32 100644 --- a/BitcoinCashKit/BitcoinCashKit/Blocks/BitcoinCashBlockValidatorHelper.swift +++ b/BitcoinCashKit/BitcoinCashKit/Blocks/BitcoinCashBlockValidatorHelper.swift @@ -12,7 +12,7 @@ class BitcoinCashBlockValidatorHelper: IBitcoinCashBlockValidatorHelper { func medianTimePast(block: Block) -> Int { let startIndex = block.height - medianTimeSpan + 1 - var median = bitcoinCashStorage.timestamps(from: startIndex, to: block.height, ascending: true) + let median = bitcoinCashStorage.timestamps(from: startIndex, to: block.height, ascending: true) guard !median.isEmpty else { return block.timestamp } diff --git a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj index bb026e34..f412a891 100644 --- a/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj +++ b/BitcoinCore/BitcoinCore.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 11B35333093D726EC8ECA159 /* BlockSyncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B352A955F667DC0C23CE1C /* BlockSyncerTests.swift */; }; 11B3534C9529906826754D6E /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3501384C4274027587EB4 /* Protocols.swift */; }; 11B3536E9A39907FCA035C33 /* GrdbStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3543D7114E1FE55B2DCC3 /* GrdbStorage.swift */; }; - 11B35377C00CCB8EEED57179 /* TransactionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C9E6B02801B0422881 /* TransactionInfo.swift */; }; + 11B35377C00CCB8EEED57179 /* InfoObjects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B357C9E6B02801B0422881 /* InfoObjects.swift */; }; 11B3543B9CDA61125C2B7D78 /* ApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3597DE3169FD250A982D5 /* ApiManager.swift */; }; 11B35464997EBBD3B20BDEBB /* Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B35352F3DA4C26A552C62B /* Input.swift */; }; 11B35468A2ACF4B8534BB719 /* DataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11B3525D146F1119A0A9128A /* DataProvider.swift */; }; @@ -256,7 +256,7 @@ 11B355C0072CA13ADBAC1351 /* VarInt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VarInt.swift; sourceTree = ""; }; 11B356D308DCF4BB33D9F488 /* ServiceFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceFlags.swift; sourceTree = ""; }; 11B356EF3A4749163293CCA3 /* BlockSyncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BlockSyncer.swift; path = ../Blocks/BlockSyncer.swift; sourceTree = ""; }; - 11B357C9E6B02801B0422881 /* TransactionInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionInfo.swift; sourceTree = ""; }; + 11B357C9E6B02801B0422881 /* InfoObjects.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InfoObjects.swift; sourceTree = ""; }; 11B358481D7E7EB156912F52 /* StateManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateManager.swift; sourceTree = ""; }; 11B358F56F6298016B1A8AFD /* TestData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; 11B3590614D2772B1436542A /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; @@ -654,7 +654,7 @@ 2FA5DF037E951950A0F7FF03 /* SentTransaction.swift */, 58AAAA63CA9758D7D97E1416 /* SigHashType.swift */, 11B35C67DB63EF992C380513 /* Transaction.swift */, - 11B357C9E6B02801B0422881 /* TransactionInfo.swift */, + 11B357C9E6B02801B0422881 /* InfoObjects.swift */, ); path = Models; sourceTree = ""; @@ -1400,7 +1400,7 @@ 2FA5D4F27E10A609FF81950E /* UnknownMessage.swift in Sources */, 58AAAD1C062938F07E90EA9D /* Address.swift in Sources */, 2FA5D9ED521660FA5EEF360E /* MerkleBlock.swift in Sources */, - 11B35377C00CCB8EEED57179 /* TransactionInfo.swift in Sources */, + 11B35377C00CCB8EEED57179 /* InfoObjects.swift in Sources */, 2FA5D19E18DC7528778D26F0 /* PeerAddress.swift in Sources */, 11B35823EEDAE4C578F2EA02 /* RequestTransactionsTask.swift in Sources */, 11B35567A6F0430D0A95A389 /* PeerTask.swift in Sources */, diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index c0fc1fb4..e7d4d8a8 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -22,7 +22,7 @@ open class AbstractKit { bitcoinCore.lastBlockInfo } - open var balance: Int { + open var balance: BalanceInfo { bitcoinCore.balance } diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index b2377206..a2df5f70 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -151,7 +151,7 @@ extension BitcoinCore { dataProvider.lastBlockInfo } - public var balance: Int { + public var balance: BalanceInfo { dataProvider.balance } @@ -252,7 +252,7 @@ extension BitcoinCore: IDataProviderDelegate { } } - func balanceUpdated(balance: Int) { + func balanceUpdated(balance: BalanceInfo) { delegateQueue.async { [weak self] in if let kit = self { kit.delegate?.balanceUpdated(balance: balance) @@ -281,7 +281,7 @@ extension BitcoinCore: IKitStateProviderDelegate { public protocol BitcoinCoreDelegate: class { func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo]) func transactionsDeleted(hashes: [String]) - func balanceUpdated(balance: Int) + func balanceUpdated(balance: BalanceInfo) func lastBlockInfoUpdated(lastBlockInfo: BlockInfo) func kitStateUpdated(state: BitcoinCore.KitState) } @@ -290,7 +290,7 @@ extension BitcoinCoreDelegate { public func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo]) {} public func transactionsDeleted(hashes: [String]) {} - public func balanceUpdated(balance: Int) {} + public func balanceUpdated(balance: BalanceInfo) {} public func lastBlockInfoUpdated(lastBlockInfo: BlockInfo) {} public func kitStateUpdated(state: BitcoinCore.KitState) {} @@ -331,4 +331,4 @@ extension BitcoinCore.KitState { } } -} \ No newline at end of file +} diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index d1d9261d..2a3379ef 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -139,7 +139,7 @@ public class BitcoinCoreBuilder { // let unspentOutputProvider = UnspentOutputProvider(storage: storage, pluginManager: pluginManager, confirmationsThreshold: confirmationsThreshold) let transactionInfoConverter = self.transactionInfoConverter ?? TransactionInfoConverter(baseTransactionInfoConverter: BaseTransactionInfoConverter()) - let dataProvider = DataProvider(storage: storage, unspentOutputProvider: unspentOutputProvider, transactionInfoConverter: transactionInfoConverter) + let dataProvider = DataProvider(storage: storage, balanceProvider: unspentOutputProvider, transactionInfoConverter: transactionInfoConverter) let reachabilityManager = ReachabilityManager() diff --git a/BitcoinCore/BitcoinCore/Core/DataProvider.swift b/BitcoinCore/BitcoinCore/Core/DataProvider.swift index 135e6408..343f7417 100644 --- a/BitcoinCore/BitcoinCore/Core/DataProvider.swift +++ b/BitcoinCore/BitcoinCore/Core/DataProvider.swift @@ -8,12 +8,12 @@ class DataProvider { private let disposeBag = DisposeBag() private let storage: IStorage - private let unspentOutputProvider: IUnspentOutputProvider + private let balanceProvider: IBalanceProvider private let transactionInfoConverter: ITransactionInfoConverter private let balanceUpdateSubject = PublishSubject() - public var balance: Int = 0 { + public var balance: BalanceInfo { didSet { if !(oldValue == balance) { delegate?.balanceUpdated(balance: balance) @@ -24,15 +24,15 @@ class DataProvider { weak var delegate: IDataProviderDelegate? - init(storage: IStorage, unspentOutputProvider: IUnspentOutputProvider, transactionInfoConverter: ITransactionInfoConverter, throttleTimeMilliseconds: Int = 500) { + init(storage: IStorage, balanceProvider: IBalanceProvider, transactionInfoConverter: ITransactionInfoConverter, throttleTimeMilliseconds: Int = 500) { self.storage = storage - self.unspentOutputProvider = unspentOutputProvider + self.balanceProvider = balanceProvider self.transactionInfoConverter = transactionInfoConverter - self.balance = unspentOutputProvider.allUnspentOutputs.map { $0.output.value }.reduce(0, +) + self.balance = balanceProvider.balanceInfo self.lastBlockInfo = storage.lastBlock.map { blockInfo(fromBlock: $0) } balanceUpdateSubject.throttle(DispatchTimeInterval.milliseconds(throttleTimeMilliseconds), scheduler: ConcurrentDispatchQueueScheduler(qos: .background)).subscribe(onNext: { [weak self] in - self?.balance = unspentOutputProvider.allUnspentOutputs.map { $0.output.value }.reduce(0, +) + self?.balance = balanceProvider.balanceInfo }).disposed(by: disposeBag) } @@ -84,7 +84,7 @@ extension DataProvider: IBlockchainDataListener { extension DataProvider: IDataProvider { func transactions(fromHash: String?, limit: Int?) -> Single<[TransactionInfo]> { - return Single.create { observer in + Single.create { observer in var fromTimestamp: Int? = nil var fromOrder: Int? = nil diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index cb639657..b8d25221 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -388,7 +388,11 @@ public protocol IUnspentOutputSelector { } public protocol IUnspentOutputProvider { - var allUnspentOutputs: [UnspentOutput] { get } + var spendableUtxo: [UnspentOutput] { get } +} + +public protocol IBalanceProvider { + var balanceInfo: BalanceInfo { get } } public protocol IBlockSyncer: class { @@ -427,7 +431,7 @@ protocol IDataProvider { var delegate: IDataProviderDelegate? { get set } var lastBlockInfo: BlockInfo? { get } - var balance: Int { get } + var balance: BalanceInfo { get } func debugInfo(network: INetwork, scriptType: ScriptType, addressConverter: IAddressConverter) -> String func transactions(fromHash: String?, limit: Int?) -> Single<[TransactionInfo]> } @@ -435,7 +439,7 @@ protocol IDataProvider { protocol IDataProviderDelegate: class { func transactionsUpdated(inserted: [TransactionInfo], updated: [TransactionInfo]) func transactionsDeleted(hashes: [String]) - func balanceUpdated(balance: Int) + func balanceUpdated(balance: BalanceInfo) func lastBlockInfoUpdated(lastBlockInfo: BlockInfo) } diff --git a/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift b/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift index b2b37cb1..f67e6e81 100644 --- a/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift +++ b/BitcoinCore/BitcoinCore/Managers/UnspentOutputProvider.swift @@ -3,7 +3,7 @@ class UnspentOutputProvider { let pluginManager: IPluginManager let confirmationsThreshold: Int - private var confirmedOutputs: [UnspentOutput] { + private var confirmedUtxo: [UnspentOutput] { let lastBlockHeight = storage.lastBlock?.height ?? 0 // Output must have a public key, that is, must belong to the user @@ -25,6 +25,10 @@ class UnspentOutputProvider { }) } + private var unspendableUtxo: [UnspentOutput] { + confirmedUtxo.filter { !pluginManager.isSpendable(output: $0.output) } + } + init(storage: IStorage, pluginManager: IPluginManager, confirmationsThreshold: Int) { self.storage = storage self.pluginManager = pluginManager @@ -34,8 +38,19 @@ class UnspentOutputProvider { extension UnspentOutputProvider: IUnspentOutputProvider { - var allUnspentOutputs: [UnspentOutput] { - confirmedOutputs.filter { pluginManager.isSpendable(output: $0.output) } + var spendableUtxo: [UnspentOutput] { + confirmedUtxo.filter { pluginManager.isSpendable(output: $0.output) } } -} \ No newline at end of file +} + +extension UnspentOutputProvider: IBalanceProvider { + + var balanceInfo: BalanceInfo { + let spendable = spendableUtxo.map { $0.output.value }.reduce(0, +) + let unspendable = unspendableUtxo.map { $0.output.value }.reduce(0, +) + + return BalanceInfo(spendable: spendable, unspendable: unspendable) + } + +} diff --git a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift index ff01d538..b3bec9b7 100644 --- a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift +++ b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift @@ -31,7 +31,7 @@ public class UnspentOutputSelector { extension UnspentOutputSelector: IUnspentOutputSelector { public func select(value: Int, feeRate: Int, outputScriptType: ScriptType = .p2pkh, changeType: ScriptType = .p2pkh, senderPay: Bool) throws -> SelectedUnspentOutputInfo { - let unspentOutputs = provider.allUnspentOutputs + let unspentOutputs = provider.spendableUtxo guard value > 0 else { throw BitcoinCoreErrors.UnspentOutputSelection.wrongValue diff --git a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift index a3e7a986..d163315f 100644 --- a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift +++ b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift @@ -15,7 +15,7 @@ public class UnspentOutputSelectorSingleNoChange { extension UnspentOutputSelectorSingleNoChange: IUnspentOutputSelector { public func select(value: Int, feeRate: Int, outputScriptType: ScriptType = .p2pkh, changeType: ScriptType = .p2pkh, senderPay: Bool) throws -> SelectedUnspentOutputInfo { - let unspentOutputs = provider.allUnspentOutputs + let unspentOutputs = provider.spendableUtxo guard value > 0 else { throw BitcoinCoreErrors.UnspentOutputSelection.wrongValue diff --git a/BitcoinCore/BitcoinCore/Models/TransactionInfo.swift b/BitcoinCore/BitcoinCore/Models/InfoObjects.swift similarity index 79% rename from BitcoinCore/BitcoinCore/Models/TransactionInfo.swift rename to BitcoinCore/BitcoinCore/Models/InfoObjects.swift index de44efef..c1b19c28 100644 --- a/BitcoinCore/BitcoinCore/Models/TransactionInfo.swift +++ b/BitcoinCore/BitcoinCore/Models/InfoObjects.swift @@ -33,3 +33,12 @@ public struct BlockInfo { public let height: Int public let timestamp: Int? } + +public struct BalanceInfo : Equatable { + public let spendable: Int + public let unspendable: Int + + public static func ==(lhs: BalanceInfo, rhs: BalanceInfo) -> Bool { + lhs.spendable == rhs.spendable && lhs.unspendable == rhs.unspendable + } +} diff --git a/DashKit/DashKit/Core/DashKit.swift b/DashKit/DashKit/Core/DashKit.swift index a0ae1778..17dba806 100644 --- a/DashKit/DashKit/Core/DashKit.swift +++ b/DashKit/DashKit/Core/DashKit.swift @@ -167,7 +167,7 @@ extension DashKit: BitcoinCoreDelegate { delegate?.transactionsDeleted(hashes: hashes) } - public func balanceUpdated(balance: Int) { + public func balanceUpdated(balance: BalanceInfo) { delegate?.balanceUpdated(balance: balance) } diff --git a/DashKit/DashKit/Core/Protocols.swift b/DashKit/DashKit/Core/Protocols.swift index e4cca286..c9f3fad4 100644 --- a/DashKit/DashKit/Core/Protocols.swift +++ b/DashKit/DashKit/Core/Protocols.swift @@ -51,7 +51,7 @@ protocol IDashPeer: IPeer { public protocol DashKitDelegate: class { func transactionsUpdated(inserted: [DashTransactionInfo], updated: [DashTransactionInfo]) func transactionsDeleted(hashes: [String]) - func balanceUpdated(balance: Int) + func balanceUpdated(balance: BalanceInfo) func lastBlockInfoUpdated(lastBlockInfo: BlockInfo) func kitStateUpdated(state: BitcoinCore.KitState) } diff --git a/DashKit/DashKit/Managers/ConfirmedUnspentOutputProvider.swift b/DashKit/DashKit/Managers/ConfirmedUnspentOutputProvider.swift index 4724eb18..422f4727 100644 --- a/DashKit/DashKit/Managers/ConfirmedUnspentOutputProvider.swift +++ b/DashKit/DashKit/Managers/ConfirmedUnspentOutputProvider.swift @@ -13,7 +13,7 @@ class ConfirmedUnspentOutputProvider { extension ConfirmedUnspentOutputProvider: IUnspentOutputProvider { - var allUnspentOutputs: [UnspentOutput] { + var spendableUtxo: [UnspentOutput] { let lastBlockHeight = storage.lastBlock?.height ?? 0 // Output must have a public key, that is, must belong to the user diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 2c6aca01..5c397002 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -89,8 +89,12 @@ extension BaseAdapter { } } - var balance: Decimal { - Decimal(abstractKit.balance) / coinRate + var spendableBalance: Decimal { + Decimal(abstractKit.balance.spendable) / coinRate + } + + var unspendableBalance: Decimal { + Decimal(abstractKit.balance.unspendable) / coinRate } var lastBlockInfo: BlockInfo? { @@ -131,7 +135,7 @@ extension BaseAdapter { } func availableBalance(for address: String?) -> Decimal { - max(0, balance - fee(for: balance, address: address)) + max(0, spendableBalance - fee(for: spendableBalance, address: address)) } func fee(for value: Decimal, address: String?) -> Decimal { diff --git a/Demo/Demo/Adapters/BitcoinAdapter.swift b/Demo/Demo/Adapters/BitcoinAdapter.swift index 1b3c3276..aecad01d 100644 --- a/Demo/Demo/Adapters/BitcoinAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinAdapter.swift @@ -29,7 +29,7 @@ extension BitcoinAdapter: BitcoinCoreDelegate { transactionsSignal.notify() } - func balanceUpdated(balance: Int) { + func balanceUpdated(balance: BalanceInfo) { balanceSignal.notify() } diff --git a/Demo/Demo/Adapters/BitcoinCashAdapter.swift b/Demo/Demo/Adapters/BitcoinCashAdapter.swift index ed0cb1f4..5a939c01 100644 --- a/Demo/Demo/Adapters/BitcoinCashAdapter.swift +++ b/Demo/Demo/Adapters/BitcoinCashAdapter.swift @@ -28,7 +28,7 @@ extension BitcoinCashAdapter: BitcoinCoreDelegate { transactionsSignal.notify() } - func balanceUpdated(balance: Int) { + func balanceUpdated(balance: BalanceInfo) { balanceSignal.notify() } diff --git a/Demo/Demo/Adapters/DashAdapter.swift b/Demo/Demo/Adapters/DashAdapter.swift index d04cc01d..3424666a 100644 --- a/Demo/Demo/Adapters/DashAdapter.swift +++ b/Demo/Demo/Adapters/DashAdapter.swift @@ -48,7 +48,7 @@ extension DashAdapter: DashKitDelegate { transactionsSignal.notify() } - func balanceUpdated(balance: Int) { + func balanceUpdated(balance: BalanceInfo) { balanceSignal.notify() } diff --git a/Demo/Demo/Controllers/BalanceController.swift b/Demo/Demo/Controllers/BalanceController.swift index 68f359f5..42cd063c 100644 --- a/Demo/Demo/Controllers/BalanceController.swift +++ b/Demo/Demo/Controllers/BalanceController.swift @@ -70,7 +70,7 @@ class BalanceController: UITableViewController { } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 160 + return 175 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { diff --git a/Demo/Demo/Controllers/Cells/BalanceCell.swift b/Demo/Demo/Controllers/Cells/BalanceCell.swift index 71c7db71..790490e0 100644 --- a/Demo/Demo/Controllers/Cells/BalanceCell.swift +++ b/Demo/Demo/Controllers/Cells/BalanceCell.swift @@ -40,14 +40,16 @@ class BalanceCell: UITableViewCell { Sync state: Last block: - Balance: + Spendable balance: + Unspendable balance: """, alignment: .left, label: titleLabel) set(string: """ \(syncStateString) \(lastBlockHeightString) \(lastBlockDateString) - \(adapter.balance) \(adapter.coinCode) + \(adapter.spendableBalance) \(adapter.coinCode) + \(adapter.unspendableBalance) \(adapter.coinCode) """, alignment: .right, label: valueLabel) } From 4fa4b21d0231afb3c2a1a08b60774acf04e0d8a4 Mon Sep 17 00:00:00 2001 From: Kydyr uulu Esenbek Date: Sat, 12 Oct 2019 18:29:03 +0600 Subject: [PATCH 048/234] Add nullData output size on fee calculation --- .../BitcoinCore/Core/AbstractKit.swift | 4 +- .../BitcoinCore/Core/BitcoinCore.swift | 8 +- .../BitcoinCore/Core/BitcoinCoreBuilder.swift | 6 +- BitcoinCore/BitcoinCore/Core/Protocols.swift | 6 +- .../Core/TransactionInfoConverter.swift | 2 +- .../Helpers/UnspentOutputSelectorChain.swift | 4 +- .../Managers/UnspentOutputSelector.swift | 6 +- .../UnspentOutputSelectorSingleNoChange.swift | 4 +- BitcoinCore/BitcoinCore/Models/Output.swift | 1 - .../Transactions/Builder/InputSetter.swift | 7 +- .../Builder/MutableTransaction.swift | 2 +- .../TransactionFeeCalculator.swift | 38 ++++++---- .../TransactionSizeCalculator.swift | 23 ++++-- Demo/Demo/Adapters/BaseAdapter.swift | 4 +- Demo/Demo/Controllers/SendController.swift | 53 ++++++++++++- Demo/Demo/Controllers/SendController.xib | 76 +++++++++++++++---- 16 files changed, 183 insertions(+), 61 deletions(-) diff --git a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift index e7d4d8a8..ddf06669 100644 --- a/BitcoinCore/BitcoinCore/Core/AbstractKit.swift +++ b/BitcoinCore/BitcoinCore/Core/AbstractKit.swift @@ -54,8 +54,8 @@ open class AbstractKit { bitcoinCore.parse(paymentAddress: paymentAddress) } - open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate) + open func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, pluginData: [String: [String: Any]] = [:]) throws -> Int { + try bitcoinCore.fee(for: value, toAddress: toAddress, senderPay: senderPay, feeRate: feeRate, pluginData: pluginData) } open func receiveAddress() -> String { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift index a2df5f70..7c3efb4c 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCore.swift @@ -183,12 +183,8 @@ extension BitcoinCore { paymentAddressParser.parse(paymentAddress: paymentAddress) } - public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int) throws -> Int { - let toAddress = try toAddress.map { try addressConverter.convert(address: $0) } - let changePubKey = try publicKeyManager.changePublicKey() - let changeAddress = try addressConverter.convert(publicKey: changePubKey, type: bip.scriptType) - - return try transactionFeeCalculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, changeAddress: changeAddress) + public func fee(for value: Int, toAddress: String? = nil, senderPay: Bool, feeRate: Int, pluginData: [String: [String: Any]] = [:]) throws -> Int { + try transactionFeeCalculator.fee(for: value, feeRate: feeRate, senderPay: senderPay, toAddress: toAddress, pluginData: pluginData) } public func receiveAddress() -> String { diff --git a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift index 2a3379ef..bd5b8094 100644 --- a/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift +++ b/BitcoinCore/BitcoinCore/Core/BitcoinCoreBuilder.swift @@ -206,9 +206,9 @@ public class BitcoinCoreBuilder { let inputSetter = InputSetter(unspentOutputSelector: unspentOutputSelector, addressConverter: addressConverter, publicKeyManager: publicKeyManager, factory: factory, changeScriptType: bip.scriptType) let lockTimeSetter = LockTimeSetter(storage: storage, pluginManager: pluginManager) let transactionSigner = TransactionSigner(inputSigner: inputSigner) - let transactionBuilder = TransactionBuilder(inputSigner: inputSigner, factory: factory, - outputSetter: outputSetter, inputSetter: inputSetter, lockTimeSetter: lockTimeSetter, signer: transactionSigner) - let transactionFeeCalculator = TransactionFeeCalculator(unspentOutputSelector: unspentOutputSelector, transactionSizeCalculator: transactionSizeCalculator) + let transactionBuilder = TransactionBuilder(inputSigner: inputSigner, factory: factory, outputSetter: outputSetter, inputSetter: inputSetter, lockTimeSetter: lockTimeSetter, signer: transactionSigner) + let transactionFeeCalculator = TransactionFeeCalculator(unspentOutputSelector: unspentOutputSelector, transactionSizeCalculator: transactionSizeCalculator, outputSetter: outputSetter, inputSetter: inputSetter, + addressConverter: addressConverter, publicKeyManager: publicKeyManager, changeScriptType: bip.scriptType) let transactionSender = TransactionSender(transactionSyncer: transactionSyncer, peerManager: peerManager, initialBlockDownload: initialBlockDownload, syncedReadyPeerManager: syncedReadyPeerManager, logger: logger) let transactionCreator = TransactionCreator(transactionBuilder: transactionBuilder, transactionProcessor: transactionProcessor, transactionSender: transactionSender, transactionFeeCalculator: transactionFeeCalculator, bloomFilterManager: bloomFilterManager, addressConverter: addressConverter, storage: storage) diff --git a/BitcoinCore/BitcoinCore/Core/Protocols.swift b/BitcoinCore/BitcoinCore/Core/Protocols.swift index b8d25221..d6485aed 100644 --- a/BitcoinCore/BitcoinCore/Core/Protocols.swift +++ b/BitcoinCore/BitcoinCore/Core/Protocols.swift @@ -351,8 +351,7 @@ protocol ITransactionBuilder { } protocol ITransactionFeeCalculator { - func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int - func feeWithUnspentOutputs(value: Int, feeRate: Int, toScriptType: ScriptType, changeScriptType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo + func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: String?, pluginData: [String: [String: Any]]) throws -> Int func fee(inputScriptType: ScriptType, outputScriptType: ScriptType, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) -> Int } @@ -378,13 +377,14 @@ protocol IInputSigner { public protocol ITransactionSizeCalculator { func transactionSize(inputs: [ScriptType], outputScriptTypes: [ScriptType]) -> Int + func transactionSize(inputs: [ScriptType], outputScriptTypes: [ScriptType], pluginDataOutputSize: Int) -> Int func outputSize(type: ScriptType) -> Int func inputSize(type: ScriptType) -> Int func toBytes(fee: Int) -> Int } public protocol IUnspentOutputSelector { - func select(value: Int, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo + func select(value: Int, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, senderPay: Bool, pluginDataOutputSize: Int) throws -> SelectedUnspentOutputInfo } public protocol IUnspentOutputProvider { diff --git a/BitcoinCore/BitcoinCore/Core/TransactionInfoConverter.swift b/BitcoinCore/BitcoinCore/Core/TransactionInfoConverter.swift index b4e6fdf0..910cc28f 100644 --- a/BitcoinCore/BitcoinCore/Core/TransactionInfoConverter.swift +++ b/BitcoinCore/BitcoinCore/Core/TransactionInfoConverter.swift @@ -6,7 +6,7 @@ open class TransactionInfoConverter: ITransactionInfoConverter { } public func transactionInfo(fromTransaction transactionForInfo: FullTransactionForInfo) -> TransactionInfo { - return baseTransactionInfoConverter.transactionInfo(fromTransaction: transactionForInfo) + baseTransactionInfoConverter.transactionInfo(fromTransaction: transactionForInfo) } } diff --git a/BitcoinCore/BitcoinCore/Helpers/UnspentOutputSelectorChain.swift b/BitcoinCore/BitcoinCore/Helpers/UnspentOutputSelectorChain.swift index a95c1379..fc0f25fa 100644 --- a/BitcoinCore/BitcoinCore/Helpers/UnspentOutputSelectorChain.swift +++ b/BitcoinCore/BitcoinCore/Helpers/UnspentOutputSelectorChain.swift @@ -1,12 +1,12 @@ class UnspentOutputSelectorChain: IUnspentOutputSelector { var concreteSelectors = [IUnspentOutputSelector]() - func select(value: Int, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo { + func select(value: Int, feeRate: Int, outputScriptType: ScriptType, changeType: ScriptType, senderPay: Bool, pluginDataOutputSize: Int) throws -> SelectedUnspentOutputInfo { var lastError: Error = BitcoinCoreErrors.Unexpected.unkown for selector in concreteSelectors { do { - return try selector.select(value: value, feeRate: feeRate, outputScriptType: outputScriptType, changeType: changeType, senderPay: senderPay) + return try selector.select(value: value, feeRate: feeRate, outputScriptType: outputScriptType, changeType: changeType, senderPay: senderPay, pluginDataOutputSize: pluginDataOutputSize) } catch { lastError = error } diff --git a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift index b3bec9b7..ac77bf4d 100644 --- a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift +++ b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelector.swift @@ -30,7 +30,7 @@ public class UnspentOutputSelector { extension UnspentOutputSelector: IUnspentOutputSelector { - public func select(value: Int, feeRate: Int, outputScriptType: ScriptType = .p2pkh, changeType: ScriptType = .p2pkh, senderPay: Bool) throws -> SelectedUnspentOutputInfo { + public func select(value: Int, feeRate: Int, outputScriptType: ScriptType = .p2pkh, changeType: ScriptType = .p2pkh, senderPay: Bool, pluginDataOutputSize: Int) throws -> SelectedUnspentOutputInfo { let unspentOutputs = provider.spendableUtxo guard value > 0 else { @@ -66,7 +66,7 @@ extension UnspentOutputSelector: IUnspentOutputSelector { totalValue -= outputValueToExclude } } - lastCalculatedFee = calculator.transactionSize(inputs: selectedOutputScriptTypes, outputScriptTypes: [outputScriptType]) * feeRate + lastCalculatedFee = calculator.transactionSize(inputs: selectedOutputScriptTypes, outputScriptTypes: [outputScriptType], pluginDataOutputSize: pluginDataOutputSize) * feeRate if senderPay { fee = lastCalculatedFee } @@ -83,7 +83,7 @@ extension UnspentOutputSelector: IUnspentOutputSelector { // if total selected unspentOutputs value more than value and fee for transaction with change output + change input -> add fee for change output and mark as need change address var addChangeOutput = false if totalValue > value + lastCalculatedFee + (senderPay ? dust : 0) { - lastCalculatedFee = calculator.transactionSize(inputs: selectedOutputScriptTypes, outputScriptTypes: [outputScriptType, changeType]) * feeRate + lastCalculatedFee = calculator.transactionSize(inputs: selectedOutputScriptTypes, outputScriptTypes: [outputScriptType, changeType], pluginDataOutputSize: pluginDataOutputSize) * feeRate addChangeOutput = true } else if senderPay { lastCalculatedFee = totalValue - value diff --git a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift index d163315f..946889b0 100644 --- a/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift +++ b/BitcoinCore/BitcoinCore/Managers/UnspentOutputSelectorSingleNoChange.swift @@ -14,7 +14,7 @@ public class UnspentOutputSelectorSingleNoChange { extension UnspentOutputSelectorSingleNoChange: IUnspentOutputSelector { - public func select(value: Int, feeRate: Int, outputScriptType: ScriptType = .p2pkh, changeType: ScriptType = .p2pkh, senderPay: Bool) throws -> SelectedUnspentOutputInfo { + public func select(value: Int, feeRate: Int, outputScriptType: ScriptType = .p2pkh, changeType: ScriptType = .p2pkh, senderPay: Bool, pluginDataOutputSize: Int) throws -> SelectedUnspentOutputInfo { let unspentOutputs = provider.spendableUtxo guard value > 0 else { @@ -28,7 +28,7 @@ extension UnspentOutputSelectorSingleNoChange: IUnspentOutputSelector { // try to find 1 unspent output with exactly matching value for unspentOutput in unspentOutputs { let output = unspentOutput.output - let fee = calculator.transactionSize(inputs: [output.scriptType], outputScriptTypes: [outputScriptType]) * feeRate + let fee = calculator.transactionSize(inputs: [output.scriptType], outputScriptTypes: [outputScriptType], pluginDataOutputSize: pluginDataOutputSize) * feeRate let totalFee = senderPay ? fee : 0 if (value + totalFee <= output.value) && (value + totalFee + dust > output.value) { return SelectedUnspentOutputInfo(unspentOutputs: [unspentOutput], totalValue: output.value, fee: senderPay ? (output.value - value) : fee, addChangeOutput: false) diff --git a/BitcoinCore/BitcoinCore/Models/Output.swift b/BitcoinCore/BitcoinCore/Models/Output.swift index e58e15b0..13e87842 100644 --- a/BitcoinCore/BitcoinCore/Models/Output.swift +++ b/BitcoinCore/BitcoinCore/Models/Output.swift @@ -12,7 +12,6 @@ public enum ScriptType: Int, DatabaseValueConvertible { case .p2wsh: return 34 case .p2wpkh: return 22 case .p2wpkhSh: return 23 - case .nullData: return 26 default: return 0 } } diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift index d7a29fb3..59c8a230 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/InputSetter.swift @@ -16,8 +16,11 @@ class InputSetter { func setInputs(to mutableTransaction: MutableTransaction, feeRate: Int, senderPay: Bool) throws { let value = mutableTransaction.recipientValue - _ = mutableTransaction.pluginDataOutputSize - let unspentOutputInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: mutableTransaction.recipientAddress.scriptType, changeType: changeScriptType, senderPay: senderPay) + let unspentOutputInfo = try unspentOutputSelector.select( + value: value, feeRate: feeRate, + outputScriptType: mutableTransaction.recipientAddress.scriptType, changeType: changeScriptType, + senderPay: senderPay, pluginDataOutputSize: mutableTransaction.pluginDataOutputSize + ) let unspentOutputs = unspentOutputInfo.unspentOutputs for unspentOutput in unspentOutputs { diff --git a/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift b/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift index f81081c3..8e2d8dc8 100644 --- a/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift +++ b/BitcoinCore/BitcoinCore/Transactions/Builder/MutableTransaction.swift @@ -38,7 +38,7 @@ public class MutableTransaction { } var pluginDataOutputSize: Int { - 0 + pluginData.count > 0 ? 1 + pluginData.reduce(into: 0) { $0 += 1 + $1.value.count } : 0 // OP_RETURN (PLUGIN_ID PLUGIN_DATA) } init() { diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift index 606f5dab..2889477c 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionFeeCalculator.swift @@ -2,35 +2,47 @@ class TransactionFeeCalculator { private let unspentOutputSelector: IUnspentOutputSelector private let transactionSizeCalculator: ITransactionSizeCalculator - - init(unspentOutputSelector: IUnspentOutputSelector, transactionSizeCalculator: ITransactionSizeCalculator) { + private let outputSetter: OutputSetter + private let inputSetter: InputSetter + private let addressConverter: IAddressConverter + private let publicKeyManager: IPublicKeyManager + private let changeScriptType: ScriptType + + init(unspentOutputSelector: IUnspentOutputSelector, transactionSizeCalculator: ITransactionSizeCalculator, outputSetter: OutputSetter, inputSetter: InputSetter, + addressConverter: IAddressConverter, publicKeyManager: IPublicKeyManager, changeScriptType: ScriptType) { self.unspentOutputSelector = unspentOutputSelector self.transactionSizeCalculator = transactionSizeCalculator + self.outputSetter = outputSetter + self.inputSetter = inputSetter + self.addressConverter = addressConverter + self.publicKeyManager = publicKeyManager + self.changeScriptType = changeScriptType } + private func sampleAddress() throws -> String { + try addressConverter.convert(publicKey: try publicKeyManager.changePublicKey(), type: changeScriptType).stringValue + } } extension TransactionFeeCalculator: ITransactionFeeCalculator { - func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: Address?, changeAddress: Address) throws -> Int { - var outputScriptType = changeAddress.scriptType - if let address = toAddress { - outputScriptType = address.scriptType - } + func fee(for value: Int, feeRate: Int, senderPay: Bool, toAddress: String?, pluginData: [String: [String: Any]] = [:]) throws -> Int { + let mutableTransaction = MutableTransaction() - let selectedOutputsInfo = try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: outputScriptType, changeType: changeAddress.scriptType, senderPay: senderPay) - return selectedOutputsInfo.fee - } + try outputSetter.setOutputs(to: mutableTransaction, toAddress: toAddress ?? (try sampleAddress()), value: value, pluginData: pluginData) + try inputSetter.setInputs(to: mutableTransaction, feeRate: feeRate, senderPay: senderPay) + + let inputsTotalValue = mutableTransaction.inputsToSign.reduce(0) { total, input in total + input.previousOutput.value } + let outputsTotalValue = mutableTransaction.recipientValue + mutableTransaction.changeValue - func feeWithUnspentOutputs(value: Int, feeRate: Int, toScriptType: ScriptType, changeScriptType: ScriptType, senderPay: Bool) throws -> SelectedUnspentOutputInfo { - return try unspentOutputSelector.select(value: value, feeRate: feeRate, outputScriptType: toScriptType, changeType: changeScriptType, senderPay: senderPay) + return inputsTotalValue - outputsTotalValue } func fee(inputScriptType: ScriptType, outputScriptType: ScriptType, feeRate: Int, signatureScriptFunction: (Data, Data) -> Data) -> Int { let emptySignature = Data(repeating: 0, count: TransactionSizeCalculator.signatureLength) let emptyPublicKey = Data(repeating: 0, count: TransactionSizeCalculator.pubKeyLength) - let transactionSize = transactionSizeCalculator.transactionSize(inputs: [inputScriptType], outputScriptTypes: [outputScriptType]) + + let transactionSize = transactionSizeCalculator.transactionSize(inputs: [inputScriptType], outputScriptTypes: [outputScriptType], pluginDataOutputSize: 0) + signatureScriptFunction(emptySignature, emptyPublicKey).count return transactionSize * feeRate diff --git a/BitcoinCore/BitcoinCore/Transactions/TransactionSizeCalculator.swift b/BitcoinCore/BitcoinCore/Transactions/TransactionSizeCalculator.swift index 128791e4..69e4aa06 100644 --- a/BitcoinCore/BitcoinCore/Transactions/TransactionSizeCalculator.swift +++ b/BitcoinCore/BitcoinCore/Transactions/TransactionSizeCalculator.swift @@ -1,4 +1,4 @@ -public class TransactionSizeCalculator: ITransactionSizeCalculator { +public class TransactionSizeCalculator { static let legacyTx = 16 + 4 + 4 + 16 //40 Version + number of inputs + number of outputs + locktime static let legacyWitnessData = 1 //1 Only 0x00 for legacy input static let witnessData = 1 + signatureLength + pubKeyLength //108 Number of stack items for input + Size of stack item 0 + Stack item 0, signature + Size of stack item 1 + Stack item 1, pubkey @@ -10,7 +10,18 @@ public class TransactionSizeCalculator: ITransactionSizeCalculator { public init() {} + private func outputSize(lockingScriptSize: Int) -> Int { + 8 + 1 + lockingScriptSize // spentValue + scriptLength + script + } +} + +extension TransactionSizeCalculator: ITransactionSizeCalculator { + public func transactionSize(inputs: [ScriptType], outputScriptTypes: [ScriptType]) -> Int { // in real bytes upped to int + transactionSize(inputs: inputs, outputScriptTypes: outputScriptTypes, pluginDataOutputSize: 0) + } + + public func transactionSize(inputs: [ScriptType], outputScriptTypes: [ScriptType], pluginDataOutputSize: Int) -> Int { // in real bytes upped to int var segWit = false var inputWeight = 0 @@ -28,15 +39,17 @@ public class TransactionSizeCalculator: ITransactionSizeCalculator { } } - let outputWeight = outputScriptTypes.reduce(0) { $0 + outputSize(type: $1) } * 4 // to vbytes + var outputWeight: Int = outputScriptTypes.reduce(0) { $0 + outputSize(type: $1) } * 4 // in vbytes + if pluginDataOutputSize > 0 { + outputWeight += outputSize(lockingScriptSize: pluginDataOutputSize) * 4 + } let txWeight = segWit ? TransactionSizeCalculator.witnessTx : TransactionSizeCalculator.legacyTx return toBytes(fee: txWeight + inputWeight + outputWeight) } public func outputSize(type: ScriptType) -> Int { // in real bytes - let outputTxSize: Int = 8 + 1 + Int(type.size) // spentValue + scriptLength + script - return outputTxSize + outputSize(lockingScriptSize: Int(type.size)) } public func inputSize(type: ScriptType) -> Int { // in real bytes @@ -59,7 +72,7 @@ public class TransactionSizeCalculator: ITransactionSizeCalculator { } public func toBytes(fee: Int) -> Int { - return fee / 4 + (fee % 4 == 0 ? 0 : 1) + fee / 4 + (fee % 4 == 0 ? 0 : 1) } } diff --git a/Demo/Demo/Adapters/BaseAdapter.swift b/Demo/Demo/Adapters/BaseAdapter.swift index 5c397002..902f686b 100644 --- a/Demo/Demo/Adapters/BaseAdapter.swift +++ b/Demo/Demo/Adapters/BaseAdapter.swift @@ -138,10 +138,10 @@ extension BaseAdapter { max(0, spendableBalance - fee(for: spendableBalance, address: address)) } - func fee(for value: Decimal, address: String?) -> Decimal { + func fee(for value: Decimal, address: String?, pluginData: [String: [String: Any]] = [:]) -> Decimal { do { let amount = convertToSatoshi(value: value) - let fee = try abstractKit.fee(for: amount, toAddress: address, senderPay: true, feeRate: feeRate) + let fee = try abstractKit.fee(for: amount, toAddress: address, senderPay: true, feeRate: feeRate, pluginData: pluginData) return Decimal(fee) / coinRate } catch BitcoinCoreErrors.UnspentOutputSelection.notEnough(let maxFee) { return Decimal(maxFee) / coinRate diff --git a/Demo/Demo/Controllers/SendController.swift b/Demo/Demo/Controllers/SendController.swift index 15c1bee9..a371fa26 100644 --- a/Demo/Demo/Controllers/SendController.swift +++ b/Demo/Demo/Controllers/SendController.swift @@ -7,10 +7,13 @@ class SendController: UIViewController { @IBOutlet weak var addressTextField: UITextField? @IBOutlet weak var amountTextField: UITextField? @IBOutlet weak var coinLabel: UILabel? + @IBOutlet weak var feeLabel: UILabel? + @IBOutlet weak var timeLockSwitch: UISwitch? @IBOutlet weak var datePicker: UIDatePicker? private var adapters = [BaseAdapter]() private let segmentedControl = UISegmentedControl() + private var timeLockEnabled = false override func viewDidLoad() { super.viewDidLoad() @@ -42,17 +45,63 @@ class SendController: UIViewController { segmentedControl.selectedSegmentIndex = 0 segmentedControl.sendActions(for: .valueChanged) } + + private func updateFee() { + guard let address = addressTextField?.text else { + feeLabel?.text = "Fee: " + return + } + + do { + try currentAdapter?.validate(address: address) + } catch { + feeLabel?.text = "Fee: " + return + } + + guard let amountString = amountTextField?.text, let amount = Decimal(string: amountString) else { + feeLabel?.text = "Fee: " + return + } + + var pluginData = [String: [String: Any]]() + if let lockUntil = datePicker?.date, timeLockEnabled { + pluginData["hodler"] = ["locked_until": Int(lockUntil.timeIntervalSince1970)] + } + + if let fee = currentAdapter?.fee(for: amount, address: address, pluginData: pluginData) { + feeLabel?.text = "Fee: \(fee)" + } + } override func touchesEnded(_ touches: Set, with event: UIEvent?) { super.touchesEnded(touches, with: event) view.endEditing(true) } - + @objc func onSegmentChanged() { coinLabel?.text = currentAdapter?.coinCode + updateFee() } + @IBAction func onAddressEditEnded(_ sender: Any) { + updateFee() + } + + @IBAction func onAmountEditEnded(_ sender: Any) { + updateFee() + } + + @IBAction func onTimeLockSwitchToggle(_ sender: Any) { + timeLockEnabled = !timeLockEnabled + updateFee() + } + + @IBAction func datePickerChanged(_ sender: Any) { + updateFee() + } + @IBAction func send() { guard let address = addressTextField?.text else { return @@ -71,7 +120,7 @@ class SendController: UIViewController { } var pluginData = [String: [String: Any]]() - if let lockUntil = datePicker?.date { + if let lockUntil = datePicker?.date, timeLockEnabled { pluginData["hodler"] = ["locked_until": Int(lockUntil.timeIntervalSince1970)] } diff --git a/Demo/Demo/Controllers/SendController.xib b/Demo/Demo/Controllers/SendController.xib index f68b67b1..ff905c5f 100644 --- a/Demo/Demo/Controllers/SendController.xib +++ b/Demo/Demo/Controllers/SendController.xib @@ -14,6 +14,8 @@ + + @@ -28,66 +30,114 @@ - + + + + -