1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 06:21:42 +01:00

Send events when HTLCs settle on-chain (#884)

* send events when htlc settle on-chain

* send events when a payment is received/relayed/sent

* send events when a payment is failed

* Add test for websocket

* Use nicer custom type hints when serializing to json (websocket)

* Fix bech32 prefix for regtest

* Separate cases for bech32 testnet and regtest for BOLT11 fallback address
This commit is contained in:
araspitzu 2019-04-03 11:41:08 +02:00 committed by GitHub
commit 3a56ad9133
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 183 additions and 25 deletions

View file

@ -34,7 +34,7 @@ import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInpu
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{ShortChannelId, UInt64}
import org.json4s.JsonAST._
import org.json4s.{CustomKeySerializer, CustomSerializer, jackson}
import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson}
import scodec.bits.ByteVector
/**
@ -186,4 +186,15 @@ object JsonSupport extends Json4sSupport {
implicit val shouldWritePretty: ShouldWritePretty = ShouldWritePretty.True
case class CustomTypeHints(custom: Map[Class[_], String]) extends TypeHints {
val reverse: Map[String, Class[_]] = custom.map(_.swap)
override val hints: List[Class[_]] = custom.keys.toList
override def hintFor(clazz: Class[_]): String = custom.getOrElse(clazz, {
throw new IllegalArgumentException(s"No type hint mapping found for $clazz")
})
override def classFor(hint: String): Option[Class[_]] = reverse.get(hint)
}
}

View file

@ -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",
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]

View file

@ -1242,7 +1242,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
}
}
// 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) =>
@ -1253,15 +1253,20 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
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)
.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) {
@ -1615,7 +1620,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
}
(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
()

View file

@ -23,6 +23,7 @@ import fr.acinq.bitcoin.{OutPoint, _}
import fr.acinq.eclair.blockchain.EclairWallet
import fr.acinq.eclair.crypto.{Generators, KeyManager}
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.payment.{Local, Origin}
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.transactions._
@ -262,9 +263,9 @@ object Helpers {
*
* @param now current timet
* @param waitingSince we have been waiting since that time
* @param delay the nominal delay that we were supposed to wait
* @param minDelay the minimum delay even if the nominal one has expired
* @return the delay we will actually wait
* @param delay the nominal delay that we were supposed to wait
* @param minDelay the minimum delay even if the nominal one has expired
* @return the delay we will actually wait
*/
def computeFundingTimeout(now: Long, waitingSince: Long, delay: FiniteDuration, minDelay: FiniteDuration): FiniteDuration = {
import scala.concurrent.duration._
@ -802,6 +803,39 @@ object Helpers {
}).toSet.flatten
}
/**
* Tells if we were the origin of this outgoing htlc
*
* @param htlcId
* @param originChannels
* @return
*/
def isSentByLocal(htlcId: Long, originChannels: Map[Long, Origin]) = originChannels.get(htlcId) match {
case Some(Local(_)) => true
case _ => false
}
/**
* As soon as a local or remote commitment reaches min_depth, we know which htlcs will be settled on-chain (whether
* or not they actually have an output in the commitment tx).
*
* @param localCommit
* @param remoteCommit
* @param nextRemoteCommit_opt
* @param tx a transaction that is sufficiently buried in the blockchain
*/
def onchainOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction): Set[UpdateAddHtlc] = {
if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) {
localCommit.spec.htlcs.filter(_.direction == OUT).map(_.add)
} else if (remoteCommit.txid == tx.txid) {
remoteCommit.spec.htlcs.filter(_.direction == IN).map(_.add)
} else if (nextRemoteCommit_opt.map(_.txid) == Some(tx.txid)) {
nextRemoteCommit_opt.get.spec.htlcs.filter(_.direction == IN).map(_.add)
} else {
Set.empty
}
}
/**
* If a local commitment tx reaches min_depth, we need to fail the outgoing htlcs that only us had signed, because
* they will never reach the blockchain.
@ -814,7 +848,7 @@ object Helpers {
* @param log
* @return
*/
def overriddenHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] =
def overriddenOutgoingHtlcs(localCommit: LocalCommit, remoteCommit: RemoteCommit, nextRemoteCommit_opt: Option[RemoteCommit], tx: Transaction)(implicit log: LoggingAdapter): Set[UpdateAddHtlc] =
if (localCommit.publishableTxs.commitTx.tx.txid == tx.txid) {
// our commit got confirmed, so any htlc that we signed but they didn't sign will never reach the chain
val mostRecentRemoteCommit = nextRemoteCommit_opt.getOrElse(remoteCommit)
@ -1053,4 +1087,4 @@ object Helpers {
}
}
}
}

