-
Notifications
You must be signed in to change notification settings - Fork 268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Send events when HTLCs settle on-chain #884
Changes from all commits
85df143
8ec8574
c557f70
cd703f2
10bc4ab
5b423b1
c8d3887
9a7e215
52b17fe
09618a8
008b2ca
14da6d3
9624383
dd52cdc
5e476fd
77eb1de
1b91609
e3f8d9b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,19 @@ | ||
/* | ||
* Copyright 2018 ACINQ SAS | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package fr.acinq.eclair.api | ||
|
||
import akka.http.scaladsl.server._ | ||
|
@@ -15,9 +31,13 @@ import akka.http.scaladsl.model.ws.{Message, TextMessage} | |
import akka.http.scaladsl.server.directives.{Credentials, LoggingMagnet} | ||
import akka.stream.{ActorMaterializer, OverflowStrategy} | ||
import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} | ||
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints | ||
import fr.acinq.eclair.io.NodeURI | ||
import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentReceived, PaymentRequest} | ||
import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed | ||
import fr.acinq.eclair.payment._ | ||
import grizzled.slf4j.Logging | ||
import org.json4s.{ShortTypeHints, TypeHints} | ||
import org.json4s.jackson.Serialization | ||
import scodec.bits.ByteVector | ||
|
||
import scala.concurrent.{ExecutionContext, Future} | ||
|
@@ -31,6 +51,15 @@ trait Service extends Directives with Logging { | |
import JsonSupport.marshaller | ||
import JsonSupport.formats | ||
import JsonSupport.serialization | ||
// used to send typed messages over the websocket | ||
val formatsWithTypeHint = formats.withTypeHintFieldName("type") + | ||
CustomTypeHints(Map( | ||
classOf[PaymentSent] -> "payment-sent", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice |
||
classOf[PaymentRelayed] -> "payment-relayed", | ||
classOf[PaymentReceived] -> "payment-received", | ||
classOf[PaymentSettlingOnChain] -> "payment-settling-onchain", | ||
classOf[PaymentFailed] -> "payment-failed" | ||
)) | ||
|
||
def password: String | ||
|
||
|
@@ -65,13 +94,19 @@ trait Service extends Directives with Logging { | |
// create a flow transforming a queue of string -> string | ||
val (flowInput, flowOutput) = Source.queue[String](10, OverflowStrategy.dropTail).toMat(BroadcastHub.sink[String])(Keep.both).run() | ||
|
||
// register an actor that feeds the queue when a payment is received | ||
// register an actor that feeds the queue on payment related events | ||
actorSystem.actorOf(Props(new Actor { | ||
override def preStart: Unit = context.system.eventStream.subscribe(self, classOf[PaymentReceived]) | ||
|
||
override def preStart: Unit = { | ||
context.system.eventStream.subscribe(self, classOf[PaymentFailed]) | ||
context.system.eventStream.subscribe(self, classOf[PaymentEvent]) | ||
} | ||
|
||
def receive: Receive = { | ||
case received: PaymentReceived => flowInput.offer(received.paymentHash.toString) | ||
case message: PaymentFailed => flowInput.offer(Serialization.write(message)(formatsWithTypeHint)) | ||
case message: PaymentEvent => flowInput.offer(Serialization.write(message)(formatsWithTypeHint)) | ||
} | ||
|
||
})) | ||
|
||
Flow[Message] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1236,7 +1236,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu | |
} | ||
} | ||
// we also need to fail outgoing htlcs that we know will never reach the blockchain | ||
val overridenHtlcs = Closing.overriddenHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) | ||
val overridenHtlcs = Closing.overriddenOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) | ||
overridenHtlcs.foreach { add => | ||
d.commitments.originChannels.get(add.id) match { | ||
case Some(origin) => | ||
|
@@ -1247,15 +1247,20 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu | |
log.info(s"cannot fail overriden htlc #${add.id} paymentHash=${add.paymentHash} (origin not found)") | ||
} | ||
} | ||
// for our outgoing payments, let's send events if we know that they will settle on chain | ||
Closing | ||
.onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sstone can you double check this? this is quite sensitive There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes LGTM |
||
.filter(add => Closing.isSentByLocal(add.id, d.commitments.originChannels)) // we only care about htlcs for which we were the original sender here | ||
.foreach(add => context.system.eventStream.publish(PaymentSettlingOnChain(amount = MilliSatoshi(add.amountMsat), add.paymentHash))) | ||
// then let's see if any of the possible close scenarii can be considered done | ||
val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed | ||
val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) | ||
val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) | ||
val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) | ||
val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) | ||
val futureRemoteCommitDone = futureRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) | ||
val revokedCommitDone = revokedCommitPublished1.map(Closing.isRevokedCommitDone(_)).exists(_ == true) // we only need one revoked commit done | ||
// finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) | ||
val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) | ||
// finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) | ||
val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) | ||
// we also send events related to fee | ||
Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } | ||
val closeType_opt = if (mutualCloseDone) { | ||
|
@@ -1609,7 +1614,7 @@ class Channel(val nodeParams: NodeParams, wallet: EclairWallet, remoteNodeId: Pu | |
} | ||
|
||
(state, nextState, stateData, nextStateData) match { | ||
// ORDER MATTERS! | ||
// ORDER MATTERS! | ||
case (_, _, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate == d2.channelUpdate && d1.channelAnnouncement == d2.channelAnnouncement => | ||
// don't do anything if neither the channel_update nor the channel_announcement didn't change | ||
() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -228,10 +228,11 @@ object PaymentRequest { | |
f.version match { | ||
case 17 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data) | ||
case 18 if prefix == "lnbc" => Base58Check.encode(Base58.Prefix.ScriptAddress, data) | ||
case 17 if prefix == "lntb" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data) | ||
case 18 if prefix == "lntb" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data) | ||
case 17 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sstone can you double check this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's correct because Base58 addresses on testnet and regtest have the same prefix, but not Bech32 addresses :( |
||
case 18 if prefix == "lntb" || prefix == "lnbcrt" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data) | ||
case version if prefix == "lnbc" => Bech32.encodeWitnessAddress("bc", version, data) | ||
case version if prefix == "lntb" => Bech32.encodeWitnessAddress("tb", version, data) | ||
case version if prefix == "lnbcrt" => Bech32.encodeWitnessAddress("bcrt", version, data) | ||
} | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be put in a separate file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it belongs here logically, it's where we define our application-wide JsonSupport and all the custom serializers.