Repository: horizontalsystems/bitcoin-kit-android Branch: master Commit: 8178f5681e4e Files: 473 Total size: 2.0 MB Directory structure: gitextract_v0r6jup7/ ├── .github/ │ └── workflows/ │ └── pull-request-notify.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ └── bitcoinkit/ │ │ └── demo/ │ │ ├── App.kt │ │ ├── BalanceFragment.kt │ │ ├── FeePriority.kt │ │ ├── MainActivity.kt │ │ ├── MainViewModel.kt │ │ ├── NumberFormatHelper.kt │ │ ├── SendReceiveFragment.kt │ │ └── TransactionsFragment.kt │ └── res/ │ ├── drawable/ │ │ └── ic_launcher_background.xml │ ├── drawable-v24/ │ │ └── ic_launcher_foreground.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── fragment_balance.xml │ │ ├── fragment_send_receive.xml │ │ ├── fragment_transactions.xml │ │ └── view_holder_transaction.xml │ ├── menu/ │ │ └── navigation.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ └── values/ │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── bitcoincashkit/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── bitcoincash/ │ │ │ ├── BitcoinCashKit.kt │ │ │ ├── MainNetBitcoinCash.kt │ │ │ ├── TestNetBitcoinCash.kt │ │ │ └── blocks/ │ │ │ ├── AsertAlgorithm.java │ │ │ ├── BitcoinCashBlockValidatorHelper.kt │ │ │ ├── Utils.java │ │ │ └── validators/ │ │ │ ├── AsertValidator.kt │ │ │ ├── DAAValidator.kt │ │ │ ├── EDAValidator.kt │ │ │ └── ForkValidator.kt │ │ └── resources/ │ │ ├── MainNetBitcoinCash-bip44.checkpoint │ │ ├── MainNetBitcoinCash.checkpoint │ │ ├── TestNetBitcoinCash-bip44.checkpoint │ │ └── TestNetBitcoinCash.checkpoint │ └── test/ │ ├── kotlin/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ ├── Fixtures.kt │ │ └── bitcoincash/ │ │ └── blocks/ │ │ ├── BitcoinCashBlockValidatorHelperTest.kt │ │ └── validators/ │ │ ├── DAAValidatorTest.kt │ │ └── ForkValidatorTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── bitcoincore/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── bitcoincore/ │ │ │ ├── DustCalculator.kt │ │ │ ├── crypto/ │ │ │ │ ├── Base58.java │ │ │ │ ├── Bech32.java │ │ │ │ ├── Bech32Cash.java │ │ │ │ ├── Bech32Segwit.java │ │ │ │ ├── CompactBits.java │ │ │ │ ├── MurmurHash3.java │ │ │ │ └── schnorr/ │ │ │ │ ├── Pair.java │ │ │ │ ├── Point.java │ │ │ │ ├── Schnorr.java │ │ │ │ ├── SchnorrOld.java │ │ │ │ └── Util.java │ │ │ ├── exceptions/ │ │ │ │ ├── AddressFormatException.java │ │ │ │ └── BitcoinException.java │ │ │ ├── io/ │ │ │ │ ├── BitcoinInput.java │ │ │ │ ├── BitcoinInputMarkable.java │ │ │ │ ├── BitcoinOutput.java │ │ │ │ └── UnsafeByteArrayOutputStream.java │ │ │ └── utils/ │ │ │ ├── HashUtils.java │ │ │ └── Utils.java │ │ ├── kotlin/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── bitcoincore/ │ │ │ ├── AbstractKit.kt │ │ │ ├── BitcoinCore.kt │ │ │ ├── BitcoinCoreBuilder.kt │ │ │ ├── WatchAddressPublicKeyManager.kt │ │ │ ├── WatchedTransactionManager.kt │ │ │ ├── apisync/ │ │ │ │ ├── BCoinApi.kt │ │ │ │ ├── BiApiTransactionProvider.kt │ │ │ │ ├── BlockHashFetcher.kt │ │ │ │ ├── BlockchainComApi.kt │ │ │ │ ├── HsBlockHashFetcher.kt │ │ │ │ ├── InsightApi.kt │ │ │ │ ├── blockchair/ │ │ │ │ │ ├── BlockchairApi.kt │ │ │ │ │ ├── BlockchairApiSyncer.kt │ │ │ │ │ ├── BlockchairBlockHashFetcher.kt │ │ │ │ │ ├── BlockchairLastBlockProvider.kt │ │ │ │ │ └── BlockchairTransactionProvider.kt │ │ │ │ ├── legacy/ │ │ │ │ │ ├── ApiSyncer.kt │ │ │ │ │ ├── BlockHashDiscoveryBatch.kt │ │ │ │ │ ├── BlockHashScanHelper.kt │ │ │ │ │ ├── BlockHashScanner.kt │ │ │ │ │ └── PublicKeyFetcher.kt │ │ │ │ └── model/ │ │ │ │ ├── AddressItem.kt │ │ │ │ ├── BlockHeaderItem.kt │ │ │ │ └── TransactionItem.kt │ │ │ ├── blocks/ │ │ │ │ ├── BlockDownload.kt │ │ │ │ ├── BlockMedianTimeHelper.kt │ │ │ │ ├── BlockSyncer.kt │ │ │ │ ├── Blockchain.kt │ │ │ │ ├── BloomFilterLoader.kt │ │ │ │ ├── IBlockchainDataListener.kt │ │ │ │ ├── IPeerSyncListener.kt │ │ │ │ ├── InitialBlockDownload.kt │ │ │ │ ├── MerkleBlockExtractor.kt │ │ │ │ └── validators/ │ │ │ │ ├── BitsValidator.kt │ │ │ │ ├── BlockValidatorChain.kt │ │ │ │ ├── BlockValidatorException.kt │ │ │ │ ├── BlockValidatorSet.kt │ │ │ │ ├── IBlockValidator.kt │ │ │ │ ├── LegacyDifficultyAdjustmentValidator.kt │ │ │ │ ├── LegacyTestNetDifficultyValidator.kt │ │ │ │ └── ProofOfWorkValidator.kt │ │ │ ├── core/ │ │ │ │ ├── AccountWallet.kt │ │ │ │ ├── BaseTransactionInfoConverter.kt │ │ │ │ ├── DataProvider.kt │ │ │ │ ├── DoubleSha256Hasher.kt │ │ │ │ ├── Extensions.kt │ │ │ │ ├── HashBytes.kt │ │ │ │ ├── IHasher.kt │ │ │ │ ├── Interfaces.kt │ │ │ │ ├── PluginManager.kt │ │ │ │ ├── TransactionDataSorterFactory.kt │ │ │ │ ├── TransactionInfoConverter.kt │ │ │ │ ├── Wallet.kt │ │ │ │ └── WatchAccountWallet.kt │ │ │ ├── crypto/ │ │ │ │ └── BloomFilter.kt │ │ │ ├── extensions/ │ │ │ │ ├── Array.kt │ │ │ │ └── String.kt │ │ │ ├── managers/ │ │ │ │ ├── AccountPublicKeyManager.kt │ │ │ │ ├── ApiManager.kt │ │ │ │ ├── ApiSyncStateManager.kt │ │ │ │ ├── BlockValidatorHelper.kt │ │ │ │ ├── BloomFilterManager.kt │ │ │ │ ├── ConnectionManager.kt │ │ │ │ ├── IUnspentOutputProvider.kt │ │ │ │ ├── IUnspentOutputSelector.kt │ │ │ │ ├── IrregularOutputFinder.kt │ │ │ │ ├── PendingOutpointsProvider.kt │ │ │ │ ├── PublicKeyManager.kt │ │ │ │ ├── RestoreKeyConverters.kt │ │ │ │ ├── SyncManager.kt │ │ │ │ ├── UnspentOutputProvider.kt │ │ │ │ ├── UnspentOutputQueue.kt │ │ │ │ ├── UnspentOutputSelector.kt │ │ │ │ └── UnspentOutputSelectorSingleNoChange.kt │ │ │ ├── models/ │ │ │ │ ├── Address.kt │ │ │ │ ├── BitcoinPaymentData.kt │ │ │ │ ├── BitcoinSendInfo.kt │ │ │ │ ├── Block.kt │ │ │ │ ├── BlockHash.kt │ │ │ │ ├── BlockHashPublicKey.kt │ │ │ │ ├── BlockchainState.kt │ │ │ │ ├── Checkpoint.kt │ │ │ │ ├── InvalidTransaction.kt │ │ │ │ ├── InventoryItem.kt │ │ │ │ ├── MerkleBlock.kt │ │ │ │ ├── NetworkAddress.kt │ │ │ │ ├── PeerAddress.kt │ │ │ │ ├── PublicKey.kt │ │ │ │ ├── SentTransaction.kt │ │ │ │ ├── Transaction.kt │ │ │ │ ├── TransactionDataSortType.kt │ │ │ │ ├── TransactionInfo.kt │ │ │ │ ├── TransactionInput.kt │ │ │ │ ├── TransactionMetadata.kt │ │ │ │ ├── TransactionOutput.kt │ │ │ │ ├── UsedAddress.kt │ │ │ │ └── WatchAddressPublicKey.kt │ │ │ ├── network/ │ │ │ │ ├── Network.kt │ │ │ │ ├── messages/ │ │ │ │ │ ├── AddrMessage.kt │ │ │ │ │ ├── FilterLoadMessage.kt │ │ │ │ │ ├── GetBlocksMessage.kt │ │ │ │ │ ├── GetDataMessage.kt │ │ │ │ │ ├── GetHeadersMessage.kt │ │ │ │ │ ├── HeadersMessage.kt │ │ │ │ │ ├── IMessage.kt │ │ │ │ │ ├── InvMessage.kt │ │ │ │ │ ├── MempoolMessage.kt │ │ │ │ │ ├── MerkleBlockMessage.kt │ │ │ │ │ ├── PingMessage.kt │ │ │ │ │ ├── PongMessage.kt │ │ │ │ │ ├── RejectMessage.kt │ │ │ │ │ ├── TransactionMessage.kt │ │ │ │ │ ├── UnknownMessage.kt │ │ │ │ │ ├── VerAckMessage.kt │ │ │ │ │ └── VersionMessage.kt │ │ │ │ └── peer/ │ │ │ │ ├── IInventoryItemsHandler.kt │ │ │ │ ├── IPeerTaskHandler.kt │ │ │ │ ├── MempoolTransactions.kt │ │ │ │ ├── Peer.kt │ │ │ │ ├── PeerAddressManager.kt │ │ │ │ ├── PeerConnection.kt │ │ │ │ ├── PeerDiscover.kt │ │ │ │ ├── PeerGroup.kt │ │ │ │ ├── PeerManager.kt │ │ │ │ ├── PeerTimer.kt │ │ │ │ └── task/ │ │ │ │ ├── GetBlockHashesTask.kt │ │ │ │ ├── GetBlockHeadersTask.kt │ │ │ │ ├── GetMerkleBlocksTask.kt │ │ │ │ ├── PeerTask.kt │ │ │ │ ├── RequestTransactionsTask.kt │ │ │ │ └── SendTransactionTask.kt │ │ │ ├── rbf/ │ │ │ │ ├── ReplacementTransaction.kt │ │ │ │ ├── ReplacementTransactionBuilder.kt │ │ │ │ └── ReplacementType.kt │ │ │ ├── serializers/ │ │ │ │ ├── BlockHeaderParser.kt │ │ │ │ ├── InputSerializer.kt │ │ │ │ ├── OutputSerializer.kt │ │ │ │ └── TransactionSerializer.kt │ │ │ ├── storage/ │ │ │ │ ├── BlockDao.kt │ │ │ │ ├── BlockHashDao.kt │ │ │ │ ├── BlockHashPublicKeyDao.kt │ │ │ │ ├── BlockchainStateDao.kt │ │ │ │ ├── CoreDatabase.kt │ │ │ │ ├── DataObjects.kt │ │ │ │ ├── DataTypeConverters.kt │ │ │ │ ├── InvalidTransactionDao.kt │ │ │ │ ├── PeerAddressDao.kt │ │ │ │ ├── PublicKeyDao.kt │ │ │ │ ├── SentTransactionDao.kt │ │ │ │ ├── Storage.kt │ │ │ │ ├── TransactionDao.kt │ │ │ │ ├── TransactionInputDao.kt │ │ │ │ ├── TransactionMetadataDao.kt │ │ │ │ ├── TransactionOutputDao.kt │ │ │ │ └── migrations/ │ │ │ │ ├── Migration_10_11.kt │ │ │ │ ├── Migration_11_12.kt │ │ │ │ ├── Migration_12_13.kt │ │ │ │ ├── Migration_13_14.kt │ │ │ │ ├── Migration_14_15.kt │ │ │ │ ├── Migration_15_16.kt │ │ │ │ ├── Migration_16_17.kt │ │ │ │ ├── Migration_17_18.kt │ │ │ │ └── Migration_18_19.kt │ │ │ ├── transactions/ │ │ │ │ ├── BlockTransactionProcessor.kt │ │ │ │ ├── PendingTransactionProcessor.kt │ │ │ │ ├── SendTransactionsOnPeersSynced.kt │ │ │ │ ├── TransactionConflictsResolver.kt │ │ │ │ ├── TransactionCreator.kt │ │ │ │ ├── TransactionFeeCalculator.kt │ │ │ │ ├── TransactionInvalidator.kt │ │ │ │ ├── TransactionSendTimer.kt │ │ │ │ ├── TransactionSender.kt │ │ │ │ ├── TransactionSizeCalculator.kt │ │ │ │ ├── TransactionSyncer.kt │ │ │ │ ├── builder/ │ │ │ │ │ ├── ECKey.kt │ │ │ │ │ ├── EcdsaInputSigner.kt │ │ │ │ │ ├── InputSetter.kt │ │ │ │ │ ├── LockTimeSetter.kt │ │ │ │ │ ├── MutableTransaction.kt │ │ │ │ │ ├── OutputSetter.kt │ │ │ │ │ ├── RecipientSetter.kt │ │ │ │ │ ├── SchnorrInputSigner.kt │ │ │ │ │ ├── TransactionBuilder.kt │ │ │ │ │ └── TransactionSigner.kt │ │ │ │ ├── extractors/ │ │ │ │ │ ├── MyOutputsCache.kt │ │ │ │ │ ├── TransactionExtractor.kt │ │ │ │ │ └── TransactionMetadataExtractor.kt │ │ │ │ └── scripts/ │ │ │ │ ├── OpCodes.kt │ │ │ │ └── Script.kt │ │ │ └── utils/ │ │ │ ├── Base58AddressConverter.kt │ │ │ ├── Bech32AddressConverter.kt │ │ │ ├── Bip69.kt │ │ │ ├── DirectExecutor.kt │ │ │ ├── IAddressConverter.kt │ │ │ ├── MainThreadExecutor.kt │ │ │ ├── MerkleBranch.kt │ │ │ ├── NetworkUtils.kt │ │ │ ├── PaymentAddressParser.kt │ │ │ └── TransactionDataSorters.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ ├── kotlin/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ └── bitcoincore/ │ │ ├── Fixtures.kt │ │ ├── RxTestRule.kt │ │ ├── TestHelpers.kt │ │ ├── blocks/ │ │ │ ├── BlockSyncerTest.kt │ │ │ ├── BlockchainTest.kt │ │ │ └── validators/ │ │ │ ├── BitsValidatorTest.kt │ │ │ ├── LegacyDifficultyAdjustmentValidatorTest.kt │ │ │ ├── LegacyTestNetDifficultyValidatorTest.kt │ │ │ └── ProofOfWorkValidatorTest.kt │ │ ├── core/ │ │ │ └── DataProviderTest.kt │ │ ├── managers/ │ │ │ ├── ApiManagerTest.kt │ │ │ ├── BlockDiscoveryBatchTest.kt │ │ │ ├── BlockHashFetcherHelperTest.kt │ │ │ ├── BlockHashFetcherTest.kt │ │ │ ├── InitialSyncerTest.kt │ │ │ ├── IrregularOutputFinderTest.kt │ │ │ ├── StateManagerTest.kt │ │ │ ├── SyncManagerTest.kt │ │ │ ├── UnspentOutputProviderTest.kt │ │ │ ├── UnspentOutputSelectorSingleNoChangeTest.kt │ │ │ └── UnspentOutputSelectorTest.kt │ │ ├── message/ │ │ │ ├── MerkleBlockExtractorTest.kt │ │ │ └── TransactionMessageParserTest.kt │ │ ├── models/ │ │ │ └── TransactionTest.kt │ │ ├── network/ │ │ │ ├── PeerGroupTest.kt │ │ │ ├── PeerManagerTest.kt │ │ │ ├── PeerTest.kt │ │ │ └── peer/ │ │ │ ├── PeerHostManagerTest.kt │ │ │ └── task/ │ │ │ └── SendTransactionTaskTest.kt │ │ ├── transactions/ │ │ │ ├── TransactionExtractorTest.kt │ │ │ ├── TransactionProcessorTest.kt │ │ │ ├── TransactionSenderTest.kt │ │ │ ├── TransactionSizeCalculatorTest.kt │ │ │ ├── TransactionSyncerTest.kt │ │ │ ├── builder/ │ │ │ │ └── InputSignerTest.kt │ │ │ └── scripts/ │ │ │ └── ScriptTest.kt │ │ └── utils/ │ │ ├── AddressConverterTest.kt │ │ ├── Bip69Test.kt │ │ ├── CashAddressConverterTest.kt │ │ ├── NetworkUtilsTest.kt │ │ ├── PaymentAddressParserTest.kt │ │ └── SegwitAddressConverterTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── bitcoinkit/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── bitcoinkit/ │ │ │ ├── BitcoinKit.kt │ │ │ ├── MainNet.kt │ │ │ ├── RegTest.kt │ │ │ ├── TestNet.kt │ │ │ ├── core/ │ │ │ │ └── DataProvider.kt │ │ │ └── utils/ │ │ │ └── AddressConverter.kt │ │ └── resources/ │ │ ├── MainNet-bip44.checkpoint │ │ ├── MainNet.checkpoint │ │ ├── TestNet-bip44.checkpoint │ │ └── TestNet.checkpoint │ └── test/ │ ├── kotlin/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ └── bitcoinkit/ │ │ ├── Fixtures.kt │ │ └── MainNetTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── build.gradle ├── dashkit/ │ ├── .gitignore │ ├── build.gradle │ ├── cpp/ │ │ ├── CMakeLists.txt │ │ ├── config.h │ │ └── dashj-bls/ │ │ ├── CMakeLists.txt │ │ ├── bls-signatures-src.cmake │ │ ├── bls-signatures.cmake │ │ ├── dashj-bls-signature-wrapper.cpp │ │ ├── pthread.c │ │ └── stdio.cpp │ ├── libs/ │ │ └── dashj-bls-0.15.3.jar │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── dashkit/ │ │ │ ├── DashKit.kt │ │ │ ├── DashKitErrors.kt │ │ │ ├── IDashStorage.kt │ │ │ ├── IInstantTransactionDelegate.kt │ │ │ ├── IMerkleHasher.kt │ │ │ ├── InstantSend.kt │ │ │ ├── InventoryType.kt │ │ │ ├── MainNetDash.kt │ │ │ ├── TestNetDash.kt │ │ │ ├── X11Hasher.kt │ │ │ ├── core/ │ │ │ │ ├── DashTransactionInfoConverter.kt │ │ │ │ └── DoubleSha256Hasher.kt │ │ │ ├── instantsend/ │ │ │ │ ├── BLS.kt │ │ │ │ ├── InstantSendFactory.kt │ │ │ │ ├── InstantSendLockValidator.kt │ │ │ │ ├── InstantTransactionManager.kt │ │ │ │ ├── QuorumMasternode.kt │ │ │ │ ├── TransactionLockVoteValidator.kt │ │ │ │ ├── instantsendlock/ │ │ │ │ │ ├── InstantSendLockHandler.kt │ │ │ │ │ └── InstantSendLockManager.kt │ │ │ │ └── transactionlockvote/ │ │ │ │ ├── TransactionLockVoteHandler.kt │ │ │ │ └── TransactionLockVoteManager.kt │ │ │ ├── managers/ │ │ │ │ ├── ConfirmedUnspentOutputProvider.kt │ │ │ │ ├── MasternodeListManager.kt │ │ │ │ ├── MasternodeListSyncer.kt │ │ │ │ ├── MasternodeSortedList.kt │ │ │ │ ├── QuorumListManager.kt │ │ │ │ └── QuorumSortedList.kt │ │ │ ├── masternodelist/ │ │ │ │ ├── MasternodeCbTxHasher.kt │ │ │ │ ├── MasternodeListMerkleRootCalculator.kt │ │ │ │ ├── MerkleRootCreator.kt │ │ │ │ ├── MerkleRootHasher.kt │ │ │ │ └── QuorumListMerkleRootCalculator.kt │ │ │ ├── messages/ │ │ │ │ ├── GetMasternodeListDiffMessage.kt │ │ │ │ ├── ISLockMessage.kt │ │ │ │ ├── MasternodeListDiffMessage.kt │ │ │ │ ├── TransactionLockMessage.kt │ │ │ │ ├── TransactionLockVoteMessage.kt │ │ │ │ └── TransactionMessage.kt │ │ │ ├── models/ │ │ │ │ ├── CoinbaseTransaction.kt │ │ │ │ ├── CoinbaseTransactionSerializer.kt │ │ │ │ ├── DashTransactionInfo.kt │ │ │ │ ├── InstantTransactionHash.kt │ │ │ │ ├── InstantTransactionInput.kt │ │ │ │ ├── InstantTransactionState.kt │ │ │ │ ├── Masternode.kt │ │ │ │ ├── MasternodeListState.kt │ │ │ │ ├── Quorum.kt │ │ │ │ └── SpecialTransaction.kt │ │ │ ├── storage/ │ │ │ │ ├── DashKitDatabase.kt │ │ │ │ └── DashStorage.kt │ │ │ ├── tasks/ │ │ │ │ ├── PeerTaskFactory.kt │ │ │ │ ├── RequestInstantSendLocksTask.kt │ │ │ │ ├── RequestMasternodeListDiffTask.kt │ │ │ │ ├── RequestTransactionLockRequestsTask.kt │ │ │ │ └── RequestTransactionLockVotesTask.kt │ │ │ └── validators/ │ │ │ ├── DarkGravityWaveTestnetValidator.kt │ │ │ └── DarkGravityWaveValidator.kt │ │ └── resources/ │ │ ├── MainNetDash-bip44.checkpoint │ │ ├── MainNetDash.checkpoint │ │ ├── TestNetDash-bip44.checkpoint │ │ └── TestNetDash.checkpoint │ └── test/ │ ├── kotlin/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ └── dashkit/ │ │ ├── messages/ │ │ │ └── MasternodeListDiffMessageParserTest.kt │ │ └── validators/ │ │ └── DarkGravityWaveValidatorTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── ecashkit/ │ ├── .gitignore │ ├── build.gradle │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ ├── chronik/ │ │ │ │ └── Chronik.java │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── ecash/ │ │ │ ├── ChronikApi.kt │ │ │ ├── ECashKit.kt │ │ │ ├── ECashRestoreKeyConverter.kt │ │ │ └── MainNetECash.kt │ │ └── resources/ │ │ ├── MainNetECash-bip44.checkpoint │ │ └── MainNetECash.checkpoint │ └── test/ │ └── java/ │ └── io/ │ └── horizontalsystems/ │ └── ecash/ │ └── ExampleUnitTest.kt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── hodler/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── hodler/ │ │ │ ├── HodlerData.kt │ │ │ ├── HodlerOutputData.kt │ │ │ ├── HodlerPlugin.kt │ │ │ └── LockTimeInterval.kt │ │ └── res/ │ │ └── values/ │ │ └── strings.xml │ └── test/ │ ├── java/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ └── hodler/ │ │ └── HodlerPluginTest.kt │ └── resources/ │ └── mockito-extensions/ │ └── org.mockito.plugins.MockMaker ├── litecoinkit/ │ ├── .gitignore │ ├── build.gradle │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── io/ │ │ └── horizontalsystems/ │ │ └── litecoinkit/ │ │ └── ExampleInstrumentedTest.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── kotlin/ │ │ │ └── io/ │ │ │ └── horizontalsystems/ │ │ │ └── litecoinkit/ │ │ │ ├── LitecoinKit.kt │ │ │ ├── MainNetLitecoin.kt │ │ │ ├── ScryptHasher.kt │ │ │ ├── TestNetLitecoin.kt │ │ │ └── validators/ │ │ │ ├── LegacyDifficultyAdjustmentValidator.kt │ │ │ └── ProofOfWorkValidator.kt │ │ └── resources/ │ │ ├── MainNetLitecoin-bip44.checkpoint │ │ ├── MainNetLitecoin.checkpoint │ │ ├── TestNetLitecoin-bip44.checkpoint │ │ └── TestNetLitecoin.checkpoint │ └── test/ │ └── java/ │ └── io/ │ └── horizontalsystems/ │ └── litecoinkit/ │ ├── ExampleUnitTest.kt │ └── ScryptHasherTest.kt ├── settings.gradle └── tools/ ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src/ └── main/ ├── AndroidManifest.xml ├── java/ │ └── io/ │ └── horizontalsystems/ │ └── tools/ │ ├── BuildCheckpoints.kt │ ├── CheckpointSyncer.kt │ ├── PeerAddressManager.kt │ └── Tools.kt └── res/ └── values/ └── strings.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/pull-request-notify.yml ================================================ name: Notify PR events on: pull_request: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Notify to Telegrem uses: appleboy/telegram-action@master with: to: ${{secrets.TELEGRAM_TO}} token: ${{secrets.TELEGRAM_TOKEN}} disable_web_page_preview: true message: | ${{ github.event.pull_request.html_url }} Title: ${{github.event.pull_request.title}} Author: ${{github.actor}} ================================================ FILE: .gitignore ================================================ *.iml .gradle .idea .DS_Store /build /captures /local.properties .externalNativeBuild ================================================ FILE: .gitmodules ================================================ [submodule "dashkit/cpp/dashj-bls/bls-signatures"] path = dashkit/cpp/dashj-bls/bls-signatures url = https://github.com/Chia-Network/bls-signatures ================================================ FILE: CONTRIBUTING.md ================================================ ## Implementing support for a new coin for external developers Support for coin is implemented as a separate module that depends on the module `bitcoincore`. This repository contains modules for supporting coins like `Bitcoin`, `BitcoinCash` and `Dash`. Support for a new coin should be implemented in the owners repository. ### Structure of module The module depends on the `bitcoincore`. This dependency can be added via JitPack repository. In the main `build.gradle` add the JitPack repository: ``` repositories { maven { url 'https://jitpack.io' } } ``` Add the following dependency to module `build.gradle` file: ``` dependencies { implementation 'com.github.horizontalsystems.bitcoin-kit-android:bitcoincore:${version}' } ``` It implements `AbstractKit` and `Network` interfaces (abstract classes). Customizing can be done in 2 places: 1. Via `BitcoinCoreBuilder` when building `BitcoinCore` 2. Via `BitcoinCore` There are multiple places that can be customized. See the modules [`bitcoinkit`](bitcoinkit), [`bitcoincashkit`](bitcoincashkit) and [`dashkit`](dashkit) for reference. If you need a new extension point please [add an issue](https://github.com/horizontalsystems/bitcoin-kit-android/issues/new). When the module is released let us know about it. We will review it and decide whether to add it to Unstoppable wallet app. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Horizontal Systems Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # BitcoinKit `bitcoin-kit-android` is a Bitcoin wallet toolkit implemented in Kotlin. It consists of following libraries: - `bitcoincore` is a core library that implements a full Simplified Payment Verification (`SPV`) client in `Kotlin`. It implements Bitcoin `P2P Protocol` and can be extended to be a client of other Bitcoin forks like BitcoinCash, Litecoin, etc. - `bitcoinkit` extends **bitcoincore**, makes it usable with `Bitcoin` network. - `bitcoincashkit` extends **bitcoincore**, makes it usable with `BitcoinCash(ABC)` network. - `litecoinkit` extends **bitcoincore**, makes it usable with `Litecoin` network. - `dashkit` extends **bitcoincore**, makes it usable with `Dash` network. - `hodler` is a plugin for `bitcoincore`, that makes it possible to lock certain amount of coins until some time in the future. Being an SPV client, **bitcoincore** downloads and validates all the block headers, inclusion of transactions in the blocks, integrity and immutability of transactions as described in the Bitcoin whitepaper or delegates validation to the extensions that implement the forks of Bitcoin. ## Core Features - [x] Bitcoin P2P Protocol implementation in Kotlin. - [x] Full SPV implementation for fast mobile performance with account security and privacy in mind - [x] `P2PK`, `P2PKH`, `P2SH-P2WPKH`, `P2WPKH` outputs support. - [x] Restoring with mnemonic seed. (Generated from private seed phrase) - [x] Restoring with [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) extended public key. (This becomes a `Watch account` unable to spend funds) - [x] Quick initial restore over node API. (optional) - [x] Handling transaction (Replacement)/(Double spend)/(Failure by expiration) - [x] Optimized UTXO selection when spending coins. - [x] [BIP69](https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki) or simple shuffle output ordering. (configurable) - [x] [BIP21](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki) URI schemes with payment address, amount, label and other parameters # bitcoinkit ## Usage ### Initialization First, you need an instance of *BitcoinKit* class. You can initialize it with Mnemonic seed or BIP32 extended key (private or public). To generate seed from mnemonic seed phrase you can use [HdWalletKit](https://github.com/horizontalsystems/hd-wallet-kit-android) to convert a word list to a seed. ```kotlin val words = listOf("mnemonic", "phrase", "words") val passphrase: String = "" val seed = Mnemonic().toSeed(words, passphrase) ``` Then you can pass a seed to initialize an instance of *BitcoinKit* ```kotlin val context = Application() val bitcoinKit = BitcoinKit( context = context, seed = seed, walletId = "unique_wallet_id", syncMode = BitcoinCore.SyncMode.Api(), networkType = NetworkType.MainNet, confirmationsThreshold = 6, purpose = HDWallet.Purpose.BIP84 ) ``` #### `purpose` *bitcoinkit* supports `BIP44`, `BIP49` and `BIP84` wallets. They have different derivation paths, so you need to specify this on kit initialization. #### `syncMode` *bitcoinkit* pulls all historical transactions of given account from bitcoin peers according to SPV protocol. This process may take several hours as it needs to download every block header with some transactions to find transactions concerning the accounts addresses. In order to speed up the initial blockchain scan, *bitcoincore* has some optimization options: - It doesn't download blocks added before the [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) was implemented by wallets, because there were no transactions concerning addresses generated by BIP44 wallets. - If you set **API()** or **NewWallet()** to *syncMode* parameter, it first requests from an **API**(currently [Blockchain.com](https://blockchain.info)) the hashes of the blocks where there are transactions we need. Then, it downloads those blocks from the bitcoin peers. This reduces the initial synchronization time to several minutes. This also carries some risks that makes it possible for a middle-man attacker to learn about the addresses requested from your IP address. But your funds are totally safe. If you set **Full()** to *syncMode*, then only decentralized peers are used. Once the initial blockchain scan is completed, the remaining synchronization works with decentralized peers only for all *syncMode*s. #### Additional parameters: - `networkType`: Mainnet or Testnet - `confirmationsThreshold`: Minimum number of confirmations required for an unspent output to be available for use (*default: 6*) #### Initializing with HD extended key You can initialize `BitcoinKit` using BIP32 Extended Private/Public Key as follows: ```kotlin val extendedKey = HDExtendedKey("xprvA1BgyAq84AiAsrMm6DKqwCXDwxLBXq76dpUfuNXNziGMzDxYLjE9AkuYBAQTpt6aJu4nFYamh6BbrRkys5fJcxGd7qixNrpVpPBxui9oYyF") val bitcoinKit = BitcoinKit( context = context, extendedKey = extendedKey, walletId = "unique_wallet_id", syncMode = BitcoinCore.SyncMode.Api(), networkType = NetworkType.MainNet, confirmationsThreshold = 6 ) ``` If you restore with a public extended key, then you only will be able to watch the wallet. You won't be able to send any transactions. This is how the **watch account** feature is implemented. ### Starting and Stopping *BitcoinKit* requires to be started with `start` command. It will be in synced state as long as it is possible. You can call `stop` to stop it ```kotlin bitcoinKit.start() bitcoinKit.stop() ``` ### Getting wallet data #### Balance Balance is provided in `Satoshis`: ```kotlin val balance = bitcoinKit.balance println(balance.spendable) println(balance.unspendable) ``` Unspendable balance is non-zero if you have UTXO that is currently not spendable due to some custom unlock script. These custom scripts can be implemented as a plugin, like **Hodler** #### Last Block Info ```kotlin val blockInfo = bitcoinKit.lastBlockInfo ?: return println(blockInfo.headerHash) println(blockInfo.height) println(blockInfo.timestamp) ``` #### Receive Address Get an address which you can receive coins to. Receive address is changed each time after you actually get some coins in that address ```kotlin bitcoinKit.receiveAddress() // "mgv1KTzGZby57K5EngZVaPdPtphPmEWjiS" ``` #### Transactions You can get your transactions using `transactions(fromUid: String? = null, type: TransactionFilterType? = null, limit: Int? = null)` method of the *BitcoinKit* instance. It returns *Single>*. You'll need to subscribe and get transactions asynchronously. See [RX Single Observers](http://reactivex.io/RxJava/3.x/javadoc/io/reactivex/rxjava3/core/Single.html) for more info. ```kotlin val disposables = CompositeDisposable() bitcoinKit.transactions().subscribe { transactionInfos -> for (transactionInfo in transactionInfos) { println("Uid: ${transactionInfo.uid}") println("Hash: ${transactionInfo.transactionHash}") } }.let { disposables.add(it) } ``` - `fromUid` and `limit` parameters can be used for pagination. - `type` parameter enables to filter transactions by coins flow. You can pass *incoming* OR *outgoing* to get filtered transactions #### TransactionInfo A sample dump: ```kotlin // transactionInfo = {TransactionInfo} // amount = 13114 // blockHeight = 740024 // conflictingTxHash = null // fee = null // inputs = {ArrayList} size = 1 // 0 = {TransactionInputInfo} // address = "16s6q8dAgLbDT3szEc4nvTh81deRCBtEa1" // mine = false // value = null // outputs = {ArrayList} size = 2 // 0 = {TransactionOutputInfo} // address = "bc1qsg9ul383f8pespcvc8u3katl6gnsr7sjyfe3pc" // changeOutput = false // mine = true // pluginData = null // pluginDataString = null // pluginId = null // value = 13114 // 1 = {TransactionOutputInfo} // address = "16VCm8mYhHE3EiELi8GiYEqAjnPu1TSgAV" // changeOutput = false // mine = false // pluginData = null // pluginDataString = null // pluginId = null // value = 1422 // status = {TransactionStatus} RELAYED // timestamp = 1654766137 // transactionHash = "cadf99db1e145dcfadfa2bc3eacb94831eb6c53d376f4f873aa4ac017b8c7f8f" // transactionIndex = 2760 // type = {TransactionType} Incoming // uid = "75934663-3c84-4b38-9b6d-810d3433de17" ``` `uid` A local unique ID `type` - *Incoming* - *Outgoing* - *SentToSelf* `status` - *NEW* -> transaction is in mempool - *RELAYED* -> transaction is in block - *INVALID* -> transaction is not included in block due to an error OR replaced by another one (RBF). ### Sending BTC ```kotlin bitcoinKit.send(address = "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value = 100000000, feeRate = 10, sortType = TransactionDataSortType.Bip69) ``` This first validates a given address and amount, creates new transaction, then sends it over the peers network. If there's any error with given address/amount or network, it raises an exception. #### Validate address ```kotlin bitcoinKit.validateAddress(address = "mrjQyzbX9SiJxRC2mQhT4LvxFEmt9KEeRY") ``` #### Evaluate fee ```kotlin bitcoinKit.fee(address = "36k1UofZ2iP2NYax9znDCsksajfKeKLLMJ", value = 100000000, feeRate = 10) ``` ### Parsing BIP21 URI You can use `parsePaymentAddress` method to parse a BIP21 URI: ```kotlin bitcoinKit.parsePaymentAddress("bitcoin:175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz") // ▿ BitcoinPaymentData // - address : "175tWpb8K1S7NmH4Zx6rewF9WQrcZv245W" // - version : null // - amount : 50.0 // - label : "Luke-Jr" // - message : "Donation for project xyz" // - parameters : null ``` ### Subscribing to BitcoinKit data Balance, transactions, last blocks synced and kit state are available in real-time. `BitcoinKit.Listener` interface must be implemented and set to *BitcoinKit* instance to receive that. ```kotlin class Manager(val bitcoinKit: BitcoinKit) : BitcoinKit.Listener { init { bitcoinKit.listener = this } override fun onBalanceUpdate(balance: BalanceInfo) { } override fun onLastBlockInfoUpdate(blockInfo: BlockInfo) { } override fun onKitStateUpdate(state: BitcoinCore.KitState) { } override fun onTransactionsUpdate(inserted: List, updated: List) { } override fun onTransactionsDelete(hashes: List) { } } ``` # bitcoincashkit ## Features - [x] `Base58` and `Bech32` - [x] Validation of BCH hard forks - [x] `ASERT`, `DAA`, `EDA` validations ## Usage Because BitcoinCash is a fork of Bitcoin, the usage of this library does not differ much from `bitcoinkit`. So we only describe some differences between them. ### Initialization All BitcoinCash wallets use default [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) derivation path where *coinType* is `145` according to [SLIP44](https://github.com/satoshilabs/slips/blob/master/slip-0044.md). But since it's a fork of Bitcoin, `0` coinType also can be restored. ```kotlin val context = Application() val seed = Mnemonic().toSeed(listOf("mnemonic", "phrase", "words"), "") val bitcoinCashKit = BitcoinCashKit( context = context, seed = seed, walletId = "unique_wallet_id", syncMode = BitcoinCore.SyncMode.Api(), networkType = NetworkType.MainNet(MainNetBitcoinCash.CoinType.Type145), confirmationsThreshold = 6 ) ``` # litecoinkit Usage identical to `bitcoinkit` # dashkit ## Features - [x] Instant send - [x] LLMQ lock, Masternodes validation ## Usage ### Initialization ```kotlin val context = Application() val seed = Mnemonic().toSeed(listOf("mnemonic", "phrase", "words"), "") val dashKit = DashKit( context = context, seed = seed, walletId = "unique_wallet_id", syncMode = BitcoinCore.SyncMode.Api(), networkType = NetworkType.MainNet, confirmationsThreshold = 6 ) ``` ### DashTransactionInfo Dash has some transactions marked `instant`. So, instead of `TransactionInfo` object *DashKit* works with `DashTransactionInfo` that has that field and a respective `DashKit.Listener` listener class. # hodler `hodler` is a plugin to `bitcoincore`, that makes it possible to lock bitcoins until some time in the future. It relies on [CHECKSEQUENCEVERIFY](https://github.com/bitcoin/bips/blob/master/bip-0112.mediawiki) and [Relative time-locks](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki). It may be used with other forks of Bitcoin that support them. `UnstooppableWallet` opts in this plugin and enables it for Bitcoin as an experimental feature. ## How it works To lock funds we create P2SH output where redeem script has `OP_CSV` OpCode that ensures that the input has a proper Sequence Number(`nSequence`) field and that it enables a relative time-lock. In [this](https://blockstream.info/tx/1cd11e80d04c82d098f19badb153ea12ec84cda408daaadc566cc129f967a435?input:1&expand) sample transaction the second input unlocks such an output. It has a signature, public key and the following redeem script in its scriptSig: `OP_PUSHBYTES_3 070040 OP_CSV OP_DROP OP_DUP OP_HASH160 OP_PUSHBYTES_20 853316620ed93e4ade18f8218f9aa15dc36c768e OP_EQUALVERIFY OP_CHECKSIG` - `OP_PUSHBYTES_3 070040 OP_CSV OP_DROP` part ensures that needed amount of time is passed. Specifically `07` part of `070040` bytes says that it's locked for 1 hour. See [here](https://github.com/horizontalsystems/bitcoin-kit-android/blob/master/hodler/src/main/kotlin/io/horizontalsystems/hodler/LockTimeInterval.kt) and [here](https://github.com/bitcoin/bips/blob/master/bip-0068.mediawiki) for how it's evaluated. - `OP_DUP OP_HASH160 OP_PUSHBYTES_20 853316620ed93e4ade18f8218f9aa15dc36c768e OP_EQUALVERIFY OP_CHECKSIG` part is the same locking script as of `P2PKH` output, that ensures the spender is the owner of the private key matching the public key hashed to `853316620ed93e4ade18f8218f9aa15dc36c768e`. ### Detection of incoming time-locked funds When you have such an `P2SH` output, you only have an address and a hash of a redeem script in the output. If you are not aware of incoming time-locked funds in advance, there's no way you can detect that a particular output is yours. For this reason, we add an extra `OP_RETURN` output beside that `P2SH` output as a hint. That output tells us - ID of the plugin (1 byte): `bitcoincore` can handle multiple plugins like this one. - Time-lock period (2 bytes) - Hash of the receiver's public key (20 bytes) For example, [this](https://blockstream.info/tx/bdc3e995100269c8813f291dd9ea5489d8a17bd163002f70b5abbe05b5dccbd3?expand) is a *hint* output for the input above. It has following data: `OP_RETURN OP_PUSHNUM_1 OP_PUSHBYTES_2 0700 OP_PUSHBYTES_20 853316620ed93e4ade18f8218f9aa15dc36c768e` ## Limitations ### Locked time periods This plugin can lock coins for `1 hour`, `1 month`, `half a year` and `1 year`. This is a limitation arising from the need of restoring those outputs using Simplified Payment Verification (SPV) `Bloom Filters`. Since each lock time generates different `P2SH` addresses, it wouldn't be possible to restore those outputs without knowing the exact lock time period in advance. So we generate 4 different addresses for each public key and use them in the bloom filters. ### BTC amount We allow maximum 0.5 BTC to be locked. We assume that's an acceptable amount to be locked if done unintentionally. ## Prerequisites * JDK >= 1.8 * Android 6 (minSdkVersion 23) or greater ## Installation Add the JitPack to module build.gradle ``` repositories { maven { url 'https://jitpack.io' } } ``` Add the following dependency to your build.gradle file: ``` dependencies { implementation 'com.github.horizontalsystems:bitcoin-kit-android:master-SNAPSHOT' } ``` ## Example App All features of the library are used in example project. It can be referred as a starting point for usage of the library. * [Example App](https://github.com/horizontalsystems/bitcoin-kit-android/tree/master/app) ## Dependencies * [HDWalletKit](https://github.com/horizontalsystems/hd-wallet-kit-android) - HD Wallet related features, mnemonic phrase ## Contributing [Contributing](CONTRIBUTING.md) ## License The `bitcoin-kit-android` is open source and available under the terms of the [MIT License](https://github.com/horizontalsystems/bitcoin-kit-android/blob/master/LICENSE) ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' } android { namespace 'io.horizontalsystems.bitcoinkit.demo' compileSdk 34 defaultConfig { applicationId "io.horizontalsystems.bitcoinkit.demo" minSdkVersion 23 targetSdkVersion 34 versionCode 1 versionName "0.3.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } packagingOptions { resources { pickFirsts += ['META-INF/atomicfu.kotlin_module'] } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.material:material:1.10.0' implementation 'io.reactivex.rxjava2:rxjava:2.2.19' implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' // ViewModel and LiveData implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' //LeakCanary debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation project(':bitcoinkit') implementation project(':dashkit') implementation project(':bitcoincashkit') implementation project(':litecoinkit') implementation project(':ecashkit') } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/App.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import android.app.Application class App : Application() { override fun onCreate() { super.onCreate() instance = this } companion object { lateinit var instance: App private set } } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/BalanceFragment.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import androidx.lifecycle.Observer import android.os.Bundle import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.lifecycle.ViewModelProvider import io.horizontalsystems.bitcoincore.BitcoinCore import java.text.SimpleDateFormat import java.util.* class BalanceFragment : Fragment() { lateinit var viewModel: MainViewModel lateinit var networkName: TextView lateinit var balanceValue: TextView lateinit var balanceUnspendableValue: TextView lateinit var lastBlockDateValue: TextView lateinit var lastBlockValue: TextView lateinit var stateValue: TextView lateinit var startButton: Button lateinit var clearButton: Button lateinit var buttonDebug: Button lateinit var buttonStatus: Button override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = activity?.let { ViewModelProvider(it).get(MainViewModel::class.java) } ?: return viewModel.balance.observe(this, Observer { balance -> when (balance) { null -> { balanceValue.text = "" balanceUnspendableValue.text = "" } else -> { balanceValue.text = NumberFormatHelper.cryptoAmountFormat.format(balance.spendable / 100_000_000.0) balanceUnspendableValue.text = NumberFormatHelper.cryptoAmountFormat.format((balance.unspendableTimeLocked + balance.unspendableNotRelayed) / 100_000_000.0) } } }) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) viewModel.lastBlock.observe(this, Observer { it?.let { blockInfo -> lastBlockValue.text = blockInfo.height.toString() val strDate = dateFormat.format(Date(blockInfo.timestamp * 1000)) lastBlockDateValue.text = strDate } }) viewModel.state.observe(this, Observer { state -> when (state) { is BitcoinCore.KitState.Synced -> { stateValue.text = "synced" } is BitcoinCore.KitState.ApiSyncing -> { stateValue.text = "api syncing ${state.transactions} txs" } is BitcoinCore.KitState.Syncing -> { stateValue.text = "syncing ${"%.3f".format(state.progress)}" } is BitcoinCore.KitState.NotSynced -> { stateValue.text = "not synced ${state.exception.javaClass.simpleName}" } } }) viewModel.status.observe(this, Observer { when (it) { MainViewModel.State.STARTED -> { startButton.isEnabled = false } else -> { startButton.isEnabled = true } } }) viewModel.statusInfo.observe(this, Observer { statusInfo -> activity?.let { val dialog = AlertDialog.Builder(it) .setMessage(formatMapToString(statusInfo)) .setTitle("Status Info") .create() dialog.show() } }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_balance, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) networkName = view.findViewById(R.id.networkName) networkName.text = viewModel.networkName balanceValue = view.findViewById(R.id.balanceValue) balanceUnspendableValue = view.findViewById(R.id.balanceUnspendableValue) lastBlockValue = view.findViewById(R.id.lastBlockValue) lastBlockDateValue = view.findViewById(R.id.lastBlockDateValue) stateValue = view.findViewById(R.id.stateValue) startButton = view.findViewById(R.id.buttonStart) clearButton = view.findViewById(R.id.buttonClear) buttonDebug = view.findViewById(R.id.buttonDebug) buttonStatus = view.findViewById(R.id.buttonStatus) startButton.setOnClickListener { viewModel.start() } clearButton.setOnClickListener { viewModel.clear() } buttonDebug.setOnClickListener { viewModel.showDebugInfo() } buttonStatus.setOnClickListener { viewModel.showStatusInfo() } } @Suppress("UNCHECKED_CAST") private fun formatMapToString(status: Map?, indentation: String = "", bullet: String = "", level: Int = 0): String? { if (status == null) return null val sb = StringBuilder() status.toList().forEach { (key, value) -> val title = "$indentation$bullet$key" when (value) { is Map<*, *> -> { val formattedValue = formatMapToString(value as? Map, "\t\t$indentation", " - ", level + 1) sb.append("$title:\n$formattedValue${if (level < 2) "\n" else ""}") } else -> { sb.appendln("$title: $value") } } } val statusString = sb.trimEnd() return if (statusString.isEmpty()) "" else "$statusString\n" } } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/FeePriority.kt ================================================ package io.horizontalsystems.bitcoinkit.demo sealed class FeePriority(val feeRate: Int) { object Low : FeePriority(5) object Medium : FeePriority(10) object High : FeePriority(15) } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/MainActivity.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import android.os.Bundle import com.google.android.material.bottomnavigation.BottomNavigationView import androidx.fragment.app.Fragment import androidx.appcompat.app.AppCompatActivity import android.view.MenuItem import androidx.lifecycle.ViewModelProvider class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemSelectedListener { private val balanceFragment = BalanceFragment() private val transactionsFragment = TransactionsFragment() private val sendReceiveFragment = SendReceiveFragment() private val fm = supportFragmentManager private var active: Fragment = balanceFragment lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val navigation = findViewById(R.id.navigation) navigation.setOnNavigationItemSelectedListener(this) fm.beginTransaction().add(R.id.fragment_container, sendReceiveFragment, "3").hide(sendReceiveFragment).commit() fm.beginTransaction().add(R.id.fragment_container, transactionsFragment, "2").hide(transactionsFragment).commit() fm.beginTransaction().add(R.id.fragment_container, balanceFragment, "1").commit() viewModel = ViewModelProvider(this).get(MainViewModel::class.java) viewModel.init() } override fun onNavigationItemSelected(item: MenuItem): Boolean { val fragment = when (item.itemId) { R.id.navigation_home -> balanceFragment R.id.navigation_transactions -> transactionsFragment R.id.navigation_send_receive -> sendReceiveFragment else -> null } if (fragment != null) { supportFragmentManager .beginTransaction() .hide(active) .show(fragment) .commit() active = fragment return true } return false } } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/MainViewModel.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import io.horizontalsystems.bitcoincore.BitcoinCore import io.horizontalsystems.bitcoincore.BitcoinCore.KitState import io.horizontalsystems.bitcoincore.core.IPluginData import io.horizontalsystems.bitcoincore.exceptions.AddressFormatException import io.horizontalsystems.bitcoincore.managers.SendValueErrors import io.horizontalsystems.bitcoincore.models.BalanceInfo import io.horizontalsystems.bitcoincore.models.BitcoinSendInfo import io.horizontalsystems.bitcoincore.models.BlockInfo import io.horizontalsystems.bitcoincore.models.TransactionDataSortType import io.horizontalsystems.bitcoincore.models.TransactionFilterType import io.horizontalsystems.bitcoincore.models.TransactionInfo import io.horizontalsystems.bitcoincore.storage.UtxoFilters import io.horizontalsystems.bitcoinkit.BitcoinKit import io.horizontalsystems.hdwalletkit.HDWallet.Purpose import io.horizontalsystems.hodler.HodlerData import io.horizontalsystems.hodler.HodlerPlugin import io.horizontalsystems.hodler.LockTimeInterval import io.reactivex.disposables.CompositeDisposable class MainViewModel : ViewModel(), BitcoinKit.Listener { enum class State { STARTED, STOPPED } private var transactionFilterType: TransactionFilterType? = null val types = listOf(null) + TransactionFilterType.values() val transactions = MutableLiveData>() val balance = MutableLiveData() val lastBlock = MutableLiveData() val state = MutableLiveData() val status = MutableLiveData() val transactionRaw = MutableLiveData() val statusInfo = MutableLiveData>() lateinit var networkName: String private val disposables = CompositeDisposable() private var started = false set(value) { field = value status.value = (if (value) State.STARTED else State.STOPPED) } private lateinit var bitcoinKit: BitcoinKit private val walletId = "MyWallet" private val networkType = BitcoinKit.NetworkType.MainNet private val syncMode = BitcoinCore.SyncMode.Api() private val purpose = Purpose.BIP44 fun init() { //TODO create unique seed phrase,perhaps using shared preferences? val words = "used ugly meat glad balance divorce inner artwork hire invest already piano".split(" ") val passphrase = "" bitcoinKit = BitcoinKit(App.instance, words, passphrase, walletId, networkType, syncMode = syncMode, purpose = purpose) bitcoinKit.listener = this networkName = bitcoinKit.networkName balance.value = bitcoinKit.balance lastBlock.value = bitcoinKit.lastBlockInfo state.value = bitcoinKit.syncState started = false } fun start() { if (started) return started = true bitcoinKit.start() } fun clear() { bitcoinKit.stop() BitcoinKit.clear(App.instance, networkType, walletId) init() } fun showDebugInfo() { bitcoinKit.showDebugInfo() } fun showStatusInfo() { statusInfo.postValue(bitcoinKit.statusInfo()) } // // BitcoinKit Listener implementations // override fun onTransactionsUpdate(inserted: List, updated: List) { setTransactionFilterType(transactionFilterType) } override fun onTransactionsDelete(hashes: List) { } override fun onBalanceUpdate(balance: BalanceInfo) { this.balance.postValue(balance) } override fun onLastBlockInfoUpdate(blockInfo: BlockInfo) { this.lastBlock.postValue(blockInfo) } override fun onKitStateUpdate(state: KitState) { this.state.postValue(state) } val receiveAddressLiveData = MutableLiveData() val feeLiveData = MutableLiveData() val errorLiveData = MutableLiveData() val addressLiveData = MutableLiveData() val amountLiveData = MutableLiveData() var amount: Long? = null set(value) { field = value updateFee() } var address: String? = null set(value) { field = value updateFee() } var feePriority: FeePriority = FeePriority.Medium set(value) { field = value updateFee() } var timeLockInterval: LockTimeInterval? = null set(value) { field = value updateFee() } fun onReceiveClick() { receiveAddressLiveData.value = bitcoinKit.receiveAddress() } fun onSendClick() { when { address.isNullOrBlank() -> { errorLiveData.value = "Send address cannot be blank" } amount == null -> { errorLiveData.value = "Send amount cannot be blank" } else -> { try { val transaction = bitcoinKit.send( address!!, null, amount!!, feeRate = feePriority.feeRate, sortType = TransactionDataSortType.Shuffle, pluginData = getPluginData(), rbfEnabled = true, changeToFirstInput = false, filters = UtxoFilters() ) amountLiveData.value = null feeLiveData.value = null addressLiveData.value = null errorLiveData.value = "Transaction sent ${transaction.header.serializedTxInfo}" } catch (e: Exception) { errorLiveData.value = when (e) { is SendValueErrors.InsufficientUnspentOutputs, is SendValueErrors.EmptyOutputs -> "Insufficient balance" is AddressFormatException -> "Could not Format Address" else -> e.message ?: "Failed to send transaction (${e.javaClass.name})" } } } } } fun onMaxClick() { try { amountLiveData.value = bitcoinKit.maximumSpendableValue( address, null, feePriority.feeRate, null, getPluginData(), false, UtxoFilters() ) } catch (e: Exception) { amountLiveData.value = 0 errorLiveData.value = when (e) { is SendValueErrors.Dust, is SendValueErrors.EmptyOutputs -> "You need at least ${e.message} satoshis to make an transaction" is AddressFormatException -> "Could not Format Address" else -> e.message ?: "Maximum could not be calculated" } } } private fun updateFee() { try { feeLiveData.value = amount?.let { fee(it, address).fee } } catch (e: Exception) { errorLiveData.value = e.message ?: e.javaClass.simpleName } } private fun fee(value: Long, address: String? = null): BitcoinSendInfo { return bitcoinKit.sendInfo( value, address, null, feeRate = feePriority.feeRate, unspentOutputs = null, pluginData = getPluginData(), changeToFirstInput = false, filters = UtxoFilters() ) } private fun getPluginData(): MutableMap { val pluginData = mutableMapOf() timeLockInterval?.let { pluginData[HodlerPlugin.id] = HodlerData(it) } return pluginData } fun onRawTransactionClick(transactionHash: String) { transactionRaw.postValue(bitcoinKit.getRawTransaction(transactionHash)) } fun setTransactionFilterType(transactionFilterType: TransactionFilterType?) { this.transactionFilterType = transactionFilterType bitcoinKit.transactions(type = transactionFilterType).subscribe { txList: List -> transactions.postValue(txList) }.let { disposables.add(it) } } } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/NumberFormatHelper.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import java.text.NumberFormat object NumberFormatHelper { val fiatAmountFormat: NumberFormat get() { val numberFormat = NumberFormat.getInstance() numberFormat.maximumFractionDigits = 2 numberFormat.minimumFractionDigits = 2 return numberFormat } val cryptoAmountFormat: NumberFormat get() { val numberFormat = NumberFormat.getInstance() numberFormat.maximumFractionDigits = 12 numberFormat.minimumFractionDigits = 2 return numberFormat } } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/SendReceiveFragment.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Button import android.widget.EditText import android.widget.RadioGroup import android.widget.Spinner import android.widget.TextView import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import io.horizontalsystems.hodler.LockTimeInterval class SendReceiveFragment : Fragment() { private lateinit var viewModel: MainViewModel lateinit var receiveAddressText: TextView lateinit var receiveAddressButton: Button lateinit var sendAmount: EditText lateinit var sendAddress: EditText lateinit var txFeeValue: TextView lateinit var sendButton: Button lateinit var maxButton: Button lateinit var radioGroup: RadioGroup lateinit var lockTimePeriodValue: Spinner override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_send_receive, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = activity?.let { ViewModelProvider(it).get(MainViewModel::class.java) } ?: return viewModel.receiveAddressLiveData.observe(viewLifecycleOwner, Observer { receiveAddressText.text = it }) viewModel.amountLiveData.observe(viewLifecycleOwner, Observer { sendAmount.setText(it?.toString()) }) viewModel.addressLiveData.observe(viewLifecycleOwner, Observer { sendAddress.setText(it) }) viewModel.feeLiveData.observe(viewLifecycleOwner, Observer { txFeeValue.text = it?.toString() }) viewModel.errorLiveData.observe(viewLifecycleOwner, Observer { //TODO TOASTS NO PREVIOUS OUTPUT SCRIPT CHANGE THE SEED? val toast = Toast.makeText(context, "$it", Toast.LENGTH_LONG) toast.setGravity(Gravity.CENTER, 0, 0) toast.show() }) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) receiveAddressText = view.findViewById(R.id.receiveAddressText) receiveAddressButton = view.findViewById(R.id.receiveAddressButton) sendAmount = view.findViewById(R.id.sendAmount) sendAddress = view.findViewById(R.id.sendAddress) txFeeValue = view.findViewById(R.id.txFeeValue) sendButton = view.findViewById(R.id.sendButton) maxButton = view.findViewById(R.id.maxButton) radioGroup = view.findViewById(R.id.radioGroup) lockTimePeriodValue = view.findViewById(R.id.lockTimePeriodValue) receiveAddressButton.setOnClickListener { viewModel.onReceiveClick() } sendButton.setOnClickListener { viewModel.onSendClick() } maxButton.setOnClickListener { viewModel.onMaxClick() } sendAmount.addTextChangedListener(SimpleTextWatcher { viewModel.amount = try { sendAmount.text?.toString()?.toLong() } catch (e: NumberFormatException) { null } }) sendAddress.addTextChangedListener(SimpleTextWatcher { viewModel.address = sendAddress.text.toString() }) radioGroup.setOnCheckedChangeListener { _, checkedId -> val feePriority = when (checkedId) { R.id.radioLow -> FeePriority.Low R.id.radioMedium -> FeePriority.Medium R.id.radioHigh -> FeePriority.High else -> throw Exception("Undefined priority") } viewModel.feePriority = feePriority } lockTimePeriodValue.adapter = ArrayAdapter(view.context, android.R.layout.simple_spinner_dropdown_item, LockTimeIntervalOption.getIntervals()) lockTimePeriodValue.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onNothingSelected(parent: AdapterView<*>?) = Unit override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { viewModel.timeLockInterval = (parent?.getItemAtPosition(position) as LockTimeIntervalOption).interval } } } } class LockTimeIntervalOption(val interval: LockTimeInterval? = null) { override fun toString(): String { return interval?.name ?: "OFF" } companion object { fun getIntervals(): Array { return arrayOf(LockTimeIntervalOption()) + LockTimeInterval.values().map { LockTimeIntervalOption(it) } } } } class SimpleTextWatcher(private val callback: (s: Editable?) -> Unit) : TextWatcher { override fun afterTextChanged(s: Editable?) { callback.invoke(s) } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit } ================================================ FILE: app/src/main/java/io/horizontalsystems/bitcoinkit/demo/TransactionsFragment.kt ================================================ package io.horizontalsystems.bitcoinkit.demo import android.annotation.SuppressLint import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.tabs.TabLayout import io.horizontalsystems.bitcoincore.models.TransactionInfo import io.horizontalsystems.bitcoincore.models.TransactionInputInfo import io.horizontalsystems.bitcoincore.models.TransactionOutputInfo import io.horizontalsystems.dashkit.models.DashTransactionInfo import io.horizontalsystems.hodler.HodlerOutputData import io.horizontalsystems.hodler.HodlerPlugin import java.text.DateFormat import java.util.Date import java.util.Locale class TransactionsFragment : Fragment(), ViewHolderTransaction.Listener { private lateinit var viewModel: MainViewModel private lateinit var transactionsRecyclerView: RecyclerView private val transactionsAdapter = TransactionsAdapter(this) lateinit var tabs: TabLayout override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = activity?.let { ViewModelProvider(it).get(MainViewModel::class.java) } ?: return viewModel.transactions.observe(this, Observer { it?.let { transactions -> transactionsAdapter.items = transactions transactionsAdapter.notifyDataSetChanged() } }) viewModel.transactionRaw.observe(this, Observer { transactionHex -> activity?.let { val dialog = AlertDialog.Builder(it) .setMessage(transactionHex) .setTitle("Transaction HEX") .create() dialog.show() } }) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_transactions, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) tabs = view.findViewById(R.id.tabs) transactionsRecyclerView = view.findViewById(R.id.transactions) transactionsRecyclerView.adapter = transactionsAdapter transactionsRecyclerView.layoutManager = LinearLayoutManager(context) viewModel.types.forEach { val text = tabs.newTab() .setText(it?.name ?: "All") .setId(it?.ordinal ?: 100) tabs.addTab(text) } tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { viewModel.setTransactionFilterType(viewModel.types.find { it?.ordinal == tab.id }) } override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabReselected(tab: TabLayout.Tab?) = Unit }) viewModel.setTransactionFilterType(null) } override fun onClickRawTransaction(transactionHash: String) { viewModel.onRawTransactionClick(transactionHash) } } class TransactionsAdapter(private val listener: ViewHolderTransaction.Listener) : RecyclerView.Adapter() { var items = listOf() override fun getItemCount() = items.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = ViewHolderTransaction(LayoutInflater.from(parent.context).inflate(R.layout.view_holder_transaction, parent, false), listener) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is ViewHolderTransaction -> holder.bind(items[position], itemCount - position) } } } class ViewHolderTransaction(val containerView: View, private val listener: Listener) : RecyclerView.ViewHolder(containerView) { interface Listener { fun onClickRawTransaction(transactionHash: String) } private val summary = containerView.findViewById(R.id.summary)!! private val buttonRaw = containerView.findViewById