View file

@ -50,7 +50,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin
case PurgeExpiredRequests =>
context.become(run(hash2preimage.filterNot { case (_, pr) => hasExpired(pr) }))
case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops) =>
case ReceivePayment(amount_opt, desc, expirySeconds_opt, extraHops, fallbackAddress_opt) =>
Try {
if (hash2preimage.size > nodeParams.maxPendingPaymentRequests) {
throw new RuntimeException(s"too many pending payment requests (max=${nodeParams.maxPendingPaymentRequests})")
@ -58,7 +58,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin
val paymentPreimage = randomBytes32
val paymentHash = Crypto.sha256(paymentPreimage)
val expirySeconds = expirySeconds_opt.getOrElse(nodeParams.paymentRequestExpiry.toSeconds)
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress = None, expirySeconds = Some(expirySeconds), extraHops = extraHops)
val paymentRequest = PaymentRequest(nodeParams.chainHash, amount_opt, paymentHash, nodeParams.privateKey, desc, fallbackAddress_opt, expirySeconds = Some(expirySeconds), extraHops = extraHops)
log.debug(s"generated payment request=${PaymentRequest.write(paymentRequest)} from amount=$amount_opt")
sender ! paymentRequest
context.become(run(hash2preimage + (paymentHash -> PendingPaymentRequest(paymentPreimage, paymentRequest))))

View file

@ -32,3 +32,5 @@ case class PaymentSent(amount: MilliSatoshi, feesPaid: MilliSatoshi, paymentHash
case class PaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent
case class PaymentReceived(amount: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent
case class PaymentSettlingOnChain(amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent

View file

@ -180,7 +180,7 @@ object PaymentLifecycle {
def props(sourceNodeId: PublicKey, router: ActorRef, register: ActorRef) = Props(classOf[PaymentLifecycle], sourceNodeId, router, register)
// @formatter:off
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil)
case class ReceivePayment(amountMsat_opt: Option[MilliSatoshi], description: String, expirySeconds_opt: Option[Long] = None, extraHops: List[List[ExtraHop]] = Nil, fallbackAddress: Option[String] = None)
/**
* @param maxFeePct set by default to 3% as a safety measure (even if a route is found, if fee is higher than that payment won't be attempted)
*/

View file

@ -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)
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)
}
}
}

View file

