├── .dockerignore ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── docker-image.yml │ ├── latest-bitcoind.yml │ └── main.yml ├── .gitignore ├── .mvn ├── checksums │ └── checksums-central.sha256 ├── maven.config └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── .readme └── logo.png ├── BUILD.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── codecov.yml ├── contrib └── eclair-cli.bash-completion ├── docs ├── API.md ├── Architecture.md ├── CircularRebalancing.md ├── Cluster.md ├── Configure.md ├── FAQ.md ├── Features.md ├── Guides.md ├── Logging.md ├── ManagingBitcoinCoreKeys.md ├── Monitoring.md ├── MultipartPayments.md ├── PostgreSQL.md ├── Tor.md ├── Usage.md └── release-notes │ ├── eclair-v0.10.0.md │ ├── eclair-v0.11.0.md │ ├── eclair-v0.12.0.md │ ├── eclair-v0.6.1.md │ ├── eclair-v0.6.2.md │ ├── eclair-v0.7.0.md │ ├── eclair-v0.8.0.md │ ├── eclair-v0.9.0.md │ └── eclair-vnext.md ├── eclair-core ├── eclair-cli ├── pom.xml └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── fr │ │ └── acinq │ │ └── eclair │ │ ├── BlockHeight.scala │ │ ├── CltvExpiry.scala │ │ ├── DBChecker.scala │ │ ├── Eclair.scala │ │ ├── EncodedNodeId.scala │ │ ├── FSMDiagnosticActorLogging.scala │ │ ├── Features.scala │ │ ├── Logs.scala │ │ ├── MilliSatoshi.scala │ │ ├── NodeParams.scala │ │ ├── Paginated.scala │ │ ├── PimpKamon.scala │ │ ├── PluginParams.scala │ │ ├── PortChecker.scala │ │ ├── Setup.scala │ │ ├── ShortChannelId.scala │ │ ├── SimpleSupervisor.scala │ │ ├── SpendFromChannelAddress.scala │ │ ├── SubscriptionsComplete.scala │ │ ├── Timestamp.scala │ │ ├── UInt64.scala │ │ ├── balance │ │ ├── BalanceActor.scala │ │ ├── ChannelsListener.scala │ │ ├── CheckBalance.scala │ │ └── Monitoring.scala │ │ ├── blockchain │ │ ├── BlockchainEvents.scala │ │ ├── Monitoring.scala │ │ ├── OnChainWallet.scala │ │ ├── bitcoind │ │ │ ├── OnChainAddressRefresher.scala │ │ │ ├── ZmqWatcher.scala │ │ │ ├── rpc │ │ │ │ ├── BasicBitcoinJsonRPCClient.scala │ │ │ │ ├── BatchingBitcoinJsonRPCClient.scala │ │ │ │ ├── BatchingClient.scala │ │ │ │ ├── BitcoinCoreClient.scala │ │ │ │ └── BitcoinJsonRPCClient.scala │ │ │ └── zmq │ │ │ │ └── ZMQActor.scala │ │ ├── fee │ │ │ ├── BitcoinCoreFeeProvider.scala │ │ │ ├── ConstantFeeProvider.scala │ │ │ ├── FallbackFeeProvider.scala │ │ │ ├── FeeProvider.scala │ │ │ ├── OnChainFeeConf.scala │ │ │ └── SmoothFeeProvider.scala │ │ └── watchdogs │ │ │ ├── BlockchainWatchdog.scala │ │ │ ├── ExplorerApi.scala │ │ │ ├── HeadersOverDns.scala │ │ │ └── Monitoring.scala │ │ ├── channel │ │ ├── ChannelConfig.scala │ │ ├── ChannelData.scala │ │ ├── ChannelEvents.scala │ │ ├── ChannelExceptions.scala │ │ ├── ChannelFeatures.scala │ │ ├── Commitments.scala │ │ ├── DustExposure.scala │ │ ├── Helpers.scala │ │ ├── Monitoring.scala │ │ ├── Register.scala │ │ ├── fsm │ │ │ ├── Channel.scala │ │ │ ├── ChannelOpenDualFunded.scala │ │ │ ├── ChannelOpenSingleFunded.scala │ │ │ ├── CommonFundingHandlers.scala │ │ │ ├── CommonHandlers.scala │ │ │ ├── DualFundingHandlers.scala │ │ │ ├── ErrorHandlers.scala │ │ │ └── SingleFundingHandlers.scala │ │ ├── fund │ │ │ ├── InteractiveTxBuilder.scala │ │ │ └── InteractiveTxFunder.scala │ │ └── publish │ │ │ ├── FinalTxPublisher.scala │ │ │ ├── MempoolTxMonitor.scala │ │ │ ├── ReplaceableTx.scala │ │ │ ├── ReplaceableTxFunder.scala │ │ │ ├── ReplaceableTxPrePublisher.scala │ │ │ ├── ReplaceableTxPublisher.scala │ │ │ ├── TxPublisher.scala │ │ │ └── TxTimeLocksMonitor.scala │ │ ├── crypto │ │ ├── ChaCha20Poly1305.scala │ │ ├── Mac.scala │ │ ├── Monitoring.scala │ │ ├── Noise.scala │ │ ├── Random.scala │ │ ├── ShaChain.scala │ │ ├── Sphinx.scala │ │ ├── TransportHandler.scala │ │ ├── WeakEntropyPool.scala │ │ └── keymanager │ │ │ ├── ChannelKeyManager.scala │ │ │ ├── ChannelKeys.scala │ │ │ ├── CommitmentKeys.scala │ │ │ ├── LocalChannelKeyManager.scala │ │ │ ├── LocalNodeKeyManager.scala │ │ │ ├── LocalOnChainKeyManager.scala │ │ │ ├── NodeKeyManager.scala │ │ │ └── OnChainKeyManager.scala │ │ ├── db │ │ ├── AuditDb.scala │ │ ├── ChannelsDb.scala │ │ ├── Databases.scala │ │ ├── DbEventHandler.scala │ │ ├── DualDatabases.scala │ │ ├── FileBackupHandler.scala │ │ ├── LiquidityDb.scala │ │ ├── Monitoring.scala │ │ ├── NetworkDb.scala │ │ ├── OffersDb.scala │ │ ├── PaymentsDb.scala │ │ ├── PeerStorageCleaner.scala │ │ ├── PeersDb.scala │ │ ├── PendingCommandsDb.scala │ │ ├── RevokedHtlcInfoCleaner.scala │ │ ├── jdbc │ │ │ └── JdbcUtils.scala │ │ ├── migration │ │ │ ├── CompareAuditDb.scala │ │ │ ├── CompareChannelsDb.scala │ │ │ ├── CompareDb.scala │ │ │ ├── CompareNetworkDb.scala │ │ │ ├── ComparePaymentsDb.scala │ │ │ ├── ComparePeersDb.scala │ │ │ ├── ComparePendingCommandsDb.scala │ │ │ ├── MigrateAuditDb.scala │ │ │ ├── MigrateChannelsDb.scala │ │ │ ├── MigrateDb.scala │ │ │ ├── MigrateNetworkDb.scala │ │ │ ├── MigratePaymentsDb.scala │ │ │ ├── MigratePeersDb.scala │ │ │ └── MigratePendingCommandsDb.scala │ │ ├── pg │ │ │ ├── PgAuditDb.scala │ │ │ ├── PgChannelsDb.scala │ │ │ ├── PgLiquidityDb.scala │ │ │ ├── PgNetworkDb.scala │ │ │ ├── PgOffersDb.scala │ │ │ ├── PgPaymentsDb.scala │ │ │ ├── PgPeersDb.scala │ │ │ ├── PgPendingCommandsDb.scala │ │ │ └── PgUtils.scala │ │ └── sqlite │ │ │ ├── SqliteAuditDb.scala │ │ │ ├── SqliteChannelsDb.scala │ │ │ ├── SqliteLiquidityDb.scala │ │ │ ├── SqliteNetworkDb.scala │ │ │ ├── SqliteOffersDb.scala │ │ │ ├── SqlitePaymentsDb.scala │ │ │ ├── SqlitePeersDb.scala │ │ │ ├── SqlitePendingCommandsDb.scala │ │ │ └── SqliteUtils.scala │ │ ├── io │ │ ├── Client.scala │ │ ├── ClientSpawner.scala │ │ ├── IncomingConnectionsTracker.scala │ │ ├── MessageRelay.scala │ │ ├── Monitoring.scala │ │ ├── NodeURI.scala │ │ ├── OpenChannelInterceptor.scala │ │ ├── Peer.scala │ │ ├── PeerChannelsCollector.scala │ │ ├── PeerConnection.scala │ │ ├── PeerEvents.scala │ │ ├── PeerReadyNotifier.scala │ │ ├── PendingChannelsRateLimiter.scala │ │ ├── RateLimiter.scala │ │ ├── ReconnectionTask.scala │ │ ├── Server.scala │ │ └── Switchboard.scala │ │ ├── json │ │ └── JsonSerializers.scala │ │ ├── message │ │ ├── OnionMessages.scala │ │ └── Postman.scala │ │ ├── package.scala │ │ ├── payment │ │ ├── Bolt11Invoice.scala │ │ ├── Bolt12Invoice.scala │ │ ├── Invoice.scala │ │ ├── Monitoring.scala │ │ ├── PaymentEvents.scala │ │ ├── PaymentPacket.scala │ │ ├── offer │ │ │ ├── DefaultOfferHandler.scala │ │ │ ├── OfferCreator.scala │ │ │ ├── OfferManager.scala │ │ │ └── OfferPaymentMetadata.scala │ │ ├── receive │ │ │ ├── ForwardHandler.scala │ │ │ ├── InvoicePurger.scala │ │ │ ├── MultiPartHandler.scala │ │ │ ├── MultiPartPaymentFSM.scala │ │ │ └── PaymentHandler.scala │ │ ├── relay │ │ │ ├── AsyncPaymentTriggerer.scala │ │ │ ├── ChannelRelay.scala │ │ │ ├── ChannelRelayer.scala │ │ │ ├── NodeRelay.scala │ │ │ ├── NodeRelayer.scala │ │ │ ├── OnTheFlyFunding.scala │ │ │ ├── PostRestartHtlcCleaner.scala │ │ │ └── Relayer.scala │ │ └── send │ │ │ ├── Autoprobe.scala │ │ │ ├── BlindedPathsResolver.scala │ │ │ ├── MultiPartPaymentLifecycle.scala │ │ │ ├── OfferPayment.scala │ │ │ ├── PaymentError.scala │ │ │ ├── PaymentInitiator.scala │ │ │ ├── PaymentLifecycle.scala │ │ │ ├── Recipient.scala │ │ │ └── TrampolinePaymentLifecycle.scala │ │ ├── remote │ │ ├── EclairInternalsSerializer.scala │ │ ├── LightningMessageSerializer.scala │ │ └── ScodecSerializer.scala │ │ ├── router │ │ ├── Announcements.scala │ │ ├── BalanceEstimate.scala │ │ ├── BlindedRouteCreation.scala │ │ ├── Graph.scala │ │ ├── Monitoring.scala │ │ ├── NetworkEvents.scala │ │ ├── PathFindingExperimentConf.scala │ │ ├── RouteCalculation.scala │ │ ├── Router.scala │ │ ├── RouterExceptions.scala │ │ ├── StaleChannels.scala │ │ ├── Sync.scala │ │ └── Validation.scala │ │ ├── tor │ │ ├── Controller.scala │ │ ├── Socks5Connection.scala │ │ └── TorProtocolHandler.scala │ │ ├── transactions │ │ ├── CommitmentSpec.scala │ │ ├── Scripts.scala │ │ └── Transactions.scala │ │ └── wire │ │ ├── Monitoring.scala │ │ ├── internal │ │ ├── CommandCodecs.scala │ │ └── channel │ │ │ ├── ChannelCodecs.scala │ │ │ ├── version0 │ │ │ ├── ChannelCodecs0.scala │ │ │ └── ChannelTypes0.scala │ │ │ ├── version1 │ │ │ └── ChannelCodecs1.scala │ │ │ ├── version2 │ │ │ └── ChannelCodecs2.scala │ │ │ ├── version3 │ │ │ ├── ChannelCodecs3.scala │ │ │ └── ChannelTypes3.scala │ │ │ └── version4 │ │ │ ├── ChannelCodecs4.scala │ │ │ └── ChannelTypes4.scala │ │ └── protocol │ │ ├── ChannelTlv.scala │ │ ├── CommonCodecs.scala │ │ ├── FailureMessage.scala │ │ ├── HtlcTlv.scala │ │ ├── InteractiveTxTlv.scala │ │ ├── LightningMessageCodecs.scala │ │ ├── LightningMessageTypes.scala │ │ ├── LiquidityAds.scala │ │ ├── MessageOnion.scala │ │ ├── OfferCodecs.scala │ │ ├── OfferTypes.scala │ │ ├── OnTheFlyFundingTlv.scala │ │ ├── OnionMessageTlv.scala │ │ ├── OnionRouting.scala │ │ ├── PaymentOnion.scala │ │ ├── PeerStorageTlv.scala │ │ ├── RouteBlinding.scala │ │ ├── RoutingTlv.scala │ │ ├── SetupAndControlTlv.scala │ │ ├── TlvCodecs.scala │ │ └── TlvTypes.scala │ └── test │ ├── java │ └── fr │ │ └── acinq │ │ └── eclair │ │ ├── MilliSatoshiTest.java │ │ └── crypto │ │ └── Curve25519.java │ ├── resources │ ├── application.conf │ ├── bolt3-tx-test-vectors-anchor-outputs-format.txt │ ├── bolt3-tx-test-vectors-anchor-outputs-zero-fee-htlc-tx-format.txt │ ├── bolt3-tx-test-vectors-default-commitment-format.txt │ ├── bolt3-tx-test-vectors-static-remotekey-format.txt │ ├── bolt4-test-onion-message.json │ ├── format-string-test.json │ ├── integration │ │ └── bitcoin.conf │ ├── logback-test.xml │ ├── nonreg │ │ └── codecs │ │ │ ├── 000003-DATA_NORMAL │ │ │ ├── fundee │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ ├── 020002-DATA_NORMAL │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ ├── 030000-DATA_WAIT_FOR_FUNDING_CONFIRMED │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ ├── 03000a-DATA_WAIT_FOR_CHANNEL_READY │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ ├── 03000c-DATA_WAIT_FOR_DUAL_FUNDING_READY │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ ├── 04000b-DATA_WAIT_FOR_CHANNEL_READY │ │ │ ├── fundee │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ ├── 04000d-DATA_WAIT_FOR_DUAL_FUNDING_READY │ │ │ ├── fundee │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ └── funder │ │ │ │ ├── data.bin │ │ │ │ └── data.json │ │ │ └── 04000e-DATA_NORMAL │ │ │ ├── announced │ │ │ ├── data.bin │ │ │ └── data.json │ │ │ └── splicing-private │ │ │ ├── data.bin │ │ │ └── data.json │ ├── normal_data_htlcs.bin │ ├── offers-test.json │ ├── scenarii │ │ ├── 01-offer1.script │ │ ├── 01-offer1.script.expected │ │ ├── 02-offer2.script │ │ ├── 02-offer2.script.expected │ │ ├── 03-fulfill1.script │ │ ├── 03-fulfill1.script.expected │ │ ├── 04-two-commits-onedir.script │ │ ├── 04-two-commits-onedir.script.expected │ │ ├── 05-two-commits-in-flight.script │ │ ├── 05-two-commits-in-flight.script.expected │ │ ├── 10-offers-crossover.script │ │ ├── 10-offers-crossover.script.expected │ │ ├── 11-commits-crossover.script │ │ └── 11-commits-crossover.script.expected │ └── short_channels-mainnet.422 │ └── scala │ └── fr │ └── acinq │ └── eclair │ ├── CltvExpirySpec.scala │ ├── EclairImplSpec.scala │ ├── FeaturesSpec.scala │ ├── MilliSatoshiSpec.scala │ ├── PackageSpec.scala │ ├── Pipe.scala │ ├── ShortChannelIdSpec.scala │ ├── StartupSpec.scala │ ├── TestBitcoinCoreClient.scala │ ├── TestConstants.scala │ ├── TestDatabases.scala │ ├── TestKitBaseClass.scala │ ├── TestUtils.scala │ ├── TimestampSpec.scala │ ├── UInt64Spec.scala │ ├── balance │ ├── ChannelsListenerSpec.scala │ └── CheckBalanceSpec.scala │ ├── blockchain │ ├── DummyOnChainWallet.scala │ ├── WatcherSpec.scala │ ├── bitcoind │ │ ├── BitcoinCoreClientSpec.scala │ │ ├── BitcoinCoreCookieAuthSpec.scala │ │ ├── BitcoindService.scala │ │ ├── OnChainAddressRefresherSpec.scala │ │ └── ZmqWatcherSpec.scala │ ├── fee │ │ ├── BitcoinCoreFeeProviderSpec.scala │ │ ├── FallbackFeeProviderSpec.scala │ │ ├── FeeProviderSpec.scala │ │ ├── OnChainFeeConfSpec.scala │ │ └── SmoothFeeProviderSpec.scala │ └── watchdogs │ │ ├── BlockchainWatchdogSpec.scala │ │ ├── ExplorerApiSpec.scala │ │ └── HeadersOverDnsSpec.scala │ ├── channel │ ├── ChannelConfigSpec.scala │ ├── ChannelDataSpec.scala │ ├── ChannelFeaturesSpec.scala │ ├── CommitmentsSpec.scala │ ├── DustExposureSpec.scala │ ├── FuzzyPipe.scala │ ├── FuzzySpec.scala │ ├── HelpersSpec.scala │ ├── InteractiveTxBuilderSpec.scala │ ├── RegisterSpec.scala │ ├── RestoreSpec.scala │ ├── publish │ │ ├── FinalTxPublisherSpec.scala │ │ ├── MempoolTxMonitorSpec.scala │ │ ├── ReplaceableTxPublisherSpec.scala │ │ ├── TxPublisherSpec.scala │ │ └── TxTimeLocksMonitorSpec.scala │ └── states │ │ ├── ChannelStateTestsHelperMethods.scala │ │ ├── a │ │ ├── WaitForAcceptChannelStateSpec.scala │ │ ├── WaitForAcceptDualFundedChannelStateSpec.scala │ │ ├── WaitForOpenChannelStateSpec.scala │ │ └── WaitForOpenDualFundedChannelStateSpec.scala │ │ ├── b │ │ ├── WaitForDualFundingCreatedStateSpec.scala │ │ ├── WaitForDualFundingSignedStateSpec.scala │ │ ├── WaitForFundingCreatedStateSpec.scala │ │ ├── WaitForFundingInternalStateSpec.scala │ │ └── WaitForFundingSignedStateSpec.scala │ │ ├── c │ │ ├── WaitForChannelReadyStateSpec.scala │ │ ├── WaitForDualFundingConfirmedStateSpec.scala │ │ ├── WaitForDualFundingReadyStateSpec.scala │ │ └── WaitForFundingConfirmedStateSpec.scala │ │ ├── e │ │ ├── NormalQuiescentStateSpec.scala │ │ ├── NormalSplicesStateSpec.scala │ │ ├── NormalStateSpec.scala │ │ └── OfflineStateSpec.scala │ │ ├── f │ │ └── ShutdownStateSpec.scala │ │ ├── g │ │ └── NegotiatingStateSpec.scala │ │ └── h │ │ └── ClosingStateSpec.scala │ ├── crypto │ ├── ChaCha20Poly1305Spec.scala │ ├── MacSpec.scala │ ├── NoiseDemo.scala │ ├── NoiseSpec.scala │ ├── RandomSpec.scala │ ├── ShaChainSpec.scala │ ├── SphinxSpec.scala │ ├── TransportHandlerSpec.scala │ └── keymanager │ │ ├── LocalChannelKeyManagerSpec.scala │ │ ├── LocalNodeKeyManagerSpec.scala │ │ └── LocalOnChainKeyManagerSpec.scala │ ├── db │ ├── AuditDbSpec.scala │ ├── ChannelsDbSpec.scala │ ├── DbMigrationSpec.scala │ ├── DualDatabasesSpec.scala │ ├── LiquidityDbSpec.scala │ ├── NetworkDbSpec.scala │ ├── OffersDbSpec.scala │ ├── PaymentsDbSpec.scala │ ├── PeersDbSpec.scala │ ├── PendingCommandsDbSpec.scala │ ├── PgUtilsSpec.scala │ ├── RevokedHtlcInfoCleanerSpec.scala │ ├── SqliteFileBackupHandlerSpec.scala │ └── SqliteUtilsSpec.scala │ ├── integration │ ├── ChannelIntegrationSpec.scala │ ├── IntegrationSpec.scala │ ├── MessageIntegrationSpec.scala │ ├── PaymentIntegrationSpec.scala │ ├── PerformanceIntegrationSpec.scala │ ├── StartupIntegrationSpec.scala │ └── basic │ │ ├── ThreeNodesIntegrationSpec.scala │ │ ├── TwoNodesIntegrationSpec.scala │ │ ├── channel │ │ └── GossipIntegrationSpec.scala │ │ ├── fixtures │ │ ├── FixtureUtils.scala │ │ ├── MinimalNodeFixture.scala │ │ └── composite │ │ │ ├── ThreeNodesFixture.scala │ │ │ └── TwoNodesFixture.scala │ │ ├── payment │ │ └── OfferPaymentSpec.scala │ │ └── zeroconf │ │ ├── ZeroConfActivationSpec.scala │ │ └── ZeroConfAliasIntegrationSpec.scala │ ├── interop │ └── rustytests │ │ ├── RustyTestsSpec.scala │ │ └── SynchronizationPipe.scala │ ├── io │ ├── IncomingConnectionsTrackerSpec.scala │ ├── MessageRelaySpec.scala │ ├── NodeURISpec.scala │ ├── OpenChannelInterceptorSpec.scala │ ├── PeerChannelsCollectorSpec.scala │ ├── PeerConnectionSpec.scala │ ├── PeerReadyManagerSpec.scala │ ├── PeerReadyNotifierSpec.scala │ ├── PeerSpec.scala │ ├── PendingChannelsRateLimiterSpec.scala │ ├── RateLimiterSpec.scala │ ├── ReconnectionTaskSpec.scala │ └── SwitchboardSpec.scala │ ├── json │ └── JsonSerializersSpec.scala │ ├── message │ ├── OnionMessagesSpec.scala │ └── PostmanSpec.scala │ ├── payment │ ├── Bolt11InvoiceSpec.scala │ ├── Bolt12InvoiceSpec.scala │ ├── MultiPartHandlerSpec.scala │ ├── MultiPartPaymentFSMSpec.scala │ ├── MultiPartPaymentLifecycleSpec.scala │ ├── PaymentHandlerSpec.scala │ ├── PaymentInitiatorSpec.scala │ ├── PaymentLifecycleSpec.scala │ ├── PaymentPacketSpec.scala │ ├── PostRestartHtlcCleanerSpec.scala │ ├── offer │ │ └── OfferManagerSpec.scala │ ├── receive │ │ └── InvoicePurgerSpec.scala │ ├── relay │ │ ├── AsyncPaymentTriggererSpec.scala │ │ ├── ChannelRelayerSpec.scala │ │ ├── NodeRelayerSpec.scala │ │ ├── OnTheFlyFundingSpec.scala │ │ └── RelayerSpec.scala │ └── send │ │ ├── BlindedPathsResolverSpec.scala │ │ └── OfferPaymentSpec.scala │ ├── remote │ └── EclairInternalsSerializerSpec.scala │ ├── router │ ├── AnnouncementsBatchValidationSpec.scala │ ├── AnnouncementsSpec.scala │ ├── BalanceEstimateSpec.scala │ ├── BaseRouterSpec.scala │ ├── BlindedRouteCreationSpec.scala │ ├── ChannelRangeQueriesSpec.scala │ ├── ChannelRouterIntegrationSpec.scala │ ├── GraphSpec.scala │ ├── RouteCalculationSpec.scala │ ├── RouterSpec.scala │ └── RoutingSyncSpec.scala │ ├── testutils │ ├── DurationBenchmarkReporter.scala │ ├── FixtureSpec.scala │ ├── MyCapturingAppender.scala │ ├── MyContextSelector.scala │ ├── MySlf4jLogger.scala │ └── PimpTestProbe.scala │ ├── tor │ ├── Socks5ConnectionSpec.scala │ └── TorProtocolHandlerSpec.scala │ ├── transactions │ ├── CommitmentSpecSpec.scala │ ├── TestVectorsSpec.scala │ └── TransactionsSpec.scala │ └── wire │ ├── internal │ ├── CommandCodecsSpec.scala │ └── channel │ │ ├── ChannelCodecsSpec.scala │ │ ├── version0 │ │ └── ChannelCodecs0Spec.scala │ │ ├── version1 │ │ └── ChannelCodecs1Spec.scala │ │ ├── version2 │ │ └── ChannelCodecs2Spec.scala │ │ ├── version3 │ │ └── ChannelCodecs3Spec.scala │ │ └── version4 │ │ └── ChannelCodecs4Spec.scala │ └── protocol │ ├── CommonCodecsSpec.scala │ ├── ExtendedQueriesCodecsSpec.scala │ ├── FailureMessageCodecsSpec.scala │ ├── LightningMessageCodecsSpec.scala │ ├── LiquidityAdsSpec.scala │ ├── MessageOnionCodecsSpec.scala │ ├── OfferTypesSpec.scala │ ├── PaymentOnionSpec.scala │ ├── RouteBlindingSpec.scala │ └── TlvCodecsSpec.scala ├── eclair-front ├── modules │ ├── assembly.xml │ ├── awseb.xml │ └── awseb │ │ ├── Procfile │ │ ├── logback_eb.xml │ │ └── run.sh ├── pom.xml └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ ├── eclair-front.sh │ │ └── logback.xml │ └── scala │ │ └── fr │ │ └── acinq │ │ └── eclair │ │ ├── Boot.scala │ │ ├── ClusterListener.scala │ │ ├── FrontSetup.scala │ │ └── router │ │ └── FrontRouter.scala │ └── test │ ├── resources │ └── application.conf │ └── scala │ └── fr │ └── acinq │ └── eclair │ └── router │ └── FrontRouterSpec.scala ├── eclair-node ├── modules │ └── assembly.xml ├── pom.xml └── src │ ├── main │ ├── resources │ │ ├── application.conf │ │ ├── eclair-node.bat │ │ ├── eclair-node.sh │ │ ├── logback.xml │ │ └── logback_colors.xml │ └── scala │ │ └── fr │ │ └── acinq │ │ └── eclair │ │ ├── Boot.scala │ │ ├── Plugin.scala │ │ └── api │ │ ├── Service.scala │ │ ├── directives │ │ ├── AuthDirective.scala │ │ ├── DefaultHeaders.scala │ │ ├── EclairDirectives.scala │ │ ├── ErrorDirective.scala │ │ ├── ErrorResponse.scala │ │ ├── ExtraDirectives.scala │ │ ├── RouteFormat.scala │ │ └── TimeoutDirective.scala │ │ ├── handlers │ │ ├── Channel.scala │ │ ├── Control.scala │ │ ├── Fees.scala │ │ ├── Invoice.scala │ │ ├── Message.scala │ │ ├── Node.scala │ │ ├── Offer.scala │ │ ├── OnChain.scala │ │ ├── PathFinding.scala │ │ ├── Payment.scala │ │ └── WebSocket.scala │ │ └── serde │ │ ├── FormParamExtractors.scala │ │ └── JsonSupport.scala │ └── test │ ├── resources │ ├── api │ │ ├── channelbalances │ │ ├── close │ │ ├── findroute-full │ │ ├── findroute-nodeid │ │ ├── findroute-scid │ │ ├── getinfo │ │ ├── help │ │ ├── networkstats │ │ ├── peers │ │ ├── received-expired │ │ ├── received-pending │ │ ├── received-success │ │ ├── sent-failed │ │ ├── sent-pending │ │ ├── sent-success │ │ └── usablebalances │ └── logback-test.xml │ └── scala │ └── fr │ └── acinq │ └── eclair │ └── api │ └── ApiServiceSpec.scala ├── monitoring └── grafana-dashboard │ ├── akka-metrics.json │ ├── eclair-metrics.json │ └── host-metrics.json ├── mvnw ├── mvnw.cmd └── pom.xml /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .git 4 | **/*.idea 5 | **/*.iml 6 | **/target 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: docker login 16 | uses: docker/login-action@v2 17 | with: 18 | username: ${{ secrets.DOCKER_USER }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: build the Docker image 22 | run: docker build -t acinq/eclair . 23 | 24 | - name: docker push 25 | run: docker push acinq/eclair 26 | -------------------------------------------------------------------------------- /.github/workflows/latest-bitcoind.yml: -------------------------------------------------------------------------------- 1 | name: Latest Bitcoin Core 2 | 3 | on: 4 | workflow_dispatch: # Build can be triggered manually from github.com 5 | schedule: 6 | # Run at midnight on Sunday and Wednesday. 7 | - cron: '0 0 * * 0,3' 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | 14 | regression-tests: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 90 17 | steps: 18 | - name: Checkout bitcoind master 19 | uses: actions/checkout@v3 20 | with: 21 | repository: bitcoin/bitcoin 22 | path: bitcoin 23 | 24 | - name: Install bitcoind dependencies 25 | run: sudo apt-get install build-essential cmake pkg-config bsdmainutils python3 libevent-dev libboost-dev libminiupnpc-dev libnatpmp-dev libzmq3-dev libsqlite3-dev systemtap-sdt-dev 26 | working-directory: ./bitcoin 27 | 28 | - name: Init and configure cmake build 29 | run: cmake -B build -DWITH_ZMQ=ON -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTS=OFF -DBUILD_BENCH=OFF 30 | working-directory: ./bitcoin 31 | 32 | - name: Build bitcoind 33 | run: cmake --build build "-j $(($(nproc)))" 34 | working-directory: ./bitcoin 35 | 36 | - name: Checkout eclair master 37 | uses: actions/checkout@v3 38 | with: 39 | path: eclair 40 | 41 | - name: Set up JDK 21 42 | uses: actions/setup-java@v3 43 | with: 44 | java-version: 21 45 | distribution: 'adopt' 46 | 47 | - name: Configure OS settings 48 | run: echo "fs.file-max = 1024000" | sudo tee -a /etc/sysctl.conf 49 | 50 | - name: Run eclair tests 51 | run: BITCOIND_DIR=$GITHUB_WORKSPACE/bitcoin/build/bin ./mvnw test 52 | working-directory: ./eclair 53 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 30 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up JDK 21 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: 21 22 | distribution: 'adopt' 23 | 24 | - name: Cache Maven dependencies 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.m2 28 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 29 | restore-keys: ${{ runner.os }}-m2 30 | 31 | - name: Configure OS settings 32 | run: echo "fs.file-max = 1024000" | sudo tee -a /etc/sysctl.conf 33 | 34 | # NB: we exclude external API tests from the CI, because we don't want our build to fail because a dependency is failing. 35 | # This means we won't automatically catch changes in external APIs, but developers should regularly run the test suite locally so in practice it shouldn't be a problem. 36 | - name: Build with Maven 37 | run: ./mvnw test-compile && ./mvnw scoverage:report -DtagsToExclude=external-api 38 | 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v3 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # mvn specific 16 | dependency-reduced-pom.xml 17 | 18 | # Scala-IDE specific 19 | .scala_dependencies 20 | .worksheet 21 | 22 | .idea/ 23 | *.iml 24 | target/ 25 | project/target 26 | DeleteMe*.* 27 | *~ 28 | jdbcUrlFile_*.tmp 29 | 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | # trusted checksum source setup 2 | -Daether.trustedChecksumsSource.summaryFile=true 3 | -Daether.trustedChecksumsSource.summaryFile.basedir=${session.rootDirectory}/.mvn/checksums/ 4 | # post processor: trusted checksums 5 | -Daether.artifactResolver.postProcessor.trustedChecksums=true 6 | -Daether.artifactResolver.postProcessor.trustedChecksums.checksumAlgorithms=SHA-256 7 | -Daether.artifactResolver.postProcessor.trustedChecksums.failIfMissing=true 8 | -Daether.artifactResolver.postProcessor.trustedChecksums.snapshots=false 9 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ACINQ/eclair/fb84a9d1115bf80bb2b3269fafa31fd5cc9dfacb/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | distributionSha256Sum=4ec3f26fb1a692473aea0235c300bd20f0f9fe741947c82c1234cefd76ac3a3c 21 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar 22 | wrapperSha256Sum=3d8f20ce6103913be8b52aef6d994e0c54705fb527324ceb9b835b338739c7a8 23 | -------------------------------------------------------------------------------- /.readme/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ACINQ/eclair/fb84a9d1115bf80bb2b3269fafa31fd5cc9dfacb/.readme/logo.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:21-jdk-alpine as BUILD 2 | 3 | # Let's fetch eclair dependencies, so that Docker can cache them 4 | # This way we won't have to fetch dependencies again if only the source code changes 5 | # The easiest way to reliably get dependencies is to build the project with no sources 6 | WORKDIR /usr/src 7 | COPY mvnw mvnw 8 | COPY .mvn .mvn 9 | COPY pom.xml pom.xml 10 | COPY eclair-core/pom.xml eclair-core/pom.xml 11 | COPY eclair-front/pom.xml eclair-front/pom.xml 12 | COPY eclair-node/pom.xml eclair-node/pom.xml 13 | COPY eclair-node/modules/assembly.xml eclair-node/modules/assembly.xml 14 | RUN mkdir -p eclair-core/src/main/scala && touch eclair-core/src/main/scala/empty.scala 15 | # Blank build. We only care about eclair-node, and we use install because eclair-node depends on eclair-core 16 | RUN ./mvnw install -pl eclair-node -am 17 | RUN ./mvnw clean 18 | 19 | # Only then do we copy the sources 20 | COPY . . 21 | 22 | # And this time we can build in offline mode, specifying 'notag' instead of git commit 23 | RUN ./mvnw package -pl eclair-node -am -DskipTests -Dgit.commit.id=notag -Dgit.commit.id.abbrev=notag -o 24 | # It might be good idea to run the tests here, so that the docker build fail if the code is bugged 25 | 26 | FROM eclipse-temurin:21-jre-alpine 27 | WORKDIR /app 28 | 29 | # install jq for eclair-cli 30 | RUN apk add bash jq curl unzip 31 | 32 | # copy and install eclair-cli executable 33 | COPY --from=BUILD /usr/src/eclair-core/eclair-cli . 34 | RUN chmod +x eclair-cli && mv eclair-cli /sbin/eclair-cli 35 | 36 | # we only need the eclair-node.zip to run 37 | COPY --from=BUILD /usr/src/eclair-node/target/eclair-node-*.zip ./eclair-node.zip 38 | RUN unzip eclair-node.zip && mv eclair-node-* eclair-node && chmod +x eclair-node/bin/eclair-node.sh 39 | 40 | ENV ECLAIR_DATADIR=/data 41 | ENV JAVA_OPTS= 42 | 43 | RUN mkdir -p "$ECLAIR_DATADIR" 44 | VOLUME [ "/data" ] 45 | 46 | ENTRYPOINT JAVA_OPTS="${JAVA_OPTS}" eclair-node/bin/eclair-node.sh "-Declair.datadir=${ECLAIR_DATADIR}" 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report security issues send an email to security@acinq.fr (not for support). 6 | 7 | The following keys may be used to communicate sensitive information to developers: 8 | 9 | | Name | Fingerprint | 10 | |---------------------|---------------------------------------------------| 11 | | Pierre-Marie Padiou | 6AA4 5A4C 209A 2D30 64CF 66BE E434 ED29 2E85 643A | 12 | | Fabrice Drouin | C25A 288A 842E AF7A A5B5 303F 7A73 FE77 DE2C 4027 | 13 | | Bastien Teinturier | 72BD 8AD9 F656 1619 37FA 8A5D 34F3 77B0 100E D6BB | 14 | 15 | You can import keys by running the following commands: 16 | 17 | ```sh 18 | gpg --keyserver https://acinq.co/pgp/padioupm.asc --recv-keys "6AA4 5A4C 209A 2D30 64CF 66BE E434 ED29 2E85 643A" 19 | gpg --keyserver https://acinq.co/pgp/drouinf.asc --recv-keys "C25A 288A 842E AF7A A5B5 303F 7A73 FE77 DE2C 4027" 20 | gpg --keyserver https://acinq.co/pgp/tbast.asc --recv-keys "72BD 8AD9 F656 1619 37FA 8A5D 34F3 77B0 100E D6BB" 21 | ``` 22 | 23 | Ensure that you put quotes around fingerprints containing spaces. 24 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | 6 | comment: 7 | layout: "diff, files" 8 | 9 | ignore: 10 | - "eclair-node/**/*" -------------------------------------------------------------------------------- /contrib/eclair-cli.bash-completion: -------------------------------------------------------------------------------- 1 | # bash completion for eclair-cli 2 | # copy to /etc/bash_completion.d/ 3 | # created by Stadicus 4 | 5 | _eclair-cli() 6 | { 7 | local cur prev opts cmds 8 | 9 | # eclair-cli might not be in $PATH 10 | local eclaircli 11 | ecli="$1" 12 | 13 | COMPREPLY=() 14 | cur="${COMP_WORDS[COMP_CWORD]}" 15 | prev="${COMP_WORDS[COMP_CWORD-1]}" 16 | 17 | case "$cur" in 18 | -*=*) 19 | return 0 20 | ;; 21 | *) 22 | # works fine, but is too slow at the moment. 23 | # allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g') 24 | allopts="allchannels allupdates audit bumpforceclose channel channelbalances channels channelstats close closedchannels connect cpfpbumpfees createinvoice deleteinvoice disconnect enableFromFutureHtlc findroute findroutebetweennodes findroutetonode forceclose getdescriptors getinfo getinvoice getmasterxpub getnewaddress getreceivedinfo getsentinfo globalbalance listinvoices listpendinginvoices listreceivedpayments networkfees node nodes onchainbalance onchaintransactions open parseinvoice payinvoice payoffer peers rbfopen sendonchain sendonionmessage sendtonode sendtoroute signmessage splicein spliceout stop updaterelayfee usablebalances verifymessage" 25 | 26 | if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments 27 | if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then 28 | opts=${allopts} 29 | fi 30 | fi 31 | 32 | if [[ -z "$cur" || "$cur" =~ ^- ]]; then 33 | cmds=$($ecli 2>&1 | awk '$1 ~ /^-/ { sub(/,/, ""); print $1}') 34 | fi 35 | 36 | COMPREPLY=( $(compgen -W "${cmds} ${opts}" -- ${cur}) ) 37 | esac 38 | } 39 | complete -F _eclair-cli eclair-cli 40 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | Eclair can setup a JSON API on the `eclair.api.port` port set in the [configuration](./Configure.md). You first need to enable it: 4 | 5 | ``` 6 | eclair.api.enabled=true 7 | eclair.api.password=changeit 8 | ``` 9 | 10 | :rotating_light: **Attention:** Eclair's API should NOT be accessible from the outside world (similarly to Bitcoin Core API). 11 | 12 | ## Payment notification 13 | 14 | Eclair accepts websocket connection on `ws://localhost:/ws`, and emits a message containing the payment hash of a payment when receiving a payment. 15 | 16 | ## API calls 17 | 18 | This API exposes all the necessary methods to read the current state of the node, open/close channels and send/receive payments. For the full documentation please visit https://acinq.github.io/eclair 19 | 20 | #### Example: open a channel 21 | 22 | Your node listens on 8081. You want to open a 140 mBTC channel with `endurance.acinq.co` on Testnet with a 30 mBTC `push` 23 | 24 | 1/ connect to the node with the URI: 25 | 26 | ```shell 27 | curl -X POST \ 28 | -u :api_password \ 29 | -F uri="03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134@endurance.acinq.co:9735" \ 30 | http://127.0.0.1:8081/connect 31 | ``` 32 | 33 | 2/ Open a channel with this node 34 | 35 | ```shell 36 | curl -X POST \ 37 | -u :api_password \ 38 | -F nodeId=03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134 \ 39 | -F fundingSatoshis=14000000 \ 40 | http://127.0.0.1:8081/open 41 | ``` 42 | 43 | Feeling tired of writing all these `curls`? Good news, a CLI bash file is available. It uses this API to interact with your node. More information about `eclair-cli` is [here](./Usage.md). -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## What does it mean for a channel to be "enabled" or "disabled" ? 4 | 5 | A channel is disabled if a `channel_update` message has been broadcast for that channel with the `disable` bit set (see [BOLT 7](https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message)). It means that the channel still exists but cannot be used to route payments, until it has been re-enabled. 6 | 7 | Suppose you're A, with the following setup: 8 | ``` 9 | A ---ab--> B --bc--> C 10 | ``` 11 | And node C goes down. B will publish a channel update for channel `bc` with the `disable` bit set. 12 | There are other cases when a channel becomes disabled, for example when its balance goes below reserve... 13 | 14 | Note that you can have multiple channels between the same nodes, and that some of them can be enabled while others are disabled (i.e. enable/disable is channel-specific, not node-specific). 15 | 16 | ## How should you stop an Eclair node ? 17 | 18 | To stop your node you just need to kill its process, there is no API command to do this. The JVM handles the quit signal and notifies the node to perform clean-up. For example, there is a hook to cleanly free DB locks when using Postgres. 19 | -------------------------------------------------------------------------------- /docs/Features.md: -------------------------------------------------------------------------------- 1 | # Customize Features 2 | 3 | Eclair ships with a set of features that are activated by default, and some experimental or optional features that can be activated by users. 4 | The list of supported features can be found in the [reference configuration](https://github.com/ACINQ/eclair/blob/master/eclair-core/src/main/resources/reference.conf). 5 | 6 | To enable a non-default feature, you simply need to add the following to your `eclair.conf`: 7 | 8 | ```conf 9 | eclair.features { 10 | official_feature_name = optional|mandatory 11 | } 12 | ``` 13 | 14 | For example, to activate `option_static_remotekey`: 15 | 16 | ```conf 17 | eclair.features { 18 | option_static_remotekey = optional 19 | } 20 | ``` 21 | 22 | Note that you can also disable some default features: 23 | 24 | ```conf 25 | eclair.features { 26 | initial_routing_sync = disabled 27 | } 28 | ``` 29 | 30 | It's usually risky to activate non-default features or disable default features: make sure you fully understand a feature (and the current implementation status, detailed in the release notes) before doing so. 31 | 32 | Eclair supports per-peer features. Suppose you are connected to Alice and Bob, you can use a different set of features with Alice than the one you use with Bob. When experimenting with non-default features, we recommend using this to scope the peers you want to experiment with. 33 | 34 | This is done with the `override-features` configuration parameter in your `eclair.conf`: 35 | 36 | ```conf 37 | eclair.override-features = [ 38 | { 39 | nodeId = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" 40 | features { 41 | initial_routing_sync = disabled 42 | option_static_remotekey = optional 43 | } 44 | }, 45 | { 46 | nodeId = "" 47 | features { 48 | option_static_remotekey = optional 49 | option_support_large_channel = optional 50 | } 51 | }, 52 | ] 53 | ``` -------------------------------------------------------------------------------- /docs/Guides.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | 3 | This section contains how-to guides for more advanced scenarios: 4 | 5 | * [Customize Logging](./Logging.md) 6 | * [Customize Features](./Features.md) 7 | * [Manage Bitcoin Core's private keys](./ManagingBitcoinCoreKeys.md) 8 | * [Use Tor with Eclair](./Tor.md) 9 | * [Multipart Payments](./MultipartPayments.md) 10 | * [Monitoring Eclair](./Monitoring.md) 11 | * [PostgreSQL Configuration](./PostgreSQL.md) 12 | * [Perform Circular Rebalancing](./CircularRebalancing.md) 13 | * [Clusterize your Eclair node](./Cluster.md) 14 | * [Architecture of Eclair code](./Architecture.md) -------------------------------------------------------------------------------- /docs/Logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | ### Customize Logging 4 | 5 | Eclair uses [logback](https://logback.qos.ch/) for logging. 6 | By default the logging level is set to `INFO` for the whole application. 7 | To use a different configuration, and override the internal `logback.xml`, run: 8 | 9 | ```shell 10 | eclair-node.sh -Dlogback.configurationFile=/path/to/logback-custom.xml 11 | ``` 12 | 13 | If you want parts of the application to log at the `DEBUG` logging level, you will also need to override `akka.loglevel`: 14 | 15 | ```shell 16 | eclair-node.sh -Dakka.loglevel=DEBUG -Dlogback.configurationFile=/path/to/logback-custom.xml 17 | ``` 18 | 19 | You can find the default `logback.xml` file [here](https://github.com/ACINQ/eclair/blob/master/eclair-node/src/main/resources/logback.xml). It logs everything more serious than `INFO` to a rolling file. 20 | 21 | If you want to debug an issue, you can change the root logger's log level to `DEBUG`: 22 | 23 | ```xml 24 | 25 | 26 | 27 | ``` 28 | 29 | This setting will produce a lot of logs. If you are investigating an issue with payments, you can instead turn on `DEBUG` logging only for payments-related components by adding a new `logger` before the `root`: 30 | 31 | ```xml 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | On the contrary, if you want a component to emit less logs, you can set its log level to `WARN` or `ERROR`: 40 | 41 | ```xml 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | To figure out the `name` you should use for the `logger` element, you need to look at the hierarchy of the source code in Eclair's [Github repository](https://github.com/ACINQ/eclair). You can even configure loggers for each specific class you're interested in: 50 | 51 | ```xml 52 | 53 | 54 | 55 | 56 | 57 | 58 | ``` 59 | 60 | For more advanced use-cases, please see logback's [official documentation](https://logback.qos.ch/documentation.html). -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | /** 20 | * Created by t-bast on 11/01/2022. 21 | */ 22 | 23 | case class BlockHeight(private val underlying: Long) extends Ordered[BlockHeight] { 24 | // @formatter:off 25 | override def compare(other: BlockHeight): Int = underlying.compareTo(other.underlying) 26 | def +(i: Int) = BlockHeight(underlying + i) 27 | def +(l: Long) = BlockHeight(underlying + l) 28 | def -(i: Int) = BlockHeight(underlying - i) 29 | def -(l: Long) = BlockHeight(underlying - l) 30 | def -(other: BlockHeight): Long = underlying - other.underlying 31 | def unary_- = BlockHeight(-underlying) 32 | 33 | def max(other: BlockHeight): BlockHeight = if (this > other) this else other 34 | def min(other: BlockHeight): BlockHeight = if (this < other) this else other 35 | 36 | def toInt: Int = underlying.toInt 37 | def toLong: Long = underlying 38 | def toDouble: Double = underlying.toDouble 39 | 40 | override def toString() = underlying.toString 41 | // @formatter:on 42 | } 43 | 44 | object BlockHeight { 45 | def apply(underlying: Int): BlockHeight = BlockHeight(underlying.toLong) 46 | } -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import fr.acinq.eclair.channel.{ChannelDataWithCommitments, PersistentChannelData} 20 | import grizzled.slf4j.Logging 21 | 22 | import scala.util.{Failure, Success, Try} 23 | 24 | object DBChecker extends Logging { 25 | 26 | /** 27 | * Tests if the channels data in the DB is valid (and throws an exception if not): 28 | * - it is compatible with the current version of eclair 29 | * - channel keys can be re-generated from the channel seed 30 | */ 31 | def checkChannelsDB(nodeParams: NodeParams): Seq[PersistentChannelData] = { 32 | Try(nodeParams.db.channels.listLocalChannels()) match { 33 | case Success(channels) => 34 | channels.foreach { 35 | case data: ChannelDataWithCommitments => 36 | val channelKeys = nodeParams.channelKeyManager.channelKeys(data.channelParams.channelConfig, data.channelParams.localParams.fundingKeyPath) 37 | if (!data.commitments.validateSeed(channelKeys)) { 38 | throw InvalidChannelSeedException(data.channelId) 39 | } 40 | case _ => () 41 | } 42 | channels 43 | case Failure(_) => throw IncompatibleDBException 44 | } 45 | } 46 | 47 | /** 48 | * Tests if the network database is readable. 49 | */ 50 | def checkNetworkDB(nodeParams: NodeParams): Unit = 51 | Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes()) match { 52 | case Success(_) => () 53 | case Failure(_) => throw IncompatibleNetworkDBException 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/EncodedNodeId.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair 2 | 3 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 4 | 5 | /** Identifying information for a remote node, used in blinded paths and onion contents. */ 6 | sealed trait EncodedNodeId 7 | 8 | object EncodedNodeId { 9 | 10 | def apply(publicKey: PublicKey): EncodedNodeId = WithPublicKey.Plain(publicKey) 11 | 12 | /** For compactness, nodes may be identified by the shortChannelId of one of their public channels. */ 13 | case class ShortChannelIdDir(isNode1: Boolean, scid: RealShortChannelId) extends EncodedNodeId { 14 | override def toString: String = if (isNode1) s"<-$scid" else s"$scid->" 15 | } 16 | 17 | // @formatter:off 18 | sealed trait WithPublicKey extends EncodedNodeId { def publicKey: PublicKey } 19 | object WithPublicKey { 20 | /** Standard case where a node is identified by its public key. */ 21 | case class Plain(publicKey: PublicKey) extends WithPublicKey { override def toString: String = publicKey.toString } 22 | /** 23 | * Wallet nodes are not part of the public graph, and may not have channels yet. 24 | * Wallet providers are usually able to contact such nodes using push notifications or similar mechanisms. 25 | */ 26 | case class Wallet(publicKey: PublicKey) extends WithPublicKey { override def toString: String = publicKey.toString } 27 | } 28 | // @formatter:on 29 | 30 | } 31 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/FSMDiagnosticActorLogging.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import akka.actor.{Actor, FSM} 20 | import akka.event.{DiagnosticLoggingAdapter, LoggingAdapter} 21 | 22 | /** 23 | * A version of akka.actor.DiagnosticActorLogging compatible with an FSM 24 | * See https://groups.google.com/forum/#!topic/akka-user/0CxR8CImr4Q 25 | */ 26 | trait FSMDiagnosticActorLogging[S, D] extends FSM[S, D] { 27 | 28 | import akka.event.Logging._ 29 | 30 | val diagLog: DiagnosticLoggingAdapter = akka.event.Logging(this) 31 | 32 | def mdc(currentMessage: Any): MDC = emptyMDC 33 | 34 | override def log: LoggingAdapter = diagLog 35 | 36 | override def aroundReceive(receive: Actor.Receive, msg: Any): Unit = try { 37 | diagLog.mdc(mdc(msg)) 38 | super.aroundReceive(receive, msg) 39 | } finally { 40 | diagLog.clearMDC() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/Paginated.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | /** 20 | * Created by rorp on 03/11/2022. 21 | */ 22 | 23 | /** 24 | * Simple class for pagination database and API queries. 25 | */ 26 | case class Paginated(count: Int, skip: Int) { 27 | require(count >= 0, "count must be a positive number") 28 | require(skip >= 0, "skip must be a positive number") 29 | } 30 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/PimpKamon.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import kamon.metric.Timer 20 | 21 | import scala.concurrent.{ExecutionContext, Future} 22 | 23 | object KamonExt { 24 | 25 | def time[T](timer: Timer)(f: => T): T = { 26 | val started = timer.start() 27 | try { 28 | f 29 | } finally { 30 | started.stop() 31 | } 32 | } 33 | 34 | def timeFuture[T](timer: Timer)(f: => Future[T])(implicit ec: ExecutionContext): Future[T] = { 35 | val started = timer.start() 36 | val res = f 37 | res onComplete (_ => started.stop()) 38 | res 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/PortChecker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import java.net.{InetAddress, InetSocketAddress, ServerSocket} 20 | 21 | import scala.util.{Failure, Success, Try} 22 | 23 | object PortChecker { 24 | 25 | /** 26 | * Tests if a port is open 27 | * See https://stackoverflow.com/questions/434718/sockets-discover-port-availability-using-java#435579 28 | * 29 | * @return 30 | */ 31 | def checkAvailable(host: String, port: Int): Unit = checkAvailable(InetAddress.getByName(host), port) 32 | 33 | def checkAvailable(socketAddress: InetSocketAddress): Unit = checkAvailable(socketAddress.getAddress, socketAddress.getPort) 34 | 35 | def checkAvailable(address: InetAddress, port: Int): Unit = { 36 | Try(new ServerSocket(port, 50, address)) match { 37 | case Success(socket) => 38 | Try(socket.close()) 39 | case Failure(_) => 40 | throw TCPBindException(port) 41 | } 42 | } 43 | 44 | } 45 | 46 | case class TCPBindException(port: Int) extends RuntimeException(s"could not bind to port $port") -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/SimpleSupervisor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import akka.actor.{Actor, ActorLogging, OneForOneStrategy, Props, SupervisorStrategy} 20 | 21 | import scala.concurrent.duration._ 22 | 23 | /** 24 | * This supervisor will supervise a single child actor using the provided SupervisorStrategy 25 | * All incoming messages will be forwarded to the child actor. 26 | * 27 | * Created by PM on 17/03/2017. 28 | */ 29 | class SimpleSupervisor(childProps: Props, childName: String, strategy: SupervisorStrategy.Directive) extends Actor with ActorLogging { 30 | 31 | val child = context.actorOf(childProps, childName) 32 | 33 | override def receive: Receive = { 34 | case msg => child forward msg 35 | } 36 | 37 | // we allow at most within , otherwise the child actor is not restarted (this avoids restart loops) 38 | override val supervisorStrategy = OneForOneStrategy(loggingEnabled = false, maxNrOfRetries = 100, withinTimeRange = 1 minute) { 39 | case t => 40 | // log this as silent errors are dangerous 41 | log.error(t, s"supervisor caught error for child=$childName strategy=$strategy ") 42 | strategy 43 | } 44 | } 45 | 46 | object SimpleSupervisor { 47 | 48 | def props(childProps: Props, childName: String, strategy: SupervisorStrategy.Directive) = Props(new SimpleSupervisor(childProps, childName, strategy)) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/SubscriptionsComplete.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair 2 | 3 | /** 4 | * Since actors are initialized asynchronously, and the initialization sometimes involves subscribing to an 5 | * [[akka.event.EventStream]], we don't know when they are ready to process messages, especially in tests, which 6 | * leads to race conditions. 7 | * By making actors publish [[SubscriptionsComplete]] on the same [[akka.event.EventStream]] they are subscribing to, we guarantee 8 | * that if we receive [[SubscriptionsComplete]] the actor has been initialized and its subscriptions have been taken into account. 9 | */ 10 | case class SubscriptionsComplete(clazz: Class[_]) 11 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/UInt64.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import com.google.common.primitives.UnsignedLongs 20 | import scodec.bits.ByteVector 21 | import scodec.bits.HexStringSyntax 22 | 23 | case class UInt64(private val underlying: Long) extends Ordered[UInt64] { 24 | 25 | override def compare(o: UInt64): Int = UnsignedLongs.compare(underlying, o.underlying) 26 | private def compare(other: MilliSatoshi): Int = other.toLong match { 27 | case l if l < 0 => 1 // if @param 'other' is negative then is always smaller than 'this' 28 | case _ => compare(UInt64(other.toLong)) // we must do an unsigned comparison here because the uint64 can exceed the capacity of MilliSatoshi class 29 | } 30 | 31 | def <(other: MilliSatoshi): Boolean = compare(other) < 0 32 | def >(other: MilliSatoshi): Boolean = compare(other) > 0 33 | def <=(other: MilliSatoshi): Boolean = compare(other) <= 0 34 | def >=(other: MilliSatoshi): Boolean = compare(other) >= 0 35 | 36 | def toByteVector: ByteVector = ByteVector.fromLong(underlying) 37 | 38 | def toBigInt: BigInt = (BigInt(underlying >>> 1) << 1) + (underlying & 1) 39 | 40 | override def toString: String = UnsignedLongs.toString(underlying, 10) 41 | } 42 | 43 | object UInt64 { 44 | 45 | val MaxValue = UInt64(hex"0xffffffffffffffff") 46 | 47 | def apply(bin: ByteVector): UInt64 = UInt64(bin.toLong(signed = false)) 48 | 49 | object Conversions { 50 | 51 | implicit def intToUint64(l: Int): UInt64 = UInt64(l) 52 | 53 | implicit def longToUint64(l: Long): UInt64 = UInt64(l) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/balance/Monitoring.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.balance 18 | 19 | import kamon.Kamon 20 | import kamon.metric.Metric 21 | 22 | object Monitoring { 23 | 24 | object Metrics { 25 | val GlobalBalance: Metric.Gauge = Kamon.gauge("globalbalance", "Global Balance (BTC)") 26 | val GlobalBalanceNormalized: Metric.Gauge = Kamon.gauge("globalbalance.normalized", "Global Balance Normalized") 27 | val GlobalBalanceDiff: Metric.Gauge = Kamon.gauge("globalbalance.diff", "Global Balance Diff (Satoshi)") 28 | val GlobalBalanceDetailed: Metric.Gauge = Kamon.gauge("globalbalance.detailed", "Global Balance Detailed (BTC)") 29 | val BitcoinBalance: Metric.Gauge = Kamon.gauge("bitcoin.balance", "Bitcoin balance (mBTC)") 30 | val UtxoCount: Metric.Gauge = Kamon.gauge("bitcoin.utxo.count", "Number of unspent outputs available") 31 | } 32 | 33 | object Tags { 34 | val BalanceType = "type" 35 | val OffchainState = "state" 36 | val DiffSign = "sign" 37 | val UtxoStatus = "status" 38 | 39 | object BalanceTypes { 40 | val OnchainConfirmed = "onchain.confirmed" 41 | val OnchainUnconfirmed = "onchain.unconfirmed" 42 | val Offchain = "offchain" 43 | } 44 | 45 | object OffchainStates { 46 | val waitForFundingConfirmed = "waitForFundingConfirmed" 47 | val waitForChannelReady = "waitForChannelReady" 48 | val normal = "normal" 49 | val shutdown = "shutdown" 50 | val negotiating = "negotiating" 51 | val closingLocal = "closing-local" 52 | val closingRemote = "closing-remote" 53 | val closingUnknown = "closing-unknown" 54 | val waitForPublishFutureCommitment = "waitForPublishFutureCommitment" 55 | } 56 | 57 | /** we can't chart negative amounts in Kamon */ 58 | object DiffSigns { 59 | val plus = "plus" 60 | val minus = "minus" 61 | } 62 | 63 | object UtxoStatuses { 64 | val Confirmed = "confirmed" 65 | val Safe = "safe" 66 | val Unsafe = "unsafe" 67 | val Unconfirmed = "unconfirmed" 68 | } 69 | 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/BlockchainEvents.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain 18 | 19 | import fr.acinq.bitcoin.scalacompat.{BlockId, Transaction} 20 | import fr.acinq.eclair.BlockHeight 21 | import fr.acinq.eclair.blockchain.fee.FeeratesPerKw 22 | 23 | /** 24 | * Created by PM on 24/08/2016. 25 | */ 26 | 27 | sealed trait BlockchainEvent 28 | 29 | case class NewBlock(blockId: BlockId) extends BlockchainEvent 30 | 31 | case class NewTransaction(tx: Transaction) extends BlockchainEvent 32 | 33 | case class CurrentBlockHeight(blockHeight: BlockHeight) extends BlockchainEvent 34 | 35 | sealed trait CurrentFeerates extends BlockchainEvent { 36 | val feeratesPerKw: FeeratesPerKw 37 | } 38 | 39 | object CurrentFeerates { 40 | //@formatter:off 41 | case class BitcoinCore(feeratesPerKw: FeeratesPerKw) extends CurrentFeerates 42 | //@formatter:on 43 | } 44 | 45 | 46 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BatchingBitcoinJsonRPCClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.bitcoind.rpc 18 | 19 | import akka.actor.{ActorSystem, Props} 20 | import akka.pattern.ask 21 | import akka.util.Timeout 22 | import fr.acinq.bitcoin.scalacompat.BlockHash 23 | import fr.acinq.eclair.KamonExt 24 | import fr.acinq.eclair.blockchain.Monitoring.Metrics 25 | import org.json4s.JsonAST 26 | 27 | import scala.concurrent.duration._ 28 | import scala.concurrent.{ExecutionContext, Future} 29 | 30 | class BatchingBitcoinJsonRPCClient(rpcClient: BasicBitcoinJsonRPCClient)(implicit system: ActorSystem, ec: ExecutionContext) extends BitcoinJsonRPCClient { 31 | override def chainHash: BlockHash = rpcClient.chainHash 32 | override def wallet: Option[String] = rpcClient.wallet 33 | 34 | implicit val timeout: Timeout = Timeout(1 hour) 35 | 36 | val batchingClient = system.actorOf(Props(new BatchingClient(rpcClient)), name = "batching-client") 37 | 38 | override def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JsonAST.JValue] = { 39 | KamonExt.timeFuture(Metrics.RpcBatchInvokeDuration.withoutTags()) { 40 | (batchingClient ? JsonRPCRequest(method = method, params = params)).mapTo[JsonAST.JValue] 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinJsonRPCClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.bitcoind.rpc 18 | 19 | import fr.acinq.bitcoin.scalacompat.BlockHash 20 | import org.json4s.JsonAST.JValue 21 | 22 | import java.io.IOException 23 | import scala.concurrent.{ExecutionContext, Future} 24 | 25 | trait BitcoinJsonRPCClient { 26 | // @formatter:off 27 | def chainHash: BlockHash 28 | def wallet: Option[String] 29 | def invoke(method: String, params: Any*)(implicit ec: ExecutionContext): Future[JValue] 30 | // @formatter:on 31 | } 32 | 33 | // @formatter:off 34 | case class JsonRPCRequest(jsonrpc: String = "1.0", id: String = "scala-client", method: String, params: Seq[Any]) 35 | case class Error(code: Int, message: String) 36 | case class JsonRPCResponse(result: JValue, error: Option[Error], id: String) 37 | case class JsonRPCError(error: Error) extends IOException(s"${error.message} (code: ${error.code})") 38 | // @formatter:on -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/ConstantFeeProvider.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.fee 18 | 19 | import scala.concurrent.Future 20 | 21 | /** 22 | * Created by PM on 09/07/2017. 23 | */ 24 | case class ConstantFeeProvider(feerates: FeeratesPerKB) extends FeeProvider { 25 | 26 | override def getFeerates: Future[FeeratesPerKB] = Future.successful(feerates) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FallbackFeeProvider.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.fee 18 | 19 | import fr.acinq.bitcoin.scalacompat.SatoshiLong 20 | import grizzled.slf4j.Logging 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | 24 | /** 25 | * This provider will try all child providers in sequence, until one of them works 26 | * 27 | * @param providers a sequence of providers; they will be tried one after the others until one of them succeeds 28 | * @param minFeeratePerByte a configurable minimum value for feerates 29 | */ 30 | case class FallbackFeeProvider(providers: Seq[FeeProvider], minFeeratePerByte: FeeratePerByte)(implicit ec: ExecutionContext) extends FeeProvider with Logging { 31 | require(providers.nonEmpty, "need at least one fee provider") 32 | require(minFeeratePerByte.feerate > 0.sat, "minimum fee rate must be strictly greater than 0") 33 | 34 | def getFeerates(fallbacks: Seq[FeeProvider]): Future[FeeratesPerKB] = 35 | fallbacks match { 36 | case last +: Nil => last.getFeerates 37 | case head +: remaining => head.getFeerates.recoverWith { case error => logger.warn(s"$head failed, falling back to next fee provider", error); getFeerates(remaining) } 38 | } 39 | 40 | override def getFeerates: Future[FeeratesPerKB] = getFeerates(providers).map(FallbackFeeProvider.enforceMinimumFeerate(_, FeeratePerKB(minFeeratePerByte))) 41 | 42 | } 43 | 44 | object FallbackFeeProvider { 45 | 46 | private def enforceMinimumFeerate(feeratesPerKB: FeeratesPerKB, minFeeratePerKB: FeeratePerKB): FeeratesPerKB = FeeratesPerKB( 47 | minimum = feeratesPerKB.minimum.max(minFeeratePerKB), 48 | fastest = feeratesPerKB.fastest.max(minFeeratePerKB), 49 | fast = feeratesPerKB.fast.max(minFeeratePerKB), 50 | medium = feeratesPerKB.medium.max(minFeeratePerKB), 51 | slow = feeratesPerKB.slow.max(minFeeratePerKB) 52 | ) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/SmoothFeeProvider.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.fee 18 | 19 | import scala.concurrent.{ExecutionContext, Future} 20 | 21 | case class SmoothFeeProvider(provider: FeeProvider, windowSize: Int)(implicit ec: ExecutionContext) extends FeeProvider { 22 | require(windowSize > 0) 23 | 24 | var queue = List.empty[FeeratesPerKB] 25 | 26 | def append(rate: FeeratesPerKB): Unit = synchronized { 27 | queue = queue :+ rate 28 | if (queue.length > windowSize) queue = queue.drop(1) 29 | } 30 | 31 | override def getFeerates: Future[FeeratesPerKB] = { 32 | for { 33 | rate <- provider.getFeerates 34 | _ = append(rate) 35 | } yield SmoothFeeProvider.smooth(queue) 36 | } 37 | 38 | } 39 | 40 | object SmoothFeeProvider { 41 | 42 | def avg(i: Seq[FeeratePerKB]): FeeratePerKB = FeeratePerKB(i.map(_.feerate).sum / i.size) 43 | 44 | def smooth(rates: Seq[FeeratesPerKB]): FeeratesPerKB = 45 | FeeratesPerKB( 46 | minimum = avg(rates.map(_.minimum)), 47 | fastest = avg(rates.map(_.fastest)), 48 | fast = avg(rates.map(_.fast)), 49 | medium = avg(rates.map(_.medium)), 50 | slow = avg(rates.map(_.slow))) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/blockchain/watchdogs/Monitoring.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.watchdogs 18 | 19 | import kamon.Kamon 20 | 21 | /** 22 | * Created by t-bast on 29/09/2020. 23 | */ 24 | 25 | object Monitoring { 26 | 27 | object Metrics { 28 | val BitcoinBlocksSkew = Kamon.gauge("bitcoin.watchdog.blocks.skew", "Number of blocks we're missing compared to other blockchain sources") 29 | val WatchdogError = Kamon.counter("bitcoin.watchdog.error", "Number of watchdog errors") 30 | } 31 | 32 | object Tags { 33 | val Source = "source" 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.channel 18 | 19 | /** 20 | * Created by t-bast on 24/06/2021. 21 | */ 22 | 23 | /** 24 | * Internal configuration option impacting the channel's structure or behavior. 25 | * This must be set when creating the channel and cannot be changed afterwards. 26 | */ 27 | trait ChannelConfigOption { 28 | // @formatter:off 29 | def supportBit: Int 30 | def name: String 31 | // @formatter:on 32 | } 33 | 34 | case class ChannelConfig(options: Set[ChannelConfigOption]) { 35 | 36 | def hasOption(option: ChannelConfigOption): Boolean = options.contains(option) 37 | 38 | } 39 | 40 | object ChannelConfig { 41 | 42 | val standard: ChannelConfig = ChannelConfig(options = Set(FundingPubKeyBasedChannelKeyPath)) 43 | 44 | def apply(opts: ChannelConfigOption*): ChannelConfig = ChannelConfig(Set.from(opts)) 45 | 46 | /** 47 | * If set, the channel's BIP32 key path will be deterministically derived from the funding public key. 48 | * It makes it very easy to retrieve funds when channel data has been lost: 49 | * - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx 50 | * - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data 51 | * - recompute your channel keys and spend your output 52 | */ 53 | case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption { 54 | override val supportBit: Int = 0 55 | override val name: String = "funding_pubkey_based_channel_keypath" 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.crypto 18 | 19 | import fr.acinq.bitcoin.scalacompat.ByteVector32 20 | import org.bouncycastle.crypto.digests.SHA256Digest 21 | import org.bouncycastle.crypto.macs.HMac 22 | import org.bouncycastle.crypto.params.KeyParameter 23 | import scodec.bits.ByteVector 24 | 25 | /** 26 | * Created by t-bast on 04/07/19. 27 | */ 28 | 29 | /** 30 | * Create and verify message authentication codes. 31 | */ 32 | trait Mac32 { 33 | 34 | def mac(message: ByteVector): ByteVector32 35 | 36 | def verify(mac: ByteVector32, message: ByteVector): Boolean 37 | 38 | } 39 | 40 | case class Hmac256(key: ByteVector) extends Mac32 { 41 | 42 | override def mac(message: ByteVector): ByteVector32 = Mac32.hmac256(key, message) 43 | 44 | override def verify(mac: ByteVector32, message: ByteVector): Boolean = this.mac(message) == mac 45 | 46 | } 47 | 48 | object Mac32 { 49 | 50 | def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { 51 | val mac = new HMac(new SHA256Digest()) 52 | mac.init(new KeyParameter(key.toArray)) 53 | mac.update(message.toArray, 0, message.length.toInt) 54 | val output = new Array[Byte](32) 55 | mac.doFinal(output, 0) 56 | ByteVector32(ByteVector.view(output)) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/crypto/Monitoring.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.crypto 18 | 19 | import kamon.Kamon 20 | import kamon.metric.MeasurementUnit 21 | 22 | object Monitoring { 23 | 24 | object Metrics { 25 | val MessageSize = Kamon.histogram("messages.size", MeasurementUnit.information.bytes) 26 | } 27 | 28 | object Tags { 29 | val MessageDirection = "direction" 30 | 31 | object MessageDirections { 32 | val IN = "IN" 33 | val OUT = "OUT" 34 | } 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/NodeKeyManager.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.crypto.keymanager 2 | 3 | import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} 4 | import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, DeterministicWallet} 5 | import scodec.bits.ByteVector 6 | 7 | trait NodeKeyManager { 8 | def nodeKey: DeterministicWallet.ExtendedPrivateKey 9 | 10 | def nodeId: PublicKey 11 | 12 | /** 13 | * Sign a channel announcement message 14 | * 15 | * @param witness channel announcement message 16 | * @return the signature of the channel announcement with our node's private key 17 | */ 18 | def signChannelAnnouncement(witness: ByteVector): ByteVector64 19 | 20 | /** 21 | * Sign a digest, primarily used to prove ownership of the current node 22 | * 23 | * When recovering a public key from an ECDSA signature for secp256k1, there are 4 possible matching curve points 24 | * that can be found. The recoveryId identifies which of these points is the correct. 25 | * 26 | * @param digest SHA256 digest 27 | * @param privateKey private key to sign with, default the one from the current node 28 | * @return a (signature, recoveryId) pair. signature is a signature of the digest parameter generated with the 29 | * private key given in parameter. recoveryId is the corresponding recoveryId of the signature 30 | */ 31 | def signDigest(digest: ByteVector32, privateKey: PrivateKey = nodeKey.privateKey): (ByteVector64, Int) 32 | } 33 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/OnChainKeyManager.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.crypto.keymanager 18 | 19 | import fr.acinq.bitcoin.psbt.Psbt 20 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 21 | import fr.acinq.bitcoin.scalacompat.DeterministicWallet.KeyPath 22 | import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.{AddressType, Descriptors} 23 | 24 | import scala.util.Try 25 | 26 | trait OnChainKeyManager { 27 | def walletName: String 28 | 29 | /** 30 | * @param account account number (0 is used by most wallets) 31 | * @return the on-chain pubkey for this account, which can then be imported into a BIP39-compatible wallet such as Electrum 32 | */ 33 | def masterPubKey(account: Long, addressType: AddressType): String 34 | 35 | /** 36 | * @param keyPath BIP32 path 37 | * @return the (public key, address) pair for this BIP32 path starting from the master key 38 | */ 39 | def derivePublicKey(keyPath: KeyPath): (PublicKey, String) 40 | 41 | def derivePublicKey(keyPath: String): (PublicKey, String) = derivePublicKey(KeyPath(keyPath)) 42 | 43 | /** 44 | * @param account account number 45 | * @return a pair of (main, change) wallet descriptors that can be imported into an on-chain wallet 46 | */ 47 | def descriptors(account: Long): Descriptors 48 | 49 | /** 50 | * Sign the inputs provided in [[ourInputs]] and verifies that [[ourOutputs]] belong to our bitcoin wallet. 51 | * 52 | * @param psbt input psbt 53 | * @param ourInputs index of inputs that belong to our on-chain wallet and need to be signed 54 | * @param ourOutputs index of outputs that belong to our on-chain wallet 55 | * @return a signed psbt, where all our inputs are signed 56 | */ 57 | def sign(psbt: Psbt, ourInputs: Seq[Int], ourOutputs: Seq[Int]): Try[Psbt] 58 | } 59 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/ChannelsDb.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import fr.acinq.bitcoin.scalacompat.ByteVector32 20 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 21 | import fr.acinq.eclair.channel.PersistentChannelData 22 | import fr.acinq.eclair.db.DbEventHandler.ChannelEvent 23 | import fr.acinq.eclair.{CltvExpiry, Paginated} 24 | 25 | trait ChannelsDb { 26 | 27 | def addOrUpdateChannel(data: PersistentChannelData): Unit 28 | 29 | def getChannel(channelId: ByteVector32): Option[PersistentChannelData] 30 | 31 | def updateChannelMeta(channelId: ByteVector32, event: ChannelEvent.EventType): Unit 32 | 33 | /** Mark a channel as closed, but keep it in the DB. */ 34 | def removeChannel(channelId: ByteVector32): Unit 35 | 36 | /** Mark revoked HTLC information as obsolete. It will be removed from the DB once [[removeHtlcInfos]] is called. */ 37 | def markHtlcInfosForRemoval(channelId: ByteVector32, beforeCommitIndex: Long): Unit 38 | 39 | /** Remove up to batchSize obsolete revoked HTLC information. */ 40 | def removeHtlcInfos(batchSize: Int): Unit 41 | 42 | def listLocalChannels(): Seq[PersistentChannelData] 43 | 44 | def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] 45 | 46 | def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit 47 | 48 | def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)] 49 | } 50 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/Monitoring.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import fr.acinq.eclair.KamonExt 20 | import kamon.Kamon 21 | import kamon.metric.Metric 22 | 23 | object Monitoring { 24 | 25 | object Metrics { 26 | val FileBackupCompleted: Metric.Counter = Kamon.counter("db.file-backup.completed") 27 | val FileBackupDuration: Metric.Timer = Kamon.timer("db.file-backup.duration") 28 | 29 | private val DbOperation: Metric.Counter = Kamon.counter("db.operation.execute") 30 | private val DbOperationDuration: Metric.Timer = Kamon.timer("db.operation.duration") 31 | 32 | def withMetrics[T](name: String, backend: String)(operation: => T): T = KamonExt.time(DbOperationDuration.withTag(Tags.DbOperation, name).withTag(Tags.DbBackend, backend)) { 33 | DbOperation.withTag(Tags.DbOperation, name).withTag(Tags.DbBackend, backend).increment() 34 | operation 35 | } 36 | } 37 | 38 | object Tags { 39 | val DbOperation = "operation" 40 | val DbBackend = "backend" 41 | 42 | object DbBackends { 43 | val Sqlite = "sqlite" 44 | val Postgres = "postgres" 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 20 | import fr.acinq.bitcoin.scalacompat.{Satoshi, TxId} 21 | import fr.acinq.eclair.router.Router.PublicChannel 22 | import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} 23 | import fr.acinq.eclair.{RealShortChannelId, ShortChannelId} 24 | 25 | import scala.collection.immutable.SortedMap 26 | 27 | trait NetworkDb { 28 | 29 | def addNode(n: NodeAnnouncement): Unit 30 | 31 | def updateNode(n: NodeAnnouncement): Unit 32 | 33 | def getNode(nodeId: PublicKey): Option[NodeAnnouncement] 34 | 35 | def removeNode(nodeId: PublicKey): Unit 36 | 37 | def listNodes(): Seq[NodeAnnouncement] 38 | 39 | def addChannel(c: ChannelAnnouncement, txid: TxId, capacity: Satoshi): Unit 40 | 41 | def updateChannel(u: ChannelUpdate): Unit 42 | 43 | def removeChannel(shortChannelId: ShortChannelId): Unit = removeChannels(Set(shortChannelId)) 44 | 45 | def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit 46 | 47 | def getChannel(shortChannelId: RealShortChannelId): Option[PublicChannel] 48 | 49 | def listChannels(): SortedMap[RealShortChannelId, PublicChannel] 50 | 51 | } 52 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import fr.acinq.bitcoin.scalacompat.ByteVector32 20 | import fr.acinq.eclair.TimestampMilli 21 | import fr.acinq.eclair.wire.protocol.OfferTypes.Offer 22 | 23 | /** 24 | * Database for offers fully managed by eclair, as opposed to offers managed by a plugin. 25 | */ 26 | trait OffersDb { 27 | /** 28 | * Add an offer managed by eclair. 29 | * 30 | * @param pathId_opt If the offer uses a blinded path, this is the corresponding pathId. 31 | */ 32 | def addOffer(offer: Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Option[OfferData] 33 | 34 | /** 35 | * Disable an offer. The offer is still stored but new invoice requests and new payment attempts for already emitted 36 | * invoices will be rejected. 37 | */ 38 | def disableOffer(offer: Offer, disabledAt: TimestampMilli = TimestampMilli.now()): Unit 39 | 40 | /** 41 | * List offers managed by eclair. 42 | * 43 | * @param onlyActive Whether to return only active offers or also disabled ones. 44 | */ 45 | def listOffers(onlyActive: Boolean): Seq[OfferData] 46 | } 47 | 48 | case class OfferData(offer: Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli, disabledAt_opt: Option[TimestampMilli]) { 49 | val disabled: Boolean = disabledAt_opt.nonEmpty 50 | } 51 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/PeerStorageCleaner.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import akka.actor.typed.Behavior 20 | import akka.actor.typed.scaladsl.Behaviors 21 | import fr.acinq.eclair.{PeerStorageConfig, TimestampSecond} 22 | 23 | /** 24 | * This actor frequently deletes from our DB peer storage from nodes with whom we don't have channels anymore, after a 25 | * grace period. 26 | */ 27 | object PeerStorageCleaner { 28 | // @formatter:off 29 | sealed trait Command 30 | private case object CleanPeerStorage extends Command 31 | // @formatter:on 32 | 33 | def apply(db: PeersDb, config: PeerStorageConfig): Behavior[Command] = { 34 | Behaviors.withTimers { timers => 35 | timers.startTimerWithFixedDelay(CleanPeerStorage, config.cleanUpFrequency) 36 | Behaviors.receiveMessage { 37 | case CleanPeerStorage => 38 | db.removePeerStorage(TimestampSecond.now() - config.removalDelay) 39 | Behaviors.same 40 | } 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 20 | import fr.acinq.eclair.payment.relay.Relayer.RelayFees 21 | import fr.acinq.eclair.wire.protocol.{NodeAddress, NodeInfo} 22 | import fr.acinq.eclair.{Features, InitFeature, TimestampSecond} 23 | import scodec.bits.ByteVector 24 | 25 | /** The PeersDb contains information about our direct peers, with whom we have or had channels. */ 26 | trait PeersDb { 27 | 28 | /** Update our DB with a verified address and features for the given peer. */ 29 | def addOrUpdatePeer(nodeId: PublicKey, address: NodeAddress, features: Features[InitFeature]): Unit 30 | 31 | /** Update our DB with the features for the given peer, without updating its address. */ 32 | def addOrUpdatePeerFeatures(nodeId: PublicKey, features: Features[InitFeature]): Unit 33 | 34 | def removePeer(nodeId: PublicKey): Unit 35 | 36 | def getPeer(nodeId: PublicKey): Option[NodeInfo] 37 | 38 | def listPeers(): Map[PublicKey, NodeInfo] 39 | 40 | def addOrUpdateRelayFees(nodeId: PublicKey, fees: RelayFees): Unit 41 | 42 | def getRelayFees(nodeId: PublicKey): Option[RelayFees] 43 | 44 | /** Update our peer's blob data when [[fr.acinq.eclair.Features.ProvideStorage]] is enabled. */ 45 | def updateStorage(nodeId: PublicKey, data: ByteVector): Unit 46 | 47 | /** Get the last blob of data we stored for that peer, if [[fr.acinq.eclair.Features.ProvideStorage]] is enabled. */ 48 | def getStorage(nodeId: PublicKey): Option[ByteVector] 49 | 50 | /** Remove storage from peers that have had no active channel with us for a while. */ 51 | def removePeerStorage(peerRemovedBefore: TimestampSecond): Unit 52 | 53 | } 54 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/RevokedHtlcInfoCleaner.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import akka.actor.typed.Behavior 20 | import akka.actor.typed.eventstream.EventStream 21 | import akka.actor.typed.scaladsl.Behaviors 22 | import fr.acinq.bitcoin.scalacompat.ByteVector32 23 | 24 | import scala.concurrent.duration.FiniteDuration 25 | 26 | /** 27 | * When a channel is closed or a splice transaction confirms, we can remove the information about old HTLCs that was 28 | * stored in the DB to punish revoked commitments. We potentially have millions of rows to delete per channel, and there 29 | * is no rush to remove them. We don't want this to negatively impact active channels, so this actor deletes that data 30 | * in small batches, at regular intervals. 31 | */ 32 | object RevokedHtlcInfoCleaner { 33 | 34 | // @formatter:off 35 | sealed trait Command 36 | case class ForgetHtlcInfos(channelId: ByteVector32, beforeCommitIndex: Long) extends Command 37 | private case object DeleteBatch extends Command 38 | // @formatter:on 39 | 40 | case class Config(batchSize: Int, interval: FiniteDuration) 41 | 42 | def apply(db: ChannelsDb, config: Config): Behavior[Command] = { 43 | Behaviors.setup { context => 44 | context.system.eventStream ! EventStream.Subscribe(context.self) 45 | Behaviors.withTimers { timers => 46 | timers.startTimerWithFixedDelay(DeleteBatch, config.interval) 47 | Behaviors.receiveMessage { 48 | case ForgetHtlcInfos(channelId, beforeCommitIndex) => 49 | db.markHtlcInfosForRemoval(channelId, beforeCommitIndex) 50 | Behaviors.same 51 | case DeleteBatch => 52 | db.removeHtlcInfos(config.batchSize) 53 | Behaviors.same 54 | } 55 | } 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/migration/CompareNetworkDb.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.db.migration 2 | 3 | import fr.acinq.eclair.db.migration.CompareDb._ 4 | import scodec.bits.ByteVector 5 | 6 | import java.sql.{Connection, ResultSet} 7 | 8 | object CompareNetworkDb { 9 | 10 | private def compareNodesTable(conn1: Connection, conn2: Connection): Boolean = { 11 | val table1 = "nodes" 12 | val table2 = "network.nodes" 13 | 14 | def hash1(rs: ResultSet): ByteVector = { 15 | bytes(rs, "node_id") ++ 16 | bytes(rs, "data") 17 | } 18 | 19 | def hash2(rs: ResultSet): ByteVector = { 20 | hex(rs, "node_id") ++ 21 | bytes(rs, "data") 22 | } 23 | 24 | compareTable(conn1, conn2, table1, table2, hash1, hash2) 25 | } 26 | 27 | private def compareChannelsTable(conn1: Connection, conn2: Connection): Boolean = { 28 | val table1 = "channels" 29 | val table2 = "network.public_channels" 30 | 31 | def hash1(rs: ResultSet): ByteVector = { 32 | long(rs, "short_channel_id") ++ 33 | string(rs, "txid") ++ 34 | bytes(rs, "channel_announcement") ++ 35 | long(rs, "capacity_sat") ++ 36 | bytesnull(rs, "channel_update_1") ++ 37 | bytesnull(rs, "channel_update_2") 38 | } 39 | 40 | def hash2(rs: ResultSet): ByteVector = { 41 | long(rs, "short_channel_id") ++ 42 | string(rs, "txid") ++ 43 | bytes(rs, "channel_announcement") ++ 44 | long(rs, "capacity_sat") ++ 45 | bytesnull(rs, "channel_update_1") ++ 46 | bytesnull(rs, "channel_update_2") 47 | } 48 | 49 | compareTable(conn1, conn2, table1, table2, hash1, hash2) 50 | } 51 | 52 | private def comparePrunedTable(conn1: Connection, conn2: Connection): Boolean = { 53 | val table1 = "pruned" 54 | val table2 = "network.pruned_channels" 55 | 56 | def hash1(rs: ResultSet): ByteVector = { 57 | long(rs, "short_channel_id") 58 | } 59 | 60 | def hash2(rs: ResultSet): ByteVector = { 61 | long(rs, "short_channel_id") 62 | } 63 | 64 | compareTable(conn1, conn2, table1, table2, hash1, hash2) 65 | } 66 | 67 | def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { 68 | compareNodesTable(conn1, conn2) && 69 | compareChannelsTable(conn1, conn2) && 70 | comparePrunedTable(conn1, conn2) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePeersDb.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.db.migration 2 | 3 | import fr.acinq.eclair.db.migration.CompareDb._ 4 | import scodec.bits.ByteVector 5 | 6 | import java.sql.{Connection, ResultSet} 7 | 8 | object ComparePeersDb { 9 | 10 | private def comparePeersTable(conn1: Connection, conn2: Connection): Boolean = { 11 | val table1 = "peers" 12 | val table2 = "local.peers" 13 | 14 | def hash1(rs: ResultSet): ByteVector = { 15 | bytes(rs, "node_id") ++ 16 | bytes(rs, "data") 17 | } 18 | 19 | def hash2(rs: ResultSet): ByteVector = { 20 | hex(rs, "node_id") ++ 21 | bytes(rs, "data") 22 | } 23 | 24 | compareTable(conn1, conn2, table1, table2, hash1, hash2) 25 | } 26 | 27 | private def compareRelayFeesTable(conn1: Connection, conn2: Connection): Boolean = { 28 | val table1 = "relay_fees" 29 | val table2 = "local.relay_fees" 30 | 31 | def hash1(rs: ResultSet): ByteVector = { 32 | bytes(rs, "node_id") ++ 33 | long(rs, "fee_base_msat") ++ 34 | long(rs, "fee_proportional_millionths") 35 | } 36 | 37 | def hash2(rs: ResultSet): ByteVector = { 38 | hex(rs, "node_id") ++ 39 | long(rs, "fee_base_msat") ++ 40 | long(rs, "fee_proportional_millionths") 41 | } 42 | 43 | compareTable(conn1, conn2, table1, table2, hash1, hash2) 44 | } 45 | 46 | def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { 47 | comparePeersTable(conn1, conn2) && 48 | compareRelayFeesTable(conn1, conn2) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/migration/ComparePendingCommandsDb.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.db.migration 2 | 3 | import fr.acinq.eclair.db.migration.CompareDb._ 4 | import scodec.bits.ByteVector 5 | 6 | import java.sql.{Connection, ResultSet} 7 | 8 | object ComparePendingCommandsDb { 9 | 10 | private def comparePendingSettlementCommandsTable(conn1: Connection, conn2: Connection): Boolean = { 11 | val table1 = "pending_settlement_commands" 12 | val table2 = "local.pending_settlement_commands" 13 | 14 | def hash1(rs: ResultSet): ByteVector = { 15 | bytes(rs, "channel_id") ++ 16 | long(rs, "htlc_id") ++ 17 | bytes(rs, "data") 18 | } 19 | 20 | def hash2(rs: ResultSet): ByteVector = { 21 | hex(rs, "channel_id") ++ 22 | long(rs, "htlc_id") ++ 23 | bytes(rs, "data") 24 | } 25 | 26 | compareTable(conn1, conn2, table1, table2, hash1, hash2) 27 | } 28 | 29 | def compareAllTables(conn1: Connection, conn2: Connection): Boolean = { 30 | comparePendingSettlementCommandsTable(conn1, conn2) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigrateDb.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.db.migration 2 | 3 | import fr.acinq.eclair.db.Databases.{PostgresDatabases, SqliteDatabases} 4 | import fr.acinq.eclair.db.DualDatabases 5 | import fr.acinq.eclair.db.jdbc.JdbcUtils 6 | import fr.acinq.eclair.db.jdbc.JdbcUtils.using 7 | import fr.acinq.eclair.db.pg.PgUtils 8 | import grizzled.slf4j.Logging 9 | 10 | import java.sql.{Connection, PreparedStatement, ResultSet} 11 | 12 | object MigrateDb extends Logging { 13 | 14 | private def getVersion(conn: Connection, dbName: String): Int = { 15 | using(conn.prepareStatement(s"SELECT version FROM versions WHERE db_name='$dbName'")) { statement => 16 | val res = statement.executeQuery() 17 | res.next() 18 | res.getInt("version") 19 | } 20 | } 21 | 22 | def checkVersions(source: Connection, 23 | destination: Connection, 24 | dbName: String, 25 | expectedSourceVersion: Int, 26 | expectedDestinationVersion: Int): Unit = { 27 | val actualSourceVersion = getVersion(source, dbName) 28 | val actualDestinationVersion = getVersion(destination, dbName) 29 | require(actualSourceVersion == expectedSourceVersion, s"unexpected version for source db=$dbName expected=$expectedSourceVersion actual=$actualSourceVersion") 30 | require(actualDestinationVersion == expectedDestinationVersion, s"unexpected version for destination db=$dbName expected=$expectedDestinationVersion actual=$actualDestinationVersion") 31 | } 32 | 33 | def migrateTable(source: Connection, 34 | destination: Connection, 35 | sourceTable: String, 36 | insertSql: String, 37 | migrate: (ResultSet, PreparedStatement) => Unit): Int = 38 | JdbcUtils.migrateTable(source, destination, sourceTable, insertSql, migrate)(logger) 39 | 40 | def migrateAll(dualDatabases: DualDatabases): Unit = { 41 | logger.info("migrating all tables...") 42 | val (sqliteDb: SqliteDatabases, postgresDb: PostgresDatabases) = DualDatabases.getDatabases(dualDatabases) 43 | PgUtils.inTransaction { postgres => 44 | MigrateChannelsDb.migrateAllTables(sqliteDb.channels.sqlite, postgres) 45 | MigratePendingCommandsDb.migrateAllTables(sqliteDb.pendingCommands.sqlite, postgres) 46 | MigratePeersDb.migrateAllTables(sqliteDb.peers.sqlite, postgres) 47 | MigratePaymentsDb.migrateAllTables(sqliteDb.payments.sqlite, postgres) 48 | MigrateNetworkDb.migrateAllTables(sqliteDb.network.sqlite, postgres) 49 | MigrateAuditDb.migrateAllTables(sqliteDb.audit.sqlite, postgres) 50 | logger.info("migration complete") 51 | }(postgresDb.dataSource) 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePeersDb.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.db.migration 2 | 3 | import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ 4 | import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} 5 | 6 | import java.sql.{Connection, PreparedStatement, ResultSet} 7 | 8 | object MigratePeersDb { 9 | 10 | private def migratePeersTable(source: Connection, destination: Connection): Int = { 11 | val sourceTable = "peers" 12 | val insertSql = "INSERT INTO local.peers (node_id, data) VALUES (?, ?)" 13 | 14 | def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { 15 | insertStatement.setString(1, rs.getByteVector("node_id").toHex) 16 | insertStatement.setBytes(2, rs.getBytes("data")) 17 | } 18 | 19 | migrateTable(source, destination, sourceTable, insertSql, migrate) 20 | } 21 | 22 | private def migrateRelayFeesTable(source: Connection, destination: Connection): Int = { 23 | val sourceTable = "relay_fees" 24 | val insertSql = "INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) VALUES (?, ?, ?)" 25 | 26 | def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { 27 | insertStatement.setString(1, rs.getByteVector("node_id").toHex) 28 | insertStatement.setLong(2, rs.getLong("fee_base_msat")) 29 | insertStatement.setLong(3, rs.getLong("fee_proportional_millionths")) 30 | } 31 | 32 | migrateTable(source, destination, sourceTable, insertSql, migrate) 33 | } 34 | 35 | def migrateAllTables(source: Connection, destination: Connection): Unit = { 36 | checkVersions(source, destination, "peers", 2, 3) 37 | migratePeersTable(source, destination) 38 | migrateRelayFeesTable(source, destination) 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/db/migration/MigratePendingCommandsDb.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.db.migration 2 | 3 | import fr.acinq.eclair.db.jdbc.JdbcUtils.ExtendedResultSet._ 4 | import fr.acinq.eclair.db.migration.MigrateDb.{checkVersions, migrateTable} 5 | 6 | import java.sql.{Connection, PreparedStatement, ResultSet} 7 | 8 | object MigratePendingCommandsDb { 9 | 10 | private def migratePendingSettlementCommandsTable(source: Connection, destination: Connection): Int = { 11 | val sourceTable = "pending_settlement_commands" 12 | val insertSql = "INSERT INTO local.pending_settlement_commands (channel_id, htlc_id, data) VALUES (?, ?, ?)" 13 | 14 | def migrate(rs: ResultSet, insertStatement: PreparedStatement): Unit = { 15 | insertStatement.setString(1, rs.getByteVector("channel_id").toHex) 16 | insertStatement.setLong(2, rs.getLong("htlc_id")) 17 | insertStatement.setBytes(3, rs.getBytes("data")) 18 | } 19 | 20 | migrateTable(source, destination, sourceTable, insertSql, migrate) 21 | } 22 | 23 | def migrateAllTables(source: Connection, destination: Connection): Unit = { 24 | checkVersions(source, destination, "pending_relay", 2, 3) 25 | migratePendingSettlementCommandsTable(source, destination) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/io/NodeURI.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.io 18 | 19 | import com.google.common.net.HostAndPort 20 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 21 | import fr.acinq.eclair.wire.protocol.NodeAddress 22 | import scodec.bits.ByteVector 23 | 24 | import scala.util.{Failure, Success, Try} 25 | 26 | case class NodeURI(nodeId: PublicKey, address: NodeAddress) { 27 | override def toString: String = s"$nodeId@$address" 28 | } 29 | 30 | object NodeURI { 31 | 32 | val DEFAULT_PORT = 9735 33 | 34 | /** 35 | * Extracts the PublicKey and InetAddress from a string URI (format pubkey@host:port). Port is optional, default is 9735. 36 | * 37 | * @param uri uri of a node, as a String 38 | * @throws IllegalArgumentException if the uri is not valid and can not be read 39 | * @return a NodeURI 40 | */ 41 | @throws[IllegalArgumentException] 42 | def parse(uri: String): NodeURI = { 43 | uri.split("@") match { 44 | case Array(nodeId, address) => (Try(PublicKey(ByteVector.fromValidHex(nodeId))), Try(HostAndPort.fromString(address)).flatMap(hostAndPort => NodeAddress.fromParts(hostAndPort.getHost, hostAndPort.getPortOrDefault(DEFAULT_PORT)))) match { 45 | case (Success(pk), Success(nodeAddress)) => NodeURI(pk, nodeAddress) 46 | case (Failure(t), _) => throw new IllegalArgumentException("Invalid node id", t) 47 | case (_, Failure(t)) => throw new IllegalArgumentException("Invalid host:port", t) 48 | } 49 | case _ => throw new IllegalArgumentException("Invalid uri, should be nodeId@host:port") 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/io/PeerEvents.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.io 18 | 19 | import akka.actor.ActorRef 20 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 21 | import fr.acinq.eclair.wire.protocol 22 | import fr.acinq.eclair.wire.protocol.{NodeAddress, UnknownMessage} 23 | 24 | import scala.concurrent.duration._ 25 | 26 | sealed trait PeerEvent 27 | 28 | case class PeerCreated(peer: ActorRef, nodeId: PublicKey) extends PeerEvent 29 | 30 | case class ConnectionInfo(address: NodeAddress, peerConnection: ActorRef, localInit: protocol.Init, remoteInit: protocol.Init) 31 | 32 | case class PeerConnected(peer: ActorRef, nodeId: PublicKey, connectionInfo: ConnectionInfo) extends PeerEvent 33 | 34 | case class PeerDisconnected(peer: ActorRef, nodeId: PublicKey) extends PeerEvent 35 | 36 | case class LastChannelClosed(peer: ActorRef, nodeId: PublicKey) extends PeerEvent 37 | 38 | case class PongReceived(nodeId: PublicKey, latency: FiniteDuration) extends PeerEvent 39 | 40 | case class UnknownMessageReceived(peer: ActorRef, nodeId: PublicKey, message: UnknownMessage, connectionInfo: ConnectionInfo) extends PeerEvent 41 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/io/RateLimiter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.io 18 | 19 | /** 20 | * @param qps maximum number or messages allowed per second 21 | * This class is not thread-safe, the rate limiter must not be shared between actors. 22 | */ 23 | class RateLimiter(qps: Int) { 24 | var i = 0 25 | val accepted: Array[Long] = Array.fill(qps)(0) 26 | 27 | /** 28 | * [[tryAcquire()]] is guaranteed to return `true` at most `qps` times per second. 29 | */ 30 | def tryAcquire(): Boolean = { 31 | val now = System.currentTimeMillis() 32 | if (now - accepted(i) > 1000) { 33 | accepted(i) = now 34 | i = (i + 1) % qps 35 | true 36 | } else { 37 | false 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/payment/receive/ForwardHandler.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.payment.receive 18 | 19 | import akka.actor.Actor.Receive 20 | import akka.actor.{ActorContext, ActorRef} 21 | import akka.event.DiagnosticLoggingAdapter 22 | 23 | /** 24 | * Simple handler that forwards all messages to an actor 25 | */ 26 | class ForwardHandler(actor: ActorRef) extends ReceiveHandler { 27 | override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = { 28 | case msg => actor forward msg 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/payment/send/PaymentError.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.payment.send 18 | 19 | import fr.acinq.eclair.{Features, InvoiceFeature} 20 | 21 | sealed trait PaymentError extends Throwable 22 | 23 | object PaymentError { 24 | 25 | // @formatter:off 26 | sealed trait InvalidInvoice extends PaymentError 27 | /** The invoice contains a feature we don't support. */ 28 | case class UnsupportedFeatures(features: Features[InvoiceFeature]) extends InvalidInvoice { override def getMessage: String = s"unsupported invoice features: ${features.toByteVector.toHex}" } 29 | // @formatter:on 30 | 31 | // @formatter:off 32 | /** Payment attempts exhausted without success. */ 33 | case object RetryExhausted extends PaymentError { override def getMessage: String = "payment attempts exhausted without success" } 34 | // @formatter:on 35 | 36 | } 37 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/remote/LightningMessageSerializer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.remote 18 | 19 | import fr.acinq.eclair.wire.protocol.LightningMessageCodecs.lightningMessageCodecWithFallback 20 | 21 | class LightningMessageSerializer extends ScodecSerializer(42, lightningMessageCodecWithFallback) -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/remote/ScodecSerializer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.remote 18 | 19 | import java.nio.ByteBuffer 20 | 21 | import akka.serialization.{ByteBufferSerializer, SerializerWithStringManifest} 22 | import scodec.Codec 23 | import scodec.bits.BitVector 24 | 25 | class ScodecSerializer[T <: AnyRef](override val identifier: Int, codec: Codec[T]) extends SerializerWithStringManifest with ByteBufferSerializer { 26 | 27 | override def toBinary(o: AnyRef, buf: ByteBuffer): Unit = buf.put(toBinary(o)) 28 | 29 | override def fromBinary(buf: ByteBuffer, manifest: String): AnyRef = { 30 | val bytes = Array.ofDim[Byte](buf.remaining()) 31 | buf.get(bytes) 32 | fromBinary(bytes, manifest) 33 | } 34 | 35 | /** we don't rely on the manifest to provide backward compatibility, we will use dedicated codecs instead */ 36 | override def manifest(o: AnyRef): String = fr.acinq.eclair.getSimpleClassName(o) 37 | 38 | override def toBinary(o: AnyRef): Array[Byte] = codec.encode(o.asInstanceOf[T]).require.toByteArray 39 | 40 | override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef = codec.decode(BitVector(bytes)).require.value 41 | } 42 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/router/NetworkEvents.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.router 18 | 19 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 20 | import fr.acinq.bitcoin.scalacompat.Satoshi 21 | import fr.acinq.eclair.ShortChannelId 22 | import fr.acinq.eclair.RealShortChannelId 23 | import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes 24 | import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAnnouncement} 25 | 26 | /** 27 | * Created by PM on 02/02/2017. 28 | */ 29 | trait NetworkEvent extends RemoteTypes 30 | 31 | case class NodesDiscovered(ann: Iterable[NodeAnnouncement]) extends NetworkEvent 32 | 33 | case class NodeUpdated(ann: NodeAnnouncement) extends NetworkEvent 34 | 35 | case class NodeLost(nodeId: PublicKey) extends NetworkEvent 36 | 37 | case class SingleChannelDiscovered(ann: ChannelAnnouncement, capacity: Satoshi, u1_opt: Option[ChannelUpdate], u2_opt: Option[ChannelUpdate]) 38 | 39 | case class ChannelsDiscovered(c: Iterable[SingleChannelDiscovered]) extends NetworkEvent 40 | 41 | case class ChannelLost(shortChannelId: RealShortChannelId) extends NetworkEvent 42 | 43 | case class ChannelUpdatesReceived(ann: Iterable[ChannelUpdate]) extends NetworkEvent 44 | 45 | case class SyncProgress(progress: Double) extends NetworkEvent 46 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/router/PathFindingExperimentConf.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.router 2 | 3 | import fr.acinq.eclair.router.Router.PathFindingConf 4 | 5 | import scala.util.Random 6 | 7 | case class PathFindingExperimentConf(experiments: Map[String, PathFindingConf]) { 8 | private val confByPercentile: Array[PathFindingConf] = experiments.values.flatMap(e => Array.fill(e.experimentPercentage)(e)).toArray 9 | 10 | require(confByPercentile.length == 100, "All experiments percentages must sum to 100.") 11 | 12 | private val rng = new Random() 13 | 14 | def getRandomConf(): PathFindingConf = { 15 | confByPercentile(rng.nextInt(100)) 16 | } 17 | 18 | def getByName(name: String): Option[PathFindingConf] = { 19 | experiments.get(name) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/router/RouterExceptions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.router 18 | 19 | /** 20 | * Created by PM on 12/04/2017. 21 | */ 22 | 23 | class RouterException(message: String) extends RuntimeException(message) 24 | 25 | object RouteNotFound extends RouterException("route not found") 26 | 27 | object BalanceTooLow extends RouterException("balance too low") 28 | 29 | object CannotRouteToSelf extends RouterException("cannot route to self") 30 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/wire/Monitoring.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.wire 18 | 19 | import kamon.Kamon 20 | 21 | object Monitoring { 22 | 23 | object Metrics { 24 | val DecodeDuration = Kamon.timer("scodec.decode.time") 25 | val EncodeDuration = Kamon.timer("scodec.encode.time") 26 | } 27 | 28 | object Tags { 29 | val MessageType = "type" 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnTheFlyFundingTlv.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.wire.protocol 18 | 19 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 20 | import fr.acinq.eclair.UInt64 21 | import fr.acinq.eclair.wire.protocol.CommonCodecs.{publicKey, varint} 22 | import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream 23 | import scodec.Codec 24 | import scodec.bits.HexStringSyntax 25 | import scodec.codecs._ 26 | 27 | /** 28 | * Created by t-bast on 07/06/2024. 29 | */ 30 | 31 | sealed trait WillAddHtlcTlv extends Tlv 32 | 33 | object WillAddHtlcTlv { 34 | /** Path key that should be used to derive shared secrets when using route blinding. */ 35 | case class PathKey(publicKey: PublicKey) extends WillAddHtlcTlv 36 | 37 | private val pathKey: Codec[PathKey] = (("length" | constant(hex"21")) :: ("pathKey" | publicKey)).as[PathKey] 38 | 39 | val willAddHtlcTlvCodec: Codec[TlvStream[WillAddHtlcTlv]] = tlvStream(discriminated[WillAddHtlcTlv].by(varint) 40 | .typecase(UInt64(0), pathKey) 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionMessageTlv.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.wire.protocol 18 | 19 | import fr.acinq.eclair.wire.protocol.CommonCodecs.varint 20 | import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream 21 | import scodec.Codec 22 | import scodec.codecs.discriminated 23 | 24 | /** 25 | * Created by thomash on 10/09/2021. 26 | */ 27 | 28 | sealed trait OnionMessageTlv extends Tlv 29 | 30 | object OnionMessageTlv { 31 | val onionMessageTlvCodec: Codec[TlvStream[OnionMessageTlv]] = tlvStream(discriminated[OnionMessageTlv].by(varint)) 32 | } 33 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/OnionRouting.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.wire.protocol 18 | 19 | import fr.acinq.bitcoin.scalacompat.ByteVector32 20 | import fr.acinq.eclair.UInt64 21 | import fr.acinq.eclair.wire.protocol.CommonCodecs.bytes32 22 | import scodec.Codec 23 | import scodec.bits.{BitVector, ByteVector} 24 | import scodec.codecs._ 25 | 26 | /** 27 | * Created by t-bast on 05/07/2019. 28 | */ 29 | 30 | case class OnionRoutingPacket(version: Int, publicKey: ByteVector, payload: ByteVector, hmac: ByteVector32) 31 | 32 | object OnionRoutingCodecs { 33 | 34 | // @formatter:off 35 | sealed trait InvalidTlvPayload { 36 | def tag: UInt64 37 | def failureMessage: FailureMessage = InvalidOnionPayload(tag, 0) 38 | } 39 | case class MissingRequiredTlv(tag: UInt64) extends InvalidTlvPayload 40 | case class ForbiddenTlv(tag: UInt64) extends InvalidTlvPayload 41 | // @formatter:on 42 | 43 | def onionRoutingPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( 44 | ("version" | uint8) :: 45 | ("publicKey" | bytes(33)) :: 46 | ("onionPayload" | bytes(payloadLength)) :: 47 | ("hmac" | bytes32)).as[OnionRoutingPacket] 48 | 49 | /** 50 | * This codec encodes onion packets of variable sizes, and decodes the whole input byte stream into an onion packet. 51 | * When decoding, the caller must ensure that they provide only the bytes that contain the onion packet. 52 | */ 53 | val variableSizeOnionRoutingPacketCodec: Codec[OnionRoutingPacket] = ( 54 | ("version" | uint8) :: 55 | ("publicKey" | bytes(33)) :: 56 | ("onionPayload" | Codec( 57 | // We simply encode the whole payload, nothing fancy here. 58 | (payload: ByteVector) => bytes(payload.length.toInt).encode(payload), 59 | // We stop 32 bytes before the end to avoid reading the hmac. 60 | (bin: BitVector) => bytes((bin.length.toInt / 8) - 32).decode(bin))) :: 61 | ("hmac" | bytes32)).as[OnionRoutingPacket] 62 | 63 | } 64 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/PeerStorageTlv.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.wire.protocol 18 | 19 | import fr.acinq.eclair.wire.protocol.CommonCodecs.varint 20 | import fr.acinq.eclair.wire.protocol.TlvCodecs.tlvStream 21 | import scodec.Codec 22 | import scodec.codecs.discriminated 23 | 24 | /** 25 | * Created by thomash on July 2024. 26 | */ 27 | 28 | sealed trait PeerStorageTlv extends Tlv 29 | 30 | object PeerStorageTlv { 31 | val peerStorageTlvCodec: Codec[TlvStream[PeerStorageTlv]] = tlvStream(discriminated[PeerStorageTlv].by(varint)) 32 | } 33 | -------------------------------------------------------------------------------- /eclair-core/src/main/scala/fr/acinq/eclair/wire/protocol/TlvTypes.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.wire.protocol 18 | 19 | import fr.acinq.eclair.UInt64 20 | import scodec.bits.ByteVector 21 | 22 | import scala.reflect.ClassTag 23 | 24 | /** 25 | * Created by t-bast on 20/06/2019. 26 | */ 27 | 28 | trait Tlv 29 | 30 | /** 31 | * Generic tlv type we fallback to if we don't understand the incoming tlv. 32 | * 33 | * @param tag tlv tag. 34 | * @param value tlv value (length is implicit, and encoded as a varint). 35 | */ 36 | case class GenericTlv(tag: UInt64, value: ByteVector) extends Tlv 37 | 38 | /** 39 | * A tlv stream is a collection of tlv records. 40 | * A tlv stream is constrained to a specific tlv namespace that dictates how to parse the tlv records. 41 | * That namespace is provided by a trait extending the top-level tlv trait. 42 | * 43 | * @param records known tlv records. 44 | * @param unknown unknown tlv records. 45 | * @tparam T the stream namespace is a trait extending the top-level tlv trait. 46 | */ 47 | case class TlvStream[T <: Tlv](records: Set[T], unknown: Set[GenericTlv] = Set.empty) { 48 | /** 49 | * 50 | * @tparam R input type parameter, must be a subtype of the main TLV type 51 | * @return the TLV record of type that matches the input type parameter if any (there can be at most one, since BOLTs specify 52 | * that TLV records are supposed to be unique) 53 | */ 54 | def get[R <: T : ClassTag]: Option[R] = records.collectFirst { case r: R => r } 55 | } 56 | 57 | object TlvStream { 58 | def empty[T <: Tlv]: TlvStream[T] = TlvStream[T](Set.empty[T], Set.empty[GenericTlv]) 59 | 60 | def apply[T <: Tlv](records: T*): TlvStream[T] = TlvStream[T](records.toSet, Set.empty[GenericTlv]) 61 | } -------------------------------------------------------------------------------- /eclair-core/src/test/java/fr/acinq/eclair/MilliSatoshiTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair; 18 | 19 | import fr.acinq.bitcoin.scalacompat.Satoshi; 20 | 21 | /** 22 | * This class is a compile-time check that we are able to compile Java code that uses MilliSatoshi utilities. 23 | */ 24 | public final class MilliSatoshiTest { 25 | 26 | public static void Test() { 27 | MilliSatoshi msat = new MilliSatoshi(561); 28 | Satoshi sat = new Satoshi(1); 29 | msat.truncateToSatoshi(); 30 | msat = msat.max(sat); 31 | msat = msat.min(sat); 32 | MilliSatoshi.toMilliSatoshi(sat); 33 | msat = MilliSatoshi.toMilliSatoshi(sat).$plus(msat); 34 | msat = msat.$plus(msat); 35 | msat = msat.$times(2.0); 36 | Boolean check1 = msat.$less$eq(new MilliSatoshi(1105)); 37 | Boolean check2 = msat.$greater(sat); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = "DEBUG" 4 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 5 | logger-startup-timeout = 60s 6 | 7 | actor { 8 | debug { 9 | # enable DEBUG logging of all LoggingFSMs for events, transitions and timers 10 | fsm = on 11 | } 12 | 13 | testkit.typed { 14 | # Factor by which to scale timeouts during tests, e.g. to account for shared 15 | # build system load. 16 | timefactor = 5.0 17 | 18 | # Duration to wait for all required logging events in LoggingTestKit.expect. 19 | # Dilated by the timefactor. 20 | filter-leeway = 10s 21 | } 22 | } 23 | 24 | test { 25 | # factor by which to scale timeouts during tests, e.g. to account for shared 26 | # build system load 27 | timefactor = 5.0 28 | } 29 | } -------------------------------------------------------------------------------- /eclair-core/src/test/resources/integration/bitcoin.conf: -------------------------------------------------------------------------------- 1 | regtest=1 2 | noprinttoconsole=1 3 | server=1 4 | rpcuser=foo 5 | rpcpassword=bar 6 | txindex=1 7 | zmqpubhashblock=tcp://127.0.0.1:28334 8 | zmqpubrawtx=tcp://127.0.0.1:28335 9 | rpcworkqueue=64 10 | fallbackfee=0.0001 # 10 sat/byte 11 | consolidatefeerate=0 # we don't want bitcoind to consolidate utxos during tests 12 | [regtest] 13 | bind=127.0.0.1 14 | port=28333 15 | rpcport=28332 16 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/01-offer1.script: -------------------------------------------------------------------------------- 1 | # Simple test that we can commit an HTLC 2 | # Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat 3 | A:offer 1000000,9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 4 | B:recvoffer 5 | A:commit 6 | B:recvcommit 7 | A:recvrevoke 8 | B:commit 9 | A:recvcommit 10 | B:recvrevoke 11 | checksync 12 | echo ***A*** 13 | A:dump 14 | echo ***B*** 15 | B:dump 16 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/01-offer1.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 1: 4 | Offered htlcs: (0,1000000 msat) 5 | Received htlcs: 6 | Balance us: 999000000 msat 7 | Balance them: 1000000000 msat 8 | Fee rate: 10000 9 | REMOTE COMMITS: 10 | Commit 1: 11 | Offered htlcs: 12 | Received htlcs: (0,1000000 msat) 13 | Balance us: 1000000000 msat 14 | Balance them: 999000000 msat 15 | Fee rate: 10000 16 | ***B*** 17 | LOCAL COMMITS: 18 | Commit 1: 19 | Offered htlcs: 20 | Received htlcs: (0,1000000 msat) 21 | Balance us: 1000000000 msat 22 | Balance them: 999000000 msat 23 | Fee rate: 10000 24 | REMOTE COMMITS: 25 | Commit 1: 26 | Offered htlcs: (0,1000000 msat) 27 | Received htlcs: 28 | Balance us: 999000000 msat 29 | Balance them: 1000000000 msat 30 | Fee rate: 10000 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/02-offer2.script: -------------------------------------------------------------------------------- 1 | # Simple test that we can commit two HTLCs 2 | # Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat 3 | A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c 4 | A:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 5 | A:commit 6 | B:recvoffer 7 | B:recvoffer 8 | B:recvcommit 9 | A:recvrevoke 10 | B:commit 11 | A:recvcommit 12 | B:recvrevoke 13 | checksync 14 | echo ***A*** 15 | A:dump 16 | echo ***B*** 17 | B:dump 18 | 19 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/02-offer2.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 1: 4 | Offered htlcs: (0,1000000 msat) (1,2000000 msat) 5 | Received htlcs: 6 | Balance us: 997000000 msat 7 | Balance them: 1000000000 msat 8 | Fee rate: 10000 9 | REMOTE COMMITS: 10 | Commit 1: 11 | Offered htlcs: 12 | Received htlcs: (0,1000000 msat) (1,2000000 msat) 13 | Balance us: 1000000000 msat 14 | Balance them: 997000000 msat 15 | Fee rate: 10000 16 | ***B*** 17 | LOCAL COMMITS: 18 | Commit 1: 19 | Offered htlcs: 20 | Received htlcs: (0,1000000 msat) (1,2000000 msat) 21 | Balance us: 1000000000 msat 22 | Balance them: 997000000 msat 23 | Fee rate: 10000 24 | REMOTE COMMITS: 25 | Commit 1: 26 | Offered htlcs: (0,1000000 msat) (1,2000000 msat) 27 | Received htlcs: 28 | Balance us: 997000000 msat 29 | Balance them: 1000000000 msat 30 | Fee rate: 10000 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/03-fulfill1.script: -------------------------------------------------------------------------------- 1 | # Simple test that we can fulfill (remove) an HTLC after fully settled. 2 | # Initial state: A=1000000 sat, B=1000000 sat, both fee rates=10000 sat 3 | A:offer 1000000,b8928207364d445daa42f4ba8be0ef661b8d7190206c01f6ee8281566b3dec0a 4 | B:recvoffer 5 | A:commit 6 | B:recvcommit 7 | A:recvrevoke 8 | B:commit 9 | A:recvcommit 10 | B:recvrevoke 11 | 12 | B:fulfill 0,60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752 13 | B:commit 14 | A:recvremove 15 | A:recvcommit 16 | B:recvrevoke 17 | A:commit 18 | B:recvcommit 19 | A:recvrevoke 20 | 21 | checksync 22 | echo ***A*** 23 | A:dump 24 | echo ***B*** 25 | B:dump 26 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/03-fulfill1.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 2: 4 | Offered htlcs: 5 | Received htlcs: 6 | Balance us: 999000000 msat 7 | Balance them: 1001000000 msat 8 | Fee rate: 10000 9 | REMOTE COMMITS: 10 | Commit 2: 11 | Offered htlcs: 12 | Received htlcs: 13 | Balance us: 1001000000 msat 14 | Balance them: 999000000 msat 15 | Fee rate: 10000 16 | ***B*** 17 | LOCAL COMMITS: 18 | Commit 2: 19 | Offered htlcs: 20 | Received htlcs: 21 | Balance us: 1001000000 msat 22 | Balance them: 999000000 msat 23 | Fee rate: 10000 24 | REMOTE COMMITS: 25 | Commit 2: 26 | Offered htlcs: 27 | Received htlcs: 28 | Balance us: 999000000 msat 29 | Balance them: 1001000000 msat 30 | Fee rate: 10000 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script: -------------------------------------------------------------------------------- 1 | # Test A can commit twice; queueing two commits onto B's queue. 2 | # Initial state: A=1000000 sat, B=0, both fee rates=10000 sat 3 | A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c 4 | B:recvoffer 5 | A:commit 6 | B:recvcommit 7 | A:recvrevoke 8 | 9 | A:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 10 | B:recvoffer 11 | A:commit 12 | B:recvcommit 13 | A:recvrevoke 14 | 15 | B:commit 16 | A:recvcommit 17 | B:recvrevoke 18 | checksync 19 | echo ***A*** 20 | A:dump 21 | echo ***B*** 22 | B:dump 23 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/04-two-commits-onedir.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 1: 4 | Offered htlcs: (0,1000000) (1,2000000) 5 | Received htlcs: 6 | Balance us: 997000000 msat 7 | Balance them: 1000000000 msat 8 | Fee rate: 10000 9 | REMOTE COMMITS: 10 | Commit 2: 11 | Offered htlcs: 12 | Received htlcs: (0,1000000) (1,2000000) 13 | Balance us: 1000000000 msat 14 | Balance them: 997000000 msat 15 | Fee rate: 10000 16 | ***B*** 17 | LOCAL COMMITS: 18 | Commit 2: 19 | Offered htlcs: 20 | Received htlcs: (0,1000000) (1,2000000) 21 | Balance us: 1000000000 msat 22 | Balance them: 997000000 msat 23 | Fee rate: 10000 24 | REMOTE COMMITS: 25 | Commit 1: 26 | Offered htlcs: (0,1000000) (1,2000000) 27 | Received htlcs: 28 | Balance us: 997000000 msat 29 | Balance them: 1000000000 msat 30 | Fee rate: 10000 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script: -------------------------------------------------------------------------------- 1 | # Test committing before receiving previous revocation. 2 | A:nocommitwait 3 | A:offer 1 4 | A:commit 5 | A:offer 3 6 | A:commit 7 | B:recvoffer 8 | B:recvcommit 9 | B:recvoffer 10 | B:recvcommit 11 | A:recvrevoke 12 | A:recvrevoke 13 | B:commit 14 | A:recvcommit 15 | B:recvrevoke 16 | checksync 17 | echo ***A*** 18 | A:dump 19 | echo ***B*** 20 | B:dump 21 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/05-two-commits-in-flight.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 1: 4 | Offered htlcs: 1 3 5 | Received htlcs: 6 | SIGNED 7 | REMOTE COMMITS: 8 | Commit 2: 9 | Offered htlcs: 10 | Received htlcs: 1 3 11 | SIGNED 12 | ***B*** 13 | LOCAL COMMITS: 14 | Commit 2: 15 | Offered htlcs: 16 | Received htlcs: 1 3 17 | SIGNED 18 | REMOTE COMMITS: 19 | Commit 1: 20 | Offered htlcs: 1 3 21 | Received htlcs: 22 | SIGNED 23 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/10-offers-crossover.script: -------------------------------------------------------------------------------- 1 | # Offers which cross over still get resolved. 2 | # Initial state: A=1000000 sat, B=1000000, both fee rates=10000 sat 3 | 4 | A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c 5 | A:commit 6 | B:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 7 | B:recvoffer 8 | B:recvcommit 9 | B:commit 10 | 11 | A:recvoffer 12 | A:recvrevoke 13 | A:recvcommit 14 | B:recvrevoke 15 | 16 | A:commit 17 | B:recvcommit 18 | A:recvrevoke 19 | 20 | checksync 21 | echo ***A*** 22 | A:dump 23 | echo ***B*** 24 | B:dump 25 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/10-offers-crossover.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 1: 4 | Offered htlcs: (0,1000000 msat) 5 | Received htlcs: (0,2000000 msat) 6 | Balance us: 999000000 msat 7 | Balance them: 998000000 msat 8 | Fee rate: 10000 9 | REMOTE COMMITS: 10 | Commit 2: 11 | Offered htlcs: (0,2000000 msat) 12 | Received htlcs: (0,1000000 msat) 13 | Balance us: 998000000 msat 14 | Balance them: 999000000 msat 15 | Fee rate: 10000 16 | ***B*** 17 | LOCAL COMMITS: 18 | Commit 2: 19 | Offered htlcs: (0,2000000 msat) 20 | Received htlcs: (0,1000000 msat) 21 | Balance us: 998000000 msat 22 | Balance them: 999000000 msat 23 | Fee rate: 10000 24 | REMOTE COMMITS: 25 | Commit 1: 26 | Offered htlcs: (0,1000000 msat) 27 | Received htlcs: (0,2000000 msat) 28 | Balance us: 999000000 msat 29 | Balance them: 998000000 msat 30 | Fee rate: 10000 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/11-commits-crossover.script: -------------------------------------------------------------------------------- 1 | # Commits which cross over still get resolved. 2 | # Initial state: A=1000000 sat, B=1000000, both fee rates=10000 sat 3 | 4 | A:offer 1000000,7b3d979ca8330a94fa7e9e1b466d8b99e0bcdea1ec90596c0dcc8d7ef6b4300c 5 | B:offer 2000000,6016bcc377c93692f2fe19fbad47eee6fb8f4cc98c56e935db5edb69806d84f6 6 | A:commit 7 | B:commit 8 | 9 | A:recvoffer 10 | B:recvoffer 11 | A:recvcommit 12 | B:recvcommit 13 | 14 | A:recvrevoke 15 | B:recvrevoke 16 | 17 | # They've got to come into sync eventually! 18 | A:commit 19 | B:commit 20 | A:recvcommit 21 | B:recvcommit 22 | A:recvrevoke 23 | B:recvrevoke 24 | 25 | checksync 26 | echo ***A*** 27 | A:dump 28 | echo ***B*** 29 | B:dump 30 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/scenarii/11-commits-crossover.script.expected: -------------------------------------------------------------------------------- 1 | ***A*** 2 | LOCAL COMMITS: 3 | Commit 2: 4 | Offered htlcs: (0,1000000 msat) 5 | Received htlcs: (0,2000000 msat) 6 | Balance us: 999000000 msat 7 | Balance them: 998000000 msat 8 | Fee rate: 10000 9 | REMOTE COMMITS: 10 | Commit 2: 11 | Offered htlcs: (0,2000000 msat) 12 | Received htlcs: (0,1000000 msat) 13 | Balance us: 998000000 msat 14 | Balance them: 999000000 msat 15 | Fee rate: 10000 16 | ***B*** 17 | LOCAL COMMITS: 18 | Commit 2: 19 | Offered htlcs: (0,2000000 msat) 20 | Received htlcs: (0,1000000 msat) 21 | Balance us: 998000000 msat 22 | Balance them: 999000000 msat 23 | Fee rate: 10000 24 | REMOTE COMMITS: 25 | Commit 2: 26 | Offered htlcs: (0,1000000 msat) 27 | Received htlcs: (0,2000000 msat) 28 | Balance us: 999000000 msat 29 | Balance them: 998000000 msat 30 | Fee rate: 10000 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/resources/short_channels-mainnet.422: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ACINQ/eclair/fb84a9d1115bf80bb2b3269fafa31fd5cc9dfacb/eclair-core/src/test/resources/short_channels-mainnet.422 -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/CltvExpirySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import org.scalatest.ParallelTestExecution 20 | import org.scalatest.funsuite.AnyFunSuite 21 | 22 | /** 23 | * Created by t-bast on 21/08/2019. 24 | */ 25 | 26 | class CltvExpirySpec extends AnyFunSuite with ParallelTestExecution { 27 | 28 | test("cltv expiry delta") { 29 | val d = CltvExpiryDelta(561) 30 | assert(d.toInt == 561) 31 | 32 | // add 33 | assert(d + 5 == CltvExpiryDelta(566)) 34 | assert(d + CltvExpiryDelta(5) == CltvExpiryDelta(566)) 35 | 36 | // subtract 37 | assert(d - CltvExpiryDelta(5) == CltvExpiryDelta(556)) 38 | assert(d - CltvExpiryDelta(562) == CltvExpiryDelta(-1)) 39 | 40 | // compare 41 | assert(d <= CltvExpiryDelta(561)) 42 | assert(d < CltvExpiryDelta(562)) 43 | assert(d >= CltvExpiryDelta(561)) 44 | assert(d > CltvExpiryDelta(560)) 45 | 46 | // convert to cltv expiry 47 | assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1105)) == CltvExpiry(1666)) 48 | assert(d.toCltvExpiry(currentBlockHeight = BlockHeight(1106)) == CltvExpiry(1667)) 49 | } 50 | 51 | test("cltv expiry") { 52 | val e = CltvExpiry(1105) 53 | assert(e.toLong == 1105) 54 | 55 | // add 56 | assert(e + CltvExpiryDelta(561) == CltvExpiry(1666)) 57 | assert(e - CltvExpiryDelta(561) == CltvExpiry(544)) 58 | assert(e - CltvExpiry(561) == CltvExpiryDelta(544)) 59 | 60 | // compare 61 | assert(e <= CltvExpiry(1105)) 62 | assert(e < CltvExpiry(1106)) 63 | assert(e >= CltvExpiry(1105)) 64 | assert(e > CltvExpiry(1104)) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/Pipe.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import akka.actor.{Actor, ActorLogging, ActorRef, Stash} 20 | import fr.acinq.eclair.channel.CommitmentChanges.msg2String 21 | import fr.acinq.eclair.wire.protocol.LightningMessage 22 | 23 | /** 24 | * Handles a bi-directional path between 2 actors 25 | * used to avoid the chicken-and-egg problem of: 26 | * a = new Channel(b) 27 | * b = new Channel(a) 28 | */ 29 | class Pipe extends Actor with Stash with ActorLogging { 30 | 31 | def receive = { 32 | case (a: ActorRef, b: ActorRef) => 33 | unstashAll() 34 | context become connected(a, b) 35 | 36 | case _ => stash() 37 | } 38 | 39 | def connected(a: ActorRef, b: ActorRef): Receive = { 40 | case msg: LightningMessage if sender() == a => 41 | log.debug(f"A ---${msg2String(msg)}%-6s--> B") 42 | b forward msg 43 | case msg: LightningMessage if sender() == b => 44 | log.debug(f"A <--${msg2String(msg)}%-6s--- B") 45 | a forward msg 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/TestKitBaseClass.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import akka.actor.ActorSystem 20 | import akka.testkit.TestKit 21 | import org.scalatest.{BeforeAndAfterAll, TestSuite} 22 | 23 | /** 24 | * This base class ensures the actor system is shutdown after the test suite ends. 25 | * Created by PM on 06/09/2016. 26 | */ 27 | abstract class TestKitBaseClass extends TestKit(ActorSystem("test")) with TestSuite with BeforeAndAfterAll { 28 | 29 | override def afterAll(): Unit = { 30 | TestKit.shutdownActorSystem(system) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/TimestampSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import org.scalatest.funsuite.AnyFunSuite 20 | 21 | 22 | class TimestampSpec extends AnyFunSuite { 23 | 24 | test("timestamp boundaries") { 25 | assert(TimestampSecond.max.toLong == 253402300799L) 26 | assert(TimestampSecond.min.toLong == 0) 27 | assert(TimestampMilli.max.toLong == 253402300799L * 1000) 28 | assert(TimestampMilli.min.toLong == 0) 29 | 30 | intercept[IllegalArgumentException] { 31 | TimestampSecond(253402300799L + 1) 32 | } 33 | 34 | intercept[IllegalArgumentException] { 35 | TimestampSecond(-1) 36 | } 37 | 38 | intercept[IllegalArgumentException] { 39 | TimestampMilli(-1) 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/balance/ChannelsListenerSpec.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.balance 2 | 3 | import akka.Done 4 | import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} 5 | import akka.actor.typed.eventstream.EventStream 6 | import akka.actor.typed.scaladsl.adapter.TypedActorRefOps 7 | import com.typesafe.config.ConfigFactory 8 | import fr.acinq.eclair.balance.ChannelsListener.{GetChannels, GetChannelsResponse} 9 | import fr.acinq.eclair.channel.ChannelRestored 10 | import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec 11 | import fr.acinq.eclair.{randomBytes32, randomKey} 12 | import org.scalatest.funsuite.AnyFunSuiteLike 13 | 14 | import scala.concurrent.Promise 15 | 16 | class ChannelsListenerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with AnyFunSuiteLike { 17 | 18 | test("channels listener basic test") { 19 | val ready = Promise[Done]() 20 | val channelsListener = testKit.spawn(ChannelsListener.apply(ready)) 21 | 22 | eventually { 23 | assert(ready.isCompleted) 24 | } 25 | 26 | val channel1 = TestProbe[Any]() 27 | system.eventStream ! EventStream.Publish(ChannelRestored(channel1.ref.toClassic, randomBytes32(), null, randomKey().publicKey, ChannelCodecsSpec.normal)) 28 | val channel2 = TestProbe[Any]() 29 | system.eventStream ! EventStream.Publish(ChannelRestored(channel2.ref.toClassic, randomBytes32(), null, randomKey().publicKey, ChannelCodecsSpec.normal)) 30 | 31 | val probe = TestProbe[GetChannelsResponse]() 32 | 33 | eventually { 34 | channelsListener ! GetChannels(probe.ref) 35 | assert(probe.expectMessageType[GetChannelsResponse].channels.size == 2) 36 | } 37 | 38 | channel2.stop() 39 | 40 | eventually { 41 | channelsListener ! GetChannels(probe.ref) 42 | assert(probe.expectMessageType[GetChannelsResponse].channels.size == 1) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreCookieAuthSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.blockchain.bitcoind 18 | 19 | import akka.pattern.pipe 20 | import akka.testkit.TestProbe 21 | import fr.acinq.eclair.{BlockHeight, TestKitBaseClass} 22 | import fr.acinq.eclair.blockchain.OnChainWallet.OnChainBalance 23 | import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient 24 | import org.scalatest.BeforeAndAfterAll 25 | import org.scalatest.funsuite.AnyFunSuiteLike 26 | 27 | import scala.concurrent.ExecutionContext.Implicits.global 28 | 29 | class BitcoinCoreCookieAuthSpec extends TestKitBaseClass with BitcoindService with AnyFunSuiteLike with BeforeAndAfterAll { 30 | 31 | override def beforeAll(): Unit = { 32 | startBitcoind(useCookie = true) 33 | waitForBitcoindReady() 34 | } 35 | 36 | override def afterAll(): Unit = { 37 | stopBitcoind() 38 | } 39 | 40 | test("call bitcoind with cookie authentication") { 41 | val sender = TestProbe() 42 | val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) 43 | bitcoinClient.onChainBalance().pipeTo(sender.ref) 44 | sender.expectMsgType[OnChainBalance] 45 | } 46 | 47 | test("automatically fetch updated credentials after restart") { 48 | val sender = TestProbe() 49 | // We make a first call before restarting bitcoind to ensure we have a first set of valid credentials. 50 | val bitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) 51 | bitcoinClient.getBlockHeight().pipeTo(sender.ref) 52 | val blockHeight = sender.expectMsgType[BlockHeight] 53 | 54 | restartBitcoind(sender, useCookie = true) 55 | 56 | // We use the previous bitcoin client: its credentials are now obsolete since bitcoind changes the cookie credentials 57 | // after restarting. 58 | bitcoinClient.getBlockHeight().pipeTo(sender.ref) 59 | sender.expectMsg(blockHeight) 60 | 61 | // A fresh bitcoin client will automatically fetch the latest credentials. 62 | val postRestartBitcoinClient = new BitcoinCoreClient(bitcoinrpcclient) 63 | postRestartBitcoinClient.getBlockHeight().pipeTo(sender.ref) 64 | sender.expectMsg(blockHeight) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/channel/ChannelConfigSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.channel 18 | 19 | import fr.acinq.eclair.channel.ChannelConfig._ 20 | import org.scalatest.funsuite.AnyFunSuiteLike 21 | 22 | class ChannelConfigSpec extends AnyFunSuiteLike { 23 | 24 | test("channel key path based on funding public key") { 25 | assert(!ChannelConfig(Set.empty[ChannelConfigOption]).hasOption(FundingPubKeyBasedChannelKeyPath)) 26 | assert(ChannelConfig.standard.hasOption(FundingPubKeyBasedChannelKeyPath)) 27 | assert(ChannelConfig(FundingPubKeyBasedChannelKeyPath).hasOption(FundingPubKeyBasedChannelKeyPath)) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/db/OffersDbSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.db 18 | 19 | import fr.acinq.bitcoin.scalacompat.Block 20 | import fr.acinq.eclair._ 21 | import fr.acinq.eclair.wire.protocol.OfferTypes.Offer 22 | import org.scalatest.funsuite.AnyFunSuite 23 | 24 | class OffersDbSpec extends AnyFunSuite { 25 | 26 | import fr.acinq.eclair.TestDatabases.forAllDbs 27 | 28 | test("add/disable/enable/list offers") { 29 | forAllDbs { dbs => 30 | val db = dbs.offers 31 | 32 | assert(db.listOffers(onlyActive = false).isEmpty) 33 | val offer1 = OfferData(Offer(None, Some("test 1"), randomKey().publicKey, Features(), Block.LivenetGenesisBlock.hash), None, TimestampMilli(100), None) 34 | assert(db.addOffer(offer1.offer, None, offer1.createdAt).nonEmpty) 35 | assert(db.addOffer(offer1.offer, None, TimestampMilli(150)).isEmpty) 36 | assert(db.listOffers(onlyActive = true) == Seq(offer1)) 37 | val pathId = randomBytes32() 38 | val offer2 = OfferData(Offer(Some(15_000 msat), Some("test 2"), randomKey().publicKey, Features(), Block.LivenetGenesisBlock.hash), Some(pathId), TimestampMilli(200), None) 39 | assert(db.addOffer(offer2.offer, Some(pathId), offer2.createdAt).nonEmpty) 40 | assert(db.listOffers(onlyActive = true) == Seq(offer2, offer1)) 41 | db.disableOffer(offer1.offer, disabledAt = TimestampMilli(250)) 42 | assert(db.listOffers(onlyActive = true) == Seq(offer2)) 43 | assert(db.listOffers(onlyActive = false) == Seq(offer2, offer1.copy(disabledAt_opt = Some(TimestampMilli(250))))) 44 | db.disableOffer(offer2.offer, disabledAt = TimestampMilli(300)) 45 | assert(db.listOffers(onlyActive = true).isEmpty) 46 | assert(db.listOffers(onlyActive = false) == Seq(offer2.copy(disabledAt_opt = Some(TimestampMilli(300))), offer1.copy(disabledAt_opt = Some(TimestampMilli(250))))) 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/ThreeNodesIntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.integration.basic 2 | 3 | import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong} 4 | import fr.acinq.eclair.MilliSatoshiLong 5 | import fr.acinq.eclair.integration.basic.fixtures.composite.ThreeNodesFixture 6 | import fr.acinq.eclair.testutils.FixtureSpec 7 | import org.scalatest.TestData 8 | import org.scalatest.concurrent.IntegrationPatience 9 | import scodec.bits.HexStringSyntax 10 | 11 | 12 | /** 13 | * This test checks the integration between Channel and Router (events, etc.) 14 | */ 15 | class ThreeNodesIntegrationSpec extends FixtureSpec with IntegrationPatience { 16 | 17 | type FixtureParam = ThreeNodesFixture 18 | 19 | import fr.acinq.eclair.integration.basic.fixtures.MinimalNodeFixture.{connect, getRouterData, knownFundingTxs, nodeParamsFor, openChannel, sendSuccessfulPayment, watcherAutopilot} 20 | 21 | override def createFixture(testData: TestData): FixtureParam = { 22 | // seeds have been chosen so that node ids start with 02aaaa for alice, 02bbbb for bob, etc. 23 | val aliceParams = nodeParamsFor("alice", ByteVector32(hex"b4acd47335b25ab7b84b8c020997b12018592bb4631b868762154d77fa8b93a3")) 24 | val bobParams = nodeParamsFor("bob", ByteVector32(hex"7620226fec887b0b2ebe76492e5a3fd3eb0e47cd3773263f6a81b59a704dc492")) 25 | val carolParams = nodeParamsFor("carol", ByteVector32(hex"ebd5a5d3abfb3ef73731eb3418d918f247445183180522674666db98a66411cc")) 26 | ThreeNodesFixture(aliceParams, bobParams, carolParams, testData.name) 27 | } 28 | 29 | override def cleanupFixture(fixture: FixtureParam): Unit = { 30 | fixture.cleanup() 31 | } 32 | 33 | test("connect alice->bob and bob->carol, pay alice->carol") { f => 34 | import f._ 35 | connect(alice, bob) 36 | connect(bob, carol) 37 | 38 | // we put watchers on auto pilot to confirm funding txs 39 | alice.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) 40 | bob.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) 41 | carol.watcher.setAutoPilot(watcherAutopilot(knownFundingTxs(alice, bob, carol))) 42 | 43 | openChannel(alice, bob, 100_000 sat).channelId 44 | openChannel(bob, carol, 100_000 sat).channelId 45 | 46 | // alice now knows about bob-carol 47 | eventually { 48 | val routerData = getRouterData(alice) 49 | //prettyPrint(routerData, alice, bob, carol) 50 | assert(routerData.channels.size == 2) // 2 channels 51 | assert(routerData.channels.values.flatMap(c => c.update_1_opt.toSeq ++ c.update_2_opt.toSeq).size == 4) // 2 channel_updates per channel 52 | } 53 | 54 | sendSuccessfulPayment(alice, carol, 100_000 msat) 55 | } 56 | 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/FixtureUtils.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.integration.basic.fixtures 2 | 3 | import com.typesafe.config.{Config, ConfigFactory} 4 | 5 | object FixtureUtils { 6 | 7 | def actorSystemConfig(testName: String): Config = 8 | ConfigFactory 9 | .parseString { 10 | s"""| akka { 11 | | logging-context = "$testName" 12 | | loggers = ["fr.acinq.eclair.testutils.MySlf4jLogger"] 13 | | // akka-typed always uses slf4j and by default will remove custom loggers 14 | | use-slf4j = false 15 | | actor.debug.event-stream = off 16 | | }""".stripMargin 17 | }.withFallback(ConfigFactory.load()) 18 | .resolve() 19 | 20 | } 21 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/composite/ThreeNodesFixture.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.integration.basic.fixtures.composite 2 | 3 | import akka.actor.ActorSystem 4 | import akka.testkit.TestKit 5 | import fr.acinq.eclair.NodeParams 6 | import fr.acinq.eclair.integration.basic.fixtures.{FixtureUtils, MinimalNodeFixture} 7 | 8 | 9 | case class ThreeNodesFixture private(system: ActorSystem, 10 | alice: MinimalNodeFixture, 11 | bob: MinimalNodeFixture, 12 | carol: MinimalNodeFixture) { 13 | implicit val implicitSystem: ActorSystem = system 14 | 15 | def cleanup(): Unit = { 16 | TestKit.shutdownActorSystem(alice.system) 17 | TestKit.shutdownActorSystem(bob.system) 18 | TestKit.shutdownActorSystem(carol.system) 19 | TestKit.shutdownActorSystem(system) 20 | } 21 | } 22 | 23 | object ThreeNodesFixture { 24 | def apply(aliceParams: NodeParams, bobParams: NodeParams, carolParams: NodeParams, testName: String): ThreeNodesFixture = { 25 | ThreeNodesFixture( 26 | system = ActorSystem("system-test", FixtureUtils.actorSystemConfig(testName)), 27 | alice = MinimalNodeFixture(aliceParams, testName), 28 | bob = MinimalNodeFixture(bobParams, testName), 29 | carol = MinimalNodeFixture(carolParams, testName), 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/integration/basic/fixtures/composite/TwoNodesFixture.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.integration.basic.fixtures.composite 2 | 3 | import akka.actor.ActorSystem 4 | import akka.testkit.TestKit 5 | import fr.acinq.eclair.NodeParams 6 | import fr.acinq.eclair.integration.basic.fixtures.{FixtureUtils, MinimalNodeFixture} 7 | 8 | case class TwoNodesFixture private(system: ActorSystem, 9 | alice: MinimalNodeFixture, 10 | bob: MinimalNodeFixture) { 11 | implicit val implicitSystem: ActorSystem = system 12 | 13 | def cleanup(): Unit = { 14 | TestKit.shutdownActorSystem(alice.system) 15 | TestKit.shutdownActorSystem(bob.system) 16 | TestKit.shutdownActorSystem(system) 17 | } 18 | } 19 | 20 | object TwoNodesFixture { 21 | def apply(aliceParams: NodeParams, bobParams: NodeParams, testName: String): TwoNodesFixture = { 22 | TwoNodesFixture( 23 | system = ActorSystem("system-test", FixtureUtils.actorSystemConfig(testName)), 24 | alice = MinimalNodeFixture(aliceParams, testName), 25 | bob = MinimalNodeFixture(bobParams, testName) 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/io/PeerReadyManagerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.io 18 | 19 | import akka.actor.testkit.typed.scaladsl.{ScalaTestWithActorTestKit, TestProbe} 20 | import com.typesafe.config.ConfigFactory 21 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 22 | import fr.acinq.eclair.randomKey 23 | import org.scalatest.funsuite.AnyFunSuiteLike 24 | 25 | class PeerReadyManagerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load("application")) with AnyFunSuiteLike { 26 | 27 | test("watch pending notifiers") { 28 | val manager = testKit.spawn(PeerReadyManager()) 29 | val remoteNodeId1 = randomKey().publicKey 30 | val notifier1a = TestProbe[PeerReadyManager.Registered]() 31 | val notifier1b = TestProbe[PeerReadyManager.Registered]() 32 | 33 | manager ! PeerReadyManager.Register(notifier1a.ref, remoteNodeId1) 34 | assert(notifier1a.expectMessageType[PeerReadyManager.Registered].otherAttempts == 0) 35 | manager ! PeerReadyManager.Register(notifier1b.ref, remoteNodeId1) 36 | assert(notifier1b.expectMessageType[PeerReadyManager.Registered].otherAttempts == 1) 37 | 38 | val remoteNodeId2 = randomKey().publicKey 39 | val notifier2a = TestProbe[PeerReadyManager.Registered]() 40 | val notifier2b = TestProbe[PeerReadyManager.Registered]() 41 | 42 | // Later attempts aren't affected by previously completed attempts. 43 | manager ! PeerReadyManager.Register(notifier2a.ref, remoteNodeId2) 44 | assert(notifier2a.expectMessageType[PeerReadyManager.Registered].otherAttempts == 0) 45 | notifier2a.stop() 46 | val probe = TestProbe[Set[PublicKey]]() 47 | probe.awaitAssert({ 48 | manager ! PeerReadyManager.List(probe.ref) 49 | assert(probe.expectMessageType[Set[PublicKey]] == Set(remoteNodeId1)) 50 | }) 51 | manager ! PeerReadyManager.Register(notifier2b.ref, remoteNodeId2) 52 | assert(notifier2b.expectMessageType[PeerReadyManager.Registered].otherAttempts == 0) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/io/RateLimiterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.io 18 | 19 | import akka.testkit.TestProbe 20 | import fr.acinq.eclair._ 21 | import org.scalatest.funsuite.AnyFunSuiteLike 22 | 23 | import scala.concurrent.duration._ 24 | 25 | class RateLimiterSpec extends TestKitBaseClass with AnyFunSuiteLike { 26 | def sleep(duration: FiniteDuration): Unit = { 27 | val probe = TestProbe() 28 | system.scheduler.scheduleOnce(duration, probe.ref, ())(system.dispatcher) 29 | probe.expectMsg(()) 30 | } 31 | 32 | test("can burst immediately") { 33 | val rateLimiter = new RateLimiter(10) 34 | for(_ <- 1 to 10) { 35 | assert(rateLimiter.tryAcquire()) 36 | } 37 | } 38 | 39 | test("blocks when limit rate is reached") { 40 | val rateLimiter = new RateLimiter(10) 41 | for(_ <- 1 to 11) { 42 | rateLimiter.tryAcquire() 43 | } 44 | for(_ <- 1 to 20) { 45 | assert(!rateLimiter.tryAcquire()) 46 | } 47 | } 48 | 49 | test("recovers after time has passed") { 50 | val rateLimiter = new RateLimiter(10) 51 | for(_ <- 1 to 11) { 52 | rateLimiter.tryAcquire() 53 | } 54 | assert(!rateLimiter.tryAcquire()) 55 | sleep(1 second) 56 | assert(rateLimiter.tryAcquire()) 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.payment 18 | 19 | import akka.actor.Actor.Receive 20 | import akka.actor.ActorContext 21 | import akka.actor.typed.scaladsl.adapter._ 22 | import akka.event.DiagnosticLoggingAdapter 23 | import akka.testkit.TestProbe 24 | import fr.acinq.eclair.TestConstants.Alice 25 | import fr.acinq.eclair.TestKitBaseClass 26 | import fr.acinq.eclair.payment.receive.{PaymentHandler, ReceiveHandler} 27 | import org.scalatest.funsuite.AnyFunSuiteLike 28 | 29 | import scala.concurrent.duration._ 30 | 31 | class PaymentHandlerSpec extends TestKitBaseClass with AnyFunSuiteLike { 32 | 33 | test("compose payment handlers") { 34 | val handler = system.actorOf(PaymentHandler.props(Alice.nodeParams, TestProbe().ref, TestProbe().ref)) 35 | 36 | val intHandler = new ReceiveHandler { 37 | override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = { 38 | case i: Int => ctx.sender() ! -i 39 | } 40 | } 41 | 42 | val stringHandler = new ReceiveHandler { 43 | override def handle(implicit ctx: ActorContext, log: DiagnosticLoggingAdapter): Receive = { 44 | case s: String => ctx.sender() ! s.reverse 45 | } 46 | } 47 | 48 | val probe = TestProbe() 49 | 50 | probe.send(handler, 42) 51 | probe.expectNoMessage(300 millis) 52 | 53 | probe.send(handler, intHandler) 54 | 55 | probe.send(handler, 42) 56 | probe.expectMsg(-42) 57 | 58 | probe.send(handler, "abcdef") 59 | probe.expectNoMessage(300 millis) 60 | 61 | probe.send(handler, stringHandler) 62 | 63 | probe.send(handler, 123) 64 | probe.expectMsg(-123) 65 | 66 | probe.send(handler, "abcdef") 67 | probe.expectMsg("fedcba") 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/remote/EclairInternalsSerializerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.remote 18 | 19 | import fr.acinq.eclair.router.Router.GossipDecision 20 | import org.scalatest.funsuite.AnyFunSuite 21 | 22 | class EclairInternalsSerializerSpec extends AnyFunSuite { 23 | 24 | test("canary test codec gossip decision") { 25 | 26 | def codec(d: GossipDecision) = d match { 27 | case _: GossipDecision.Accepted => () 28 | case _: GossipDecision.Duplicate => () 29 | case _: GossipDecision.InvalidSignature => () 30 | case _: GossipDecision.NoKnownChannel => () 31 | case _: GossipDecision.ValidationFailure => () 32 | case _: GossipDecision.InvalidAnnouncement => () 33 | case _: GossipDecision.ChannelPruned => () 34 | case _: GossipDecision.ChannelClosing => () 35 | case _: GossipDecision.ChannelClosed => () 36 | case _: GossipDecision.Stale => () 37 | case _: GossipDecision.NoRelatedChannel => () 38 | case _: GossipDecision.RelatedChannelPruned => () 39 | // NB: if a new gossip decision is added, this pattern matching will fail 40 | // this serves as a reminder that a new codec is to be added in EclairInternalsSerializer.codec 41 | } 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/testutils/DurationBenchmarkReporter.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.testutils 2 | 3 | import org.scalatest.Reporter 4 | import org.scalatest.events.{Event, RunCompleted, SuiteCompleted, TestSucceeded} 5 | 6 | /** 7 | * A scalatest reporter that displays the slowest tests and suites. Useful to detect pain points and speed up the 8 | * overall build. 9 | * 10 | * To activate, edit the project's pom.xml and set: 11 | * {{{ 12 | * scalatest-maven-plugin 13 | * ... 14 | * 15 | * false 16 | * fr.acinq.eclair.testutils.DurationBenchmarkReporter 17 | * ... 18 | * 19 | * }}} 20 | * 21 | * Then run the tests as usual. 22 | * 23 | * Note that test parallelization needs to be disabled to have accurate measurement of durations, which will considerably 24 | * slow down the build time. 25 | */ 26 | class DurationBenchmarkReporter() extends Reporter { 27 | 28 | val suites = collection.mutable.Map.empty[String, Long] 29 | val tests = collection.mutable.Map.empty[String, Long] 30 | 31 | override def apply(event: Event): Unit = event match { 32 | case e: TestSucceeded => 33 | e.duration.map(d => tests.addOne(e.testName, d)) 34 | case e: SuiteCompleted => 35 | e.duration.map(d => suites.addOne(e.suiteName, d)) 36 | case _: RunCompleted => 37 | println("top 20 slowest tests:") 38 | tests.toList.sortBy(-_._2).take(20).foreach { case (name, duration) => println(s" ${duration}ms\t${name} ") } 39 | println("top 20 slowest suites:") 40 | suites.toList.sortBy(-_._2).take(20).foreach { case (name, duration) => println(s" ${duration}ms\t${name} ") } 41 | case _ => 42 | () 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/testutils/MyCapturingAppender.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.testutils 2 | 3 | import ch.qos.logback.classic.LoggerContext 4 | import ch.qos.logback.classic.spi.ILoggingEvent 5 | import ch.qos.logback.core.AppenderBase 6 | 7 | /** 8 | * Heavily inspired by [[akka.actor.testkit.typed.internal.CapturingAppender]] 9 | */ 10 | class MyCapturingAppender extends AppenderBase[ILoggingEvent] { 11 | 12 | private var buffer: Vector[ILoggingEvent] = Vector.empty 13 | 14 | // invocations are synchronized via doAppend in AppenderBase 15 | override def append(event: ILoggingEvent): Unit = { 16 | event.prepareForDeferredProcessing() 17 | buffer :+= event 18 | } 19 | 20 | /** 21 | * Flush buffered logging events to the output appenders 22 | * Also clears the buffer. 23 | */ 24 | def flush(): Unit = synchronized { 25 | val deferredTestLogger = this.getContext.asInstanceOf[LoggerContext].getLogger("MyCapturingAppenderDelegate") 26 | for (event <- buffer) { 27 | deferredTestLogger.callAppenders(event) 28 | } 29 | clear() 30 | } 31 | 32 | /** 33 | * Discards the buffered logging events without output. 34 | */ 35 | def clear(): Unit = synchronized { 36 | buffer = Vector.empty 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /eclair-core/src/test/scala/fr/acinq/eclair/testutils/MyContextSelector.scala: -------------------------------------------------------------------------------- 1 | package fr.acinq.eclair.testutils 2 | 3 | import ch.qos.logback.classic.LoggerContext 4 | import ch.qos.logback.classic.selector.ContextSelector 5 | import ch.qos.logback.classic.util.{ContextInitializer, LogbackMDCAdapter} 6 | 7 | import java.util 8 | import scala.jdk.CollectionConverters.SeqHasAsJava 9 | 10 | /** 11 | * We use a dedicated [[LoggerContext]] for each test, so the logging output do not get mixed up when we run tests in 12 | * parallel. It allows us to only display the output of the test that failed. 13 | * 14 | * See https://logback.qos.ch/manual/contextSelector.html. 15 | */ 16 | class MyContextSelector extends ContextSelector { 17 | 18 | val default = "default" 19 | var contexts: Map[String, LoggerContext] = Map.empty 20 | 21 | def this(context: LoggerContext) = { 22 | this() 23 | synchronized { 24 | contexts = contexts + (default -> context) 25 | } 26 | } 27 | 28 | override def getLoggerContext: LoggerContext = getLoggerContext(default) 29 | 30 | override def getLoggerContext(name: String): LoggerContext = { 31 | synchronized { 32 | val context = contexts.getOrElse(name, { 33 | val context = new LoggerContext() 34 | context.setName(name) 35 | context.setMDCAdapter(new LogbackMDCAdapter()) 36 | new ContextInitializer(context).autoConfig() 37 | contexts = contexts + (name -> context) 38 | context 39 | }) 40 | context 41 | } 42 | } 43 | 44 | override def getDefaultLoggerContext: LoggerContext = getLoggerContext(default) 45 | 46 | override def detachLoggerContext(name: String): LoggerContext = { 47 | synchronized { 48 | val context = contexts.getOrElse(name, null) 49 | contexts = contexts - name 50 | context 51 | } 52 | } 53 | 54 | override def getContextNames: util.List[String] = contexts.keys.toList.asJava 55 | } 56 | 57 | object MyContextSelector { 58 | /** 59 | * The standard logback way would be to resolve the ContextSelector by setting: 60 | * {{{-Dlogback.ContextSelector=fr.acinq.eclair.testutils.MyContextSelector}}} 61 | * But relying on system properties is not convenient because intellij ignore 62 | * values set in pom.xml, so we must set them manually when running tests within 63 | * the IDE. 64 | */ 65 | val Singleton: MyContextSelector = new MyContextSelector() 66 | } -------------------------------------------------------------------------------- /eclair-front/modules/assembly.xml: -------------------------------------------------------------------------------- 1 | 4 | bin 5 | 6 | zip 7 | 8 | 9 | 10 | lib 11 | true 12 | false 13 | runtime 14 | 15 | 16 | 17 | 18 | ../ 19 | 20 | docs/** 21 | README.md 22 | LICENSE* 23 | 24 | 25 | 26 | ${project.basedir}/src/main/resources 27 | bin 28 | 29 | eclair-front.sh 30 | 31 | 0755 32 | unix 33 | 34 | 35 | -------------------------------------------------------------------------------- /eclair-front/modules/awseb.xml: -------------------------------------------------------------------------------- 1 | 4 | awseb-bundle 5 | 6 | zip 7 | 8 | false 9 | 10 | 11 | ${project.build.directory} 12 | . 13 | 14 | eclair-front-*-bin.zip 15 | 16 | 17 | 18 | ${project.basedir}/modules/awseb 19 | . 20 | 21 | Procfile 22 | logback_eb.xml 23 | 24 | unix 25 | 26 | 27 | 36 | 37 | ${project.basedir}/modules/awseb 38 | . 39 | 40 | run.sh 41 | 42 | 0755 43 | unix 44 | 45 | 46 | -------------------------------------------------------------------------------- /eclair-front/modules/awseb/Procfile: -------------------------------------------------------------------------------- 1 | web: ./run.sh -------------------------------------------------------------------------------- /eclair-front/modules/awseb/logback_eb.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | return formattedMessage.contains("connected to /10.") || 26 | (formattedMessage.contains("connection closed") && !mdc.containsKey("nodeId")) || 27 | (formattedMessage.contains("transport died") && !mdc.containsKey("nodeId")) || 28 | (formattedMessage.contains("stopping") && !mdc.containsKey("nodeId")) || 29 | (formattedMessage.contains("buffering send data")); 30 | 31 | 32 | NEUTRAL 33 | DENY 34 | 35 | System.out 36 | false 37 | 38 | 39 | ${HOSTNAME} %d %-5level %replace(%logger{24}%X{category}%X{nodeId}%X{channelId}%X{paymentHash}%.-11X{parentPaymentId}%.-11X{paymentId} - %msg){'(?<=[a-f0-9]{8})[a-f0-9]{56,}', ''}%ex{12}%n 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /eclair-front/modules/awseb/run.sh: -------------------------------------------------------------------------------- 1 | # see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html 2 | TOKEN_IMDSV2=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") 3 | export LOCAL_IP=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN_IMDSV2" http://169.254.169.254/latest/meta-data/local-ipv4) 4 | export HOSTNAME=$(hostname) 5 | 6 | # make the eclair home directory 7 | mkdir -p /home/webapp/.eclair 8 | 9 | # if provided, copy the certificate to the proper directory 10 | [[ -e akka-cluster-tls.jks ]] && cp -v akka-cluster-tls.jks /home/webapp/.eclair/ 11 | 12 | unzip -o eclair-front-*-bin.zip 13 | exec ./eclair-front-*/bin/eclair-front.sh -with-kanela -Dlogback.configurationFile=logback_eb.xml 14 | -------------------------------------------------------------------------------- /eclair-front/src/main/scala/fr/acinq/eclair/Boot.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import java.io.File 20 | 21 | import akka.actor.ActorSystem 22 | import com.typesafe.config.{ConfigFactory, ConfigParseOptions, ConfigSyntax} 23 | import grizzled.slf4j.Logging 24 | import kamon.Kamon 25 | 26 | import scala.concurrent.ExecutionContext 27 | import scala.util.{Failure, Success} 28 | 29 | object Boot extends App with Logging { 30 | try { 31 | val datadir = new File(sys.props.getOrElse("eclair.datadir", sys.props("user.home") + "/.eclair")) 32 | val config = ConfigFactory.parseString( 33 | sys.env.getOrElse("AKKA_CONF", "").replace(";", "\n"), 34 | ConfigParseOptions.defaults().setSyntax(ConfigSyntax.PROPERTIES)) 35 | .withFallback(ConfigFactory.parseProperties(System.getProperties)) 36 | .withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf"))) 37 | .withFallback(ConfigFactory.load()) 38 | 39 | // the actor system name needs to be the same for all members of the cluster 40 | implicit val system: ActorSystem = ActorSystem("eclair-node", config) 41 | implicit val ec: ExecutionContext = system.dispatcher 42 | 43 | val setup = new FrontSetup(datadir) 44 | 45 | if (config.getBoolean("eclair.enable-kamon")) { 46 | Kamon.init(config) 47 | } 48 | 49 | setup.bootstrap onComplete { 50 | case Success(_) => () 51 | case Failure(t) => onError(t) 52 | } 53 | } catch { 54 | case t: Throwable => onError(t) 55 | } 56 | 57 | def onError(t: Throwable): Unit = { 58 | val errorMsg = if (t.getMessage != null) t.getMessage else t.getClass.getSimpleName 59 | System.err.println(s"fatal error: $errorMsg") 60 | logger.error(s"fatal error: $errorMsg", t) 61 | sys.exit(1) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /eclair-front/src/main/scala/fr/acinq/eclair/ClusterListener.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import akka.Done 20 | import akka.actor.{Actor, ActorLogging, Address} 21 | import akka.cluster.ClusterEvent.{InitialStateAsEvents, MemberDowned, MemberLeft, MemberUp, UnreachableMember} 22 | import akka.cluster.{Cluster, Member} 23 | 24 | import scala.concurrent.Promise 25 | 26 | class ClusterListener(frontJoinedCluster: Promise[Done], backendAddressFound: Promise[Address]) extends Actor with ActorLogging { 27 | 28 | val cluster = Cluster(context.system) 29 | cluster.subscribe(self, initialStateMode = InitialStateAsEvents, classOf[MemberUp], classOf[MemberLeft], classOf[MemberDowned], classOf[UnreachableMember]) 30 | val BackendRole = "backend" 31 | 32 | override def receive: Receive = { 33 | case MemberUp(member) => 34 | if (member.roles.contains(BackendRole)) { 35 | log.info(s"found backend=$member") 36 | backendAddressFound.success(member.address) 37 | } else if (member == cluster.selfMember) { 38 | log.info("we have joined the cluster") 39 | frontJoinedCluster.success(Done) 40 | } 41 | case MemberLeft(member) => maybeShutdown(member) 42 | case MemberDowned(member) => maybeShutdown(member) 43 | case UnreachableMember(member) => maybeShutdown(member) 44 | } 45 | 46 | private def maybeShutdown(member: Member): Unit = { 47 | if (member.roles.contains(BackendRole)) { 48 | log.info(s"backend is down, stopping...") 49 | sys.exit(0) 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /eclair-front/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | eclair.front { 2 | // just placeholders because this conf key is mandatory 3 | pub = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa01" 4 | } 5 | 6 | kamon.environment.host = "auto" 7 | 8 | akka { 9 | actor.provider = local 10 | cluster.seed-nodes = [] 11 | coordinated-shutdown.exit-jvm = off 12 | extensions = [] 13 | } -------------------------------------------------------------------------------- /eclair-node/modules/assembly.xml: -------------------------------------------------------------------------------- 1 | 4 | bin 5 | 6 | zip 7 | 8 | 9 | 10 | lib 11 | true 12 | false 13 | runtime 14 | 15 | 16 | 17 | 18 | ../ 19 | 20 | docs/** 21 | README.md 22 | LICENSE* 23 | 24 | 25 | 26 | ${project.basedir}/src/main/resources 27 | bin 28 | 29 | eclair-node.sh 30 | eclair-node.bat 31 | 32 | 0755 33 | unix 34 | 35 | 36 | ../eclair-core/ 37 | bin 38 | 39 | eclair-cli 40 | 41 | 0755 42 | unix 43 | 44 | 45 | -------------------------------------------------------------------------------- /eclair-node/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | eclair { 2 | enable-kamon = false 3 | } 4 | 5 | akka { 6 | actor.provider = local // set to cluster when using eclair-front (must also enable the distributed pubsub extension below) 7 | remote { 8 | artery { 9 | canonical.hostname = "127.0.0.1" 10 | canonical.port = 25520 11 | 12 | untrusted-mode = on 13 | trusted-selection-paths = [ 14 | "/user/*/switchboard", 15 | "/user/*/router", 16 | "/system/cluster/core/daemon", 17 | "/system/cluster/heartbeatReceiver", 18 | "/system/distributedPubSubMediator", 19 | "/system/clusterReceptionist/replicator" 20 | ] 21 | } 22 | deployment { 23 | enable-whitelist = on 24 | whitelist = [] // no remote deployment 25 | } 26 | } 27 | cluster.roles = [backend] 28 | // It is recommended to load the extension when the actor system is started by defining it in akka.extensions 29 | // configuration property. Otherwise it will be activated when first used and then it takes a while for it to be populated. 30 | // Uncomment the line below in cluster mode 31 | #extensions = ["akka.cluster.pubsub.DistributedPubSub"] 32 | } 33 | 34 | kamon.instrumentation.akka { 35 | filters { 36 | actors { 37 | # Decides which actors generate Spans for the messages they process, given that there is already an ongoing trace 38 | # in the Context of the processed message (i.e. there is a Sampled Span in the Context). 39 | trace { 40 | includes = [ ] 41 | excludes = [ "**" ] # we don't want automatically generated spans; we're not using them 42 | } 43 | } 44 | } 45 | } 46 | // See documentation here: https://kamon.io/docs/latest/reporters/prometheus/ 47 | kamon.prometheus { 48 | // If you want to expose a scraping endpoint for Prometheus metrics, set this to true. 49 | start-embedded-http-server = no 50 | embedded-server { 51 | hostname = 0.0.0.0 52 | port = 9095 53 | } 54 | } 55 | 56 | kanela.modules { 57 | jdbc { 58 | enabled = false 59 | } 60 | } 61 | 62 | akka { 63 | loggers = ["akka.event.slf4j.Slf4jLogger"] 64 | logger-startup-timeout = 30s 65 | loglevel = "DEBUG" # akka doc: You can enable DEBUG level for akka.loglevel and control the actual level in the SLF4J backend without any significant overhead, also for production. 66 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 67 | } -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/Plugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair 18 | 19 | import java.io.File 20 | import java.net.{JarURLConnection, URI, URL, URLClassLoader} 21 | import akka.http.scaladsl.server.Route 22 | import fr.acinq.eclair.api.directives.EclairDirectives 23 | import grizzled.slf4j.Logging 24 | 25 | import scala.util.{Failure, Success, Try} 26 | 27 | trait Plugin { 28 | 29 | def params: PluginParams 30 | 31 | def onSetup(setup: Setup): Unit 32 | 33 | def onKit(kit: Kit): Unit 34 | } 35 | 36 | trait RouteProvider { 37 | def route(directives: EclairDirectives): Route 38 | } 39 | 40 | object Plugin extends Logging { 41 | 42 | /** 43 | * The files passed to this function must be jars containing a manifest entry for "Main-Class" with the 44 | * FQDN of the entry point of the plugin. The entry point is the implementation of the interface "fr.acinq.eclair.Plugin" 45 | * @param jars 46 | * @return 47 | */ 48 | def loadPlugins(jars: Seq[File]): Seq[Plugin] = { 49 | val urls = jars.flatMap(openJar) 50 | val loader = new URLClassLoader(urls.map(_.getJarFileURL).toArray, ClassLoader.getSystemClassLoader) 51 | val pluginClasses = urls 52 | .flatMap(j => Option(j.getMainAttributes.getValue("Main-Class"))) 53 | .flatMap(classFQDN => Try[Class[_]](loader.loadClass(classFQDN)).toOption) 54 | .flatMap(c => Try(c.getDeclaredConstructor().newInstance().asInstanceOf[Plugin]).toOption) 55 | 56 | logger.info(s"loading ${pluginClasses.size} plugins") 57 | pluginClasses 58 | } 59 | 60 | def openJar(jar: File): Option[JarURLConnection] = 61 | Try(URI.create(s"jar:${jar.toURI}!/").toURL.openConnection().asInstanceOf[JarURLConnection]) match { 62 | case Success(url) => Some(url) 63 | case Failure(t) => logger.error(s"unable to load plugin file:${jar.getAbsolutePath} ", t); None 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.server._ 21 | import fr.acinq.eclair.{Eclair, RouteProvider} 22 | import fr.acinq.eclair.api.directives.EclairDirectives 23 | import fr.acinq.eclair.api.handlers._ 24 | import grizzled.slf4j.Logging 25 | 26 | trait Service extends EclairDirectives with WebSocket with Node with Control with Channel with Fees with PathFinding with Invoice with Offer with Payment with Message with OnChain with Logging { 27 | 28 | /** 29 | * Allows router access to the API password as configured in eclair.conf 30 | */ 31 | def password: String 32 | 33 | /** 34 | * The API of Eclair core. 35 | */ 36 | val eclairApi: Eclair 37 | 38 | /** 39 | * ActorSystem on which to run the http service. 40 | */ 41 | implicit val actorSystem: ActorSystem 42 | 43 | /** 44 | * Collect routes from all sub-routers here. 45 | * This is the main entrypoint for the global http request router of the API service. 46 | * This is where we handle errors to ensure all routes are correctly tried before rejecting. 47 | */ 48 | def finalRoutes(extraRouteProviders: Seq[RouteProvider] = Nil): Route = securedHandler { 49 | val baseRoutes = nodeRoutes ~ controlRoutes ~ channelRoutes ~ feeRoutes ~ pathFindingRoutes ~ invoiceRoutes ~ offerRoutes ~ paymentRoutes ~ messageRoutes ~ onChainRoutes ~ webSocket 50 | extraRouteProviders.map(_.route(this)).foldLeft(baseRoutes)(_ ~ _) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/AuthDirective.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | import akka.http.scaladsl.server.Directive0 20 | import akka.http.scaladsl.server.directives.Credentials 21 | import fr.acinq.eclair.api.Service 22 | 23 | import scala.concurrent.Future 24 | import scala.concurrent.duration.DurationInt 25 | 26 | trait AuthDirective { 27 | this: Service with EclairDirectives => 28 | 29 | /** 30 | * A directive0 that passes whenever valid basic credentials are provided. We 31 | * are not interested in the extracted username. 32 | * 33 | * @return 34 | */ 35 | def authenticated: Directive0 = authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator).tflatMap { _ => pass } 36 | 37 | private def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match { 38 | case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id)) 39 | case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/DefaultHeaders.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | import akka.http.scaladsl.model.HttpMethods.POST 20 | import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public} 21 | import akka.http.scaladsl.model.headers._ 22 | import akka.http.scaladsl.server.Directive0 23 | import akka.http.scaladsl.server.Directives.respondWithDefaultHeaders 24 | 25 | trait DefaultHeaders { 26 | 27 | /** 28 | * Adds customHeaders to all http responses. 29 | */ 30 | def eclairHeaders: Directive0 = respondWithDefaultHeaders(customHeaders) 31 | 32 | private val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") :: 33 | `Access-Control-Allow-Methods`(POST) :: 34 | `Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil 35 | } 36 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/EclairDirectives.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | import akka.http.scaladsl.server.{Directive0, Directive1, Directives} 20 | import akka.util.Timeout 21 | import fr.acinq.eclair.api.Service 22 | 23 | import scala.concurrent.duration.DurationInt 24 | 25 | trait EclairDirectives extends Directives with TimeoutDirective with ErrorDirective with AuthDirective with DefaultHeaders with ExtraDirectives { 26 | this: Service => 27 | 28 | /** 29 | * Prepares inner routes to be exposed as public API with default headers, basic authentication and error handling. 30 | * Must be applied *after* aggregating all the inner routes. 31 | */ 32 | def securedHandler: Directive0 = toStrictEntity(5 seconds) & eclairHeaders & handled & authenticated 33 | 34 | /** 35 | * Provides a Timeout to the inner route either from request param or the default. 36 | */ 37 | private def standardHandler: Directive1[Timeout] = toStrictEntity(5 seconds) & withTimeout 38 | 39 | /** 40 | * Handles POST requests with given simple path. The inner route is wrapped in a standard handler and provides a Timeout as parameter. 41 | */ 42 | def postRequest(p: String): Directive1[Timeout] = standardHandler & post & path(p) 43 | 44 | /** 45 | * Handles GET requests with given simple path. The inner route is wrapped in a standard handler and provides a Timeout as parameter. 46 | */ 47 | def getRequest(p: String): Directive1[Timeout] = standardHandler & get & path(p) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ErrorDirective.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes} 20 | import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RejectionHandler} 21 | import fr.acinq.eclair.api.Service 22 | 23 | trait ErrorDirective { 24 | this: Service with EclairDirectives => 25 | 26 | /** 27 | * Handles API exceptions and rejections. Produces json formatted error responses. 28 | */ 29 | def handled: Directive0 = handleExceptions(apiExceptionHandler) & handleRejections(apiRejectionHandler) 30 | 31 | import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} 32 | 33 | private val apiExceptionHandler = ExceptionHandler { 34 | case t: IllegalArgumentException => 35 | logger.error(s"API call failed with cause=${t.getMessage}") 36 | complete(StatusCodes.BadRequest, ErrorResponse(t.getMessage)) 37 | case t: Throwable => 38 | logger.error(s"API call failed with cause=${t.getMessage}") 39 | complete(StatusCodes.InternalServerError, ErrorResponse(t.getMessage)) 40 | } 41 | 42 | // map all the rejections to a JSON error object ErrorResponse 43 | private val apiRejectionHandler = RejectionHandler.default.mapRejectionResponse { 44 | case res@HttpResponse(_, _, ent: HttpEntity.Strict, _) => 45 | res.withEntity(HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String)))) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ErrorResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | case class ErrorResponse(error: String) 20 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/RouteFormat.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | import akka.http.scaladsl.model.StatusCodes.OK 20 | import akka.http.scaladsl.model.{ContentTypes, HttpResponse} 21 | import fr.acinq.eclair.api.serde.JsonSupport._ 22 | import fr.acinq.eclair.json.{JsonSerializers, RouteFullSerializer, RouteNodeIdsSerializer, RouteShortChannelIdsSerializer} 23 | import fr.acinq.eclair.router.Router.RouteResponse 24 | import org.json4s.Formats 25 | 26 | // @formatter:off 27 | sealed trait RouteFormat 28 | case object NodeIdRouteFormat extends RouteFormat 29 | case object ShortChannelIdRouteFormat extends RouteFormat 30 | case object FullRouteFormat extends RouteFormat 31 | // @formatter:on 32 | 33 | object RouteFormat { 34 | 35 | val NODE_ID = "nodeId" 36 | val SHORT_CHANNEL_ID = "shortChannelId" 37 | val FULL = "full" 38 | 39 | def fromString(s: String): RouteFormat = s match { 40 | case NODE_ID => NodeIdRouteFormat 41 | case SHORT_CHANNEL_ID => ShortChannelIdRouteFormat 42 | case FULL => FullRouteFormat 43 | case _ => throw new IllegalArgumentException(s"invalid route format, possible values are ($NODE_ID, $SHORT_CHANNEL_ID, $FULL)") 44 | } 45 | 46 | def format(route: RouteResponse, format_opt: Option[RouteFormat]): HttpResponse = format(route, format_opt.getOrElse(NodeIdRouteFormat)) 47 | 48 | def format(route: RouteResponse, format: RouteFormat): HttpResponse = { 49 | val serializationFormats: Formats = format match { 50 | case NodeIdRouteFormat => JsonSerializers.formats + RouteNodeIdsSerializer 51 | case ShortChannelIdRouteFormat => JsonSerializers.formats + RouteShortChannelIdsSerializer 52 | case FullRouteFormat => JsonSerializers.formats + RouteFullSerializer 53 | } 54 | HttpResponse(OK).withEntity(ContentTypes.`application/json`, serialization.write(route)(serializationFormats)) 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/directives/TimeoutDirective.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.directives 18 | 19 | import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes} 20 | import akka.http.scaladsl.server.{Directive0, Directive1, Directives} 21 | import akka.util.Timeout 22 | import fr.acinq.eclair.api.serde.FormParamExtractors._ 23 | import fr.acinq.eclair.api.serde.JsonSupport 24 | import fr.acinq.eclair.api.serde.JsonSupport._ 25 | 26 | import scala.concurrent.duration.DurationInt 27 | 28 | trait TimeoutDirective extends Directives { 29 | 30 | import JsonSupport.{formats, serialization} 31 | 32 | /** 33 | * Extracts a given request timeout from an optional form field. Provides either the 34 | * extracted Timeout or a default Timeout to the inner route. 35 | */ 36 | def withTimeout: Directive1[Timeout] = extractTimeout.tflatMap { timeout => 37 | withTimeoutRequest(timeout._1) & provide(timeout._1) 38 | } 39 | 40 | private val timeoutResponse: HttpRequest => HttpResponse = { _ => 41 | HttpResponse(StatusCodes.RequestTimeout).withEntity( 42 | ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out")) 43 | ) 44 | } 45 | 46 | private def withTimeoutRequest(t: Timeout): Directive0 = withRequestTimeout(t.duration + 2.seconds) & 47 | withRequestTimeoutResponse(timeoutResponse) 48 | 49 | private def extractTimeout: Directive1[Timeout] = formField("timeoutSeconds".as[Timeout].?).tflatMap { opt => 50 | provide(opt._1.getOrElse(Timeout(30 seconds))) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.handlers 18 | 19 | import akka.http.scaladsl.server.Route 20 | import fr.acinq.eclair.MilliSatoshi 21 | import fr.acinq.eclair.api.Service 22 | import fr.acinq.eclair.api.directives.EclairDirectives 23 | import fr.acinq.eclair.api.serde.FormParamExtractors._ 24 | 25 | trait Fees { 26 | this: Service with EclairDirectives => 27 | 28 | import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} 29 | 30 | val networkFees: Route = postRequest("networkfees") { implicit t => 31 | formFields(fromFormParam(), toFormParam()) { (from, to) => 32 | complete(eclairApi.networkFees(from, to)) 33 | } 34 | } 35 | 36 | val updateRelayFee: Route = postRequest("updaterelayfee") { implicit t => 37 | withNodesIdentifier { nodes => 38 | formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => 39 | complete(eclairApi.updateRelayFee(nodes, feeBase, feeProportional)) 40 | } 41 | } 42 | } 43 | 44 | val feeRoutes: Route = networkFees ~ updateRelayFee 45 | 46 | } 47 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Node.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.handlers 18 | 19 | import akka.http.scaladsl.server.Route 20 | import fr.acinq.eclair.api.Service 21 | import fr.acinq.eclair.api.directives.EclairDirectives 22 | import fr.acinq.eclair.api.serde.FormParamExtractors._ 23 | import fr.acinq.eclair.io.NodeURI 24 | import fr.acinq.eclair.wire.protocol.NodeAddress 25 | 26 | trait Node { 27 | this: Service with EclairDirectives => 28 | 29 | import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} 30 | 31 | val getInfo: Route = postRequest("getinfo") { implicit t => 32 | complete(eclairApi.getInfo()) 33 | } 34 | 35 | val connect: Route = postRequest("connect") { implicit t => 36 | formFields("uri".as[NodeURI]) { uri => 37 | complete(eclairApi.connect(Left(uri))) 38 | } ~ formFields(nodeIdFormParam, "host".as[String], "port".as[Int].?) { (nodeId, host, port_opt) => 39 | complete { 40 | eclairApi.connect( 41 | Left(NodeURI(nodeId, NodeAddress.fromParts(host, port_opt.getOrElse(NodeURI.DEFAULT_PORT)).get)) 42 | ) 43 | } 44 | } ~ formFields(nodeIdFormParam) { nodeId => 45 | complete(eclairApi.connect(Right(nodeId))) 46 | } 47 | } 48 | 49 | val disconnect: Route = postRequest("disconnect") { implicit t => 50 | formFields(nodeIdFormParam) { nodeId => 51 | complete(eclairApi.disconnect(nodeId)) 52 | } 53 | } 54 | 55 | val peers: Route = postRequest("peers") { implicit t => 56 | complete(eclairApi.peers()) 57 | } 58 | 59 | val audit: Route = postRequest("audit") { implicit t => 60 | withPaginated { paginated_opt => 61 | formFields(fromFormParam(), toFormParam()) { (from, to) => 62 | complete(eclairApi.audit(from, to, paginated_opt)) 63 | } 64 | } 65 | } 66 | 67 | val stop: Route = postRequest("stop") { implicit t => 68 | complete(eclairApi.stop()) 69 | } 70 | 71 | val nodeRoutes: Route = getInfo ~ connect ~ disconnect ~ peers ~ audit ~ stop 72 | } 73 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Offer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.handlers 18 | 19 | import akka.http.scaladsl.server.Route 20 | import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 21 | import fr.acinq.eclair.api.Service 22 | import fr.acinq.eclair.api.directives.EclairDirectives 23 | import fr.acinq.eclair.api.serde.FormParamExtractors._ 24 | 25 | trait Offer { 26 | this: Service with EclairDirectives => 27 | 28 | import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} 29 | 30 | val createOffer: Route = postRequest("createoffer") { implicit t => 31 | formFields("description".?, amountMsatFormParam.?, "expireInSeconds".as[Long].?, "issuer".?, "blindedPathsFirstNodeId".as[PublicKey].?) { 32 | (description_opt, amount_opt, expireInSeconds_opt, issuer_opt, blindedPathsFirstNodeId_opt) => complete(eclairApi.createOffer(description_opt, amount_opt, expireInSeconds_opt, issuer_opt, blindedPathsFirstNodeId_opt)) 33 | } 34 | } 35 | 36 | val disableOffer: Route = postRequest("disableoffer") { implicit t => 37 | formFields(offerFormParam) { offer => 38 | complete(eclairApi.disableOffer(offer)) 39 | } 40 | } 41 | 42 | val listoffers: Route = postRequest("listoffers") { implicit t => 43 | formFields("activeOnly".as[Boolean].?) { onlyActive => 44 | complete(eclairApi.listOffers(onlyActive.getOrElse(true))) 45 | } 46 | } 47 | 48 | val offerRoutes: Route = createOffer ~ disableOffer ~ listoffers 49 | 50 | } 51 | -------------------------------------------------------------------------------- /eclair-node/src/main/scala/fr/acinq/eclair/api/serde/JsonSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 ACINQ SAS 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package fr.acinq.eclair.api.serde 18 | 19 | import de.heikoseeberger.akkahttpjson4s.Json4sSupport 20 | import fr.acinq.eclair.json.JsonSerializers 21 | import org.json4s.{Formats, Serialization} 22 | 23 | object JsonSupport extends Json4sSupport { 24 | implicit val serialization: Serialization = JsonSerializers.serialization 25 | implicit val formats: Formats = JsonSerializers.formats 26 | } 27 | -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/channelbalances: -------------------------------------------------------------------------------- 1 | [{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","realScid":"0x0x1","aliases":{"localAlias":"0x2"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","realScid":"0x0x3","aliases":{"localAlias":"0x3","remoteAlias":"0x3"},"canSend":0,"canReceive":30000000,"isPublic":false,"isEnabled":false}] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/close: -------------------------------------------------------------------------------- 1 | {"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e":"closed channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e","0e7d63ce98dbaccd9c3061509e93b45adbeaf10997c4708213804da0edd6d756":"channel not found","42000x27x3":"closed channel e0d736ec89bdcadcc9031605e9394ba5bdae1f90794c07283108d40ade6d7d65"} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/findroute-full: -------------------------------------------------------------------------------- 1 | {"routes":[{"amount":456,"hops":[{"nodeId":"03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","nextNodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x3","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"messageFlags":{"dontForward":false},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{}}}},{"nodeId":"026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","nextNodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x4","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"messageFlags":{"dontForward":false},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{}}}},{"nodeId":"038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","nextNodeId":"02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df","source":{"type":"announcement","channelUpdate":{"signature":"92cf3f12e161391986eb2cd7106ddab41a23c734f8f1ed120fb64f4b91f98f690ecf930388e62965f8aefbf1adafcd25a572669a125396dcfb83615208754679","chainHash":"024b7b3626554c44dcc2454ee3812458bfa68d9fced466edfab470844cb7ffe2","shortChannelId":"1x2x5","timestamp":{"iso":"1970-01-01T00:00:00Z","unix":0},"messageFlags":{"dontForward":false},"channelFlags":{"isEnabled":true,"isNode1":true},"cltvExpiryDelta":0,"htlcMinimumMsat":1,"feeBaseMsat":1,"feeProportionalMillionths":1,"htlcMaximumMsat":20000000,"tlvStream":{}}}}]}]} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/findroute-nodeid: -------------------------------------------------------------------------------- 1 | {"routes":[{"amount":456,"nodeIds":["03007e67dc5a8fd2b2ef21cb310ab6359ddb51f3f86a8b79b8b1e23bc3a6ea150a","026105f6cb4862810be989385d16f04b0f748f6f2a14040338b1a534d45b4be1c1","038cfa2b5857843ee90cff91b06f692c0d8fe201921ee6387aee901d64f43699f0","02be60276e294c6921240daae33a361d214d02578656df0e74c61a09c3196e51df"]}]} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/findroute-scid: -------------------------------------------------------------------------------- 1 | {"routes":[{"amount":456,"shortChannelIds":["1x2x3","1x2x4","1x2x5"]}]} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/getinfo: -------------------------------------------------------------------------------- 1 | {"version":"1.0.0-SNAPSHOT-e3f1ec0","nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","alias":"alice","color":"#000102","features":{"activated":{"option_data_loss_protect":"mandatory","gossip_queries_ex":"optional"},"unknown":[]},"chainHash":"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f","network":"regtest","blockHeight":9999,"publicAddresses":["127.0.0.1:9731"],"instanceId":"01234567-0123-4567-89ab-0123456789ab"} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/help: -------------------------------------------------------------------------------- 1 | ["connect (uri): open a secure connection to a lightning node","connect (nodeId, host, port): open a secure connection to a lightning node","open (nodeId, fundingSatoshis, pushMsat = 0, feerateSatPerByte = ?, channelFlags = 0x01): open a channel with another lightning node, by default push = 0, feerate for the funding tx targets 6 blocks, and channel is announced","updaterelayfee (channelId, feeBaseMsat, feeProportionalMillionths): update relay fee for payments going through this channel","peers: list existing local peers","channels: list existing local channels","channels (nodeId): list existing local channels to a particular nodeId","channel (channelId): retrieve detailed information about a given channel","channelstats: retrieves statistics about channel usage (fees, number and average amount of payments)","allnodes: list all known nodes","allchannels: list all known channels","allupdates: list all channels updates","allupdates (nodeId): list all channels updates for this nodeId","receive (amountMsat, description): generate a payment request for a given amount","receive (amountMsat, description, expirySeconds): generate a payment request for a given amount with a description and a number of seconds till it expires","parseinvoice (paymentRequest): returns node, amount and payment hash in a payment request","findroute (paymentRequest): returns nodes and channels of the route if there is any","findroute (paymentRequest, amountMsat): returns nodes and channels of the route if there is any","findroute (nodeId, amountMsat): returns nodes and channels of the route if there is any","send (amountMsat, paymentHash, nodeId): send a payment to a lightning node","send (paymentRequest): send a payment to a lightning node using a BOLT11 payment request","send (paymentRequest, amountMsat): send a payment to a lightning node using a BOLT11 payment request and a custom amount","close (channelId): close a channel","close (channelId, scriptPubKey): close a channel and send the funds to the given scriptPubKey","forceclose (channelId): force-close a channel by publishing the local commitment tx (careful: this is more expensive than a regular close and will incur a delay before funds are spendable)","checkpayment (paymentHash): returns true if the payment has been received, false otherwise","checkpayment (paymentRequest): returns true if the payment has been received, false otherwise","audit: list all send/received/relayed payments","audit (from, to): list send/received/relayed payments in that interval (from <= timestamp < to)","networkfees: list all network fees paid to the miners, by transaction","networkfees (from, to): list network fees paid to the miners, by transaction, in that interval (from <= timestamp < to)","getinfo: returns info about the blockchain and this node","help: display this message"] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/networkstats: -------------------------------------------------------------------------------- 1 | {"channels":1,"nodes":2,"capacity":{"median":30,"percentile5":12,"percentile10":14,"percentile25":20,"percentile75":40,"percentile90":46,"percentile95":48},"cltvExpiryDelta":{"median":32,"percentile5":11,"percentile10":13,"percentile25":22,"percentile75":42,"percentile90":51,"percentile95":53},"feeBase":{"median":32,"percentile5":11,"percentile10":13,"percentile25":22,"percentile75":42,"percentile90":51,"percentile95":53},"feeProportional":{"median":32,"percentile5":11,"percentile10":13,"percentile25":22,"percentile75":42,"percentile90":51,"percentile95":53}} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/peers: -------------------------------------------------------------------------------- 1 | [{"nodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","state":"CONNECTED","address":"127.0.0.1:9731","channels":1},{"nodeId":"039dc0e0b1d25905e44fdf6f8e89755a5e219685840d0bc1d28d3308f9628a3585","state":"DISCONNECTED","channels":1}] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/received-expired: -------------------------------------------------------------------------------- 1 | {"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03b20720d18c5f4c9c90c281fc511993556abc8c35a1a7c0f050a24b332c1cc105","serialized":"lnbc2500u1pvjluezsp5ggx65m9the9g2fjxhkhmguafsjv832charwnrpqtr4gwq3lae9sqpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspv9fxh2","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"expired"}} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/received-pending: -------------------------------------------------------------------------------- 1 | {"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03e2d79e66bc01619697188e9847e463c70b35e49e0e86b09dcb4b124925c4c550","serialized":"lnbc2500u1pvjluezsp5p0yyewlxryakfx0pt6fu2j7ct2jhgaw7hd62nqwm9yvh2cp8eurspp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rsp9dda9d","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"pending"}} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/received-success: -------------------------------------------------------------------------------- 1 | {"invoice":{"prefix":"lnbc","timestamp":1496314658,"nodeId":"03779dc8b593b74509fab7c8accebc7a9b91d85d9df456d5b885464a34e5751d52","serialized":"lnbc2500u1pvjluezsp5cssgls5lpvunj7zallxsn3v8g3f9wqfs75hsdmkrtxwgkafers0spp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpuaztrnwngzn3kdzw5hydlzf03qdgm2hdq27cqv3agm2awhz5se903vruatfhq77w3ls4evs3ch9zw97j25emudupq63nyw24cg27h2rspzma2k0","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"amount":250000000,"features":{"activated":{},"unknown":[]},"routingInfo":[]},"paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","createdAt":{"iso":"1970-01-01T00:00:00.042Z","unix":0},"status":{"type":"received","amount":42,"receivedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}} -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/sent-failed: -------------------------------------------------------------------------------- 1 | [{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":{"iso":"2021-10-05T13:10:29.123Z","unix":1633439429},"status":{"type":"failed","failures":[],"completedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}}] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/sent-pending: -------------------------------------------------------------------------------- 1 | [{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":{"iso":"2021-10-05T13:10:29.123Z","unix":1633439429},"status":{"type":"pending"}}] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/sent-success: -------------------------------------------------------------------------------- 1 | [{"id":"00000000-0000-0000-0000-000000000000","parentId":"11111111-1111-1111-1111-111111111111","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentType":"Standard","amount":42,"recipientAmount":50,"recipientNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","createdAt":{"iso":"2021-10-05T13:10:29.123Z","unix":1633439429},"status":{"type":"sent","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","feesPaid":5,"route":[],"completedAt":{"iso":"2021-10-05T13:12:23.777Z","unix":1633439543}}}] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/api/usablebalances: -------------------------------------------------------------------------------- 1 | [{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","aliases":{"localAlias":"0x1"},"canSend":100000000,"canReceive":20000000,"isPublic":true,"isEnabled":true},{"remoteNodeId":"03af0ed6052cf28d670665549bc86f4b721c9fdb309d40c58f5811f63966e005d0","realScid":"0x0x2","aliases":{"localAlias":"0x3","remoteAlias":"0x4"},"canSend":400000000,"canReceive":30000000,"isPublic":false,"isEnabled":true}] -------------------------------------------------------------------------------- /eclair-node/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | --------------------------------------------------------------------------------