├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── gradle.yml │ └── mutationtests.yml ├── .gitignore ├── .java-version ├── LICENSE ├── README.md ├── backend ├── address-transactions │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ ├── AddressTransactionsRequest.java │ │ │ ├── SimpleAddressTransactionsProvider.java │ │ │ ├── TransactionsRequestKey.java │ │ │ └── deserialization │ │ │ └── AddressTransactionsDto.java │ │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ ├── AddressTransactionsRequestTest.java │ │ │ ├── SimpleAddressTransactionsProviderTest.java │ │ │ ├── TransactionsRequestKeyTest.java │ │ │ └── deserialization │ │ │ └── AddressTransactionsDtoTest.java │ │ └── testFixtures │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── transaction │ │ └── TransactionsRequestKeyFixtures.java ├── blockheight │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ ├── BlockHeightProvider.java │ │ │ └── BlockHeightRequest.java │ │ └── test │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── transaction │ │ ├── BlockHeightProviderTest.java │ │ └── BlockHeightRequestTest.java ├── build.gradle.kts ├── models │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── model │ │ │ ├── Address.java │ │ │ ├── AddressTransactions.java │ │ │ ├── AddressWithDescription.java │ │ │ ├── Base58Address.java │ │ │ ├── Base58Encoder.java │ │ │ ├── Bech32Address.java │ │ │ ├── Bech32Base.java │ │ │ ├── CashAddrAddress.java │ │ │ ├── Chain.java │ │ │ ├── Coins.java │ │ │ ├── HashAndChain.java │ │ │ ├── HexString.java │ │ │ ├── Input.java │ │ │ ├── InputOutput.java │ │ │ ├── ModelWithDescription.java │ │ │ ├── Output.java │ │ │ ├── Provider.java │ │ │ ├── ProviderException.java │ │ │ ├── Transaction.java │ │ │ ├── TransactionHash.java │ │ │ └── TransactionWithDescription.java │ │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── model │ │ │ ├── AddressTest.java │ │ │ ├── AddressTransactionsTest.java │ │ │ ├── AddressWithDescriptionTest.java │ │ │ ├── Base58AddressTest.java │ │ │ ├── Base58EncoderTest.java │ │ │ ├── Bech32AddressTest.java │ │ │ ├── CashAddrAddressTest.java │ │ │ ├── ChainTest.java │ │ │ ├── CoinsTest.java │ │ │ ├── HashAndChainTest.java │ │ │ ├── HexStringTest.java │ │ │ ├── InputTest.java │ │ │ ├── OutputTest.java │ │ │ ├── ProviderExceptionTest.java │ │ │ ├── ProviderTest.java │ │ │ ├── TransactionHashTest.java │ │ │ ├── TransactionTest.java │ │ │ └── TransactionWithDescriptionTest.java │ │ └── testFixtures │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── model │ │ ├── AddressFixtures.java │ │ ├── AddressTransactionsFixtures.java │ │ ├── InputFixtures.java │ │ ├── OutputFixtures.java │ │ ├── TestableProvider.java │ │ ├── TransactionFixtures.java │ │ └── TransactionHashFixtures.java ├── price │ ├── build.gradle.kts │ └── src │ │ ├── integrationTest │ │ ├── java │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ ├── SchedulingConfiguration.java │ │ │ │ ├── SpringBootConfiguration.java │ │ │ │ └── price │ │ │ │ └── PriceServiceIT.java │ │ └── resources │ │ │ └── application.properties │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── price │ │ │ ├── AsyncConfiguration.java │ │ │ ├── PriceDao.java │ │ │ ├── PriceRequest.java │ │ │ ├── PriceService.java │ │ │ ├── PrioritizingPriceProvider.java │ │ │ ├── kraken │ │ │ ├── KrakenClient.java │ │ │ ├── KrakenOhlcDataDto.java │ │ │ ├── KrakenPriceProvider.java │ │ │ └── KrakenTradesDto.java │ │ │ ├── model │ │ │ ├── Price.java │ │ │ ├── PriceContext.java │ │ │ └── PriceWithContext.java │ │ │ └── persistence │ │ │ ├── PriceDaoImpl.java │ │ │ ├── PriceJpaDto.java │ │ │ ├── PriceRepository.java │ │ │ ├── PriceWithContextId.java │ │ │ └── PriceWithContextJpaDto.java │ │ └── test │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── price │ │ ├── PriceProvider.java │ │ ├── PriceRequestTest.java │ │ ├── PriceServiceTest.java │ │ ├── PrioritizingPriceProviderTest.java │ │ ├── kraken │ │ ├── KrakenOhlcDataDtoTest.java │ │ ├── KrakenPriceProviderTest.java │ │ └── KrakenTradesDtoTest.java │ │ ├── model │ │ ├── PriceTest.java │ │ └── PriceWithContextTest.java │ │ └── persistence │ │ ├── PriceDaoImplTest.java │ │ ├── PriceJpaDtoTest.java │ │ ├── PriceWithContextIdTest.java │ │ └── PriceWithContextJpaDtoTest.java ├── provider │ ├── all │ │ └── build.gradle.kts │ ├── base │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ ├── AddressTransactionsDeserializer.java │ │ │ │ └── deserialization │ │ │ │ ├── DefaultInputOutputDtoDeserializer.java │ │ │ │ ├── InputDto.java │ │ │ │ ├── InputOutputDto.java │ │ │ │ ├── InputOutputDtoDeserializer.java │ │ │ │ ├── MultiAddressInputOutputDtoDeserializer.java │ │ │ │ ├── OutputDto.java │ │ │ │ ├── TransactionDto.java │ │ │ │ └── TransactionDtoDeserializer.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ ├── AddressTransactionsDeserializerTest.java │ │ │ │ └── deserialization │ │ │ │ ├── DefaultInputOutputDtoDeserializerTest.java │ │ │ │ ├── DummyDeserializer.java │ │ │ │ ├── InputDtoTest.java │ │ │ │ ├── MultiAddressInputOutputDtoDeserializerTest.java │ │ │ │ ├── OutputDtoTest.java │ │ │ │ ├── TestableTransactionDto.java │ │ │ │ ├── TestableTransactionDtoDeserializer.java │ │ │ │ ├── TransactionDtoDeserializerTest.java │ │ │ │ └── TransactionDtoTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── deserialization │ │ │ ├── InputDtoFixtures.java │ │ │ ├── OutputDtoFixtures.java │ │ │ └── TestObjectMapper.java │ ├── bitaps │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── bitaps │ │ │ │ ├── BitapsAddressTransactionsDto.java │ │ │ │ ├── BitapsAddressTransactionsProvider.java │ │ │ │ ├── BitapsBlockHeightDto.java │ │ │ │ ├── BitapsBlockHeightProvider.java │ │ │ │ ├── BitapsClient.java │ │ │ │ ├── BitapsTransactionDto.java │ │ │ │ └── BitapsTransactionProvider.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── bitaps │ │ │ │ ├── BitapsAddressTransactionsDtoTest.java │ │ │ │ ├── BitapsAddressTransactionsProviderTest.java │ │ │ │ ├── BitapsBlockHeightDtoTest.java │ │ │ │ ├── BitapsBlockHeightProviderTest.java │ │ │ │ ├── BitapsTransactionDtoTest.java │ │ │ │ └── BitapsTransactionProviderTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── bitaps │ │ │ ├── BitapsAddressTransactionDtoFixtures.java │ │ │ └── BitapsTransactionDtoFixtures.java │ ├── bitcoind │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── bitcoind │ │ │ │ ├── BitcoinCliWrapper.java │ │ │ │ └── BitcoindBlockHeightProvider.java │ │ │ └── test │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── bitcoind │ │ │ ├── BitcoinCliWrapperTest.java │ │ │ └── BitcoindBlockHeightProviderTest.java │ ├── blockchaininfo │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockchaininfo │ │ │ │ ├── BlockchainInfoBlockHeightProvider.java │ │ │ │ ├── BlockchainInfoClient.java │ │ │ │ ├── BlockchainInfoTransactionDto.java │ │ │ │ └── BlockchainInfoTransactionProvider.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockchaininfo │ │ │ │ ├── BlockchainInfoBlockHeightProviderTest.java │ │ │ │ ├── BlockchainInfoTransactionDtoTest.java │ │ │ │ └── BlockchainInfoTransactionProviderTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── blockchaininfo │ │ │ └── BlockchainInfoTransactionDtoFixtures.java │ ├── blockchair │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockchair │ │ │ │ ├── BlockchairAddressTransactionsDto.java │ │ │ │ ├── BlockchairAddressTransactionsProvider.java │ │ │ │ ├── BlockchairBlockHeightDto.java │ │ │ │ ├── BlockchairBlockHeightProvider.java │ │ │ │ ├── BlockchairChainName.java │ │ │ │ ├── BlockchairClient.java │ │ │ │ ├── BlockchairTransactionDto.java │ │ │ │ └── BlockchairTransactionProvider.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockchair │ │ │ │ ├── BlockchairAddressTransactionsDtoTest.java │ │ │ │ ├── BlockchairAddressTransactionsProviderTest.java │ │ │ │ ├── BlockchairBlockHeightDtoTest.java │ │ │ │ ├── BlockchairBlockHeightProviderTest.java │ │ │ │ ├── BlockchairChainNameTest.java │ │ │ │ ├── BlockchairTransactionDtoTest.java │ │ │ │ └── BlockchairTransactionProviderTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── blockchair │ │ │ ├── BlockchairAddressTransactionsFixtures.java │ │ │ └── BlockchairTransactionDtoFixtures.java │ ├── blockcypher │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockcypher │ │ │ │ ├── BlockcypherClient.java │ │ │ │ ├── BlockcypherTransactionDto.java │ │ │ │ └── BlockcypherTransactionProvider.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockcypher │ │ │ │ ├── BlockcypherTransactionDtoTest.java │ │ │ │ └── BlockcypherTransactionProviderTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── blockcypher │ │ │ └── BlockcypherTransactionDtoFixtures.java │ ├── blockstreaminfo │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockstream │ │ │ │ ├── BlockstreamAddressTransactionsDto.java │ │ │ │ ├── BlockstreamAddressTransactionsProvider.java │ │ │ │ └── BlockstreamInfoClient.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── blockstream │ │ │ │ ├── BlockstreamAddressTransactionsDtoTest.java │ │ │ │ └── BlockstreamAddressTransactionsProviderTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── blockstream │ │ │ └── BlockstreamAddressTransactionsFixtures.java │ ├── btccom │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── btccom │ │ │ │ ├── BtcComAddressTransactionsDto.java │ │ │ │ ├── BtcComAddressTransactionsProvider.java │ │ │ │ ├── BtcComClient.java │ │ │ │ ├── BtcComTransactionDto.java │ │ │ │ └── BtcComTransactionProvider.java │ │ │ ├── test │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── btccom │ │ │ │ ├── BtcComAddressTransactionsDtoTest.java │ │ │ │ ├── BtcComAddressTransactionsProviderTest.java │ │ │ │ ├── BtcComTransactionDtoTest.java │ │ │ │ └── BtcComTransactionProviderTest.java │ │ │ └── testFixtures │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── btccom │ │ │ ├── BtcComAddressTransactionsFixtures.java │ │ │ └── BtcComTransactionDtoFixtures.java │ ├── electrs │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── electrs │ │ │ │ ├── ElectrsAddressTransactionsProvider.java │ │ │ │ ├── ElectrsClient.java │ │ │ │ └── jsonrpc │ │ │ │ ├── ClientSessionHandler.java │ │ │ │ ├── CodecFactory.java │ │ │ │ ├── Decoder.java │ │ │ │ ├── Encoder.java │ │ │ │ ├── JsonRpcClient.java │ │ │ │ └── JsonRpcMessage.java │ │ │ └── test │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── electrs │ │ │ ├── ElectrsAddressTransactionsProviderTest.java │ │ │ ├── ElectrsClientTest.java │ │ │ └── jsonrpc │ │ │ ├── ClientSessionHandlerTest.java │ │ │ ├── CodecFactoryTest.java │ │ │ ├── DecoderTest.java │ │ │ ├── EncoderTest.java │ │ │ ├── JsonRpcClientTest.java │ │ │ └── JsonRpcMessageTest.java │ ├── fullstackcash │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── transaction │ │ │ │ └── fullstackcash │ │ │ │ ├── FullstackCashBlockHeightProvider.java │ │ │ │ └── FullstackCashClient.java │ │ │ └── test │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── fullstackcash │ │ │ └── FullstackCashBlockHeightProviderTest.java │ └── mempoolspace │ │ ├── build.gradle.kts │ │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── mempoolspace │ │ │ ├── MempoolSpaceAddressTransactionsDto.java │ │ │ ├── MempoolSpaceAddressTransactionsProvider.java │ │ │ └── MempoolSpaceClient.java │ │ ├── test │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── transaction │ │ │ └── mempoolspace │ │ │ ├── MempoolSpaceAddressTransactionsDtoTest.java │ │ │ └── MempoolSpaceAddressTransactionsProviderTest.java │ │ └── testFixtures │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── transaction │ │ └── mempoolspace │ │ └── MempoolSpaceAddressTransactionsFixtures.java ├── request │ ├── build.gradle.kts │ ├── models │ │ ├── build.gradle.kts │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ └── request │ │ │ │ ├── AllProvidersFailedException.java │ │ │ │ ├── NotSupportedByAnyProviderException.java │ │ │ │ ├── PrioritizedRequest.java │ │ │ │ ├── PrioritizedRequestWithResult.java │ │ │ │ ├── RequestPriority.java │ │ │ │ ├── ResultFromProvider.java │ │ │ │ └── ResultFuture.java │ │ │ └── test │ │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── request │ │ │ ├── AllProvidersFailedExceptionTest.java │ │ │ ├── NotSupportedByAnyProviderExceptionTest.java │ │ │ ├── PrioritizedRequestTest.java │ │ │ ├── PrioritizedRequestWithResultTest.java │ │ │ ├── RequestPriorityTest.java │ │ │ ├── ResultFromProviderTest.java │ │ │ └── ResultFutureTest.java │ └── src │ │ ├── integrationTest │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ ├── SpringBootConfiguration.java │ │ │ └── request │ │ │ └── PrioritizingProviderIT.java │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ └── request │ │ │ ├── PrioritizingProvider.java │ │ │ ├── QueueStatus.java │ │ │ ├── RequestWorker.java │ │ │ ├── Score.java │ │ │ └── ScoreUpdate.java │ │ └── test │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── request │ │ ├── PrioritizingProviderTest.java │ │ ├── QueueStatusTest.java │ │ ├── RequestWorkerTest.java │ │ ├── ScoreTest.java │ │ └── ScoreUpdateTest.java ├── src │ ├── main │ │ ├── java │ │ │ └── de │ │ │ │ └── cotto │ │ │ │ └── bitbook │ │ │ │ └── backend │ │ │ │ ├── AddressDescriptionService.java │ │ │ │ ├── AddressWithDescriptionDao.java │ │ │ │ ├── DescriptionDao.java │ │ │ │ ├── DescriptionService.java │ │ │ │ ├── FeignConfiguration.java │ │ │ │ ├── SchedulingConfiguration.java │ │ │ │ ├── TransactionDescriptionService.java │ │ │ │ ├── TransactionWithDescriptionDao.java │ │ │ │ └── persistence │ │ │ │ ├── AddressWithDescriptionDaoImpl.java │ │ │ │ ├── AddressWithDescriptionJpaDto.java │ │ │ │ ├── AddressWithDescriptionRepository.java │ │ │ │ ├── TransactionWithDescriptionDaoImpl.java │ │ │ │ ├── TransactionWithDescriptionJpaDto.java │ │ │ │ └── TransactionWithDescriptionRepository.java │ │ └── resources │ │ │ └── application.properties │ └── test │ │ ├── java │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ ├── AddressDescriptionServiceTest.java │ │ │ ├── FeignConfigurationTest.java │ │ │ ├── TransactionDescriptionServiceTest.java │ │ │ └── persistence │ │ │ ├── AddressWithDescriptionDaoImplTest.java │ │ │ ├── AddressWithDescriptionJpaDtoTest.java │ │ │ ├── TransactionWithDescriptionDaoImplTest.java │ │ │ └── TransactionWithDescriptionJpaDtoTest.java │ │ └── resources │ │ └── logback-test.xml └── transaction │ ├── build.gradle.kts │ └── src │ ├── integrationTest │ ├── java │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── backend │ │ │ ├── SpringBootConfiguration.java │ │ │ └── transaction │ │ │ ├── AddressTransactionsProvider.java │ │ │ ├── AddressTransactionsServiceIT.java │ │ │ └── TransactionServiceIT.java │ └── resources │ │ └── application.properties │ ├── main │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── transaction │ │ ├── AddressCompletionDao.java │ │ ├── AddressTransactionsDao.java │ │ ├── AddressTransactionsService.java │ │ ├── BalanceService.java │ │ ├── BlockHeightService.java │ │ ├── PrioritizingAddressTransactionsProvider.java │ │ ├── PrioritizingBlockHeightProvider.java │ │ ├── PrioritizingTransactionProvider.java │ │ ├── TransactionCompletionDao.java │ │ ├── TransactionDao.java │ │ ├── TransactionRequest.java │ │ ├── TransactionService.java │ │ ├── TransactionUpdateHeuristics.java │ │ └── persistence │ │ ├── AddressCompletionDaoImpl.java │ │ ├── AddressTransactionsDaoImpl.java │ │ ├── AddressTransactionsJpaDto.java │ │ ├── AddressTransactionsJpaDtoId.java │ │ ├── AddressTransactionsRepository.java │ │ ├── AddressView.java │ │ ├── InputJpaDto.java │ │ ├── InputOutputJpaDto.java │ │ ├── InputRepository.java │ │ ├── OutputJpaDto.java │ │ ├── OutputRepository.java │ │ ├── TransactionCompletionDaoImpl.java │ │ ├── TransactionDaoImpl.java │ │ ├── TransactionHashView.java │ │ ├── TransactionJpaDto.java │ │ ├── TransactionJpaDtoId.java │ │ └── TransactionRepository.java │ ├── test │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── backend │ │ └── transaction │ │ ├── AddressTransactionsProvider.java │ │ ├── AddressTransactionsServiceTest.java │ │ ├── BalanceServiceTest.java │ │ ├── BlockHeightServiceTest.java │ │ ├── PrioritizingAddressTransactionsProviderTest.java │ │ ├── PrioritizingBlockchainInfoBlockHeightProviderTest.java │ │ ├── PrioritizingTransactionProviderTest.java │ │ ├── TransactionProvider.java │ │ ├── TransactionRequestTest.java │ │ ├── TransactionServiceTest.java │ │ ├── TransactionUpdateHeuristicsTest.java │ │ └── persistence │ │ ├── AddressCompletionDaoImplTest.java │ │ ├── AddressTransactionsDaoImplTest.java │ │ ├── AddressTransactionsJpaDtoIdTest.java │ │ ├── AddressTransactionsJpaDtoTest.java │ │ ├── InputJpaDtoTest.java │ │ ├── OutputJpaDtoTest.java │ │ ├── TransactionCompletionDaoImplTest.java │ │ ├── TransactionDaoImplTest.java │ │ ├── TransactionJpaDtoIdTest.java │ │ └── TransactionJpaDtoTest.java │ └── testFixtures │ └── java │ └── de │ └── cotto │ └── bitbook │ └── backend │ └── transaction │ ├── TransactionRequestFixtures.java │ └── persistence │ ├── AddressTransactionsJpaDtoFixtures.java │ ├── InputJpaDtoFixtures.java │ ├── OutputJpaDtoFixtures.java │ └── TransactionJpaDtoFixtures.java ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── bitbook.java-conventions.gradle.kts │ └── bitbook.java-library-conventions.gradle.kts ├── cli ├── base │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── cli │ │ │ ├── AbstractAddressCompletionProvider.java │ │ │ ├── AbstractCompletionProvider.java │ │ │ ├── AbstractTransactionCompletionProvider.java │ │ │ ├── AddressCompletionProvider.java │ │ │ ├── AddressConverter.java │ │ │ ├── AddressFormatter.java │ │ │ ├── AddressWithDescriptionCompletionProvider.java │ │ │ ├── AddressWithOwnershipCompletionProvider.java │ │ │ ├── CliAddress.java │ │ │ ├── CliString.java │ │ │ ├── CliTransactionHash.java │ │ │ ├── PriceFormatter.java │ │ │ ├── SelectedChain.java │ │ │ ├── TransactionFormatter.java │ │ │ ├── TransactionHashCompletionProvider.java │ │ │ ├── TransactionHashConverter.java │ │ │ ├── TransactionSortOrder.java │ │ │ ├── TransactionSorter.java │ │ │ └── TransactionWithDescriptionCompletionProvider.java │ │ └── test │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── cli │ │ ├── AddressCompletionProviderTest.java │ │ ├── AddressConverterTest.java │ │ ├── AddressFormatterTest.java │ │ ├── AddressWithDescriptionCompletionProviderTest.java │ │ ├── AddressWithOwnershipCompletionProviderTest.java │ │ ├── CliAddressTest.java │ │ ├── CliTransactionHashTest.java │ │ ├── PriceFormatterTest.java │ │ ├── SelectedChainTest.java │ │ ├── TransactionFormatterTest.java │ │ ├── TransactionHashCompletionProviderTest.java │ │ ├── TransactionHashConverterTest.java │ │ ├── TransactionSorterTest.java │ │ └── TransactionWithDescriptionCompletionProviderTest.java ├── build.gradle.kts ├── lnd │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── lnd │ │ │ └── cli │ │ │ ├── LndCommands.java │ │ │ └── PoolCommands.java │ │ └── test │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── lnd │ │ └── cli │ │ ├── LndCommandsTest.java │ │ ├── PoolCommandsTest.java │ │ └── TempFileUtil.java ├── ownership │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── java │ │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ └── ownership │ │ │ └── cli │ │ │ └── OwnershipCommands.java │ │ └── test │ │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── ownership │ │ └── cli │ │ └── OwnershipCommandsTest.java └── src │ ├── integrationTest │ ├── java │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ ├── ArchUnitIT.java │ │ │ ├── BitBookApplicationIT.java │ │ │ └── TestApplicationRunner.java │ └── resources │ │ └── application.properties │ ├── main │ ├── java │ │ └── de │ │ │ └── cotto │ │ │ └── bitbook │ │ │ ├── BitBookApplication.java │ │ │ └── cli │ │ │ ├── AddressCommands.java │ │ │ ├── ChainCommand.java │ │ │ ├── CustomPromptProvider.java │ │ │ ├── History.java │ │ │ ├── QuitCommand.java │ │ │ └── TransactionsCommands.java │ └── resources │ │ ├── application.properties │ │ └── db │ │ └── migration │ │ ├── V1_0_0__base.sql │ │ ├── V1_0_1__prices_with_chain.sql │ │ ├── V1_0_2__address_transactions_with_chain.sql │ │ └── V1_0_3__transactions_with_chain.sql │ └── test │ └── java │ └── de │ └── cotto │ └── bitbook │ └── cli │ ├── AddressCommandsTest.java │ ├── ChainCommandTest.java │ ├── CustomPromptProviderTest.java │ ├── HistoryTest.java │ ├── QuitCommandTest.java │ └── TransactionsCommandsTest.java ├── documentation ├── bitbook.gif ├── commands.md ├── contributing.md ├── example.md ├── faq.md ├── features_and_bugs.md ├── ideas.md ├── limitations.md ├── lnd.md ├── qr.png ├── technical.md └── thumbsup.png ├── gradle.properties ├── gradle ├── meta-plugins │ ├── platform │ │ └── build.gradle.kts │ └── settings.gradle.kts └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lnd ├── build.gradle.kts └── src │ ├── main │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── lnd │ │ ├── AbstractJsonService.java │ │ ├── ChannelPointParser.java │ │ ├── ChannelsParser.java │ │ ├── ClosedChannelsParser.java │ │ ├── LndService.java │ │ ├── OnchainTransactionsParser.java │ │ ├── PoolService.java │ │ ├── features │ │ ├── AbstractTransactionsService.java │ │ ├── ChannelsService.java │ │ ├── ClosedChannelsService.java │ │ ├── OnchainTransactionsService.java │ │ ├── PoolLeasesService.java │ │ ├── PoolTransactionService.java │ │ ├── SweepTransactionsService.java │ │ └── UnspentOutputsService.java │ │ └── model │ │ ├── Channel.java │ │ ├── CloseType.java │ │ ├── ClosedChannel.java │ │ ├── Initiator.java │ │ ├── OnchainTransaction.java │ │ ├── PoolLease.java │ │ └── Resolution.java │ ├── test │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── lnd │ │ ├── ChannelPointParserTest.java │ │ ├── ChannelsParserTest.java │ │ ├── ClosedChannelsParserTest.java │ │ ├── LndServiceTest.java │ │ ├── OnchainTransactionsParserTest.java │ │ ├── PoolServiceTest.java │ │ ├── features │ │ ├── ChannelsServiceTest.java │ │ ├── ClosedChannelsServiceTest.java │ │ ├── OnchainTransactionsServiceTest.java │ │ ├── PoolLeasesServiceTest.java │ │ ├── PoolTransactionServiceTest.java │ │ ├── SweepTransactionsServiceTest.java │ │ └── UnspentOutputsServiceTest.java │ │ └── model │ │ ├── ChannelTest.java │ │ ├── CloseTypeTest.java │ │ ├── ClosedChannelTest.java │ │ ├── InitiatorTest.java │ │ ├── OnchainTransactionTest.java │ │ ├── PoolLeaseTest.java │ │ └── ResolutionTest.java │ └── testFixtures │ └── java │ └── de │ └── cotto │ └── bitbook │ └── lnd │ └── model │ ├── ChannelFixtures.java │ ├── ClosedChannelFixtures.java │ ├── OnchainTransactionFixtures.java │ └── PoolLeaseFixtures.java ├── ownership ├── build.gradle.kts └── src │ ├── integrationTest │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ ├── SpringBootConfiguration.java │ │ └── ownership │ │ └── AddressOwnershipServiceIT.java │ ├── main │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── ownership │ │ ├── AddressOwnershipDao.java │ │ ├── AddressOwnershipService.java │ │ ├── OwnershipStatus.java │ │ └── persistence │ │ ├── AddressOwnershipDaoImpl.java │ │ ├── AddressOwnershipJpaDto.java │ │ └── AddressOwnershipRepository.java │ ├── test │ └── java │ │ └── de │ │ └── cotto │ │ └── bitbook │ │ └── ownership │ │ ├── AddressOwnershipServiceTest.java │ │ ├── OwnershipStatusTest.java │ │ └── persistence │ │ ├── AddressOwnershipDaoTest.java │ │ └── AddressOwnershipJpaDtoTest.java │ └── testFixtures │ └── java │ └── de │ └── cotto │ └── bitbook │ └── ownership │ └── persistence │ └── AddressOwnershipJpaDtoFixtures.java ├── settings.gradle.kts └── start.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'Bug: ' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Which commands did you run to reproduce this? 15 | If this only happens for specific transactions/addresses, please include this in the report. 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | 26 | * Which version (git commit) are you running? 27 | * Which operating system and terminal software are you using? 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Feature Request: ' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | *Thank you for suggesting an idea, I value your feedback. 11 | Please let me know how BitBook can help you!* 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '17' 21 | distribution: 'adopt' 22 | - name: Setup Gradle 23 | uses: gradle/gradle-build-action@v2 24 | - name: Build with Gradle 25 | run: ./gradlew build 26 | -------------------------------------------------------------------------------- /.github/workflows/mutationtests.yml: -------------------------------------------------------------------------------- 1 | name: Run mutation tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | mutationtests: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '17' 21 | distribution: 'adopt' 22 | - name: Setup Gradle 23 | uses: gradle/gradle-build-action@v2 24 | - name: Run mutation tests 25 | run: ./gradlew pitest 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | out/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | .idea 7 | 8 | bitbook.log 9 | bitbook.db.* -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Carsten Otto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/address-transactions/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend:request:models")) 7 | implementation(project(":backend:models")) 8 | testImplementation(testFixtures(project(":backend:models"))) 9 | testFixturesImplementation(testFixtures(project(":backend:models"))) 10 | testFixturesImplementation(project(":backend:request:models")) 11 | } 12 | -------------------------------------------------------------------------------- /backend/address-transactions/src/main/java/de/cotto/bitbook/backend/transaction/AddressTransactionsRequest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.AddressTransactions; 4 | import de.cotto.bitbook.backend.request.PrioritizedRequest; 5 | import de.cotto.bitbook.backend.request.RequestPriority; 6 | 7 | public final class AddressTransactionsRequest extends PrioritizedRequest { 8 | private AddressTransactionsRequest(TransactionsRequestKey transactionsRequestKey, RequestPriority priority) { 9 | super(transactionsRequestKey, priority); 10 | } 11 | 12 | public static AddressTransactionsRequest create( 13 | TransactionsRequestKey transactionsRequestKey, 14 | RequestPriority requestPriority 15 | ) { 16 | return new AddressTransactionsRequest(transactionsRequestKey, requestPriority); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "AddressTransactionsRequest{" + 22 | "transactionsRequestKey='" + getKey() + '\'' + 23 | ", priority=" + getPriority() + 24 | '}'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/address-transactions/src/main/java/de/cotto/bitbook/backend/transaction/TransactionsRequestKey.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import com.google.common.base.Preconditions; 4 | import de.cotto.bitbook.backend.model.Address; 5 | import de.cotto.bitbook.backend.model.AddressTransactions; 6 | import de.cotto.bitbook.backend.model.Chain; 7 | 8 | public record TransactionsRequestKey( 9 | Address address, 10 | Chain chain, 11 | int blockHeight, 12 | AddressTransactions addressTransactions 13 | ) { 14 | public TransactionsRequestKey(Address address, Chain chain, int blockHeight) { 15 | this(address, chain, blockHeight, AddressTransactions.unknown(chain)); 16 | } 17 | 18 | public TransactionsRequestKey(AddressTransactions addressTransactions, int blockHeight) { 19 | this(addressTransactions.address(), addressTransactions.chain(), blockHeight, addressTransactions); 20 | } 21 | 22 | public boolean hasKnownAddressTransactions() { 23 | return !AddressTransactions.unknown(addressTransactions.chain()).equals(addressTransactions); 24 | } 25 | 26 | @Override 27 | public AddressTransactions addressTransactions() { 28 | Preconditions.checkArgument(hasKnownAddressTransactions()); 29 | return addressTransactions; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/address-transactions/src/testFixtures/java/de/cotto/bitbook/backend/transaction/TransactionsRequestKeyFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import static de.cotto.bitbook.backend.model.AddressFixtures.ADDRESS; 4 | import static de.cotto.bitbook.backend.model.Chain.BTC; 5 | import static de.cotto.bitbook.backend.model.TransactionFixtures.BLOCK_HEIGHT; 6 | import static de.cotto.bitbook.backend.request.RequestPriority.LOWEST; 7 | import static de.cotto.bitbook.backend.request.RequestPriority.STANDARD; 8 | 9 | public class TransactionsRequestKeyFixtures { 10 | public static final TransactionsRequestKey TRANSACTIONS_REQUEST_KEY = 11 | new TransactionsRequestKey(ADDRESS, BTC, BLOCK_HEIGHT); 12 | public static final AddressTransactionsRequest ADDRESS_TRANSACTIONS_REQUEST = 13 | AddressTransactionsRequest.create(TRANSACTIONS_REQUEST_KEY, STANDARD); 14 | public static final AddressTransactionsRequest ADDRESS_TRANSACTIONS_LOWEST = 15 | AddressTransactionsRequest.create(TRANSACTIONS_REQUEST_KEY, LOWEST); 16 | } 17 | -------------------------------------------------------------------------------- /backend/blockheight/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend:request:models")) 7 | implementation(project(":backend:models")) 8 | testFixturesImplementation(testFixtures(project(":backend:models"))) 9 | } 10 | -------------------------------------------------------------------------------- /backend/blockheight/src/main/java/de/cotto/bitbook/backend/transaction/BlockHeightProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.Provider; 5 | 6 | public interface BlockHeightProvider extends Provider { 7 | } 8 | -------------------------------------------------------------------------------- /backend/blockheight/src/main/java/de/cotto/bitbook/backend/transaction/BlockHeightRequest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.request.PrioritizedRequest; 5 | 6 | import static de.cotto.bitbook.backend.request.RequestPriority.STANDARD; 7 | 8 | public final class BlockHeightRequest extends PrioritizedRequest { 9 | public BlockHeightRequest(Chain chain) { 10 | super(chain, STANDARD); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/blockheight/src/test/java/de/cotto/bitbook/backend/transaction/BlockHeightProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Optional; 7 | 8 | import static de.cotto.bitbook.backend.model.Chain.BCH; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class BlockHeightProviderTest { 12 | 13 | private final BlockHeightProvider provider = new TestableBlockHeightProvider(); 14 | 15 | @Test 16 | void get_with_argument() throws Exception { 17 | assertThat(provider.get(BCH)).contains(123); 18 | } 19 | 20 | private static class TestableBlockHeightProvider implements BlockHeightProvider { 21 | @Override 22 | public String getName() { 23 | return "x"; 24 | } 25 | 26 | @Override 27 | public Optional get(Chain chain) { 28 | return Optional.of(123); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /backend/blockheight/src/test/java/de/cotto/bitbook/backend/transaction/BlockHeightRequestTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.Chain.BCH; 6 | import static de.cotto.bitbook.backend.model.Chain.BTC; 7 | import static de.cotto.bitbook.backend.request.RequestPriority.STANDARD; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class BlockHeightRequestTest { 11 | @Test 12 | void getPriority() { 13 | assertThat(new BlockHeightRequest(BTC).getPriority()).isEqualTo(STANDARD); 14 | } 15 | 16 | @Test 17 | void getKey() { 18 | assertThat(new BlockHeightRequest(BCH).getKey()).isEqualTo(BCH); 19 | } 20 | } -------------------------------------------------------------------------------- /backend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | api("org.springframework.cloud:spring-cloud-starter-openfeign") 7 | api("io.github.resilience4j:resilience4j-spring-boot2") 8 | api("org.springframework.boot:spring-boot-starter-data-jpa") 9 | runtimeOnly("io.vavr:vavr") 10 | implementation(project(":backend:models")) 11 | runtimeOnly("org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j") 12 | runtimeOnly("com.h2database:h2") 13 | testImplementation(testFixtures(project(":backend:models"))) 14 | } 15 | 16 | tasks.jacocoTestCoverageVerification { 17 | violationRules { 18 | rules.forEach {rule -> 19 | rule.limits.forEach {limit -> 20 | if (limit.counter == "CLASS") { 21 | limit.minimum = 0.8.toBigDecimal() 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /backend/models/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation("commons-codec:commons-codec") 7 | } 8 | 9 | tasks.jar { 10 | archiveBaseName.set("backend-models") 11 | } 12 | 13 | pitest { 14 | testStrengthThreshold.set(98) 15 | } 16 | 17 | tasks.jacocoTestCoverageVerification { 18 | violationRules { 19 | rules.forEach {rule -> 20 | rule.limits.forEach {limit -> 21 | if (limit.counter == "BRANCH") { 22 | limit.minimum = 0.96.toBigDecimal() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/Address.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public record Address(String address) implements Comparable
{ 4 | public static final Address NONE = new Address(""); 5 | 6 | public Address(String address) { 7 | this.address = new CashAddrAddress(address).getLegacyAddress(); 8 | } 9 | 10 | public boolean isValid() { 11 | return !isInvalid(); 12 | } 13 | 14 | public boolean isInvalid() { 15 | return address.isEmpty(); 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return address; 21 | } 22 | 23 | @Override 24 | public int compareTo(Address other) { 25 | return address.compareTo(other.address); 26 | } 27 | 28 | public HexString getScript() { 29 | Base58Address base58Address = new Base58Address(address); 30 | if (base58Address.isValid()) { 31 | return base58Address.getScript(); 32 | } 33 | Bech32Address bech32Address = new Bech32Address(address); 34 | if (bech32Address.isValid()) { 35 | return bech32Address.getScript(); 36 | } 37 | throw new IllegalStateException("unsupported address type for address " + address); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/AddressWithDescription.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class AddressWithDescription extends ModelWithDescription { 4 | public AddressWithDescription(Address address) { 5 | this(address, ""); 6 | } 7 | 8 | public AddressWithDescription(Address address, String description) { 9 | super(address, description); 10 | } 11 | 12 | @Override 13 | protected String getFormattedString() { 14 | return padOrShorten(getModel().toString(), 45); 15 | } 16 | 17 | public Address getAddress() { 18 | return getModel(); 19 | } 20 | 21 | public String getFormattedAddress() { 22 | return getFormattedString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/Bech32Base.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import javax.annotation.CheckForNull; 4 | import java.util.Objects; 5 | 6 | public class Bech32Base { 7 | protected static final int BITS_FOR_BECH32_CHAR = 5; 8 | protected static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; 9 | private static final int CHECKSUM_BYTES = 6; 10 | @CheckForNull 11 | protected byte[] payloadWithChecksum; 12 | 13 | Bech32Base() { 14 | // default constructor 15 | } 16 | 17 | protected byte[] getPayload() { 18 | byte[] payload = new byte[Objects.requireNonNull(payloadWithChecksum).length - CHECKSUM_BYTES]; 19 | System.arraycopy(payloadWithChecksum, 0, payload, 0, payload.length); 20 | return payload; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/Chain.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import javax.annotation.Nullable; 4 | import java.time.LocalDate; 5 | 6 | public enum Chain { 7 | BTC(null, LocalDate.of(2009, 1, 3)), 8 | BCH(BTC, LocalDate.of(2017, 8, 1)), 9 | BTG(BTC, LocalDate.of(2017, 10, 24)), 10 | BCD(BTC, LocalDate.of(2017, 11, 24)), 11 | BSV(BCH, LocalDate.of(2018, 11, 15)); 12 | 13 | @Nullable 14 | private final Chain originalChain; 15 | private final LocalDate forkDate; 16 | 17 | Chain(@Nullable Chain originalChain, LocalDate forkDate) { 18 | this.originalChain = originalChain; 19 | this.forkDate = forkDate; 20 | } 21 | 22 | public Chain getChainForDate(LocalDate date) { 23 | if (date.isBefore(forkDate) && originalChain != null) { 24 | return originalChain.getChainForDate(date); 25 | } 26 | return this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/HashAndChain.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public record HashAndChain(TransactionHash hash, Chain chain) { 4 | } 5 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/Input.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class Input extends InputOutput { 4 | public static final Input EMPTY = new Input(Coins.NONE, Address.NONE); 5 | 6 | public Input(Coins value, Address address) { 7 | super(value, address); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/Output.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class Output extends InputOutput { 4 | public static final Output EMPTY = new Output(Coins.NONE, Address.NONE); 5 | 6 | public Output(Coins value, Address targetAddress) { 7 | super(value, targetAddress); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/Provider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import java.util.Optional; 4 | 5 | public interface Provider { 6 | String getName(); 7 | 8 | Optional get(K key) throws ProviderException; 9 | 10 | default boolean isSupported(K key) { 11 | return true; 12 | } 13 | 14 | default void throwIfUnsupported(K key) throws ProviderException { 15 | if (!isSupported(key)) { 16 | throw new ProviderException(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/ProviderException.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class ProviderException extends Exception { 4 | public ProviderException() { 5 | super(); 6 | } 7 | 8 | public ProviderException(Throwable cause) { 9 | super(cause); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/TransactionHash.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public record TransactionHash(String hash) implements Comparable { 4 | public static final TransactionHash NONE = new TransactionHash(""); 5 | 6 | public boolean isInvalid() { 7 | return hash.isBlank(); 8 | } 9 | 10 | public boolean isValid() { 11 | return !isInvalid(); 12 | } 13 | 14 | @Override 15 | public int compareTo(TransactionHash other) { 16 | return hash.compareTo(other.hash); 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return hash; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/models/src/main/java/de/cotto/bitbook/backend/model/TransactionWithDescription.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class TransactionWithDescription extends ModelWithDescription { 4 | public TransactionWithDescription(TransactionHash transactionHash) { 5 | this(transactionHash, ""); 6 | } 7 | 8 | public TransactionWithDescription(TransactionHash transactionHash, String description) { 9 | super(transactionHash, description); 10 | } 11 | 12 | @Override 13 | protected String getFormattedString() { 14 | return getModel().toString(); 15 | } 16 | 17 | public TransactionHash getTransactionHash() { 18 | return getModel(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/models/src/test/java/de/cotto/bitbook/backend/model/Base58EncoderTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class Base58EncoderTest { 8 | @Test 9 | void encode_leading_zeros() { 10 | assertThat(new Base58Encoder(new HexString("0000FF")).encode()).isEqualTo("115Q"); 11 | } 12 | 13 | @Test 14 | void encode_zeros_not_leading() { 15 | assertThat(new Base58Encoder(new HexString("00FF00")).encode()).isEqualTo("1LQX"); 16 | } 17 | } -------------------------------------------------------------------------------- /backend/models/src/test/java/de/cotto/bitbook/backend/model/HashAndChainTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.Chain.BTC; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class HashAndChainTest { 10 | @Test 11 | void hash() { 12 | assertThat(new HashAndChain(TRANSACTION_HASH, BTC).hash()).isEqualTo(TRANSACTION_HASH); 13 | } 14 | 15 | @Test 16 | void chain() { 17 | assertThat(new HashAndChain(TRANSACTION_HASH, BTC).chain()).isEqualTo(BTC); 18 | } 19 | } -------------------------------------------------------------------------------- /backend/models/src/test/java/de/cotto/bitbook/backend/model/ProviderExceptionTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class ProviderExceptionTest { 8 | @Test 9 | void isException() { 10 | assertThat(new ProviderException()).isInstanceOf(Exception.class); 11 | } 12 | 13 | @Test 14 | void withCause() { 15 | ArithmeticException cause = new ArithmeticException(); 16 | assertThat(new ProviderException(cause)).hasCause(cause); 17 | } 18 | 19 | @Test 20 | void noParameters() { 21 | assertThat(new ProviderException()).isNotNull(); 22 | } 23 | } -------------------------------------------------------------------------------- /backend/models/src/test/java/de/cotto/bitbook/backend/model/ProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 6 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; 7 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; 8 | 9 | class ProviderTest { 10 | private final TestableProvider provider = new TestableProvider(); 11 | 12 | @Test 13 | void supports_everything_by_default() { 14 | assertThat(provider.isSupported("abc")).isTrue(); 15 | } 16 | 17 | @Test 18 | void isSupported_false() { 19 | assertThat(provider.isSupported("unsupported")).isFalse(); 20 | } 21 | 22 | @Test 23 | void get() throws Exception { 24 | assertThat(provider.get("supported")).isNotEmpty(); 25 | } 26 | 27 | @Test 28 | void throwIfUnsupported_supported() { 29 | assertThatCode(() -> provider.throwIfUnsupported("supported")).doesNotThrowAnyException(); 30 | } 31 | 32 | @Test 33 | void throwIfUnsupported_unsupported() { 34 | assertThatExceptionOfType(ProviderException.class).isThrownBy( 35 | () -> provider.throwIfUnsupported("unsupported") 36 | ); 37 | } 38 | } -------------------------------------------------------------------------------- /backend/models/src/testFixtures/java/de/cotto/bitbook/backend/model/AddressFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class AddressFixtures { 4 | public static final Address P2SH = 5 | new Address("34nSkinWC9rDDJiUY438qQN1JHmGqBHGW7"); 6 | public static final Address P2SH_2 = 7 | new Address("3CK4fEwbMP7heJarmU4eqA3sMbVJyEnU3V"); 8 | public static final Address P2PKH = 9 | new Address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"); 10 | public static final Address ADDRESS = 11 | new Address("1DEP8i3QJCsomS4BSMY2RpU1upv62aGvhD"); 12 | public static final Address ADDRESS_2 = 13 | new Address("191sNkKTG8pzUsNgZYKo7DH2odg39XDAGo"); 14 | public static final Address ADDRESS_3 = 15 | new Address("bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej"); 16 | public static final Address P2WPKH = 17 | new Address("bc1q42lja79elem0anu8q8s3h2n687re9jax556pcc"); 18 | public static final Address P2WSH = 19 | new Address("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3"); 20 | public static final Address P2TR = 21 | new Address("bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"); 22 | } 23 | -------------------------------------------------------------------------------- /backend/models/src/testFixtures/java/de/cotto/bitbook/backend/model/InputFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class InputFixtures { 4 | public static final Coins INPUT_VALUE_1 = Coins.ofSatoshis(22_749); 5 | public static final Coins INPUT_VALUE_2 = Coins.ofSatoshis(Integer.MAX_VALUE - 1L); 6 | public static final Address INPUT_ADDRESS_1 = new Address("bc1xxxn59nfqcw2la4ms7zsphqllm5789syhrgcupw"); 7 | public static final Address INPUT_ADDRESS_2 = new Address("bc1yyyn59nfqcw2la4ms7zsphqllm5789syhrgcupw"); 8 | public static final Input INPUT_1 = new Input(INPUT_VALUE_1, INPUT_ADDRESS_1); 9 | public static final Input INPUT_2 = new Input(INPUT_VALUE_2, INPUT_ADDRESS_2); 10 | } 11 | -------------------------------------------------------------------------------- /backend/models/src/testFixtures/java/de/cotto/bitbook/backend/model/OutputFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class OutputFixtures { 4 | public static final Coins OUTPUT_VALUE_1 = Coins.ofSatoshis(Integer.MAX_VALUE + 1L); 5 | public static final Coins OUTPUT_VALUE_2 = Coins.ofSatoshis(1_234); 6 | public static final Address OUTPUT_ADDRESS_1 = 7 | new Address("bc1qt9n59nfqcw2la4ms7zsphqllm5789syhrgcupw"); 8 | public static final Address OUTPUT_ADDRESS_2 = 9 | new Address("bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9"); 10 | public static final Output OUTPUT_1 = new Output(OUTPUT_VALUE_1, OUTPUT_ADDRESS_1); 11 | public static final Output OUTPUT_2 = new Output(OUTPUT_VALUE_2, OUTPUT_ADDRESS_2); 12 | } 13 | -------------------------------------------------------------------------------- /backend/models/src/testFixtures/java/de/cotto/bitbook/backend/model/TestableProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public class TestableProvider implements Provider { 9 | public final List seenKeys = Collections.synchronizedList(new ArrayList<>()); 10 | 11 | public TestableProvider() { 12 | // default constructor 13 | } 14 | 15 | @Override 16 | public boolean isSupported(String key) { 17 | if ("unsupported".equals(key)) { 18 | return false; 19 | } 20 | return Provider.super.isSupported(key); 21 | } 22 | 23 | @Override 24 | public Optional get(String key) throws ProviderException { 25 | seenKeys.add(key); 26 | if (key == null) { 27 | return Optional.empty(); 28 | } 29 | if ("".equals(key)) { 30 | return Optional.empty(); 31 | } 32 | if ("wait".equals(key)) { 33 | try { 34 | Thread.sleep(200); 35 | } catch (InterruptedException e) { 36 | // ignore 37 | } 38 | } 39 | if ("providerException".equals(key)) { 40 | throw new ProviderException(); 41 | } 42 | return Optional.of(key.length()); 43 | } 44 | 45 | @Override 46 | public String getName() { 47 | return "TestableProvider"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/models/src/testFixtures/java/de/cotto/bitbook/backend/model/TransactionHashFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.model; 2 | 3 | public class TransactionHashFixtures { 4 | public static final TransactionHash TRANSACTION_HASH = 5 | new TransactionHash("c56c2a4ec7099879c2c4da74f4e5105a5a5d0ed94aa7d64518fa7e4256d42d9e"); 6 | public static final TransactionHash TRANSACTION_HASH_2 = 7 | new TransactionHash("aad0e9e8f453da1a207600f856325f10b2e1a03c39c308481855925f15ed4cfe"); 8 | public static final TransactionHash TRANSACTION_HASH_3 = 9 | new TransactionHash("0003"); 10 | public static final TransactionHash TRANSACTION_HASH_4 = 11 | new TransactionHash("0004"); 12 | } 13 | -------------------------------------------------------------------------------- /backend/price/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:request")) 9 | implementation(project(":backend:request:models")) 10 | implementation("com.fasterxml.jackson.core:jackson-databind") 11 | integrationTestImplementation(project(":backend:models")) 12 | } 13 | -------------------------------------------------------------------------------- /backend/price/src/integrationTest/java/de/cotto/bitbook/backend/SchedulingConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.EnableScheduling; 5 | 6 | @Configuration 7 | @EnableScheduling 8 | public class SchedulingConfiguration { 9 | public SchedulingConfiguration() { 10 | // default constructor 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/price/src/integrationTest/java/de/cotto/bitbook/backend/SpringBootConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class SpringBootConfiguration { 7 | public SpringBootConfiguration() { 8 | // default constructor 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/price/src/integrationTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE 2 | logging.level.root=warn 3 | logging.level.de.cotto.bitbook=debug -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/AsyncConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.EnableAsync; 5 | 6 | @Configuration 7 | @EnableAsync 8 | public class AsyncConfiguration { 9 | public AsyncConfiguration() { 10 | // default constructor 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/PriceDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price; 2 | 3 | import de.cotto.bitbook.backend.price.model.Price; 4 | import de.cotto.bitbook.backend.price.model.PriceContext; 5 | import de.cotto.bitbook.backend.price.model.PriceWithContext; 6 | 7 | import java.util.Collection; 8 | import java.util.Optional; 9 | 10 | public interface PriceDao { 11 | Optional getPrice(PriceContext priceContext); 12 | 13 | void savePrices(Collection prices); 14 | } 15 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/PrioritizingPriceProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price; 2 | 3 | import de.cotto.bitbook.backend.model.Provider; 4 | import de.cotto.bitbook.backend.price.model.PriceContext; 5 | import de.cotto.bitbook.backend.price.model.PriceWithContext; 6 | import de.cotto.bitbook.backend.request.PrioritizingProvider; 7 | import de.cotto.bitbook.backend.request.ResultFuture; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | @Component 14 | public class PrioritizingPriceProvider extends PrioritizingProvider> { 15 | public PrioritizingPriceProvider(List>> providers) { 16 | super(providers, "Price"); 17 | } 18 | 19 | public ResultFuture> getPrices(PriceRequest request) { 20 | return getForRequest(request); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/kraken/KrakenClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.kraken; 2 | 3 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 4 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | 9 | import java.util.Optional; 10 | 11 | @FeignClient(value = "kraken", url = "https://api.kraken.com/") 12 | @RateLimiter(name = "kraken") 13 | @CircuitBreaker(name = "kraken") 14 | public interface KrakenClient { 15 | @GetMapping("/0/public/Trades?pair=BTCEUR&since={sinceEpochSeconds}") 16 | Optional getTrades(@PathVariable long sinceEpochSeconds); 17 | 18 | @GetMapping("/0/public/OHLC?pair=BTCEUR&interval=1440") 19 | Optional getOhlcData(); 20 | } 21 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/model/PriceContext.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.model; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | 5 | import java.time.LocalDate; 6 | 7 | public record PriceContext(LocalDate date, Chain chain) { 8 | public PriceContext(LocalDate date, Chain chain) { 9 | this.chain = chain.getChainForDate(date); 10 | this.date = date; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/model/PriceWithContext.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.model; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | public class PriceWithContext { 6 | private final Price price; 7 | private final PriceContext priceContext; 8 | 9 | public PriceWithContext(@Nonnull Price price, @Nonnull PriceContext priceContext) { 10 | this.price = price; 11 | this.priceContext = priceContext; 12 | } 13 | 14 | public Price getPrice() { 15 | return price; 16 | } 17 | 18 | public PriceContext getPriceContext() { 19 | return priceContext; 20 | } 21 | 22 | @Override 23 | public boolean equals(Object other) { 24 | if (this == other) { 25 | return true; 26 | } 27 | if (other == null || getClass() != other.getClass()) { 28 | return false; 29 | } 30 | 31 | PriceWithContext that = (PriceWithContext) other; 32 | 33 | if (!price.equals(that.price)) { 34 | return false; 35 | } 36 | return priceContext.equals(that.priceContext); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | int result = price.hashCode(); 42 | result = 31 * result + priceContext.hashCode(); 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/persistence/PriceDaoImpl.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.persistence; 2 | 3 | import de.cotto.bitbook.backend.price.PriceDao; 4 | import de.cotto.bitbook.backend.price.model.Price; 5 | import de.cotto.bitbook.backend.price.model.PriceContext; 6 | import de.cotto.bitbook.backend.price.model.PriceWithContext; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.transaction.Transactional; 10 | import java.util.Collection; 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | import static java.util.stream.Collectors.toList; 15 | 16 | @Component 17 | @Transactional 18 | public class PriceDaoImpl implements PriceDao { 19 | private final PriceRepository priceRepository; 20 | 21 | public PriceDaoImpl(PriceRepository priceRepository) { 22 | this.priceRepository = priceRepository; 23 | } 24 | 25 | @Override 26 | public Optional getPrice(PriceContext priceContext) { 27 | return priceRepository.findById(PriceWithContextId.fromModel(priceContext)) 28 | .map(PriceWithContextJpaDto::toModel) 29 | .map(PriceWithContext::getPrice); 30 | } 31 | 32 | @Override 33 | public void savePrices(Collection prices) { 34 | List dtos = prices.stream().map(PriceWithContextJpaDto::fromModel).collect(toList()); 35 | priceRepository.saveAll(dtos); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/persistence/PriceJpaDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.persistence; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import de.cotto.bitbook.backend.price.model.Price; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | import javax.persistence.Column; 9 | import javax.persistence.Embeddable; 10 | import java.math.BigDecimal; 11 | import java.util.Objects; 12 | 13 | @Embeddable 14 | class PriceJpaDto { 15 | private static final int SCALE = 8; 16 | 17 | @Nullable 18 | @Column(name = "price", precision = 16, scale = SCALE) 19 | private BigDecimal asBigDecimal; 20 | 21 | PriceJpaDto() { 22 | // for JPA 23 | } 24 | 25 | protected static PriceJpaDto fromModel(Price price) { 26 | PriceJpaDto dto = new PriceJpaDto(); 27 | dto.asBigDecimal = price.getAsBigDecimal(); 28 | return dto; 29 | } 30 | 31 | protected Price toModel() { 32 | return Price.of(Objects.requireNonNull(asBigDecimal)); 33 | } 34 | 35 | @VisibleForTesting 36 | protected void setAsBigDecimal(@Nonnull BigDecimal asBigDecimal) { 37 | this.asBigDecimal = asBigDecimal; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "PriceJpaDto{" + 43 | "asBigDecimal=" + asBigDecimal + 44 | '}'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/persistence/PriceRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | interface PriceRepository extends JpaRepository { 6 | } 7 | -------------------------------------------------------------------------------- /backend/price/src/main/java/de/cotto/bitbook/backend/price/persistence/PriceWithContextId.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.persistence; 2 | 3 | import de.cotto.bitbook.backend.price.model.PriceContext; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | import java.io.Serializable; 8 | import java.time.LocalDate; 9 | import java.util.Objects; 10 | 11 | public class PriceWithContextId implements Serializable { 12 | @Nullable 13 | private LocalDate date; 14 | 15 | @Nullable 16 | private String chain; 17 | 18 | @SuppressWarnings("unused") 19 | public PriceWithContextId() { 20 | // for JPA 21 | } 22 | 23 | public PriceWithContextId(@Nonnull LocalDate date, @Nonnull String chain) { 24 | this.date = date; 25 | this.chain = chain; 26 | } 27 | 28 | public static PriceWithContextId fromModel(PriceContext priceContext) { 29 | return new PriceWithContextId(priceContext.date(), priceContext.chain().toString()); 30 | } 31 | 32 | @Override 33 | public boolean equals(Object other) { 34 | if (this == other) { 35 | return true; 36 | } 37 | if (other == null || getClass() != other.getClass()) { 38 | return false; 39 | } 40 | PriceWithContextId that = (PriceWithContextId) other; 41 | return Objects.equals(date, that.date) && Objects.equals(chain, that.chain); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return Objects.hash(date, chain); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /backend/price/src/test/java/de/cotto/bitbook/backend/price/PriceProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price; 2 | 3 | import de.cotto.bitbook.backend.model.Provider; 4 | import de.cotto.bitbook.backend.price.model.PriceContext; 5 | import de.cotto.bitbook.backend.price.model.PriceWithContext; 6 | 7 | import java.util.Collection; 8 | 9 | public abstract class PriceProvider implements Provider> { 10 | protected PriceProvider() { 11 | // just used for tests 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/price/src/test/java/de/cotto/bitbook/backend/price/persistence/PriceJpaDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.persistence; 2 | 3 | import de.cotto.bitbook.backend.price.model.Price; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.math.BigDecimal; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class PriceJpaDtoTest { 11 | @Test 12 | void toModel() { 13 | Price price = Price.of(123); 14 | PriceJpaDto dto = new PriceJpaDto(); 15 | dto.setAsBigDecimal(price.getAsBigDecimal()); 16 | 17 | assertThat(dto.toModel()).isEqualTo(price); 18 | } 19 | 20 | @Test 21 | void fromModel() { 22 | Price model = Price.of(500); 23 | PriceJpaDto expected = new PriceJpaDto(); 24 | expected.setAsBigDecimal(model.getAsBigDecimal()); 25 | 26 | assertThat(PriceJpaDto.fromModel(model)).usingRecursiveComparison().isEqualTo(expected); 27 | } 28 | 29 | @Test 30 | void testToString() { 31 | PriceJpaDto dto = new PriceJpaDto(); 32 | dto.setAsBigDecimal(BigDecimal.ONE); 33 | assertThat(dto).hasToString("PriceJpaDto{asBigDecimal=1}"); 34 | } 35 | } -------------------------------------------------------------------------------- /backend/price/src/test/java/de/cotto/bitbook/backend/price/persistence/PriceWithContextIdTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.price.persistence; 2 | 3 | import de.cotto.bitbook.backend.price.model.PriceContext; 4 | import nl.jqno.equalsverifier.EqualsVerifier; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.time.LocalDate; 8 | import java.time.ZoneOffset; 9 | 10 | import static de.cotto.bitbook.backend.model.Chain.BTC; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class PriceWithContextIdTest { 14 | 15 | private static final LocalDate DATE = LocalDate.now(ZoneOffset.UTC); 16 | 17 | @Test 18 | void fromModel() { 19 | assertThat(PriceWithContextId.fromModel(new PriceContext(DATE, BTC))) 20 | .isEqualTo(new PriceWithContextId(DATE, "BTC")); 21 | } 22 | 23 | @Test 24 | void testEquals() { 25 | EqualsVerifier.simple().forClass(PriceWithContextId.class).verify(); 26 | } 27 | } -------------------------------------------------------------------------------- /backend/provider/all/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import de.cotto.javaconventions.plugins.JacocoPlugin 2 | 3 | plugins { 4 | id("bitbook.java-library-conventions") 5 | } 6 | 7 | dependencies { 8 | implementation(project(":backend:provider:bitaps")) 9 | implementation(project(":backend:provider:bitcoind")) 10 | implementation(project(":backend:provider:blockchaininfo")) 11 | implementation(project(":backend:provider:blockchair")) 12 | implementation(project(":backend:provider:blockcypher")) 13 | implementation(project(":backend:provider:blockstreaminfo")) 14 | implementation(project(":backend:provider:btccom")) 15 | implementation(project(":backend:provider:electrs")) 16 | implementation(project(":backend:provider:fullstackcash")) 17 | implementation(project(":backend:provider:mempoolspace")) 18 | } 19 | 20 | tasks.withType { 21 | enabled = false 22 | } 23 | -------------------------------------------------------------------------------- /backend/provider/base/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend:models")) 7 | api("com.fasterxml.jackson.core:jackson-databind") 8 | testImplementation(testFixtures(project(":backend:models"))) 9 | testFixturesImplementation(testFixtures(project(":backend:models"))) 10 | testFixturesImplementation("com.fasterxml.jackson.core:jackson-databind") 11 | } 12 | 13 | tasks.jar { 14 | archiveBaseName.set("backend-provider-base") 15 | } 16 | -------------------------------------------------------------------------------- /backend/provider/base/src/main/java/de/cotto/bitbook/backend/transaction/AddressTransactionsDeserializer.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.JsonDeserializer; 5 | import com.fasterxml.jackson.databind.JsonNode; 6 | import de.cotto.bitbook.backend.model.TransactionHash; 7 | 8 | import java.io.IOException; 9 | import java.util.LinkedHashSet; 10 | import java.util.Optional; 11 | import java.util.Set; 12 | 13 | public abstract class AddressTransactionsDeserializer extends JsonDeserializer { 14 | protected AddressTransactionsDeserializer() { 15 | super(); 16 | } 17 | 18 | protected Set parseHashesWithLimit(JsonParser jsonParser, int limit) throws IOException { 19 | JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); 20 | 21 | Set transactionHashes = getTransactionHashes(rootNode); 22 | if (transactionHashes.size() >= limit) { 23 | throw new IllegalStateException(); 24 | } 25 | return transactionHashes; 26 | } 27 | 28 | protected Set getTransactionHashes(JsonNode rootNode) { 29 | Set result = new LinkedHashSet<>(); 30 | for (JsonNode transactionReferenceNode : rootNode) { 31 | getHash(transactionReferenceNode).ifPresent(result::add); 32 | } 33 | return result; 34 | } 35 | 36 | protected abstract Optional getHash(JsonNode transactionReferenceNode); 37 | } 38 | -------------------------------------------------------------------------------- /backend/provider/base/src/main/java/de/cotto/bitbook/backend/transaction/deserialization/InputDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.Coins; 5 | import de.cotto.bitbook.backend.model.Input; 6 | 7 | public class InputDto extends InputOutputDto { 8 | public static final InputDto COINBASE = new InputDto(); 9 | 10 | public InputDto() { 11 | super(); 12 | } 13 | 14 | public InputDto(Coins value, String address) { 15 | super(value, address); 16 | } 17 | 18 | @Override 19 | public Input toModel() { 20 | return new Input(getValue(), new Address(getAddress())); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/provider/base/src/main/java/de/cotto/bitbook/backend/transaction/deserialization/InputOutputDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import de.cotto.bitbook.backend.model.Coins; 4 | import de.cotto.bitbook.backend.model.InputOutput; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | import java.util.Objects; 9 | 10 | public abstract class InputOutputDto { 11 | @Nullable 12 | private Coins value; 13 | 14 | @Nullable 15 | private String address; 16 | 17 | public InputOutputDto() { 18 | // for Jackson 19 | } 20 | 21 | public InputOutputDto(@Nonnull Coins value, @Nonnull String address) { 22 | this.address = address; 23 | this.value = value; 24 | } 25 | 26 | public abstract InputOutput toModel(); 27 | 28 | protected Coins getValue() { 29 | return Objects.requireNonNull(value); 30 | } 31 | 32 | public void setValue(@Nonnull Coins value) { 33 | this.value = value; 34 | } 35 | 36 | public String getAddress() { 37 | return Objects.requireNonNull(address); 38 | } 39 | 40 | public void setAddress(@Nonnull String address) { 41 | this.address = address; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/provider/base/src/main/java/de/cotto/bitbook/backend/transaction/deserialization/InputOutputDtoDeserializer.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | 5 | import java.util.List; 6 | 7 | public interface InputOutputDtoDeserializer { 8 | List getOutputs(JsonNode transactionNode); 9 | 10 | List getInputs(JsonNode transactionNode); 11 | } 12 | -------------------------------------------------------------------------------- /backend/provider/base/src/main/java/de/cotto/bitbook/backend/transaction/deserialization/OutputDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.Coins; 5 | import de.cotto.bitbook.backend.model.Output; 6 | 7 | public class OutputDto extends InputOutputDto { 8 | public OutputDto() { 9 | super(); 10 | } 11 | 12 | public OutputDto(Coins value, String address) { 13 | super(value, address); 14 | } 15 | 16 | @Override 17 | public Output toModel() { 18 | return new Output(getValue(), new Address(getAddress())); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/provider/base/src/test/java/de/cotto/bitbook/backend/transaction/deserialization/InputDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_1; 6 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_1; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class InputDtoTest { 10 | @Test 11 | void toModel() { 12 | assertThat(INPUT_DTO_1.toModel()).isEqualTo(INPUT_1); 13 | } 14 | } -------------------------------------------------------------------------------- /backend/provider/base/src/test/java/de/cotto/bitbook/backend/transaction/deserialization/OutputDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_1; 6 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_1; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class OutputDtoTest { 10 | @Test 11 | void toModel() { 12 | assertThat(OUTPUT_DTO_1.toModel()).isEqualTo(OUTPUT_1); 13 | } 14 | } -------------------------------------------------------------------------------- /backend/provider/base/src/test/java/de/cotto/bitbook/backend/transaction/deserialization/TestableTransactionDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import de.cotto.bitbook.backend.model.TransactionHash; 5 | 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | @JsonDeserialize(using = TestableTransactionDtoDeserializer.class) 10 | class TestableTransactionDto extends TransactionDto { 11 | public TestableTransactionDto( 12 | TransactionHash hash, 13 | int blockHeight, 14 | LocalDateTime time, 15 | long fees, 16 | List inputs, 17 | List outputs 18 | ) { 19 | super(hash, blockHeight, time, fees, inputs, outputs); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/provider/base/src/testFixtures/java/de/cotto/bitbook/backend/transaction/deserialization/InputDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_ADDRESS_1; 4 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_ADDRESS_2; 5 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_VALUE_1; 6 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_VALUE_2; 7 | 8 | public class InputDtoFixtures { 9 | public static final InputDto INPUT_DTO_1; 10 | public static final InputDto INPUT_DTO_2; 11 | 12 | static { 13 | INPUT_DTO_1 = new InputDto(); 14 | INPUT_DTO_1.setValue(INPUT_VALUE_1); 15 | INPUT_DTO_1.setAddress(INPUT_ADDRESS_1.toString()); 16 | 17 | INPUT_DTO_2 = new InputDto(); 18 | INPUT_DTO_2.setValue(INPUT_VALUE_2); 19 | INPUT_DTO_2.setAddress(INPUT_ADDRESS_2.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/provider/base/src/testFixtures/java/de/cotto/bitbook/backend/transaction/deserialization/OutputDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_ADDRESS_1; 4 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_ADDRESS_2; 5 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_VALUE_1; 6 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_VALUE_2; 7 | 8 | public class OutputDtoFixtures { 9 | public static final OutputDto OUTPUT_DTO_1; 10 | public static final OutputDto OUTPUT_DTO_2; 11 | 12 | static { 13 | OUTPUT_DTO_1 = new OutputDto(); 14 | OUTPUT_DTO_1.setValue(OUTPUT_VALUE_1); 15 | OUTPUT_DTO_1.setAddress(OUTPUT_ADDRESS_1.toString()); 16 | 17 | OUTPUT_DTO_2 = new OutputDto(); 18 | OUTPUT_DTO_2.setValue(OUTPUT_VALUE_2); 19 | OUTPUT_DTO_2.setAddress(OUTPUT_ADDRESS_2.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/provider/base/src/testFixtures/java/de/cotto/bitbook/backend/transaction/deserialization/TestObjectMapper.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.deserialization; 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | public class TestObjectMapper extends ObjectMapper { 7 | public TestObjectMapper() { 8 | super(); 9 | configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/provider/bitaps/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:blockheight")) 10 | implementation(project(":backend:address-transactions")) 11 | testImplementation(testFixtures(project(":backend:models"))) 12 | testImplementation(testFixtures(project(":backend:provider:base"))) 13 | testFixturesImplementation(testFixtures(project(":backend:models"))) 14 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 15 | } 16 | -------------------------------------------------------------------------------- /backend/provider/bitaps/src/main/java/de/cotto/bitbook/backend/transaction/bitaps/BitapsBlockHeightDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitaps; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | 9 | import java.io.IOException; 10 | 11 | @JsonDeserialize(using = BitapsBlockHeightDto.Deserializer.class) 12 | public class BitapsBlockHeightDto { 13 | private final int blockHeight; 14 | 15 | public BitapsBlockHeightDto(int blockHeight) { 16 | this.blockHeight = blockHeight; 17 | } 18 | 19 | public int getBlockHeight() { 20 | return blockHeight; 21 | } 22 | 23 | public static class Deserializer extends JsonDeserializer { 24 | @Override 25 | public BitapsBlockHeightDto deserialize( 26 | JsonParser jsonParser, 27 | DeserializationContext deserializationContext 28 | ) throws IOException { 29 | JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); 30 | return new BitapsBlockHeightDto(rootNode.get("data").get("height").intValue()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/provider/bitaps/src/main/java/de/cotto/bitbook/backend/transaction/bitaps/BitapsBlockHeightProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitaps; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.ProviderException; 5 | import de.cotto.bitbook.backend.transaction.BlockHeightProvider; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | 10 | import static de.cotto.bitbook.backend.model.Chain.BTC; 11 | 12 | @Component 13 | public class BitapsBlockHeightProvider implements BlockHeightProvider { 14 | private final BitapsClient bitapsClient; 15 | 16 | public BitapsBlockHeightProvider(BitapsClient bitapsClient) { 17 | this.bitapsClient = bitapsClient; 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return "BitapsBlockHeightProvider"; 23 | } 24 | 25 | @Override 26 | public Optional get(Chain chain) throws ProviderException { 27 | throwIfUnsupported(chain); 28 | BitapsBlockHeightDto dto = bitapsClient.getBlockHeight().orElseThrow(ProviderException::new); 29 | return Optional.of(dto.getBlockHeight()); 30 | } 31 | 32 | @Override 33 | public boolean isSupported(Chain chain) { 34 | return chain == BTC; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/provider/bitaps/src/main/java/de/cotto/bitbook/backend/transaction/bitaps/BitapsClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitaps; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.TransactionHash; 5 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 6 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 7 | import org.springframework.cloud.openfeign.FeignClient; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | 11 | import java.util.Optional; 12 | 13 | @FeignClient(value = "bitaps", url = "https://api.bitaps.com") 14 | @RateLimiter(name = "bitaps") 15 | @CircuitBreaker(name = "bitaps") 16 | public interface BitapsClient { 17 | @GetMapping("/btc/v1/blockchain/transaction/{transactionHash}") 18 | Optional getTransaction(@PathVariable TransactionHash transactionHash); 19 | 20 | @GetMapping("/btc/v1/blockchain/block/last") 21 | Optional getBlockHeight(); 22 | 23 | @GetMapping("/btc/v1/blockchain/address/transactions/{address}") 24 | Optional getAddressTransactions(@PathVariable Address address); 25 | } 26 | -------------------------------------------------------------------------------- /backend/provider/bitaps/src/test/java/de/cotto/bitbook/backend/transaction/bitaps/BitapsBlockHeightDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitaps; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import de.cotto.bitbook.backend.transaction.deserialization.TestObjectMapper; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @ExtendWith(MockitoExtension.class) 12 | class BitapsBlockHeightDtoTest { 13 | private final ObjectMapper objectMapper = new TestObjectMapper(); 14 | 15 | @Test 16 | void deserialization() throws Exception { 17 | String json = "{\"data\":" + 18 | " {\"height\": 673466," + 19 | " \"hash\": \"xxx\"" + 20 | ", \"header\": \"yyy\"," + 21 | " \"adjustedTimestamp\": 1615064015" + 22 | "}, \"time\": 0.0016}"; 23 | BitapsBlockHeightDto bitapsBlockHeightDto = 24 | objectMapper.readValue(json, BitapsBlockHeightDto.class); 25 | assertThat(bitapsBlockHeightDto.getBlockHeight()).isEqualTo(673_466); 26 | } 27 | } -------------------------------------------------------------------------------- /backend/provider/bitaps/src/testFixtures/java/de/cotto/bitbook/backend/transaction/bitaps/BitapsAddressTransactionDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitaps; 2 | 3 | import java.util.Set; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_2; 7 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_3; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_4; 9 | 10 | public class BitapsAddressTransactionDtoFixtures { 11 | public static final BitapsAddressTransactionsDto BITAPS_ADDRESS_TRANSACTIONS; 12 | public static final BitapsAddressTransactionsDto BITAPS_TRANSACTIONS_UPDATED; 13 | 14 | static { 15 | BITAPS_ADDRESS_TRANSACTIONS = new BitapsAddressTransactionsDto( 16 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2) 17 | ); 18 | 19 | BITAPS_TRANSACTIONS_UPDATED = new BitapsAddressTransactionsDto( 20 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2, TRANSACTION_HASH_3, TRANSACTION_HASH_4) 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/provider/bitaps/src/testFixtures/java/de/cotto/bitbook/backend/transaction/bitaps/BitapsTransactionDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitaps; 2 | 3 | import java.util.List; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionFixtures.BLOCK_HEIGHT; 6 | import static de.cotto.bitbook.backend.model.TransactionFixtures.DATE_TIME; 7 | import static de.cotto.bitbook.backend.model.TransactionFixtures.FEES; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 9 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_1; 10 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_2; 11 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_1; 12 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_2; 13 | 14 | public class BitapsTransactionDtoFixtures { 15 | public static final BitapsTransactionDto BITAPS_TRANSACTION; 16 | 17 | static { 18 | BITAPS_TRANSACTION = new BitapsTransactionDto( 19 | TRANSACTION_HASH, 20 | BLOCK_HEIGHT, 21 | DATE_TIME, 22 | FEES.satoshis(), 23 | List.of(INPUT_DTO_1, INPUT_DTO_2), 24 | List.of(OUTPUT_DTO_1, OUTPUT_DTO_2) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/provider/bitcoind/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend:models")) 7 | implementation(project(":backend:provider:base")) 8 | implementation(project(":backend:blockheight")) 9 | testFixturesApi(testFixtures(project(":backend:provider:base"))) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 12 | } 13 | 14 | tasks.jacocoTestCoverageVerification { 15 | violationRules { 16 | rules.forEach {rule -> 17 | rule.limits.forEach {limit -> 18 | if (limit.counter == "INSTRUCTION") { 19 | limit.minimum = 0.66.toBigDecimal() 20 | } 21 | if (limit.counter == "METHOD") { 22 | limit.minimum = 0.83.toBigDecimal() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/provider/bitcoind/src/main/java/de/cotto/bitbook/backend/transaction/bitcoind/BitcoindBlockHeightProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.bitcoind; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.ProviderException; 5 | import de.cotto.bitbook.backend.transaction.BlockHeightProvider; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | 10 | import static de.cotto.bitbook.backend.model.Chain.BTC; 11 | 12 | @Component 13 | public class BitcoindBlockHeightProvider implements BlockHeightProvider { 14 | private final BitcoinCliWrapper bitcoinCliWrapper; 15 | 16 | public BitcoindBlockHeightProvider(BitcoinCliWrapper bitcoinCliWrapper) { 17 | this.bitcoinCliWrapper = bitcoinCliWrapper; 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return "BitcoindBlockHeightProvider"; 23 | } 24 | 25 | @Override 26 | public Optional get(Chain chain) throws ProviderException { 27 | throwIfUnsupported(chain); 28 | int blockHeight = bitcoinCliWrapper.getBlockCount().orElseThrow(ProviderException::new); 29 | return Optional.of(blockHeight); 30 | } 31 | 32 | @Override 33 | public boolean isSupported(Chain chain) { 34 | return chain == BTC; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/provider/blockchaininfo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:blockheight")) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:provider:base"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/blockchaininfo/src/main/java/de/cotto/bitbook/backend/transaction/blockchaininfo/BlockchainInfoBlockHeightProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchaininfo; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.ProviderException; 5 | import de.cotto.bitbook.backend.transaction.BlockHeightProvider; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | 10 | import static de.cotto.bitbook.backend.model.Chain.BTC; 11 | 12 | @Component 13 | public class BlockchainInfoBlockHeightProvider implements BlockHeightProvider { 14 | private final BlockchainInfoClient blockchainInfoClient; 15 | 16 | public BlockchainInfoBlockHeightProvider(BlockchainInfoClient blockchainInfoClient) { 17 | this.blockchainInfoClient = blockchainInfoClient; 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return "BlockchainInfoBlockHeightProvider"; 23 | } 24 | 25 | @Override 26 | public Optional get(Chain chain) throws ProviderException { 27 | throwIfUnsupported(chain); 28 | try { 29 | return Optional.of(Integer.parseInt(blockchainInfoClient.getBlockHeight())); 30 | } catch (NumberFormatException exception) { 31 | throw new ProviderException(exception); 32 | } 33 | } 34 | 35 | @Override 36 | public boolean isSupported(Chain chain) { 37 | return chain == BTC; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/provider/blockchaininfo/src/main/java/de/cotto/bitbook/backend/transaction/blockchaininfo/BlockchainInfoClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchaininfo; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 5 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | 10 | import java.util.Optional; 11 | 12 | @FeignClient(value = "blockchainInfo", url = "https://blockchain.info/") 13 | @RateLimiter(name = "blockchainInfo") 14 | @CircuitBreaker(name = "blockchainInfo") 15 | public interface BlockchainInfoClient { 16 | @GetMapping("/q/getblockcount") 17 | String getBlockHeight(); 18 | 19 | @GetMapping("/rawtx/{transactionHash}") 20 | Optional getTransaction(@PathVariable TransactionHash transactionHash); 21 | } 22 | -------------------------------------------------------------------------------- /backend/provider/blockchaininfo/src/testFixtures/java/de/cotto/bitbook/backend/transaction/blockchaininfo/BlockchainInfoTransactionDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchaininfo; 2 | 3 | import java.util.List; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionFixtures.BLOCK_HEIGHT; 6 | import static de.cotto.bitbook.backend.model.TransactionFixtures.DATE_TIME; 7 | import static de.cotto.bitbook.backend.model.TransactionFixtures.FEES; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 9 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_1; 10 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_2; 11 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_1; 12 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_2; 13 | 14 | public class BlockchainInfoTransactionDtoFixtures { 15 | public static final BlockchainInfoTransactionDto BLOCKCHAIN_INFO_TRANSACTION = 16 | new BlockchainInfoTransactionDto( 17 | TRANSACTION_HASH, 18 | BLOCK_HEIGHT, 19 | DATE_TIME, 20 | FEES.satoshis(), 21 | List.of(INPUT_DTO_1, INPUT_DTO_2), 22 | List.of(OUTPUT_DTO_1, OUTPUT_DTO_2) 23 | ); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /backend/provider/blockchair/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:blockheight")) 10 | implementation(project(":backend:address-transactions")) 11 | testImplementation(testFixtures(project(":backend:models"))) 12 | testImplementation(testFixtures(project(":backend:provider:base"))) 13 | testFixturesImplementation(testFixtures(project(":backend:models"))) 14 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 15 | } 16 | 17 | tasks.jacocoTestCoverageVerification { 18 | violationRules { 19 | rules.forEach {rule -> 20 | rule.limits.forEach {limit -> 21 | if (limit.counter == "BRANCH") { 22 | limit.minimum = 0.93.toBigDecimal() 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/provider/blockchair/src/main/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairBlockHeightDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.DeserializationContext; 5 | import com.fasterxml.jackson.databind.JsonDeserializer; 6 | import com.fasterxml.jackson.databind.JsonNode; 7 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 8 | 9 | import java.io.IOException; 10 | 11 | @JsonDeserialize(using = BlockchairBlockHeightDto.Deserializer.class) 12 | public class BlockchairBlockHeightDto { 13 | private final int blockHeight; 14 | 15 | public BlockchairBlockHeightDto(int blockHeight) { 16 | this.blockHeight = blockHeight; 17 | } 18 | 19 | public int getBlockHeight() { 20 | return blockHeight; 21 | } 22 | 23 | public static class Deserializer extends JsonDeserializer { 24 | @Override 25 | public BlockchairBlockHeightDto deserialize( 26 | JsonParser jsonParser, 27 | DeserializationContext deserializationContext 28 | ) throws IOException { 29 | JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); 30 | return new BlockchairBlockHeightDto(rootNode.get("data").get("blocks").intValue()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/provider/blockchair/src/main/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairChainName.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | 5 | public final class BlockchairChainName { 6 | private BlockchairChainName() { 7 | // utility class 8 | } 9 | 10 | static String get(Chain chain) { 11 | return switch (chain) { 12 | case BTC -> "bitcoin"; 13 | case BCH -> "bitcoin-cash"; 14 | case BSV -> "bitcoin-sv"; 15 | default -> throw new IllegalArgumentException(); 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/provider/blockchair/src/main/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.TransactionHash; 5 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 6 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 7 | import org.springframework.cloud.openfeign.FeignClient; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | 11 | import java.util.Optional; 12 | 13 | @FeignClient(value = "blockchair", url = "https://api.blockchair.com") 14 | @RateLimiter(name = "blockchair") 15 | @CircuitBreaker(name = "blockchair") 16 | public interface BlockchairClient { 17 | @GetMapping("/{chainName}/dashboards/transaction/{transactionHash}") 18 | Optional getTransaction( 19 | @PathVariable String chainName, 20 | @PathVariable TransactionHash transactionHash 21 | ); 22 | 23 | @GetMapping("/{chainName}/dashboards/address/{address}") 24 | Optional getAddressDetails( 25 | @PathVariable String chainName, 26 | @PathVariable Address address 27 | ); 28 | 29 | @GetMapping("/{chainName}/stats") 30 | Optional getBlockHeight(@PathVariable String chainName); 31 | } 32 | -------------------------------------------------------------------------------- /backend/provider/blockchair/src/test/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairBlockHeightDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import de.cotto.bitbook.backend.transaction.deserialization.TestObjectMapper; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @ExtendWith(MockitoExtension.class) 12 | class BlockchairBlockHeightDtoTest { 13 | private final ObjectMapper objectMapper = new TestObjectMapper(); 14 | 15 | @Test 16 | void deserialization() throws Exception { 17 | String json = "{\"data\": {\"blocks\": 673466, \"foo\": \"bar\"}}"; 18 | BlockchairBlockHeightDto blockchairBlockHeightDto = 19 | objectMapper.readValue(json, BlockchairBlockHeightDto.class); 20 | assertThat(blockchairBlockHeightDto.getBlockHeight()).isEqualTo(673_466); 21 | } 22 | } -------------------------------------------------------------------------------- /backend/provider/blockchair/src/test/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairChainNameTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.Chain.BCH; 6 | import static de.cotto.bitbook.backend.model.Chain.BSV; 7 | import static de.cotto.bitbook.backend.model.Chain.BTC; 8 | import static de.cotto.bitbook.backend.model.Chain.BTG; 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 11 | 12 | class BlockchairChainNameTest { 13 | @Test 14 | void btc() { 15 | assertThat(BlockchairChainName.get(BTC)).isEqualTo("bitcoin"); 16 | } 17 | 18 | @Test 19 | void bch() { 20 | assertThat(BlockchairChainName.get(BCH)).isEqualTo("bitcoin-cash"); 21 | } 22 | 23 | @Test 24 | void bsv() { 25 | assertThat(BlockchairChainName.get(BSV)).isEqualTo("bitcoin-sv"); 26 | } 27 | 28 | @Test 29 | @SuppressWarnings("ResultOfMethodCallIgnored") 30 | void unsupported() { 31 | assertThatIllegalArgumentException().isThrownBy(() -> BlockchairChainName.get(BTG)); 32 | } 33 | } -------------------------------------------------------------------------------- /backend/provider/blockchair/src/testFixtures/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairAddressTransactionsFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import java.util.Set; 4 | 5 | import static de.cotto.bitbook.backend.model.AddressFixtures.ADDRESS; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 7 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_2; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_3; 9 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_4; 10 | 11 | public class BlockchairAddressTransactionsFixtures { 12 | public static final BlockchairAddressTransactionsDto BLOCKCHAIR_ADDRESS_DETAILS; 13 | public static final BlockchairAddressTransactionsDto BLOCKCHAIR_ADDRESS_UPDATED; 14 | 15 | static { 16 | BLOCKCHAIR_ADDRESS_DETAILS = new BlockchairAddressTransactionsDto( 17 | ADDRESS, 18 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2) 19 | ); 20 | BLOCKCHAIR_ADDRESS_UPDATED = new BlockchairAddressTransactionsDto( 21 | ADDRESS, 22 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2, TRANSACTION_HASH_3, TRANSACTION_HASH_4) 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/provider/blockchair/src/testFixtures/java/de/cotto/bitbook/backend/transaction/blockchair/BlockchairTransactionDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockchair; 2 | 3 | import java.util.List; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionFixtures.BLOCK_HEIGHT; 6 | import static de.cotto.bitbook.backend.model.TransactionFixtures.DATE_TIME; 7 | import static de.cotto.bitbook.backend.model.TransactionFixtures.FEES; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 9 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_1; 10 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_2; 11 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_1; 12 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_2; 13 | 14 | public class BlockchairTransactionDtoFixtures { 15 | public static final BlockchairTransactionDto BLOCKCHAIR_TRANSACTION; 16 | 17 | static { 18 | BLOCKCHAIR_TRANSACTION = new BlockchairTransactionDto( 19 | TRANSACTION_HASH, 20 | BLOCK_HEIGHT, 21 | DATE_TIME, 22 | FEES.satoshis(), 23 | List.of(INPUT_DTO_1, INPUT_DTO_2), 24 | List.of(OUTPUT_DTO_1, OUTPUT_DTO_2) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/provider/blockcypher/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:address-transactions")) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:provider:base"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/blockcypher/src/main/java/de/cotto/bitbook/backend/transaction/blockcypher/BlockcypherClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockcypher; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 5 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | 10 | import java.util.Optional; 11 | 12 | @FeignClient(value = "blockcypher", url = "https://api.blockcypher.com") 13 | @RateLimiter(name = "blockcypher") 14 | @CircuitBreaker(name = "blockcypher") 15 | public interface BlockcypherClient { 16 | @GetMapping("/v1/btc/main/txs/{transactionHash}") 17 | Optional getTransaction(@PathVariable TransactionHash transactionHash); 18 | } 19 | -------------------------------------------------------------------------------- /backend/provider/blockcypher/src/testFixtures/java/de/cotto/bitbook/backend/transaction/blockcypher/BlockcypherTransactionDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockcypher; 2 | 3 | import java.util.List; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionFixtures.BLOCK_HEIGHT; 6 | import static de.cotto.bitbook.backend.model.TransactionFixtures.DATE_TIME; 7 | import static de.cotto.bitbook.backend.model.TransactionFixtures.FEES; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 9 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_1; 10 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_2; 11 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_1; 12 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_2; 13 | 14 | public class BlockcypherTransactionDtoFixtures { 15 | public static final BlockcypherTransactionDto BLOCKCYPHER_TRANSACTION; 16 | 17 | static { 18 | BLOCKCYPHER_TRANSACTION = new BlockcypherTransactionDto( 19 | TRANSACTION_HASH, 20 | BLOCK_HEIGHT, 21 | DATE_TIME, 22 | FEES.satoshis(), 23 | List.of(INPUT_DTO_1, INPUT_DTO_2), 24 | List.of(OUTPUT_DTO_1, OUTPUT_DTO_2) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/provider/blockstreaminfo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:address-transactions")) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:provider:base"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/blockstreaminfo/src/main/java/de/cotto/bitbook/backend/transaction/blockstream/BlockstreamInfoClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockstream; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 5 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | 10 | import java.util.Optional; 11 | 12 | @FeignClient(value = "blockstreaminfo", url = "https://blockstream.info/api") 13 | @RateLimiter(name = "blockstreaminfo") 14 | @CircuitBreaker(name = "blockstreaminfo") 15 | public interface BlockstreamInfoClient { 16 | @GetMapping("/address/{address}/txs") 17 | Optional getAddressDetails(@PathVariable Address address); 18 | } 19 | -------------------------------------------------------------------------------- /backend/provider/blockstreaminfo/src/testFixtures/java/de/cotto/bitbook/backend/transaction/blockstream/BlockstreamAddressTransactionsFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.blockstream; 2 | 3 | import java.util.Set; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_2; 7 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_3; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_4; 9 | 10 | public class BlockstreamAddressTransactionsFixtures { 11 | public static final BlockstreamAddressTransactionsDto BLOCKSTREAM_ADDRESS_DETAILS; 12 | public static final BlockstreamAddressTransactionsDto BLOCKSTREAM_ADDRESS_UPDATED; 13 | 14 | static { 15 | BLOCKSTREAM_ADDRESS_DETAILS = new BlockstreamAddressTransactionsDto( 16 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2) 17 | ); 18 | BLOCKSTREAM_ADDRESS_UPDATED = new BlockstreamAddressTransactionsDto( 19 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2, TRANSACTION_HASH_3, TRANSACTION_HASH_4) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/provider/btccom/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:address-transactions")) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:provider:base"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/btccom/src/main/java/de/cotto/bitbook/backend/transaction/btccom/BtcComClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.btccom; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.TransactionHash; 5 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 6 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 7 | import org.springframework.cloud.openfeign.FeignClient; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | 11 | import java.util.Optional; 12 | 13 | @FeignClient(value = "btccom", url = "https://chain.api.btc.com") 14 | @RateLimiter(name = "btccom") 15 | @CircuitBreaker(name = "btccom") 16 | public interface BtcComClient { 17 | @GetMapping("/v3/tx/{transactionHash}?verbose=2") 18 | Optional getTransaction(@PathVariable TransactionHash transactionHash); 19 | 20 | @GetMapping("/v3/address/{address}/tx") 21 | Optional getAddressDetails(@PathVariable Address address); 22 | } 23 | -------------------------------------------------------------------------------- /backend/provider/btccom/src/testFixtures/java/de/cotto/bitbook/backend/transaction/btccom/BtcComAddressTransactionsFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.btccom; 2 | 3 | import java.util.Set; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_2; 7 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_3; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_4; 9 | 10 | public class BtcComAddressTransactionsFixtures { 11 | public static final BtcComAddressTransactionsDto BTCCOM_ADDRESS_DETAILS; 12 | public static final BtcComAddressTransactionsDto BTCCOM_ADDRESS_UPDATED; 13 | 14 | static { 15 | BTCCOM_ADDRESS_DETAILS = new BtcComAddressTransactionsDto( 16 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2) 17 | ); 18 | BTCCOM_ADDRESS_UPDATED = new BtcComAddressTransactionsDto( 19 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2, TRANSACTION_HASH_3, TRANSACTION_HASH_4) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/provider/btccom/src/testFixtures/java/de/cotto/bitbook/backend/transaction/btccom/BtcComTransactionDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.btccom; 2 | 3 | import java.util.List; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionFixtures.BLOCK_HEIGHT; 6 | import static de.cotto.bitbook.backend.model.TransactionFixtures.DATE_TIME; 7 | import static de.cotto.bitbook.backend.model.TransactionFixtures.FEES; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 9 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_1; 10 | import static de.cotto.bitbook.backend.transaction.deserialization.InputDtoFixtures.INPUT_DTO_2; 11 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_1; 12 | import static de.cotto.bitbook.backend.transaction.deserialization.OutputDtoFixtures.OUTPUT_DTO_2; 13 | 14 | public class BtcComTransactionDtoFixtures { 15 | public static final BtcComTransactionDto BTCCOM_TRANSACTION; 16 | 17 | static { 18 | BTCCOM_TRANSACTION = new BtcComTransactionDto( 19 | TRANSACTION_HASH, 20 | BLOCK_HEIGHT, 21 | DATE_TIME, 22 | FEES.satoshis(), 23 | List.of(INPUT_DTO_1, INPUT_DTO_2), 24 | List.of(OUTPUT_DTO_1, OUTPUT_DTO_2) 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/provider/electrs/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend:models")) 7 | implementation(project(":backend:address-transactions")) 8 | implementation(project(":backend:provider:base")) 9 | implementation("org.apache.mina:mina-core") 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testFixturesImplementation(testFixtures(project(":backend:address-transactions"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | } 14 | 15 | tasks.jacocoTestCoverageVerification { 16 | violationRules { 17 | rules.forEach {rule -> 18 | rule.limits.forEach {limit -> 19 | if (limit.counter == "BRANCH") { 20 | limit.minimum = 0.94.toBigDecimal() 21 | } 22 | if (limit.counter == "INSTRUCTION") { 23 | limit.minimum = 0.91.toBigDecimal() 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | pitest { 31 | testStrengthThreshold.set(93) 32 | } 33 | -------------------------------------------------------------------------------- /backend/provider/electrs/src/main/java/de/cotto/bitbook/backend/transaction/electrs/jsonrpc/CodecFactory.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.electrs.jsonrpc; 2 | 3 | import org.apache.mina.core.session.IoSession; 4 | import org.apache.mina.filter.codec.ProtocolCodecFactory; 5 | 6 | public class CodecFactory implements ProtocolCodecFactory { 7 | private final Encoder encoder; 8 | private final Decoder decoder; 9 | 10 | public CodecFactory() { 11 | encoder = new Encoder(); 12 | decoder = new Decoder(); 13 | } 14 | 15 | @Override 16 | public Encoder getEncoder(IoSession ioSession) { 17 | return encoder; 18 | } 19 | 20 | @Override 21 | public Decoder getDecoder(IoSession ioSession) { 22 | return decoder; 23 | } 24 | } -------------------------------------------------------------------------------- /backend/provider/electrs/src/main/java/de/cotto/bitbook/backend/transaction/electrs/jsonrpc/Encoder.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.electrs.jsonrpc; 2 | 3 | import com.google.common.base.Charsets; 4 | import org.apache.mina.core.buffer.IoBuffer; 5 | import org.apache.mina.core.session.IoSession; 6 | import org.apache.mina.filter.codec.ProtocolEncoder; 7 | import org.apache.mina.filter.codec.ProtocolEncoderOutput; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | public class Encoder implements ProtocolEncoder { 12 | public Encoder() { 13 | // default constructor 14 | } 15 | 16 | @Override 17 | public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { 18 | byte[] payload = getPayload(message); 19 | IoBuffer buffer = IoBuffer.allocate(payload.length, false); 20 | buffer.put(payload, 0, payload.length); 21 | buffer.flip(); 22 | out.write(buffer); 23 | } 24 | 25 | @Nonnull 26 | private byte[] getPayload(Object message) { 27 | JsonRpcMessage request = (JsonRpcMessage) message; 28 | String stringToSend = request.toString() + "\n"; 29 | return stringToSend.getBytes(Charsets.UTF_8); 30 | } 31 | 32 | @Override 33 | public void dispose(IoSession session) { 34 | // nothing to dispose 35 | } 36 | } -------------------------------------------------------------------------------- /backend/provider/electrs/src/test/java/de/cotto/bitbook/backend/transaction/electrs/jsonrpc/CodecFactoryTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.electrs.jsonrpc; 2 | 3 | import org.apache.mina.core.session.IoSession; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | @ExtendWith(MockitoExtension.class) 12 | class CodecFactoryTest { 13 | private final CodecFactory codecFactory = new CodecFactory(); 14 | 15 | @Mock 16 | private IoSession ioSession; 17 | 18 | @Test 19 | void encoder() { 20 | assertThat(codecFactory.getEncoder(ioSession)).isInstanceOf(Encoder.class); 21 | } 22 | 23 | @Test 24 | void decoder() { 25 | assertThat(codecFactory.getDecoder(ioSession)).isInstanceOf(Decoder.class); 26 | } 27 | } -------------------------------------------------------------------------------- /backend/provider/electrs/src/test/java/de/cotto/bitbook/backend/transaction/electrs/jsonrpc/JsonRpcClientTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.electrs.jsonrpc; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | class JsonRpcClientTest { 6 | @SuppressWarnings("PMD.AvoidUsingHardCodedIP") 7 | private static final String LOCALHOST = "127.0.0.1"; 8 | 9 | private final JsonRpcClient jsonRpcClient = new JsonRpcClient(LOCALHOST, 1); 10 | 11 | @Test 12 | void does_nothing_if_no_message_should_be_sent() { 13 | jsonRpcClient.sendMessages(); 14 | } 15 | } -------------------------------------------------------------------------------- /backend/provider/fullstackcash/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:blockheight")) 8 | implementation(project(":backend:models")) 9 | implementation(project(":backend:provider:base")) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:provider:base"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/fullstackcash/src/main/java/de/cotto/bitbook/backend/transaction/fullstackcash/FullstackCashBlockHeightProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.fullstackcash; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.ProviderException; 5 | import de.cotto.bitbook.backend.transaction.BlockHeightProvider; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Optional; 9 | 10 | @Component 11 | public class FullstackCashBlockHeightProvider implements BlockHeightProvider { 12 | private final FullstackCashClient fullstackCashClient; 13 | 14 | public FullstackCashBlockHeightProvider(FullstackCashClient fullstackCashClient) { 15 | this.fullstackCashClient = fullstackCashClient; 16 | } 17 | 18 | @Override 19 | public String getName() { 20 | return "FullstackCashBlockHeightProvider"; 21 | } 22 | 23 | @Override 24 | public Optional get(Chain chain) throws ProviderException { 25 | throwIfUnsupported(chain); 26 | try { 27 | return Optional.of(Integer.parseInt(fullstackCashClient.getBlockHeight())); 28 | } catch (NumberFormatException exception) { 29 | throw new ProviderException(exception); 30 | } 31 | } 32 | 33 | @Override 34 | public boolean isSupported(Chain chain) { 35 | return chain == Chain.BCH; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/provider/fullstackcash/src/main/java/de/cotto/bitbook/backend/transaction/fullstackcash/FullstackCashClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.fullstackcash; 2 | 3 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 4 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | 8 | @FeignClient(value = "fullstackCash", url = "https://api.fullstack.cash/") 9 | @RateLimiter(name = "fullstackCash") 10 | @CircuitBreaker(name = "fullstackCash") 11 | public interface FullstackCashClient { 12 | @GetMapping("/v5/blockchain/getBlockCount") 13 | String getBlockHeight(); 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/mempoolspace/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:provider:base")) 9 | implementation(project(":backend:address-transactions")) 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:provider:base"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | testFixturesImplementation(testFixtures(project(":backend:provider:base"))) 14 | } 15 | -------------------------------------------------------------------------------- /backend/provider/mempoolspace/src/main/java/de/cotto/bitbook/backend/transaction/mempoolspace/MempoolSpaceClient.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.mempoolspace; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 5 | import io.github.resilience4j.ratelimiter.annotation.RateLimiter; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | 10 | import java.util.Optional; 11 | 12 | @FeignClient(value = "mempoolspace", url = "https://mempool.space/api") 13 | @RateLimiter(name = "mempoolspace") 14 | @CircuitBreaker(name = "mempoolspace") 15 | public interface MempoolSpaceClient { 16 | @GetMapping("/address/{address}/txs/chain") 17 | Optional getAddressDetails(@PathVariable Address address); 18 | } 19 | -------------------------------------------------------------------------------- /backend/provider/mempoolspace/src/testFixtures/java/de/cotto/bitbook/backend/transaction/mempoolspace/MempoolSpaceAddressTransactionsFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.mempoolspace; 2 | 3 | import java.util.Set; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_2; 7 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_3; 8 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_4; 9 | 10 | public class MempoolSpaceAddressTransactionsFixtures { 11 | public static final MempoolSpaceAddressTransactionsDto MEMPOOLSPACE_ADDRESS_DETAILS; 12 | public static final MempoolSpaceAddressTransactionsDto MEMPOOLSPACE_ADDRESS_UPDATED; 13 | 14 | static { 15 | MEMPOOLSPACE_ADDRESS_DETAILS = new MempoolSpaceAddressTransactionsDto( 16 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2) 17 | ); 18 | MEMPOOLSPACE_ADDRESS_UPDATED = new MempoolSpaceAddressTransactionsDto( 19 | Set.of(TRANSACTION_HASH, TRANSACTION_HASH_2, TRANSACTION_HASH_3, TRANSACTION_HASH_4) 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/request/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:request:models")) 9 | implementation("org.apache.commons:commons-collections4") 10 | testImplementation("com.github.valfirst:slf4j-test") 11 | testImplementation(testFixtures(project(":backend:models"))) 12 | integrationTestImplementation(project(":backend:models")) 13 | integrationTestImplementation(project(":backend:request:models")) 14 | configurations.named("testRuntimeOnly") { 15 | exclude(group = "ch.qos.logback", module = "logback-classic") 16 | exclude(group = "org.slf4j", module = "slf4j-nop") 17 | } 18 | } 19 | 20 | tasks.jar { 21 | archiveBaseName.set("backend-request-models") 22 | } 23 | -------------------------------------------------------------------------------- /backend/request/models/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | tasks.jar { 6 | archiveBaseName.set("request-models") 7 | } 8 | -------------------------------------------------------------------------------- /backend/request/models/src/main/java/de/cotto/bitbook/backend/request/AllProvidersFailedException.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | public class AllProvidersFailedException extends Exception { 4 | public AllProvidersFailedException() { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/request/models/src/main/java/de/cotto/bitbook/backend/request/NotSupportedByAnyProviderException.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | public class NotSupportedByAnyProviderException extends Exception { 4 | public NotSupportedByAnyProviderException() { 5 | super(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /backend/request/models/src/main/java/de/cotto/bitbook/backend/request/PrioritizedRequest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import java.util.Objects; 4 | 5 | public class PrioritizedRequest { 6 | private final K key; 7 | private final RequestPriority priority; 8 | 9 | protected PrioritizedRequest(K key, RequestPriority priority) { 10 | super(); 11 | this.key = key; 12 | this.priority = priority; 13 | } 14 | 15 | public RequestPriority getPriority() { 16 | return priority; 17 | } 18 | 19 | public K getKey() { 20 | return key; 21 | } 22 | 23 | public PrioritizedRequestWithResult getWithResultFuture() { 24 | return new PrioritizedRequestWithResult<>(key, priority); 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "PrioritizedRequest{" + 30 | "key=" + key + 31 | ", priority=" + priority + 32 | '}'; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object other) { 37 | if (this == other) { 38 | return true; 39 | } 40 | if (other == null || getClass() != other.getClass()) { 41 | return false; 42 | } 43 | PrioritizedRequest that = (PrioritizedRequest) other; 44 | return Objects.equals(key, that.key) 45 | && priority == that.priority; 46 | } 47 | 48 | @Override 49 | public int hashCode() { 50 | return Objects.hash(key, priority); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /backend/request/models/src/main/java/de/cotto/bitbook/backend/request/RequestPriority.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | public enum RequestPriority { 4 | LOWEST(2), 5 | MEDIUM(1), 6 | STANDARD(0); 7 | 8 | private final int integerForComparison; 9 | 10 | RequestPriority(int integerForComparison) { 11 | this.integerForComparison = integerForComparison; 12 | } 13 | 14 | public int getIntegerForComparison() { 15 | return integerForComparison; 16 | } 17 | 18 | public boolean isAtLeast(RequestPriority other) { 19 | return integerForComparison <= other.getIntegerForComparison(); 20 | } 21 | 22 | public RequestPriority getHighestPriority(RequestPriority other) { 23 | if (isAtLeast(other)) { 24 | return this; 25 | } 26 | return other; 27 | } 28 | } -------------------------------------------------------------------------------- /backend/request/models/src/main/java/de/cotto/bitbook/backend/request/ResultFromProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import java.util.Optional; 6 | 7 | public final class ResultFromProvider { 8 | @Nullable 9 | private final R result; 10 | private final boolean successful; 11 | 12 | private ResultFromProvider(@Nonnull R result) { 13 | this.result = result; 14 | successful = true; 15 | } 16 | 17 | @SuppressWarnings("PMD.NullAssignment") 18 | private ResultFromProvider(boolean successful) { 19 | this.result = null; 20 | this.successful = successful; 21 | } 22 | 23 | public static ResultFromProvider of(R result) { 24 | return new ResultFromProvider<>(result); 25 | } 26 | 27 | public static ResultFromProvider empty() { 28 | return new ResultFromProvider<>(true); 29 | } 30 | 31 | public static ResultFromProvider failure() { 32 | return new ResultFromProvider<>(false); 33 | } 34 | 35 | public Optional getAsOptional() { 36 | return Optional.ofNullable(result); 37 | } 38 | 39 | public boolean isSuccessful() { 40 | return successful; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/request/models/src/test/java/de/cotto/bitbook/backend/request/AllProvidersFailedExceptionTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class AllProvidersFailedExceptionTest { 8 | @Test 9 | void isException() { 10 | assertThat(new AllProvidersFailedException()).isInstanceOf(Exception.class); 11 | } 12 | } -------------------------------------------------------------------------------- /backend/request/models/src/test/java/de/cotto/bitbook/backend/request/NotSupportedByAnyProviderExceptionTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class NotSupportedByAnyProviderExceptionTest { 8 | @Test 9 | void isException() { 10 | assertThat(new NotSupportedByAnyProviderException()).isInstanceOf(Exception.class); 11 | } 12 | } -------------------------------------------------------------------------------- /backend/request/models/src/test/java/de/cotto/bitbook/backend/request/ResultFromProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class ResultFromProviderTest { 8 | @Test 9 | void failure() { 10 | assertThat(ResultFromProvider.failure().isSuccessful()).isFalse(); 11 | } 12 | 13 | @Test 14 | void success() { 15 | ResultFromProvider success = ResultFromProvider.of("x"); 16 | assertThat(success.isSuccessful()).isTrue(); 17 | assertThat(success.getAsOptional()).contains("x"); 18 | } 19 | 20 | @Test 21 | void empty() { 22 | ResultFromProvider success = ResultFromProvider.empty(); 23 | assertThat(success.isSuccessful()).isTrue(); 24 | assertThat(success.getAsOptional()).isEmpty(); 25 | } 26 | } -------------------------------------------------------------------------------- /backend/request/src/integrationTest/java/de/cotto/bitbook/backend/SpringBootConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class SpringBootConfiguration { 7 | public SpringBootConfiguration() { 8 | // default constructor 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/request/src/main/java/de/cotto/bitbook/backend/request/ScoreUpdate.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public class ScoreUpdate { 6 | public static final ScoreUpdate RATE_LIMITED = new ScoreUpdate(1_500); 7 | public static final ScoreUpdate CIRCUIT_BREAKER = new ScoreUpdate(2_000); 8 | public static final ScoreUpdate PROVIDER_EXCEPTION = new ScoreUpdate(4_500); 9 | public static final ScoreUpdate UNKNOWN_EXCEPTION = new ScoreUpdate(5_000); 10 | 11 | private final long value; 12 | 13 | protected ScoreUpdate(long value) { 14 | this.value = value; 15 | } 16 | 17 | public static ScoreUpdate forHttpStatus(int status) { 18 | if (status == HttpStatus.TOO_MANY_REQUESTS.value()) { 19 | return new ScoreUpdate(4_000); 20 | } 21 | return new ScoreUpdate(3_000); 22 | } 23 | 24 | public static ScoreUpdate forSuccess(long durationInMilliSeconds) { 25 | return new ScoreUpdate(durationInMilliSeconds); 26 | } 27 | 28 | public long getValue() { 29 | return value; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/request/src/test/java/de/cotto/bitbook/backend/request/ScoreUpdateTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.request; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.http.HttpStatus; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class ScoreUpdateTest { 9 | 10 | @Test 11 | void circuitBreakerException_smallerThan_httpFailure() { 12 | assertThat(ScoreUpdate.CIRCUIT_BREAKER.getValue()).isLessThan(ScoreUpdate.forHttpStatus(0).getValue()); 13 | } 14 | 15 | @Test 16 | void rateLimited_smallerThan_circuitBreaker() { 17 | assertThat(ScoreUpdate.RATE_LIMITED.getValue()).isLessThan(ScoreUpdate.CIRCUIT_BREAKER.getValue()); 18 | } 19 | 20 | @Test 21 | void rateLimited_smallerThan_httpFailure() { 22 | assertThat(ScoreUpdate.RATE_LIMITED.getValue()).isLessThan(ScoreUpdate.forHttpStatus(0).getValue()); 23 | } 24 | 25 | @Test 26 | void httpFailure_smallerThan_tooManyRequests() { 27 | assertThat(ScoreUpdate.forHttpStatus(0).getValue()) 28 | .isLessThan(ScoreUpdate.forHttpStatus(HttpStatus.TOO_MANY_REQUESTS.value()).getValue()); 29 | } 30 | 31 | @Test 32 | void fast_success_smallerThan_slow_success() { 33 | assertThat(ScoreUpdate.forSuccess(100).getValue()).isLessThan(ScoreUpdate.forSuccess(1_000).getValue()); 34 | } 35 | 36 | @Test 37 | void slow_success_smallerThan_rate_limited() { 38 | assertThat(ScoreUpdate.forSuccess(1_000).getValue()).isLessThan(ScoreUpdate.RATE_LIMITED.getValue()); 39 | } 40 | } -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/AddressDescriptionService.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.AddressWithDescription; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class AddressDescriptionService extends DescriptionService { 9 | public AddressDescriptionService(AddressWithDescriptionDao dao) { 10 | super(dao); 11 | } 12 | 13 | public void set(Address address, String description) { 14 | set(new AddressWithDescription(address, description)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/AddressWithDescriptionDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.AddressWithDescription; 5 | 6 | public interface AddressWithDescriptionDao extends DescriptionDao { 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/DescriptionDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import java.util.Set; 4 | 5 | public interface DescriptionDao { 6 | T get(K key); 7 | 8 | void save(T value); 9 | 10 | Set findWithDescriptionInfix(String infix); 11 | 12 | void remove(K key); 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/DescriptionService.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import de.cotto.bitbook.backend.model.ModelWithDescription; 4 | 5 | import java.util.Set; 6 | 7 | public class DescriptionService, T extends ModelWithDescription> { 8 | private static final int MINIMUM_LENGTH_FOR_COMPLETION = 3; 9 | 10 | private final DescriptionDao dao; 11 | 12 | public DescriptionService(DescriptionDao dao) { 13 | this.dao = dao; 14 | } 15 | 16 | public T get(K key) { 17 | return dao.get(key); 18 | } 19 | 20 | public String getDescription(K key) { 21 | return get(key).getDescription(); 22 | } 23 | 24 | protected void set(T stringWithDescription) { 25 | if (stringWithDescription.getDescription().isBlank()) { 26 | return; 27 | } 28 | dao.save(stringWithDescription); 29 | } 30 | 31 | public void remove(K key) { 32 | dao.remove(key); 33 | } 34 | 35 | public Set getWithDescriptionInfix(String infix) { 36 | if (infix.length() < MINIMUM_LENGTH_FOR_COMPLETION) { 37 | return Set.of(); 38 | } 39 | return dao.findWithDescriptionInfix(infix); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/FeignConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.springframework.cloud.openfeign.EnableFeignClients; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @EnableFeignClients 8 | public class FeignConfiguration { 9 | public FeignConfiguration() { 10 | // default constructor 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/SchedulingConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.EnableScheduling; 5 | 6 | @Configuration 7 | @EnableScheduling 8 | public class SchedulingConfiguration { 9 | public SchedulingConfiguration() { 10 | // default constructor 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/TransactionDescriptionService.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | import de.cotto.bitbook.backend.model.TransactionWithDescription; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class TransactionDescriptionService extends DescriptionService { 9 | public TransactionDescriptionService(TransactionWithDescriptionDao dao) { 10 | super(dao); 11 | } 12 | 13 | public void set(TransactionHash transactionHash, String description) { 14 | set(new TransactionWithDescription(transactionHash, description)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/TransactionWithDescriptionDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | import de.cotto.bitbook.backend.model.TransactionWithDescription; 5 | 6 | public interface TransactionWithDescriptionDao extends DescriptionDao { 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/persistence/AddressWithDescriptionRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Set; 6 | 7 | public interface AddressWithDescriptionRepository extends JpaRepository { 8 | Set findByDescriptionContaining(String infix); 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/main/java/de/cotto/bitbook/backend/persistence/TransactionWithDescriptionRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Set; 6 | 7 | public interface TransactionWithDescriptionRepository extends JpaRepository { 8 | Set findByDescriptionContaining(String infix); 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=warn 2 | logging.level.de.cotto.bitbook=debug -------------------------------------------------------------------------------- /backend/src/test/java/de/cotto/bitbook/backend/FeignConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class FeignConfigurationTest { 8 | @Test 9 | void coverage_test() { 10 | assertThat(new FeignConfiguration()).isNotNull(); 11 | } 12 | } -------------------------------------------------------------------------------- /backend/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %relative %-3p %m%rEx{2} [%logger]%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/transaction/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:address-transactions")) 8 | implementation(project(":backend:blockheight")) 9 | implementation(project(":backend:models")) 10 | implementation(project(":backend:price")) 11 | implementation(project(":backend:request")) 12 | implementation(project(":backend:request:models")) 13 | testImplementation(testFixtures(project(":backend:address-transactions"))) 14 | testImplementation(testFixtures(project(":backend:models"))) 15 | integrationTestImplementation(project(":backend:request")) 16 | integrationTestImplementation(project(":backend:request:models")) 17 | integrationTestImplementation(testFixtures(project(":backend:address-transactions"))) 18 | integrationTestImplementation(testFixtures(project(":backend:models"))) 19 | testFixturesImplementation(testFixtures(project(":backend:address-transactions"))) 20 | testFixturesImplementation(testFixtures(project(":backend:models"))) 21 | testFixturesImplementation(project(":backend:request:models")) 22 | testFixturesImplementation("javax.persistence:javax.persistence-api") 23 | } 24 | -------------------------------------------------------------------------------- /backend/transaction/src/integrationTest/java/de/cotto/bitbook/backend/SpringBootConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class SpringBootConfiguration { 7 | public SpringBootConfiguration() { 8 | // default constructor 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /backend/transaction/src/integrationTest/java/de/cotto/bitbook/backend/transaction/AddressTransactionsProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.AddressTransactions; 4 | import de.cotto.bitbook.backend.model.Provider; 5 | 6 | import java.util.Optional; 7 | 8 | public class AddressTransactionsProvider implements Provider { 9 | public AddressTransactionsProvider() { 10 | // default constructor 11 | } 12 | 13 | @Override 14 | public String getName() { 15 | return "for test"; 16 | } 17 | 18 | @Override 19 | public Optional get(TransactionsRequestKey key) { 20 | return Optional.empty(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /backend/transaction/src/integrationTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | logging.level.root=warn 2 | spring.task.scheduling.pool.size=20 -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/AddressCompletionDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | 5 | import java.util.Set; 6 | 7 | public interface AddressCompletionDao { 8 | Set
completeFromAddressTransactions(String prefix); 9 | 10 | Set
completeFromInputsAndOutputs(String prefix); 11 | } 12 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/AddressTransactionsDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.AddressTransactions; 5 | import de.cotto.bitbook.backend.model.Chain; 6 | 7 | import java.util.Set; 8 | 9 | public interface AddressTransactionsDao { 10 | void saveAddressTransactions(AddressTransactions addressTransactions); 11 | 12 | AddressTransactions getAddressTransactions(Address address, Chain chain); 13 | 14 | Set
getAddressesStartingWith(String addressPrefix); 15 | } 16 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/BalanceService.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.AddressTransactions; 5 | import de.cotto.bitbook.backend.model.Chain; 6 | import de.cotto.bitbook.backend.model.Coins; 7 | import de.cotto.bitbook.backend.model.TransactionHash; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Set; 11 | 12 | @Component 13 | public class BalanceService { 14 | private final AddressTransactionsService addressTransactionsService; 15 | private final TransactionService transactionService; 16 | 17 | public BalanceService( 18 | AddressTransactionsService addressTransactionsService, 19 | TransactionService transactionService 20 | ) { 21 | this.addressTransactionsService = addressTransactionsService; 22 | this.transactionService = transactionService; 23 | } 24 | 25 | public Coins getBalance(Address address, Chain chain) { 26 | AddressTransactions transactions = addressTransactionsService.getTransactions(address, chain); 27 | Set transactionHashes = transactions.transactionHashes(); 28 | return transactionService.getTransactionDetails(transactionHashes, chain).stream() 29 | .map(transactionDetails -> transactionDetails.getDifferenceForAddress(address)) 30 | .reduce(Coins.NONE, Coins::add); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/PrioritizingAddressTransactionsProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.AddressTransactions; 4 | import de.cotto.bitbook.backend.model.Provider; 5 | import de.cotto.bitbook.backend.request.PrioritizingProvider; 6 | import de.cotto.bitbook.backend.request.ResultFuture; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.List; 10 | 11 | @Component 12 | public class PrioritizingAddressTransactionsProvider 13 | extends PrioritizingProvider { 14 | 15 | public PrioritizingAddressTransactionsProvider( 16 | List> providers 17 | ) { 18 | super(providers, "Transactions for address"); 19 | } 20 | 21 | public ResultFuture getAddressTransactions(AddressTransactionsRequest request) { 22 | return getForRequest(request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/PrioritizingBlockHeightProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.request.PrioritizingProvider; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.List; 8 | 9 | @Component 10 | public class PrioritizingBlockHeightProvider extends PrioritizingProvider { 11 | protected static final int INVALID = -1; 12 | 13 | public PrioritizingBlockHeightProvider(List providers) { 14 | super(providers, "Block height"); 15 | } 16 | 17 | public int getBlockHeight(Chain chain) { 18 | return getForRequestBlocking(new BlockHeightRequest(chain)).orElse(INVALID); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/PrioritizingTransactionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.HashAndChain; 4 | import de.cotto.bitbook.backend.model.Provider; 5 | import de.cotto.bitbook.backend.model.Transaction; 6 | import de.cotto.bitbook.backend.request.PrioritizingProvider; 7 | import de.cotto.bitbook.backend.request.ResultFuture; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.List; 11 | 12 | @Component 13 | public class PrioritizingTransactionProvider extends PrioritizingProvider { 14 | public PrioritizingTransactionProvider(List> providers) { 15 | super(providers, "Transaction details"); 16 | } 17 | 18 | public ResultFuture getTransaction(TransactionRequest transactionRequest) { 19 | return getForRequest(transactionRequest); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/TransactionCompletionDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | 5 | import java.util.Set; 6 | 7 | public interface TransactionCompletionDao { 8 | Set completeFromTransactionDetails(String hashPrefix); 9 | 10 | Set completeFromAddressTransactionHashes(String hashPrefix); 11 | } 12 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/TransactionDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.Transaction; 5 | import de.cotto.bitbook.backend.model.TransactionHash; 6 | 7 | public interface TransactionDao { 8 | Transaction getTransaction(TransactionHash transactionHash, Chain chain); 9 | 10 | void saveTransaction(Transaction transaction); 11 | } 12 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/TransactionRequest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.HashAndChain; 5 | import de.cotto.bitbook.backend.model.Transaction; 6 | import de.cotto.bitbook.backend.model.TransactionHash; 7 | import de.cotto.bitbook.backend.request.PrioritizedRequest; 8 | import de.cotto.bitbook.backend.request.RequestPriority; 9 | 10 | public final class TransactionRequest extends PrioritizedRequest { 11 | public TransactionRequest(TransactionHash transactionHash, Chain chain, RequestPriority priority) { 12 | super(new HashAndChain(transactionHash, chain), priority); 13 | } 14 | 15 | public HashAndChain getHashAndChain() { 16 | return getKey(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/AddressTransactionsRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | public interface AddressTransactionsRepository 10 | extends JpaRepository { 11 | List findByAddressStartingWith(String prefix); 12 | 13 | @Query("SELECT hash FROM AddressTransactionsJpaDto addressTransactions INNER JOIN " + 14 | "addressTransactions.transactionHashes hash WHERE hash LIKE :hashPrefix%") 15 | Set findTransactionHashesByPrefix(String hashPrefix); 16 | } 17 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/AddressView.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | public interface AddressView { 4 | String getAddress(); 5 | } 6 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/InputOutputJpaDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | public interface InputOutputJpaDto { 4 | String getAddress(); 5 | } 6 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/InputRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Set; 6 | 7 | public interface InputRepository extends JpaRepository { 8 | Set findBySourceAddressStartingWith(String prefix); 9 | } 10 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/OutputRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Set; 6 | 7 | public interface OutputRepository extends JpaRepository { 8 | Set findByTargetAddressStartingWith(String prefix); 9 | } 10 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/TransactionDaoImpl.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import de.cotto.bitbook.backend.model.Transaction; 5 | import de.cotto.bitbook.backend.model.TransactionHash; 6 | import de.cotto.bitbook.backend.transaction.TransactionDao; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.transaction.Transactional; 10 | 11 | @Component 12 | @Transactional 13 | public class TransactionDaoImpl implements TransactionDao { 14 | private final TransactionRepository transactionRepository; 15 | 16 | public TransactionDaoImpl(TransactionRepository transactionRepository) { 17 | this.transactionRepository = transactionRepository; 18 | } 19 | 20 | @Override 21 | public Transaction getTransaction(TransactionHash transactionHash, Chain chain) { 22 | return transactionRepository.findById(new TransactionJpaDtoId(transactionHash.toString(), chain.toString())) 23 | .map(TransactionJpaDto::toModel) 24 | .orElse(Transaction.unknown(chain)); 25 | } 26 | 27 | @Override 28 | public void saveTransaction(Transaction transaction) { 29 | transactionRepository.save(TransactionJpaDto.fromModel(transaction)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/TransactionHashView.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | public interface TransactionHashView { 4 | String getHash(); 5 | } 6 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/TransactionJpaDtoId.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | import java.io.Serializable; 6 | import java.util.Objects; 7 | 8 | public class TransactionJpaDtoId implements Serializable { 9 | @Nullable 10 | private String hash; 11 | 12 | @Nullable 13 | private String chain; 14 | 15 | @SuppressWarnings("unused") 16 | public TransactionJpaDtoId() { 17 | // for JPA 18 | } 19 | 20 | public TransactionJpaDtoId(@Nonnull String hash, @Nonnull String chain) { 21 | this.hash = hash; 22 | this.chain = chain; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object other) { 27 | if (this == other) { 28 | return true; 29 | } 30 | if (other == null || getClass() != other.getClass()) { 31 | return false; 32 | } 33 | TransactionJpaDtoId that = (TransactionJpaDtoId) other; 34 | return Objects.equals(hash, that.hash) && Objects.equals(chain, that.chain); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(hash, chain); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/transaction/src/main/java/de/cotto/bitbook/backend/transaction/persistence/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Set; 6 | 7 | public interface TransactionRepository extends JpaRepository { 8 | Set findByHashStartingWith(String hashPrefix); 9 | } 10 | -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/AddressTransactionsProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.AddressTransactions; 4 | import de.cotto.bitbook.backend.model.Provider; 5 | 6 | public interface AddressTransactionsProvider extends Provider { 7 | } 8 | -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/TransactionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import de.cotto.bitbook.backend.model.HashAndChain; 4 | import de.cotto.bitbook.backend.model.Provider; 5 | import de.cotto.bitbook.backend.model.Transaction; 6 | 7 | public abstract class TransactionProvider implements Provider { 8 | protected TransactionProvider() { 9 | // just used for tests 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/persistence/AddressTransactionsJpaDtoIdTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import nl.jqno.equalsverifier.EqualsVerifier; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static de.cotto.bitbook.backend.model.AddressFixtures.ADDRESS; 7 | import static de.cotto.bitbook.backend.model.Chain.BTC; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class AddressTransactionsJpaDtoIdTest { 11 | @Test 12 | void fromModel() { 13 | assertThat(AddressTransactionsJpaDtoId.fromModels(ADDRESS, BTC)) 14 | .isEqualTo(new AddressTransactionsJpaDtoId(ADDRESS.toString(), "BTC")); 15 | } 16 | 17 | @Test 18 | void testEquals() { 19 | EqualsVerifier.simple().forClass(AddressTransactionsJpaDtoId.class).verify(); 20 | } 21 | } -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/persistence/AddressTransactionsJpaDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.AddressTransactionsFixtures.ADDRESS_TRANSACTIONS; 6 | import static de.cotto.bitbook.backend.transaction.persistence.AddressTransactionsJpaDtoFixtures.ADDRESS_TRANSACTIONS_JPA_DTO; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class AddressTransactionsJpaDtoTest { 10 | @Test 11 | void toModel() { 12 | assertThat(ADDRESS_TRANSACTIONS_JPA_DTO.toModel()).isEqualTo(ADDRESS_TRANSACTIONS); 13 | } 14 | 15 | @Test 16 | void fromModel() { 17 | assertThat(AddressTransactionsJpaDto.fromModel(ADDRESS_TRANSACTIONS)) 18 | .usingRecursiveComparison().isEqualTo(ADDRESS_TRANSACTIONS_JPA_DTO); 19 | } 20 | } -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/persistence/InputJpaDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.junit.jupiter.MockitoExtension; 6 | 7 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_1; 8 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_ADDRESS_1; 9 | import static de.cotto.bitbook.backend.transaction.persistence.InputJpaDtoFixtures.INPUT_JPA_DTO_1; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | class InputJpaDtoTest { 14 | @Test 15 | void toModel() { 16 | assertThat(INPUT_JPA_DTO_1.toModel()).isEqualTo(INPUT_1); 17 | } 18 | 19 | @Test 20 | void fromModel() { 21 | assertThat(InputJpaDto.fromModel(INPUT_1)).usingRecursiveComparison().isEqualTo(INPUT_JPA_DTO_1); 22 | } 23 | 24 | @Test 25 | void getAddress() { 26 | assertThat(INPUT_JPA_DTO_1.getAddress()).isEqualTo(INPUT_ADDRESS_1.toString()); 27 | } 28 | } -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/persistence/OutputJpaDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_1; 6 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_ADDRESS_1; 7 | import static de.cotto.bitbook.backend.transaction.persistence.OutputJpaDtoFixtures.OUTPUT_JPA_DTO_1; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class OutputJpaDtoTest { 11 | @Test 12 | void toModel() { 13 | assertThat(OUTPUT_JPA_DTO_1.toModel()).isEqualTo(OUTPUT_1); 14 | } 15 | 16 | @Test 17 | void fromModel() { 18 | assertThat(OutputJpaDto.fromModel(OUTPUT_1)).usingRecursiveComparison().isEqualTo(OUTPUT_JPA_DTO_1); 19 | } 20 | 21 | @Test 22 | void getAddress() { 23 | assertThat(OUTPUT_JPA_DTO_1.getAddress()).isEqualTo(OUTPUT_ADDRESS_1.toString()); 24 | } 25 | } -------------------------------------------------------------------------------- /backend/transaction/src/test/java/de/cotto/bitbook/backend/transaction/persistence/TransactionJpaDtoIdTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import nl.jqno.equalsverifier.EqualsVerifier; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class TransactionJpaDtoIdTest { 7 | @Test 8 | void testEquals() { 9 | EqualsVerifier.simple().forClass(TransactionJpaDtoId.class).verify(); 10 | } 11 | } -------------------------------------------------------------------------------- /backend/transaction/src/testFixtures/java/de/cotto/bitbook/backend/transaction/TransactionRequestFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction; 2 | 3 | import static de.cotto.bitbook.backend.model.Chain.BTC; 4 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 5 | import static de.cotto.bitbook.backend.request.RequestPriority.STANDARD; 6 | 7 | public class TransactionRequestFixtures { 8 | public static final TransactionRequest TRANSACTION_REQUEST = 9 | new TransactionRequest(TRANSACTION_HASH, BTC, STANDARD); 10 | } 11 | -------------------------------------------------------------------------------- /backend/transaction/src/testFixtures/java/de/cotto/bitbook/backend/transaction/persistence/AddressTransactionsJpaDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | 8 | import static de.cotto.bitbook.backend.model.AddressTransactionsFixtures.ADDRESS_TRANSACTIONS; 9 | import static de.cotto.bitbook.backend.model.Chain.BTC; 10 | 11 | public class AddressTransactionsJpaDtoFixtures { 12 | public static final AddressTransactionsJpaDto ADDRESS_TRANSACTIONS_JPA_DTO; 13 | 14 | static { 15 | ADDRESS_TRANSACTIONS_JPA_DTO = new AddressTransactionsJpaDto(); 16 | ADDRESS_TRANSACTIONS_JPA_DTO.setAddress(ADDRESS_TRANSACTIONS.address().toString()); 17 | ADDRESS_TRANSACTIONS_JPA_DTO.setChain(BTC.toString()); 18 | Set hashes = ADDRESS_TRANSACTIONS.transactionHashes().stream() 19 | .map(TransactionHash::toString) 20 | .collect(Collectors.toSet()); 21 | ADDRESS_TRANSACTIONS_JPA_DTO.setTransactionHashes(hashes); 22 | ADDRESS_TRANSACTIONS_JPA_DTO.setLastCheckedAtBlockheight(ADDRESS_TRANSACTIONS.lastCheckedAtBlockHeight()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/transaction/src/testFixtures/java/de/cotto/bitbook/backend/transaction/persistence/InputJpaDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_ADDRESS_1; 4 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_ADDRESS_2; 5 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_VALUE_1; 6 | import static de.cotto.bitbook.backend.model.InputFixtures.INPUT_VALUE_2; 7 | 8 | public class InputJpaDtoFixtures { 9 | public static final InputJpaDto INPUT_JPA_DTO_1; 10 | public static final InputJpaDto INPUT_JPA_DTO_2; 11 | 12 | static { 13 | INPUT_JPA_DTO_1 = new InputJpaDto(); 14 | INPUT_JPA_DTO_1.setValue(INPUT_VALUE_1.satoshis()); 15 | INPUT_JPA_DTO_1.setSourceAddress(INPUT_ADDRESS_1.toString()); 16 | 17 | INPUT_JPA_DTO_2 = new InputJpaDto(); 18 | INPUT_JPA_DTO_2.setValue(INPUT_VALUE_2.satoshis()); 19 | INPUT_JPA_DTO_2.setSourceAddress(INPUT_ADDRESS_2.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/transaction/src/testFixtures/java/de/cotto/bitbook/backend/transaction/persistence/OutputJpaDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.backend.transaction.persistence; 2 | 3 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_ADDRESS_1; 4 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_ADDRESS_2; 5 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_VALUE_1; 6 | import static de.cotto.bitbook.backend.model.OutputFixtures.OUTPUT_VALUE_2; 7 | 8 | public class OutputJpaDtoFixtures { 9 | public static final OutputJpaDto OUTPUT_JPA_DTO_1; 10 | public static final OutputJpaDto OUTPUT_JPA_DTO_2; 11 | 12 | static { 13 | OUTPUT_JPA_DTO_1 = new OutputJpaDto(); 14 | OUTPUT_JPA_DTO_1.setValue(OUTPUT_VALUE_1.satoshis()); 15 | OUTPUT_JPA_DTO_1.setTargetAddress(OUTPUT_ADDRESS_1.toString()); 16 | 17 | OUTPUT_JPA_DTO_2 = new OutputJpaDto(); 18 | OUTPUT_JPA_DTO_2.setValue(OUTPUT_VALUE_2.satoshis()); 19 | OUTPUT_JPA_DTO_2.setTargetAddress(OUTPUT_ADDRESS_2.toString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | implementation(platform("de.c-otto.bitbook:platform")) 12 | implementation("de.c-otto:java-conventions:2023.03.22") 13 | implementation("org.springframework.boot:spring-boot-gradle-plugin") 14 | } 15 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/bitbook.java-conventions.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("de.c-otto.java-conventions") 3 | id("org.springframework.boot") 4 | id("io.spring.dependency-management") 5 | id("java-test-fixtures") 6 | } 7 | 8 | dependencies { 9 | implementation(platform("de.c-otto.bitbook:platform")) 10 | testFixturesImplementation(platform("de.c-otto.bitbook:platform")) 11 | implementation("org.springframework.boot:spring-boot-starter") 12 | implementation("org.apache.commons:commons-lang3") 13 | implementation("com.google.guava:guava") 14 | } 15 | 16 | testing { 17 | suites { 18 | named("integrationTest", JvmTestSuite::class).configure { 19 | dependencies { 20 | implementation("com.tngtech.archunit:archunit") 21 | } 22 | } 23 | withType().configureEach { 24 | dependencies { 25 | implementation(project.dependencies.platform("de.c-otto.bitbook:platform")) 26 | implementation("org.springframework.boot:spring-boot-starter-test") 27 | implementation("org.awaitility:awaitility") 28 | } 29 | } 30 | } 31 | } 32 | 33 | configurations.named("testRuntimeOnly") { 34 | exclude(group = "ch.qos.logback", module = "logback-classic") 35 | } 36 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/bitbook.java-library-conventions.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-library") 3 | id("bitbook.java-conventions") 4 | } 5 | 6 | tasks.bootJar { 7 | enabled = false 8 | archiveClassifier.set("boot") 9 | } 10 | tasks.jar { 11 | enabled = true 12 | } 13 | -------------------------------------------------------------------------------- /cli/base/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | api("org.springframework.shell:spring-shell-starter") 7 | implementation(project(":backend")) 8 | implementation(project(":backend:models")) 9 | implementation(project(":backend:transaction")) 10 | implementation(project(":backend:price")) 11 | implementation(project(":ownership")) 12 | testImplementation(testFixtures(project(":backend:transaction"))) 13 | testImplementation(testFixtures(project(":backend:models"))) 14 | } 15 | 16 | tasks.jacocoTestCoverageVerification { 17 | violationRules { 18 | rules.forEach {rule -> 19 | rule.limits.forEach {limit -> 20 | if (limit.counter == "BRANCH") { 21 | limit.minimum = 0.91.toBigDecimal() 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | tasks.jar { 29 | archiveBaseName.set("cli-base") 30 | } 31 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/AbstractAddressCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.AddressDescriptionService; 4 | import de.cotto.bitbook.backend.model.Address; 5 | import de.cotto.bitbook.backend.model.AddressWithDescription; 6 | import de.cotto.bitbook.backend.transaction.AddressCompletionDao; 7 | 8 | import java.util.Set; 9 | import java.util.function.Function; 10 | 11 | public abstract class AbstractAddressCompletionProvider 12 | extends AbstractCompletionProvider { 13 | private final AddressCompletionDao addressCompletionDao; 14 | 15 | public AbstractAddressCompletionProvider( 16 | AddressDescriptionService addressDescriptionService, 17 | AddressCompletionDao addressCompletionDao 18 | ) { 19 | super(addressDescriptionService); 20 | this.addressCompletionDao = addressCompletionDao; 21 | } 22 | 23 | @Override 24 | protected Set>> getStringCompleters() { 25 | return Set.of( 26 | addressCompletionDao::completeFromAddressTransactions, 27 | addressCompletionDao::completeFromInputsAndOutputs 28 | ); 29 | } 30 | 31 | @Override 32 | protected boolean isTooShort(String input, int minimumLengthForCompletion) { 33 | if (input.startsWith("bc1")) { 34 | return input.length() < minimumLengthForCompletion + 3; 35 | } 36 | return input.length() < minimumLengthForCompletion; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/AbstractTransactionCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.TransactionDescriptionService; 4 | import de.cotto.bitbook.backend.model.TransactionHash; 5 | import de.cotto.bitbook.backend.model.TransactionWithDescription; 6 | import de.cotto.bitbook.backend.transaction.TransactionCompletionDao; 7 | 8 | import java.util.Set; 9 | import java.util.function.Function; 10 | 11 | public abstract class AbstractTransactionCompletionProvider 12 | extends AbstractCompletionProvider { 13 | 14 | private final TransactionCompletionDao transactionCompletionDao; 15 | 16 | public AbstractTransactionCompletionProvider( 17 | TransactionCompletionDao transactionCompletionDao, 18 | TransactionDescriptionService transactionDescriptionService 19 | ) { 20 | super(transactionDescriptionService); 21 | this.transactionCompletionDao = transactionCompletionDao; 22 | } 23 | 24 | @Override 25 | protected Set>> getStringCompleters() { 26 | return Set.of( 27 | transactionCompletionDao::completeFromTransactionDetails, 28 | transactionCompletionDao::completeFromAddressTransactionHashes 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/AddressCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.AddressDescriptionService; 4 | import de.cotto.bitbook.backend.model.Address; 5 | import de.cotto.bitbook.backend.model.AddressWithDescription; 6 | import de.cotto.bitbook.backend.transaction.AddressCompletionDao; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Optional; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | @Component 14 | public class AddressCompletionProvider extends AbstractAddressCompletionProvider { 15 | 16 | public AddressCompletionProvider( 17 | AddressDescriptionService addressDescriptionService, 18 | AddressCompletionDao addressCompletionDao 19 | ) { 20 | super(addressDescriptionService, addressCompletionDao); 21 | } 22 | 23 | @Override 24 | protected boolean shouldConsider(AddressWithDescription addressWithDescription) { 25 | return true; 26 | } 27 | 28 | @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 29 | public Optional
completeIfUnique(String addressPrefix) { 30 | Set
proposals = completeUsingStringCompleters(getStringToComplete(addressPrefix)) 31 | .flatMap(Set::stream) 32 | .collect(Collectors.toSet()); 33 | if (proposals.size() == 1) { 34 | return proposals.stream().findFirst(); 35 | } 36 | return Optional.empty(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/AddressConverter.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.util.Optional; 9 | 10 | @Component 11 | public class AddressConverter implements Converter { 12 | private static final String ELLIPSIS = "…"; 13 | private final AddressCompletionProvider addressCompletionProvider; 14 | 15 | public AddressConverter(AddressCompletionProvider addressCompletionProvider) { 16 | this.addressCompletionProvider = addressCompletionProvider; 17 | } 18 | 19 | @Override 20 | public CliAddress convert(@Nonnull String address) { 21 | if (address.endsWith(ELLIPSIS)) { 22 | Optional
completed = addressCompletionProvider.completeIfUnique(address); 23 | if (completed.isPresent()) { 24 | return new CliAddress(completed.get()); 25 | } 26 | } 27 | return new CliAddress(address); 28 | } 29 | } -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/AddressWithDescriptionCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.AddressDescriptionService; 4 | import de.cotto.bitbook.backend.model.AddressWithDescription; 5 | import de.cotto.bitbook.backend.transaction.AddressCompletionDao; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class AddressWithDescriptionCompletionProvider extends AbstractAddressCompletionProvider { 10 | 11 | public AddressWithDescriptionCompletionProvider( 12 | AddressDescriptionService addressDescriptionService, 13 | AddressCompletionDao addressCompletionDao 14 | ) { 15 | super(addressDescriptionService, addressCompletionDao); 16 | } 17 | 18 | @Override 19 | protected boolean shouldConsider(AddressWithDescription addressWithDescription) { 20 | return !addressWithDescription.getDescription().isBlank(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/AddressWithOwnershipCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.AddressDescriptionService; 4 | import de.cotto.bitbook.backend.model.Address; 5 | import de.cotto.bitbook.backend.model.AddressWithDescription; 6 | import de.cotto.bitbook.backend.transaction.AddressCompletionDao; 7 | import de.cotto.bitbook.ownership.AddressOwnershipService; 8 | import de.cotto.bitbook.ownership.OwnershipStatus; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | public class AddressWithOwnershipCompletionProvider extends AbstractAddressCompletionProvider { 13 | 14 | private final AddressOwnershipService addressOwnershipService; 15 | 16 | public AddressWithOwnershipCompletionProvider( 17 | AddressDescriptionService addressDescriptionService, 18 | AddressOwnershipService addressOwnershipService, 19 | AddressCompletionDao addressCompletionDao 20 | ) { 21 | super(addressDescriptionService, addressCompletionDao); 22 | this.addressOwnershipService = addressOwnershipService; 23 | } 24 | 25 | @Override 26 | protected boolean shouldConsider(AddressWithDescription addressWithDescription) { 27 | Address address = addressWithDescription.getAddress(); 28 | return addressOwnershipService.getOwnershipStatus(address) != OwnershipStatus.UNKNOWN; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/CliAddress.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | public class CliAddress extends CliString { 8 | public static final String ERROR_MESSAGE = "Expected base58 or bech32 address"; 9 | 10 | private static final String INVALID_CHARACTERS_REGEX = "[^0-9a-zA-Z]"; 11 | 12 | private static final String BECH_32 = "bc\\d[ac-hj-np-zA-HJ-NP-Z02-9]{6,87}"; 13 | private static final String BASE_58 = "[1-9A-HJ-NP-Za-km-z]{20,35}"; 14 | private static final String CASH_ADDR = "[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{6,}"; 15 | private static final Pattern PATTERN = Pattern.compile(BECH_32 + "|" + BASE_58 + "|" + CASH_ADDR); 16 | private static final String BITCOIN_CASH_PREFIX = "bitcoincash:"; 17 | 18 | public CliAddress(Address address) { 19 | this(address.toString()); 20 | } 21 | 22 | public CliAddress(String address) { 23 | super(getWithoutBitcoinCashPrefix(address), PATTERN, INVALID_CHARACTERS_REGEX); 24 | } 25 | 26 | private static String getWithoutBitcoinCashPrefix(String address) { 27 | if (address.startsWith(BITCOIN_CASH_PREFIX)) { 28 | return address.substring(BITCOIN_CASH_PREFIX.length()); 29 | } 30 | return address; 31 | } 32 | 33 | public Address getAddress() { 34 | return new Address(toString()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/CliTransactionHash.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | public class CliTransactionHash extends CliString { 8 | public static final String ERROR_MESSAGE = "Expected: 64 hex characters"; 9 | 10 | private static final Pattern PATTERN = Pattern.compile("[\\da-fA-F]{64}"); 11 | private static final String INVALID_CHARACTERS_REGEX = "[^0-9a-fA-F]"; 12 | 13 | public CliTransactionHash(String transactionHash) { 14 | super(transactionHash, PATTERN, INVALID_CHARACTERS_REGEX); 15 | } 16 | 17 | public CliTransactionHash(TransactionHash transactionHash) { 18 | this(transactionHash.toString()); 19 | } 20 | 21 | public TransactionHash getTransactionHash() { 22 | return new TransactionHash(toString()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/PriceFormatter.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Coins; 4 | import de.cotto.bitbook.backend.price.model.Price; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.math.BigDecimal; 9 | import java.math.RoundingMode; 10 | import java.text.DecimalFormat; 11 | import java.text.DecimalFormatSymbols; 12 | import java.util.Locale; 13 | 14 | @Component 15 | public class PriceFormatter { 16 | private static final int MINIMUM_LENGTH = 13; 17 | 18 | private final DecimalFormat decimalFormat; 19 | 20 | public PriceFormatter() { 21 | DecimalFormatSymbols decimalFormatSymbols = DecimalFormatSymbols.getInstance(new Locale("en", "US")); 22 | decimalFormat = new DecimalFormat("#,##0.00", decimalFormatSymbols); 23 | } 24 | 25 | public String format(Coins coins, Price price) { 26 | if (price.equals(Price.UNKNOWN)) { 27 | return " Price unknown"; 28 | } 29 | BigDecimal valueAtPrice = BigDecimal.valueOf(coins.satoshis()) 30 | .multiply(price.getAsBigDecimal()) 31 | .divide(Coins.SATOSHIS_IN_COIN, RoundingMode.HALF_UP) 32 | .setScale(2, RoundingMode.HALF_UP); 33 | String formatted = decimalFormat.format(valueAtPrice); 34 | return StringUtils.leftPad(formatted, MINIMUM_LENGTH) + "€"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/SelectedChain.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import org.springframework.stereotype.Component; 5 | 6 | import static de.cotto.bitbook.backend.model.Chain.BTC; 7 | 8 | @Component 9 | public class SelectedChain { 10 | private Chain chain; 11 | 12 | public SelectedChain() { 13 | chain = BTC; 14 | } 15 | 16 | public Chain getChain() { 17 | return chain; 18 | } 19 | 20 | public void selectChain(Chain chain) { 21 | this.chain = chain; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/TransactionHashCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.TransactionDescriptionService; 4 | import de.cotto.bitbook.backend.model.TransactionWithDescription; 5 | import de.cotto.bitbook.backend.transaction.TransactionCompletionDao; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class TransactionHashCompletionProvider extends AbstractTransactionCompletionProvider { 10 | 11 | public TransactionHashCompletionProvider( 12 | TransactionCompletionDao transactionCompletionDao, 13 | TransactionDescriptionService transactionDescriptionService 14 | ) { 15 | super(transactionCompletionDao, transactionDescriptionService); 16 | } 17 | 18 | @Override 19 | protected boolean shouldConsider(TransactionWithDescription transactionWithDescription) { 20 | return true; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/TransactionHashConverter.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.stereotype.Component; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | @Component 9 | public class TransactionHashConverter implements Converter { 10 | public TransactionHashConverter() { 11 | // default constructor 12 | } 13 | 14 | @Override 15 | public CliTransactionHash convert(@Nonnull String transactionHash) { 16 | return new CliTransactionHash(transactionHash); 17 | } 18 | } -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/TransactionSortOrder.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | public enum TransactionSortOrder { 4 | BY_HASH, 5 | 6 | BY_COINS_THEN_HASH, 7 | BY_COINS_THEN_DATE_THEN_HASH, 8 | 9 | BY_COINS_ABSOLUTE_THEN_HASH, 10 | BY_COINS_ABSOLUTE_THEN_DATE_THEN_HASH, 11 | 12 | BY_DATE_THEN_COINS_THEN_HASH, 13 | BY_DATE_THEN_COINS_ABSOLUTE_THEN_HASH, 14 | 15 | BY_DATE_THEN_HASH 16 | } 17 | -------------------------------------------------------------------------------- /cli/base/src/main/java/de/cotto/bitbook/cli/TransactionWithDescriptionCompletionProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.TransactionDescriptionService; 4 | import de.cotto.bitbook.backend.model.TransactionWithDescription; 5 | import de.cotto.bitbook.backend.transaction.TransactionCompletionDao; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class TransactionWithDescriptionCompletionProvider extends AbstractTransactionCompletionProvider { 10 | public TransactionWithDescriptionCompletionProvider( 11 | TransactionCompletionDao transactionCompletionDao, 12 | TransactionDescriptionService transactionDescriptionService 13 | ) { 14 | super(transactionCompletionDao, transactionDescriptionService); 15 | } 16 | 17 | @Override 18 | protected boolean shouldConsider(TransactionWithDescription transactionWithDescription) { 19 | return !transactionWithDescription.getDescription().isBlank(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cli/base/src/test/java/de/cotto/bitbook/cli/AddressConverterTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.extension.ExtendWith; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.junit.jupiter.MockitoExtension; 9 | 10 | import java.util.Optional; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.mockito.ArgumentMatchers.anyString; 14 | import static org.mockito.Mockito.never; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class AddressConverterTest { 20 | @Mock 21 | private AddressCompletionProvider addressCompletionProvider; 22 | 23 | @InjectMocks 24 | private AddressConverter addressConverter; 25 | 26 | @Test 27 | void convert() { 28 | assertThat(addressConverter.convert("x")).isEqualTo(new CliAddress("x")); 29 | verify(addressCompletionProvider, never()).completeIfUnique(anyString()); 30 | } 31 | 32 | @Test 33 | void autocompletes_if_ends_in_ellipsis() { 34 | when(addressCompletionProvider.completeIfUnique("x…")).thenReturn(Optional.of(new Address("xyz"))); 35 | assertThat(addressConverter.convert("x…")).isEqualTo(new CliAddress("xyz")); 36 | } 37 | 38 | @Test 39 | void tries_to_autocompletes_but_not_found() { 40 | assertThat(addressConverter.convert("x…")).isEqualTo(new CliAddress("x…")); 41 | } 42 | } -------------------------------------------------------------------------------- /cli/base/src/test/java/de/cotto/bitbook/cli/SelectedChainTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.InjectMocks; 6 | import org.mockito.junit.jupiter.MockitoExtension; 7 | 8 | import static de.cotto.bitbook.backend.model.Chain.BCH; 9 | import static de.cotto.bitbook.backend.model.Chain.BTC; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | @ExtendWith(MockitoExtension.class) 13 | class SelectedChainTest { 14 | @InjectMocks 15 | private SelectedChain selectedChain; 16 | 17 | @Test 18 | void initially_btc() { 19 | assertThat(selectedChain.getChain()).isEqualTo(BTC); 20 | } 21 | 22 | @Test 23 | void selectChain_bch() { 24 | selectedChain.selectChain(BCH); 25 | assertThat(selectedChain.getChain()).isEqualTo(BCH); 26 | } 27 | } -------------------------------------------------------------------------------- /cli/base/src/test/java/de/cotto/bitbook/cli/TransactionHashConverterTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class TransactionHashConverterTest { 8 | @Test 9 | void convert() { 10 | assertThat(new TransactionHashConverter().convert("x")).isEqualTo(new CliTransactionHash("x")); 11 | } 12 | } -------------------------------------------------------------------------------- /cli/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:price")) 9 | implementation(project(":backend:transaction")) 10 | implementation(project(":cli:base")) 11 | runtimeOnly(project(":backend:provider:all")) 12 | runtimeOnly(project(":cli:lnd")) 13 | runtimeOnly(project(":cli:ownership")) 14 | runtimeOnly("org.flywaydb:flyway-core") 15 | integrationTestImplementation(project(":cli:ownership")) 16 | testImplementation(testFixtures(project(":backend:models"))) 17 | integrationTestImplementation("org.springframework.boot:spring-boot-starter-data-jpa") 18 | } 19 | 20 | tasks.bootJar { 21 | archiveClassifier.set("boot") 22 | } 23 | tasks.bootRun { 24 | standardInput = System.`in` 25 | } 26 | -------------------------------------------------------------------------------- /cli/lnd/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":cli:base")) 7 | implementation(project(":lnd")) 8 | } 9 | 10 | tasks.jar { 11 | archiveBaseName.set("lnd-cli") 12 | } 13 | -------------------------------------------------------------------------------- /cli/lnd/src/main/java/de/cotto/bitbook/lnd/cli/PoolCommands.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.cli; 2 | 3 | import de.cotto.bitbook.lnd.PoolService; 4 | import org.springframework.shell.standard.ShellComponent; 5 | import org.springframework.shell.standard.ShellMethod; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | 12 | @ShellComponent 13 | public class PoolCommands { 14 | private final PoolService poolService; 15 | 16 | public PoolCommands(PoolService poolService) { 17 | this.poolService = poolService; 18 | } 19 | 20 | @ShellMethod("Add information from pool leases information obtained by `pool auction leases`") 21 | public String poolAddFromLeases(File jsonFile) throws IOException { 22 | long numberOfLeases = poolService.addFromLeases(readFile(jsonFile)); 23 | if (numberOfLeases == 0) { 24 | return "Unable to find leases in file"; 25 | } 26 | return "Added information for " + numberOfLeases + " leases"; 27 | } 28 | 29 | private String readFile(File jsonFile) throws IOException { 30 | return Files.readString(jsonFile.toPath(), StandardCharsets.US_ASCII); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cli/lnd/src/test/java/de/cotto/bitbook/lnd/cli/TempFileUtil.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.cli; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | 7 | public final class TempFileUtil { 8 | 9 | private TempFileUtil() { 10 | // utility class 11 | } 12 | 13 | public static File createTempFileWithContent(String content) throws IOException { 14 | File file = createTempFile(); 15 | Files.writeString(file.toPath(), content); 16 | return file; 17 | } 18 | 19 | public static File createTempFile() throws IOException { 20 | return File.createTempFile("temp", "bitbook"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cli/ownership/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend:models")) 7 | implementation(project(":backend:transaction")) 8 | implementation(project(":backend:price")) 9 | implementation(project(":ownership")) 10 | implementation(project(":cli:base")) 11 | testImplementation(testFixtures(project(":backend:transaction"))) 12 | testImplementation(testFixtures(project(":backend:models"))) 13 | } 14 | 15 | tasks.jar { 16 | archiveBaseName.set("ownership-cli") 17 | } 18 | -------------------------------------------------------------------------------- /cli/src/integrationTest/java/de/cotto/bitbook/BitBookApplicationIT.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook; 2 | 3 | import de.cotto.bitbook.ownership.cli.OwnershipCommands; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | @SpringBootTest 11 | class BitBookApplicationIT { 12 | @Autowired 13 | private OwnershipCommands ownershipCommands; 14 | 15 | @Test 16 | void contextLoads() { 17 | assertThat(ownershipCommands).isNotNull(); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cli/src/integrationTest/java/de/cotto/bitbook/TestApplicationRunner.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook; 2 | 3 | import org.springframework.boot.ApplicationArguments; 4 | import org.springframework.boot.ApplicationRunner; 5 | import org.springframework.boot.test.context.TestConfiguration; 6 | 7 | @TestConfiguration 8 | public class TestApplicationRunner implements ApplicationRunner { 9 | 10 | public TestApplicationRunner() { 11 | // default constructor 12 | } 13 | 14 | @Override 15 | public void run(ApplicationArguments args) { 16 | // empty to avoid issues in tests 17 | } 18 | } -------------------------------------------------------------------------------- /cli/src/integrationTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.shell.script.spring.shell.script=false 2 | spring.shell.interactive.enabled=false 3 | spring.main.allow-circular-references=true 4 | logging.level.root=warn 5 | -------------------------------------------------------------------------------- /cli/src/main/java/de/cotto/bitbook/BitBookApplication.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class BitBookApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(BitBookApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /cli/src/main/java/de/cotto/bitbook/cli/ChainCommand.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import org.springframework.shell.standard.ShellComponent; 5 | import org.springframework.shell.standard.ShellMethod; 6 | 7 | @ShellComponent 8 | public class ChainCommand { 9 | private final SelectedChain selectedChain; 10 | 11 | public ChainCommand(SelectedChain selectedChain) { 12 | this.selectedChain = selectedChain; 13 | } 14 | 15 | @ShellMethod("Use the given chain for future commands") 16 | public void selectChain(Chain chain) { 17 | selectedChain.selectChain(chain); 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /cli/src/main/java/de/cotto/bitbook/cli/CustomPromptProvider.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import de.cotto.bitbook.backend.model.Chain; 4 | import org.jline.utils.AttributedString; 5 | import org.jline.utils.AttributedStyle; 6 | import org.springframework.shell.jline.PromptProvider; 7 | import org.springframework.stereotype.Component; 8 | 9 | import static de.cotto.bitbook.backend.model.Chain.BTC; 10 | import static org.jline.utils.AttributedStyle.BOLD; 11 | import static org.jline.utils.AttributedStyle.DEFAULT; 12 | 13 | @Component 14 | public class CustomPromptProvider implements PromptProvider { 15 | 16 | private final SelectedChain selectedChain; 17 | 18 | public CustomPromptProvider(SelectedChain selectedChain) { 19 | this.selectedChain = selectedChain; 20 | } 21 | 22 | @Override 23 | public AttributedString getPrompt() { 24 | Chain chain = selectedChain.getChain(); 25 | AttributedString prefix = new AttributedString("BitBook", DEFAULT.foreground(AttributedStyle.YELLOW)); 26 | AttributedString middle; 27 | if (chain == BTC) { 28 | middle = AttributedString.EMPTY; 29 | } else { 30 | middle = new AttributedString("(%s)".formatted(chain.name()), BOLD.foreground(AttributedStyle.CYAN)); 31 | } 32 | AttributedString suffix = new AttributedString("₿ ", DEFAULT.foreground(AttributedStyle.YELLOW)); 33 | return AttributedString.join(AttributedString.EMPTY, prefix, middle, suffix); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /cli/src/main/java/de/cotto/bitbook/cli/History.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.jline.reader.LineReader; 4 | import org.jline.reader.impl.history.DefaultHistory; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.IOException; 11 | import java.nio.file.Paths; 12 | 13 | @Component 14 | public class History extends DefaultHistory { 15 | 16 | private final Logger logger = LoggerFactory.getLogger(getClass()); 17 | 18 | public History(LineReader lineReader, @Value("${spring.application.name:spring-shell}.log") String historyPath) { 19 | super(getLineReaderWithSetPath(lineReader, historyPath)); 20 | } 21 | 22 | private static LineReader getLineReaderWithSetPath(LineReader lineReader, String historyPath) { 23 | lineReader.setVariable(LineReader.HISTORY_FILE, Paths.get(historyPath)); 24 | return lineReader; 25 | } 26 | 27 | public void close() { 28 | try { 29 | save(); 30 | } catch (IOException e) { 31 | logger.warn("Unable to save history", e); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cli/src/main/java/de/cotto/bitbook/cli/QuitCommand.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Lazy; 5 | import org.springframework.scheduling.concurrent.ExecutorConfigurationSupport; 6 | import org.springframework.shell.ExitRequest; 7 | import org.springframework.shell.standard.ShellComponent; 8 | import org.springframework.shell.standard.ShellMethod; 9 | import org.springframework.shell.standard.commands.Quit; 10 | 11 | import java.util.Set; 12 | 13 | @ShellComponent 14 | public class QuitCommand implements Quit.Command { 15 | 16 | private final Set executors; 17 | 18 | @Lazy 19 | @Autowired 20 | @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") 21 | private History history; 22 | 23 | public QuitCommand(Set executors) { 24 | // default constructor 25 | this.executors = executors; 26 | } 27 | 28 | @ShellMethod(value = "Exit the shell.", key = {"quit", "exit"}) 29 | public void quit() { 30 | executors.forEach(ExecutorConfigurationSupport::shutdown); 31 | history.close(); 32 | throw new ExitRequest(); 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /cli/src/main/resources/db/migration/V1_0_1__prices_with_chain.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE prices RENAME TO prices_old; 2 | CREATE TABLE prices 3 | ( 4 | date DATE NOT NULL, 5 | price DECIMAL(16, 8), 6 | chain VARCHAR(255) NOT NULL, 7 | PRIMARY KEY(chain, date) 8 | ); 9 | INSERT INTO prices (date, price, chain) SELECT date, price, 'BTC' FROM prices_old; 10 | DROP TABLE prices_old; -------------------------------------------------------------------------------- /cli/src/test/java/de/cotto/bitbook/cli/ChainCommandTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.InjectMocks; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static de.cotto.bitbook.backend.model.Chain.BTC; 10 | import static de.cotto.bitbook.backend.model.Chain.BTG; 11 | import static org.mockito.Mockito.verify; 12 | 13 | @ExtendWith(MockitoExtension.class) 14 | class ChainCommandTest { 15 | @InjectMocks 16 | private ChainCommand chainCommand; 17 | 18 | @Mock 19 | private SelectedChain selectedChain; 20 | 21 | @Test 22 | void selectChain_btc() { 23 | chainCommand.selectChain(BTC); 24 | verify(selectedChain).selectChain(BTC); 25 | } 26 | 27 | @Test 28 | void selectChain_btg() { 29 | chainCommand.selectChain(BTG); 30 | verify(selectedChain).selectChain(BTG); 31 | } 32 | } -------------------------------------------------------------------------------- /cli/src/test/java/de/cotto/bitbook/cli/CustomPromptProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.mockito.InjectMocks; 6 | import org.mockito.Mock; 7 | import org.mockito.junit.jupiter.MockitoExtension; 8 | 9 | import static de.cotto.bitbook.backend.model.Chain.BCH; 10 | import static de.cotto.bitbook.backend.model.Chain.BTC; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.mockito.Mockito.when; 13 | 14 | @ExtendWith(MockitoExtension.class) 15 | class CustomPromptProviderTest { 16 | 17 | @InjectMocks 18 | private CustomPromptProvider customPromptProvider; 19 | 20 | @Mock 21 | private SelectedChain selectedChain; 22 | 23 | @Test 24 | void customPrompt_text_btc() { 25 | when(selectedChain.getChain()).thenReturn(BTC); 26 | assertThat(customPromptProvider.getPrompt()).hasToString("BitBook₿ "); 27 | } 28 | 29 | @Test 30 | void customPrompt_text_bch() { 31 | when(selectedChain.getChain()).thenReturn(BCH); 32 | assertThat(customPromptProvider.getPrompt()).hasToString("BitBook(BCH)₿ "); 33 | } 34 | } -------------------------------------------------------------------------------- /cli/src/test/java/de/cotto/bitbook/cli/HistoryTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.cli; 2 | 3 | import org.jline.reader.LineReader; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.io.TempDir; 7 | 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.Map; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.assertj.core.api.Assertions.assertThatCode; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.times; 16 | import static org.mockito.Mockito.verify; 17 | import static org.mockito.Mockito.when; 18 | 19 | class HistoryTest { 20 | 21 | private final LineReader lineReader = mock(LineReader.class); 22 | private History historyPath; 23 | 24 | @BeforeEach 25 | void setUp() { 26 | historyPath = new History(lineReader, "historyPath"); 27 | } 28 | 29 | @Test 30 | void setsHistoryFile() { 31 | verify(lineReader).setVariable("history-file", Paths.get("historyPath")); 32 | } 33 | 34 | @Test 35 | void closeInteractsWithLineReader() { 36 | historyPath.close(); 37 | verify(lineReader, times(2)).getVariables(); 38 | } 39 | 40 | @Test 41 | void ignoresIoException(@TempDir Path tempDir) { 42 | assertThat(tempDir.toFile().setWritable(false)).isTrue(); 43 | when(lineReader.getVariables()).thenReturn(Map.of("history-file", tempDir)); 44 | assertThatCode(() -> historyPath.close()).doesNotThrowAnyException(); 45 | } 46 | } -------------------------------------------------------------------------------- /documentation/bitbook.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Otto/BitBook/db9b4880404a405f72ca407c92c83508320b2f3d/documentation/bitbook.gif -------------------------------------------------------------------------------- /documentation/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to BitBook 2 | I'd like you to be happy using BitBook and as such I'd like to know what's missing. 3 | Please have a look at [Feature requests and bug reports](features_and_bugs.md) to provide feedback! 4 | 5 | Furthermore, feel free to fix/change the code according to your needs, or comment on the code. 6 | Please also have a look at [Technical Aspects](technical.md). 7 | 8 | ## Donations 9 | If you want to, you may send Bitcoin donations, preferably via the lightning network. 10 | * Lightning network via keysend: `027ce055380348d7812d2ae7745701c9f93e70c1adeb2657f053f91df4f2843c71` 11 | * Lightning network by opening a channel: `027ce055380348d7812d2ae7745701c9f93e70c1adeb2657f053f91df4f2843c71@157.90.112.145:9735` 12 | * Lightning network via invoice: please contact me via [mail](mailto:bitcoin@c-otto.de) 13 | * On-chain transaction: `bc1qwgk42zlrfwzc8e7uq8xd3edw40rrlqpnuv455k` ![QR-Code](qr.png) -------------------------------------------------------------------------------- /documentation/features_and_bugs.md: -------------------------------------------------------------------------------- 1 | # Feature Requests and Bug Reports 2 | BitBook doesn't have a lot of features, and several apparent flaws. 3 | Please have a look at the existing issues, leave feedback, and create issues yourself! 4 | I need to know how YOU think about BitBook, so that I can help create the product you need 5 | or want to use. 6 | 7 | Please have a look at the [list of known issues](https://github.com/C-Otto/BitBook/issues). 8 | 9 | ## Upvote Issues 10 | If you have a GitHub account, please make use of the emoji reactions which you can find in the top 11 | right corner of each issue (and comment). If you leave a "thumbs up", I know that you're 12 | interested in the feature/fix! 13 | 14 | ![emoji buttons](thumbsup.png) 15 | 16 | ## Create Issue 17 | If you found something that didn't work as expected, or if you'd like to see something that doesn't exist, yet: please 18 | create a new GitHub issue! 19 | 20 | [Create a new issue](https://github.com/C-Otto/BitBook/issues/new/choose) -------------------------------------------------------------------------------- /documentation/ideas.md: -------------------------------------------------------------------------------- 1 | # Future Ideas 2 | ## Interactive CLI 3 | Running `get-neighbour-transactions` and the other commands repeatedly is quite frustrating. 4 | BitBook should offer an interactive mode that guides the user to completion. -------------------------------------------------------------------------------- /documentation/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Otto/BitBook/db9b4880404a405f72ca407c92c83508320b2f3d/documentation/qr.png -------------------------------------------------------------------------------- /documentation/thumbsup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Otto/BitBook/db9b4880404a405f72ca407c92c83508320b2f3d/documentation/thumbsup.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.caching=true 3 | org.gradle.jvmargs=-Xmx2G 4 | -------------------------------------------------------------------------------- /gradle/meta-plugins/platform/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-platform` 3 | } 4 | 5 | group = "de.c-otto.bitbook" 6 | 7 | javaPlatform { 8 | allowDependencies() 9 | } 10 | 11 | dependencies { 12 | val springBootVersion = "2.6.15" 13 | 14 | api(platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion")) 15 | api(platform("org.springframework.cloud:spring-cloud-dependencies:2021.0.5")) 16 | 17 | constraints { 18 | api("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion") 19 | api("io.vavr:vavr:0.10.4") 20 | api("org.apache.mina:mina-core:2.2.1") 21 | api("org.springframework.shell:spring-shell-starter:2.0.0.RELEASE") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gradle/meta-plugins/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | includeBuild("platform/") 3 | } 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Otto/BitBook/db9b4880404a405f72ca407c92c83508320b2f3d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /lnd/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:transaction")) 9 | implementation(project(":ownership")) 10 | implementation("com.fasterxml.jackson.core:jackson-databind") 11 | testImplementation(testFixtures(project(":backend:models"))) 12 | testFixturesImplementation(testFixtures(project(":backend:models"))) 13 | } 14 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/AbstractJsonService.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | 7 | import java.io.IOException; 8 | import java.util.Set; 9 | import java.util.function.Function; 10 | 11 | public class AbstractJsonService { 12 | private final ObjectMapper objectMapper; 13 | 14 | public AbstractJsonService(ObjectMapper objectMapper) { 15 | this.objectMapper = objectMapper; 16 | } 17 | 18 | protected Set parse(String json, Function> parseFunction) { 19 | try (JsonParser parser = objectMapper.createParser(json)) { 20 | JsonNode rootNode = parser.getCodec().readTree(parser); 21 | if (rootNode != null) { 22 | return parseFunction.apply(rootNode); 23 | } 24 | } catch (IOException e) { 25 | return Set.of(); 26 | } 27 | return Set.of(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/ChannelPointParser.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | 5 | public final class ChannelPointParser { 6 | private ChannelPointParser() { 7 | // utility class 8 | } 9 | 10 | public static TransactionHash getTransactionHash(String channelPoint) { 11 | return new TransactionHash(channelPoint.substring(0, channelPoint.indexOf(':'))); 12 | } 13 | 14 | public static int getOutputIndex(String channelPoint) { 15 | return Integer.parseInt(channelPoint.substring(channelPoint.indexOf(':') + 1)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/features/UnspentOutputsService.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.features; 2 | 3 | import de.cotto.bitbook.backend.AddressDescriptionService; 4 | import de.cotto.bitbook.backend.model.Address; 5 | import de.cotto.bitbook.ownership.AddressOwnershipService; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Set; 9 | 10 | import static de.cotto.bitbook.backend.model.Chain.BTC; 11 | 12 | @Component 13 | public class UnspentOutputsService { 14 | private static final String DEFAULT_ADDRESS_DESCRIPTION = "lnd"; 15 | 16 | private final AddressOwnershipService addressOwnershipService; 17 | private final AddressDescriptionService addressDescriptionService; 18 | 19 | public UnspentOutputsService( 20 | AddressOwnershipService addressOwnershipService, 21 | AddressDescriptionService addressDescriptionService 22 | ) { 23 | this.addressOwnershipService = addressOwnershipService; 24 | this.addressDescriptionService = addressDescriptionService; 25 | } 26 | 27 | public long addFromUnspentOutputs(Set
addresses) { 28 | addresses.forEach(address -> addressOwnershipService.setAddressAsOwned(address, BTC)); 29 | addresses.forEach(address -> addressDescriptionService.set(address, DEFAULT_ADDRESS_DESCRIPTION)); 30 | return addresses.size(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/model/CloseType.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | public enum CloseType { 4 | COOPERATIVE("cooperative"), 5 | COOPERATIVE_REMOTE("cooperative remote"), 6 | COOPERATIVE_LOCAL("cooperative local"), 7 | FORCE_REMOTE("force remote"), 8 | FORCE_LOCAL("force local"); 9 | 10 | private final String stringRepresentation; 11 | 12 | CloseType(String stringRepresentation) { 13 | this.stringRepresentation = stringRepresentation; 14 | } 15 | 16 | public static CloseType fromStringAndInitiator(String closeType, String closeInitiatorString) { 17 | if ("REMOTE_FORCE_CLOSE".equals(closeType)) { 18 | return FORCE_REMOTE; 19 | } 20 | if ("LOCAL_FORCE_CLOSE".equals(closeType)) { 21 | return FORCE_LOCAL; 22 | } 23 | Initiator closeInitiator = Initiator.fromString(closeInitiatorString); 24 | if (closeInitiator.equals(Initiator.REMOTE)) { 25 | return COOPERATIVE_REMOTE; 26 | } 27 | if (closeInitiator.equals(Initiator.LOCAL)) { 28 | return COOPERATIVE_LOCAL; 29 | } 30 | return COOPERATIVE; 31 | } 32 | 33 | public boolean isCooperative() { 34 | return this == COOPERATIVE || this == COOPERATIVE_LOCAL || this == COOPERATIVE_REMOTE; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return stringRepresentation; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/model/Initiator.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | import java.util.Locale; 4 | 5 | public enum Initiator { 6 | LOCAL, REMOTE, UNKNOWN; 7 | 8 | public static Initiator fromString(String string) { 9 | if ("INITIATOR_LOCAL".equals(string)) { 10 | return LOCAL; 11 | } else if ("INITIATOR_REMOTE".equals(string)) { 12 | return REMOTE; 13 | } 14 | return UNKNOWN; 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return name().toLowerCase(Locale.US); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/model/OnchainTransaction.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | import de.cotto.bitbook.backend.model.Coins; 5 | import de.cotto.bitbook.backend.model.TransactionHash; 6 | 7 | import java.util.Set; 8 | 9 | public record OnchainTransaction( 10 | TransactionHash transactionHash, 11 | String label, 12 | Coins amount, 13 | Coins fees, 14 | Set
ownedAddresses 15 | ) { 16 | 17 | public OnchainTransaction(TransactionHash transactionHash, String label, Coins amount, Coins fees) { 18 | this(transactionHash, label, amount, fees, Set.of()); 19 | } 20 | 21 | public boolean hasLabel() { 22 | return !label.isBlank(); 23 | } 24 | 25 | public boolean hasFees() { 26 | return fees.isPositive(); 27 | } 28 | 29 | public Coins getAbsoluteAmountWithoutFees() { 30 | return amount.add(fees).absolute(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lnd/src/main/java/de/cotto/bitbook/lnd/model/Resolution.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | 5 | public record Resolution( 6 | TransactionHash sweepTransactionHash, 7 | String resolutionType, 8 | String outcome 9 | ) { 10 | private static final String OUTGOING_HTLC = "OUTGOING_HTLC"; 11 | private static final String INCOMING_HTLC = "INCOMING_HTLC"; 12 | private static final String TIMEOUT = "TIMEOUT"; 13 | private static final String CLAIMED = "CLAIMED"; 14 | 15 | public boolean sweepTransactionClaimsFunds() { 16 | if (OUTGOING_HTLC.equals(resolutionType) && CLAIMED.equals(outcome)) { 17 | return false; 18 | } 19 | return !(INCOMING_HTLC.equals(resolutionType) && TIMEOUT.equals(outcome)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lnd/src/test/java/de/cotto/bitbook/lnd/ChannelPointParserTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd; 2 | 3 | import de.cotto.bitbook.backend.model.TransactionHash; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class ChannelPointParserTest { 9 | 10 | @Test 11 | void getTransactionHash() { 12 | assertThat(ChannelPointParser.getTransactionHash("abc:123")).isEqualTo(new TransactionHash("abc")); 13 | } 14 | 15 | @Test 16 | void getOutputIndex() { 17 | assertThat(ChannelPointParser.getOutputIndex("abc:123")).isEqualTo(123); 18 | } 19 | } -------------------------------------------------------------------------------- /lnd/src/test/java/de/cotto/bitbook/lnd/model/InitiatorTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class InitiatorTest { 8 | @Test 9 | void testToString_remote() { 10 | assertThat(Initiator.REMOTE).hasToString("remote"); 11 | } 12 | 13 | @Test 14 | void testToString_local() { 15 | assertThat(Initiator.LOCAL).hasToString("local"); 16 | } 17 | 18 | @Test 19 | void testToString_unknown() { 20 | assertThat(Initiator.UNKNOWN).hasToString("unknown"); 21 | } 22 | 23 | @Test 24 | void fromString_local() { 25 | assertThat(Initiator.fromString("INITIATOR_LOCAL")).isEqualTo(Initiator.LOCAL); 26 | } 27 | 28 | @Test 29 | void fromString_remote() { 30 | assertThat(Initiator.fromString("INITIATOR_REMOTE")).isEqualTo(Initiator.REMOTE); 31 | } 32 | 33 | @Test 34 | void fromString_unknown() { 35 | assertThat(Initiator.fromString("INITIATOR_UNKNOWN")).isEqualTo(Initiator.UNKNOWN); 36 | } 37 | } -------------------------------------------------------------------------------- /lnd/src/test/java/de/cotto/bitbook/lnd/model/PoolLeaseTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | import de.cotto.bitbook.backend.model.Coins; 4 | import nl.jqno.equalsverifier.EqualsVerifier; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 8 | import static de.cotto.bitbook.lnd.model.PoolLeaseFixtures.POOL_LEASE; 9 | import static de.cotto.bitbook.lnd.model.PoolLeaseFixtures.POOL_LEASE_2; 10 | import static de.cotto.bitbook.lnd.model.PoolLeaseFixtures.PUBKEY; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | class PoolLeaseTest { 14 | @Test 15 | void getTransactionHash() { 16 | assertThat(POOL_LEASE.getTransactionHash()).isEqualTo(TRANSACTION_HASH); 17 | } 18 | 19 | @Test 20 | void getOutputIndex() { 21 | assertThat(POOL_LEASE_2.getOutputIndex()).isEqualTo(1); 22 | } 23 | 24 | @Test 25 | void getPubKey() { 26 | assertThat(POOL_LEASE.getPubKey()).isEqualTo(PUBKEY); 27 | } 28 | 29 | @Test 30 | void testEquals() { 31 | EqualsVerifier.forClass(PoolLease.class).usingGetClass().verify(); 32 | } 33 | 34 | @Test 35 | void getPremiumWithoutFees() { 36 | assertThat(POOL_LEASE.getPremiumWithoutFees()).isEqualTo(Coins.ofSatoshis(1500 - 150 - 114)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lnd/src/testFixtures/java/de/cotto/bitbook/lnd/model/PoolLeaseFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.lnd.model; 2 | 3 | import de.cotto.bitbook.backend.model.Coins; 4 | 5 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH; 6 | import static de.cotto.bitbook.backend.model.TransactionHashFixtures.TRANSACTION_HASH_2; 7 | 8 | public class PoolLeaseFixtures { 9 | public static final String PUBKEY = "pubkey"; 10 | public static final PoolLease POOL_LEASE = new PoolLease( 11 | TRANSACTION_HASH, 12 | 0, 13 | PUBKEY, 14 | Coins.ofSatoshis(1500), 15 | Coins.ofSatoshis(150), 16 | Coins.ofSatoshis(114) 17 | ); 18 | public static final PoolLease POOL_LEASE_2 = new PoolLease( 19 | TRANSACTION_HASH_2, 20 | 1, 21 | PUBKEY, 22 | Coins.ofSatoshis(1000), 23 | Coins.ofSatoshis(200), 24 | Coins.ofSatoshis(50) 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /ownership/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("bitbook.java-library-conventions") 3 | } 4 | 5 | dependencies { 6 | implementation(project(":backend")) 7 | implementation(project(":backend:models")) 8 | implementation(project(":backend:transaction")) 9 | testFixturesImplementation("javax.persistence:javax.persistence-api") 10 | testImplementation(testFixtures(project(":backend:models"))) 11 | testImplementation(testFixtures(project(":backend:transaction"))) 12 | integrationTestImplementation(project(":backend")) 13 | integrationTestImplementation(project(":backend:models")) 14 | integrationTestImplementation(project(":backend:transaction")) 15 | } 16 | -------------------------------------------------------------------------------- /ownership/src/integrationTest/java/de/cotto/bitbook/SpringBootConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class SpringBootConfiguration { 7 | public SpringBootConfiguration() { 8 | // default constructor 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ownership/src/main/java/de/cotto/bitbook/ownership/AddressOwnershipDao.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership; 2 | 3 | import de.cotto.bitbook.backend.model.Address; 4 | 5 | import java.util.Set; 6 | 7 | public interface AddressOwnershipDao { 8 | Set
getOwnedAddresses(); 9 | 10 | Set
getForeignAddresses(); 11 | 12 | void setAddressAsOwned(Address address); 13 | 14 | void setAddressAsForeign(Address address); 15 | 16 | void remove(Address address); 17 | 18 | OwnershipStatus getOwnershipStatus(Address address); 19 | } 20 | -------------------------------------------------------------------------------- /ownership/src/main/java/de/cotto/bitbook/ownership/OwnershipStatus.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership; 2 | 3 | public enum OwnershipStatus { 4 | UNKNOWN, OWNED, FOREIGN 5 | } 6 | -------------------------------------------------------------------------------- /ownership/src/main/java/de/cotto/bitbook/ownership/persistence/AddressOwnershipJpaDto.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership.persistence; 2 | 3 | import de.cotto.bitbook.ownership.OwnershipStatus; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | import javax.persistence.Entity; 8 | import javax.persistence.EnumType; 9 | import javax.persistence.Enumerated; 10 | import javax.persistence.Id; 11 | import javax.persistence.Table; 12 | 13 | import static java.util.Objects.requireNonNull; 14 | 15 | @Entity 16 | @Table(name = "AddressOwnership") 17 | public class AddressOwnershipJpaDto { 18 | @Id 19 | @Nullable 20 | private String address; 21 | 22 | @Nullable 23 | @Enumerated(EnumType.STRING) 24 | private OwnershipStatus ownershipStatus; 25 | 26 | public AddressOwnershipJpaDto() { 27 | // for JPA 28 | } 29 | 30 | public AddressOwnershipJpaDto(@Nonnull String address, @Nonnull OwnershipStatus ownershipStatus) { 31 | this.address = address; 32 | this.ownershipStatus = ownershipStatus; 33 | } 34 | 35 | public String getAddress() { 36 | return requireNonNull(address); 37 | } 38 | 39 | public OwnershipStatus getOwnershipStatus() { 40 | if (ownershipStatus == null) { 41 | return OwnershipStatus.UNKNOWN; 42 | } 43 | return ownershipStatus; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ownership/src/main/java/de/cotto/bitbook/ownership/persistence/AddressOwnershipRepository.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership.persistence; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | import java.util.Optional; 6 | 7 | public interface AddressOwnershipRepository extends JpaRepository { 8 | Optional findByAddress(String address); 9 | } 10 | -------------------------------------------------------------------------------- /ownership/src/test/java/de/cotto/bitbook/ownership/OwnershipStatusTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static de.cotto.bitbook.ownership.OwnershipStatus.FOREIGN; 6 | import static de.cotto.bitbook.ownership.OwnershipStatus.OWNED; 7 | import static de.cotto.bitbook.ownership.OwnershipStatus.UNKNOWN; 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | class OwnershipStatusTest { 11 | @Test 12 | void unknown() { 13 | assertThat(OwnershipStatus.valueOf("UNKNOWN")).isEqualTo(UNKNOWN); 14 | } 15 | 16 | @Test 17 | void owned() { 18 | assertThat(OwnershipStatus.valueOf("OWNED")).isEqualTo(OWNED); 19 | } 20 | 21 | @Test 22 | void foreign() { 23 | assertThat(OwnershipStatus.valueOf("FOREIGN")).isEqualTo(FOREIGN); 24 | } 25 | } -------------------------------------------------------------------------------- /ownership/src/test/java/de/cotto/bitbook/ownership/persistence/AddressOwnershipJpaDtoTest.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership.persistence; 2 | 3 | import de.cotto.bitbook.ownership.OwnershipStatus; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class AddressOwnershipJpaDtoTest { 9 | 10 | @Test 11 | void getAddress() { 12 | AddressOwnershipJpaDto addressOwnershipJpaDto = new AddressOwnershipJpaDto("x", OwnershipStatus.OWNED); 13 | assertThat(addressOwnershipJpaDto.getAddress()).isEqualTo("x"); 14 | } 15 | 16 | @Test 17 | void getOwnershipStatus_not_set() { 18 | assertThat(new AddressOwnershipJpaDto().getOwnershipStatus()).isEqualTo(OwnershipStatus.UNKNOWN); 19 | } 20 | 21 | @Test 22 | void getOwnershipStatus() { 23 | assertThat(new AddressOwnershipJpaDto("x", OwnershipStatus.FOREIGN).getOwnershipStatus()) 24 | .isEqualTo(OwnershipStatus.FOREIGN); 25 | } 26 | } -------------------------------------------------------------------------------- /ownership/src/testFixtures/java/de/cotto/bitbook/ownership/persistence/AddressOwnershipJpaDtoFixtures.java: -------------------------------------------------------------------------------- 1 | package de.cotto.bitbook.ownership.persistence; 2 | 3 | import de.cotto.bitbook.ownership.OwnershipStatus; 4 | 5 | public class AddressOwnershipJpaDtoFixtures { 6 | public static final AddressOwnershipJpaDto OWNED_ADDRESS_JPA_DTO_1 = 7 | new AddressOwnershipJpaDto("abc", OwnershipStatus.OWNED); 8 | public static final AddressOwnershipJpaDto FOREIGN_ADDRESS_JPA_DTO_1 = 9 | new AddressOwnershipJpaDto("def", OwnershipStatus.FOREIGN); 10 | public static final AddressOwnershipJpaDto FOREIGN_ADDRESS_JPA_DTO_2 = 11 | new AddressOwnershipJpaDto("foo", OwnershipStatus.FOREIGN); 12 | public static final AddressOwnershipJpaDto OWNED_ADDRESS_JPA_DTO_2 = 13 | new AddressOwnershipJpaDto("xyz", OwnershipStatus.OWNED); 14 | } 15 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "bitbook" 2 | include("cli") 3 | include("cli:base") 4 | include("cli:lnd") 5 | include("cli:ownership") 6 | include("backend") 7 | include("backend:price") 8 | include("backend:blockheight") 9 | include("backend:address-transactions") 10 | include("backend:transaction") 11 | include("backend:models") 12 | include("backend:provider:all") 13 | include("backend:provider:base") 14 | include("backend:provider:bitaps") 15 | include("backend:provider:bitcoind") 16 | include("backend:provider:blockchaininfo") 17 | include("backend:provider:blockchair") 18 | include("backend:provider:blockcypher") 19 | include("backend:provider:blockstreaminfo") 20 | include("backend:provider:btccom") 21 | include("backend:provider:electrs") 22 | include("backend:provider:fullstackcash") 23 | include("backend:provider:mempoolspace") 24 | include("backend:request") 25 | include("backend:request:models") 26 | include("lnd") 27 | include("ownership") 28 | 29 | dependencyResolutionManagement { 30 | includeBuild("gradle/meta-plugins") 31 | repositories { 32 | mavenCentral() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | ./gradlew :cli:bootJar && clear && java -jar cli/build/libs/cli-boot.jar 2 | --------------------------------------------------------------------------------