@ -19,20 +19,22 @@ package fr.acinq.eclair.api
import akka.actor.{Actor, ActorSystem, Props, Scheduler}
import org.scalatest.FunSuite
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest}
import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest, WSProbe}
import fr.acinq.eclair._
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import TestConstants._
import akka.http.scaladsl.model.headers.BasicHttpCredentials
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.{Directives, Route}
import akka.stream.ActorMaterializer
import akka.http.scaladsl.model.{ContentTypes, FormData, MediaTypes, Multipart}
import fr.acinq.bitcoin.{ByteVector32, Crypto}
import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi}
import fr.acinq.eclair.channel.RES_GETINFO
import fr.acinq.eclair.db.{NetworkFee, Stats}
import fr.acinq.eclair.payment.{PaymentLifecycle, PaymentRequest}
import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{ChannelDesc, RouteResponse}
import fr.acinq.eclair.wire.{ChannelUpdate, NodeAddress, NodeAnnouncement}
import org.json4s.jackson.Serialization
import scodec.bits.ByteVector
import scala.concurrent.Future
import scala.concurrent.duration._
@ -254,6 +256,53 @@ class ApiServiceSpec extends FunSuite with ScalatestRouteTest {
}
}
test("the websocket should return typed objects") {
val mockService = new MockService(new EclairMock {})
val websocketRoute = Directives.path("ws") {
Directives.handleWebSocketMessages(mockService.makeSocketHandler)
}
val wsClient = WSProbe()
WS("/ws", wsClient.flow) ~> websocketRoute ~>
check {
val pf = PaymentFailed(ByteVector32.Zeroes, failures = Seq.empty)
val expectedSerializedPf = """{"type":"payment-failed","paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","failures":[]}"""
Serialization.write(pf)(mockService.formatsWithTypeHint) === expectedSerializedPf
system.eventStream.publish(pf)
wsClient.expectMessage(expectedSerializedPf)
val ps = PaymentSent(amount = MilliSatoshi(21), feesPaid = MilliSatoshi(1), paymentHash = ByteVector32.Zeroes, paymentPreimage = ByteVector32.One, toChannelId = ByteVector32.Zeroes, timestamp = 1553784337711L)
val expectedSerializedPs = """{"type":"payment-sent","amount":21,"feesPaid":1,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","paymentPreimage":"0100000000000000000000000000000000000000000000000000000000000000","toChannelId":"0000000000000000000000000000000000000000000000000000000000000000","timestamp":1553784337711}"""
Serialization.write(ps)(mockService.formatsWithTypeHint) === expectedSerializedPs
system.eventStream.publish(ps)
wsClient.expectMessage(expectedSerializedPs)
val prel = PaymentRelayed(amountIn = MilliSatoshi(21), amountOut = MilliSatoshi(20), paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.Zeroes, ByteVector32.One, timestamp = 1553784963659L)
val expectedSerializedPrel = """{"type":"payment-relayed","amountIn":21,"amountOut":20,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0000000000000000000000000000000000000000000000000000000000000000","toChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}"""
Serialization.write(prel)(mockService.formatsWithTypeHint) === expectedSerializedPrel
system.eventStream.publish(prel)
wsClient.expectMessage(expectedSerializedPrel)
val precv = PaymentReceived(amount = MilliSatoshi(21), paymentHash = ByteVector32.Zeroes, fromChannelId = ByteVector32.One, timestamp = 1553784963659L)
val expectedSerializedPrecv = """{"type":"payment-received","amount":21,"paymentHash":"0000000000000000000000000000000000000000000000000000000000000000","fromChannelId":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553784963659}"""
Serialization.write(precv)(mockService.formatsWithTypeHint) === expectedSerializedPrecv
system.eventStream.publish(precv)
wsClient.expectMessage(expectedSerializedPrecv)
val pset = PaymentSettlingOnChain(amount = MilliSatoshi(21), paymentHash = ByteVector32.One, timestamp = 1553785442676L)
val expectedSerializedPset = """{"type":"payment-settling-onchain","amount":21,"paymentHash":"0100000000000000000000000000000000000000000000000000000000000000","timestamp":1553785442676}"""
Serialization.write(pset)(mockService.formatsWithTypeHint) === expectedSerializedPset
system.eventStream.publish(pset)
wsClient.expectMessage(expectedSerializedPset)
}
}
private def matchTestJson(apiName: String, response: String) = {
val resource = getClass.getResourceAsStream(s"/api/$apiName")
val expectedResponse = Try(Source.fromInputStream(resource).mkString).getOrElse {

View file

@ -18,11 +18,16 @@ package fr.acinq.eclair.api
import java.net.InetAddress
import fr.acinq.bitcoin.{MilliSatoshi, OutPoint}
import fr.acinq.eclair._
import fr.acinq.eclair.payment.{PaymentRequest, PaymentSettlingOnChain}
import fr.acinq.bitcoin.{ByteVector32, OutPoint}
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints
import fr.acinq.eclair.payment.PaymentRequest
import fr.acinq.eclair.transactions.{IN, OUT}
import fr.acinq.eclair.wire.{NodeAddress, Tor2, Tor3}
import org.json4s.jackson.Serialization
import org.json4s.{DefaultFormats, ShortTypeHints}
import org.scalatest.{FunSuite, Matchers}
import scodec.bits._
@ -71,4 +76,10 @@ class JsonSerializersSpec extends FunSuite with Matchers {
val pr = PaymentRequest.read(ref)
Serialization.write(pr)(org.json4s.DefaultFormats + new PaymentRequestSerializer) shouldBe """{"prefix":"lnbc","amount":250000000,"timestamp":1496314658,"nodeId":"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad","description":"1 cup coffee","paymentHash":"0001020304050607080900010203040506070809000102030405060708090102","expiry":60,"minFinalCltvExpiry":null}"""
}
test("type hints") {
implicit val formats = DefaultFormats.withTypeHintFieldName("type") + CustomTypeHints(Map(classOf[PaymentSettlingOnChain] -> "payment-settling-onchain")) + new MilliSatoshiSerializer
val e1 = PaymentSettlingOnChain(MilliSatoshi(42), randomBytes32)
assert(Serialization.writePretty(e1).contains("\"type\" : \"payment-settling-onchain\""))
}
}

View file

@ -24,7 +24,7 @@ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.fee.FeeratesPerKw
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.payment.{CommandBuffer, ForwardAdd, ForwardFulfill, Local}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass, randomBytes32}
@ -230,6 +230,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
import f._
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[LocalCommitConfirmed])
system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain])
// alice sends an htlc to bob
val (ra1, htlca1) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
crossSign(alice, bob, alice2bob, bob2alice)
@ -251,6 +252,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// actual test starts here
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(aliceCommitTx), 42, 0)
assert(listener.expectMsgType[LocalCommitConfirmed].refundAtBlock == 42 + TestConstants.Bob.channelParams.toSelfDelay)
assert(listener.expectMsgType[PaymentSettlingOnChain].paymentHash == htlca1.paymentHash)
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimMainDelayedTx), 200, 0)
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(htlcTimeoutTx), 201, 0)
alice ! WatchEventConfirmed(BITCOIN_TX_CONFIRMED(claimDelayedTx), 202, 0)
@ -260,6 +262,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
test("recv BITCOIN_TX_CONFIRMED (local commit with htlcs only signed by local)") { f =>
import f._
val sender = TestProbe()
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain])
val aliceCommitTx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
// alice sends an htlc
val (r, htlc) = addHtlc(4200000, alice, bob, alice2bob, bob2alice)
@ -282,11 +286,15 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// so she fails it
val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id)
relayerA.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None)))
// the htlc will not settle on chain
listener.expectNoMsg(2 seconds)
}
test("recv BITCOIN_TX_CONFIRMED (remote commit with htlcs only signed by local in next remote commit)") { f =>
import f._
val sender = TestProbe()
val listener = TestProbe()
system.eventStream.subscribe(listener.ref, classOf[PaymentSettlingOnChain])
val bobCommitTx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx
// alice sends an htlc
val (r, htlc) = addHtlc(4200000, alice, bob, alice2bob, bob2alice)
@ -307,6 +315,8 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
// so she fails it
val origin = alice.stateData.asInstanceOf[DATA_CLOSING].commitments.originChannels(htlc.id)
relayerA.expectMsg(Status.Failure(AddHtlcFailed(aliceData.channelId, htlc.paymentHash, HtlcOverridenByLocalCommit(aliceData.channelId), origin, None, None)))
// the htlc will not settle on chain
listener.expectNoMsg(2 seconds)
}
test("recv BITCOIN_FUNDING_SPENT (remote commit)") { f =>