Skip to content

Commit

Permalink
Add swap-out support and basic tests
Browse files Browse the repository at this point in the history
 - use only shortChannelId, not channelId
 - add isInitiator so same SwapData class works for both senders and receivers
  • Loading branch information
remyers committed Sep 14, 2022
1 parent 1ef5a99 commit c612e69
Show file tree
Hide file tree
Showing 17 changed files with 856 additions and 295 deletions.
11 changes: 8 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,9 @@ trait Eclair {

def stop(): Future[Unit]

def swapIn(channelId: ByteVector32, amount: Satoshi)(implicit timeout: Timeout): Future[Response]
def swapIn(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response]

def swapOut(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response]

def listSwaps()(implicit timeout: Timeout): Future[Iterable[Status]]

Expand Down Expand Up @@ -589,8 +591,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
Future.successful(())
}

override def swapIn(channelId: ByteVector32, amount: Satoshi)(implicit timeout: Timeout): Future[Response] =
appKit.swapRegister.ask(ref => SwapRegister.SwapInRequested(ref, amount, channelId))(timeout, appKit.system.scheduler.toTyped)
override def swapIn(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response] =
appKit.swapRegister.ask(ref => SwapRegister.SwapInRequested(ref, amount, shortChannelId))(timeout, appKit.system.scheduler.toTyped)

override def swapOut(shortChannelId: ShortChannelId, amount: Satoshi)(implicit timeout: Timeout): Future[Response] =
appKit.swapRegister.ask(ref => SwapRegister.SwapOutRequested(ref, amount, shortChannelId))(timeout, appKit.system.scheduler.toTyped)

override def listSwaps()(implicit timeout: Timeout): Future[Iterable[Status]] =
appKit.swapRegister.ask(ref => SwapRegister.ListPendingSwaps(ref))(timeout, appKit.system.scheduler.toTyped)
Expand Down
36 changes: 21 additions & 15 deletions eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapCommands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,34 @@
package fr.acinq.eclair.swap

import akka.actor.typed.ActorRef
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi}
import fr.acinq.bitcoin.scalacompat.Satoshi
import fr.acinq.eclair.ShortChannelId
import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{WatchFundingDeeplyBuriedTriggered, WatchOutputSpentTriggered, WatchTxConfirmedTriggered}
import fr.acinq.eclair.channel.{CMD_GET_CHANNEL_DATA, ChannelData, RES_GET_CHANNEL_DATA, Register}
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent}
import fr.acinq.eclair.swap.SwapData._
import fr.acinq.eclair.swap.SwapResponses.{Response, Status}
import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted}
import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted, SwapInRequest, SwapOutRequest}

object SwapCommands {

sealed trait SwapCommand

// @formatter:off
case class StartSwapInSender(amount: Satoshi, swapId: String, channelId: ByteVector32) extends SwapCommand
case class RestoreSwapInSender(swapData: SwapInSenderData) extends SwapCommand
case class StartSwapInSender(amount: Satoshi, swapId: String, shortChannelId: ShortChannelId) extends SwapCommand
case class StartSwapOutReceiver(request: SwapOutRequest) extends SwapCommand
case class RestoreSwapInSender(swapData: SwapData) extends SwapCommand
case object AbortSwapInSender extends SwapCommand

sealed trait CreateSwapMessages extends SwapCommand
case object StateTimeout extends CreateSwapMessages with AwaitAgreementMessages with CreateOpeningTxMessages with ClaimSwapCsvMessages with WaitCsvMessages with SendAgreementMessages with ClaimSwapMessages
case class ChannelDataFailure(failure: Register.ForwardFailure[CMD_GET_CHANNEL_DATA]) extends CreateSwapMessages
case object StateTimeout extends CreateSwapMessages with AwaitAgreementMessages with CreateOpeningTxMessages with ClaimSwapCsvMessages with WaitCsvMessages with AwaitFeePaymentMessages with ClaimSwapMessages with PayFeeInvoiceMessages with SendAgreementMessages
case class ChannelDataFailure(failure: Register.ForwardShortIdFailure[CMD_GET_CHANNEL_DATA]) extends CreateSwapMessages
case class ChannelDataResult(channelData: RES_GET_CHANNEL_DATA[ChannelData]) extends CreateSwapMessages

sealed trait AwaitAgreementMessages extends SwapCommand

case class SwapMessageReceived(message: HasSwapId) extends AwaitAgreementMessages with CreateOpeningTxMessages with AwaitClaimPaymentMessages with SendAgreementMessages with AwaitOpeningTxConfirmedMessages with ValidateTxMessages with ClaimSwapMessages
case class SwapMessageReceived(message: HasSwapId) extends AwaitAgreementMessages with CreateOpeningTxMessages with AwaitClaimPaymentMessages with AwaitFeePaymentMessages with AwaitOpeningTxConfirmedMessages with ValidateTxMessages with ClaimSwapMessages with PayFeeInvoiceMessages with SendAgreementMessages
case class ForwardFailureAdapter(result: Register.ForwardFailure[HasSwapId]) extends AwaitAgreementMessages

sealed trait CreateOpeningTxMessages extends SwapCommand
Expand All @@ -55,11 +57,11 @@ object SwapCommands {

sealed trait AwaitOpeningTxConfirmedMessages extends SwapCommand
case class OpeningTxConfirmed(openingConfirmedTriggered: WatchTxConfirmedTriggered) extends AwaitOpeningTxConfirmedMessages with ClaimSwapCoopMessages
case object InvoiceExpired extends AwaitOpeningTxConfirmedMessages with AwaitClaimPaymentMessages
case object InvoiceExpired extends AwaitOpeningTxConfirmedMessages with AwaitClaimPaymentMessages with AwaitFeePaymentMessages

sealed trait AwaitClaimPaymentMessages extends SwapCommand
case class CsvDelayConfirmed(csvDelayTriggered: WatchFundingDeeplyBuriedTriggered) extends SwapCommand with WaitCsvMessages
case class PaymentEventReceived(paymentEvent: PaymentEvent) extends AwaitClaimPaymentMessages with PayClaimInvoiceMessages
case class PaymentEventReceived(paymentEvent: PaymentEvent) extends AwaitClaimPaymentMessages with PayClaimInvoiceMessages with AwaitFeePaymentMessages with PayFeeInvoiceMessages

sealed trait ClaimSwapCoopMessages extends SwapCommand
case object ClaimTxCommitted extends ClaimSwapCoopMessages with ClaimSwapCsvMessages with ClaimSwapMessages
Expand All @@ -73,12 +75,14 @@ object SwapCommands {
// @Formatter:on

// @formatter:off
case object StartSwapInReceiver extends SwapCommand
case class RestoreSwapInReceiver(swapData: SwapInReceiverData) extends SwapCommand
case class StartSwapInReceiver(request: SwapInRequest) extends SwapCommand
case class StartSwapOutSender(amount: Satoshi, swapId: String, shortChannelId: ShortChannelId) extends SwapCommand
case class RestoreSwapInReceiver(swapData: SwapData) extends SwapCommand
case object AbortSwapInReceiver extends SwapCommand

sealed trait SendAgreementMessages extends SwapCommand
case class ForwardShortIdFailureAdapter(result: Register.ForwardShortIdFailure[HasSwapId]) extends SendAgreementMessages with SendCoopCloseMessages
sealed trait AwaitFeePaymentMessages extends SwapCommand
case class ForwardShortIdFailureAdapter(result: Register.ForwardShortIdFailure[HasSwapId]) extends AwaitFeePaymentMessages with SendCoopCloseMessages with SendAgreementMessages

sealed trait ValidateTxMessages extends SwapCommand
case class ValidInvoice(invoice: Bolt11Invoice) extends ValidateTxMessages
Expand All @@ -91,8 +95,10 @@ object SwapCommands {

sealed trait ClaimSwapMessages extends SwapCommand

sealed trait UserMessages extends SendAgreementMessages with AwaitAgreementMessages with CreateOpeningTxMessages with AwaitOpeningTxConfirmedMessages with ValidateTxMessages with PayClaimInvoiceMessages with AwaitClaimPaymentMessages with ClaimSwapMessages with SendCoopCloseMessages with ClaimSwapCoopMessages with WaitCsvMessages with ClaimSwapCsvMessages
case class GetStatus(replyTo: ActorRef[Status]) extends UserMessages
case class CancelRequested(replyTo: ActorRef[Response]) extends UserMessages
sealed trait PayFeeInvoiceMessages extends SwapCommand

sealed trait UserMessages extends AwaitFeePaymentMessages with AwaitAgreementMessages with CreateOpeningTxMessages with AwaitOpeningTxConfirmedMessages with ValidateTxMessages with PayClaimInvoiceMessages with AwaitClaimPaymentMessages with ClaimSwapMessages with SendCoopCloseMessages with ClaimSwapCoopMessages with WaitCsvMessages with ClaimSwapCsvMessages
case class GetStatus(replyTo: ActorRef[Status]) extends UserMessages with PayFeeInvoiceMessages with SendAgreementMessages
case class CancelRequested(replyTo: ActorRef[Response]) extends UserMessages with PayFeeInvoiceMessages with SendAgreementMessages
// @Formatter:on
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,9 @@

package fr.acinq.eclair.swap

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.payment.Bolt11Invoice
import fr.acinq.eclair.wire.protocol.{OpeningTxBroadcasted, SwapInAgreement, SwapInRequest}
import fr.acinq.eclair.wire.protocol.{OpeningTxBroadcasted, SwapAgreement, SwapRequest}

object SwapData {

final case class SwapInSenderData(channelId: ByteVector32, request: SwapInRequest, agreement: SwapInAgreement, invoice: Bolt11Invoice, openingTxBroadcasted: OpeningTxBroadcasted)

final case class SwapInReceiverData(request: SwapInRequest, agreement: SwapInAgreement, invoice: Bolt11Invoice, openingTxBroadcasted: OpeningTxBroadcasted)
final case class SwapData(request: SwapRequest, agreement: SwapAgreement, invoice: Bolt11Invoice, openingTxBroadcasted: OpeningTxBroadcasted, isInitiator: Boolean)
}
38 changes: 26 additions & 12 deletions eclair-core/src/main/scala/fr/acinq/eclair/swap/SwapHelpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,37 +22,38 @@ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.typed.{ActorRef, Behavior}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi, Transaction}
import fr.acinq.eclair.MilliSatoshi.toMilliSatoshi
import fr.acinq.eclair.blockchain.OnChainWallet
import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.{CMD_GET_CHANNEL_DATA, ChannelData, RES_GET_CHANNEL_DATA, Register}
import fr.acinq.eclair.db.PaymentType
import fr.acinq.eclair.payment.send.PaymentInitiator.SendPaymentToNode
import fr.acinq.eclair.payment.{Bolt11Invoice, PaymentEvent}
import fr.acinq.eclair.swap.SwapCommands._
import fr.acinq.eclair.swap.SwapEvents.TransactionPublished
import fr.acinq.eclair.swap.SwapTransactions.makeSwapOpeningTxOut
import fr.acinq.eclair.transactions.Transactions.{TransactionWithInputInfo, checkSpendable}
import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted, SwapInAgreement, SwapInRequest}
import fr.acinq.eclair.{NodeParams, ShortChannelId}
import scodec.bits.ByteVector
import fr.acinq.eclair.wire.protocol.{HasSwapId, OpeningTxBroadcasted}
import fr.acinq.eclair.{NodeParams, ShortChannelId, TimestampSecond, randomBytes32}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.reflect.ClassTag
import scala.util.{Failure, Success}
import scala.util.{Failure, Success, Try}

object SwapHelpers {

def queryChannelData(register: actor.ActorRef, channelId: ByteVector32)(implicit context: ActorContext[SwapCommand]): Unit =
register ! Register.Forward[CMD_GET_CHANNEL_DATA](channelDataFailureAdapter(context), channelId, CMD_GET_CHANNEL_DATA(channelDataResultAdapter(context).toClassic))
def queryChannelData(register: actor.ActorRef, shortChannelId: ShortChannelId)(implicit context: ActorContext[SwapCommand]): Unit =
register ! Register.ForwardShortId[CMD_GET_CHANNEL_DATA](channelDataFailureAdapter(context), shortChannelId, CMD_GET_CHANNEL_DATA(channelDataResultAdapter(context).toClassic))

def channelDataResultAdapter(context: ActorContext[SwapCommand]): ActorRef[RES_GET_CHANNEL_DATA[ChannelData]] =
context.messageAdapter[RES_GET_CHANNEL_DATA[ChannelData]](ChannelDataResult)

def channelDataFailureAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardFailure[CMD_GET_CHANNEL_DATA]] =
context.messageAdapter[Register.ForwardFailure[CMD_GET_CHANNEL_DATA]](ChannelDataFailure)
def channelDataFailureAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardShortIdFailure[CMD_GET_CHANNEL_DATA]] =
context.messageAdapter[Register.ForwardShortIdFailure[CMD_GET_CHANNEL_DATA]](ChannelDataFailure)

def receiveSwapMessage[B <: SwapCommand : ClassTag](context: ActorContext[SwapCommand], stateName: String)(f: B => Behavior[SwapCommand]): Behavior[SwapCommand] = {
context.log.debug(s"$stateName: waiting for messages, context: ${context.self.toString}")
Expand All @@ -66,6 +67,8 @@ object SwapHelpers {

def swapInvoiceExpiredTimer(swapId: String): String = "swap-invoice-expired-timer-" + swapId

def swapFeeExpiredTimer(swapId: String): String = "swap-fee-expired-timer-" + swapId

def watchForTxConfirmation(watcher: ActorRef[ZmqWatcher.Command])(replyTo: ActorRef[WatchTxConfirmedTriggered], txId: ByteVector32, minDepth: Long): Unit =
watcher ! WatchTxConfirmed(replyTo, txId, minDepth)

Expand Down Expand Up @@ -96,11 +99,11 @@ object SwapHelpers {
def forwardAdapter(context: ActorContext[SwapCommand]): ActorRef[Register.ForwardFailure[HasSwapId]] =
context.messageAdapter[Register.ForwardFailure[HasSwapId]](ForwardFailureAdapter)

def fundOpening(wallet: OnChainWallet, feeRatePerKw: FeeratePerKw)(request: SwapInRequest, agreement: SwapInAgreement, invoice: Bolt11Invoice)(implicit context: ActorContext[SwapCommand]): Unit = {
def fundOpening(wallet: OnChainWallet, feeRatePerKw: FeeratePerKw)(amount: Satoshi, makerPubkey: PublicKey, takerPubkey: PublicKey, invoice: Bolt11Invoice)(implicit context: ActorContext[SwapCommand]): Unit = {
// setup conditions satisfied, create the opening tx
val openingTx = makeSwapOpeningTxOut((request.amount + agreement.premium).sat, PublicKey(ByteVector.fromValidHex(request.pubkey)), PublicKey(ByteVector.fromValidHex(agreement.pubkey)), invoice.paymentHash)
val openingTx = makeSwapOpeningTxOut(amount, makerPubkey, takerPubkey, invoice.paymentHash)
// funding successful, commit the opening tx
context.pipeToSelf(wallet.makeFundingTx(openingTx.publicKeyScript, (request.amount + agreement.premium).sat, feeRatePerKw)) {
context.pipeToSelf(wallet.makeFundingTx(openingTx.publicKeyScript, amount, feeRatePerKw)) {
case Success(r) => OpeningTxFunded(invoice, r)
case Failure(cause) => OpeningTxFailed(s"error while funding swap open tx: $cause")
}
Expand Down Expand Up @@ -137,4 +140,15 @@ object SwapHelpers {
case Success(status) => RollbackSuccess(error, status)
case Failure(t) => RollbackFailure(error, t)
}

def createInvoice(nodeParams: NodeParams, amount: Satoshi, description: String)(implicit context: ActorContext[SwapCommand]): Try[Bolt11Invoice] =
Try {
val paymentPreimage = randomBytes32()
val invoice: Bolt11Invoice = Bolt11Invoice(nodeParams.chainHash, Some(toMilliSatoshi(amount)), Crypto.sha256(paymentPreimage), nodeParams.privateKey, Left(description),
nodeParams.channelConf.minFinalExpiryDelta, fallbackAddress = None, expirySeconds = Some(nodeParams.invoiceExpiry.toSeconds),
extraHops = Nil, timestamp = TimestampSecond.now(), paymentSecret = paymentPreimage, paymentMetadata = None, features = nodeParams.features.invoiceFeatures())
context.log.debug("generated invoice={} from amount={} sat, description={}", invoice.toString, amount, description)
nodeParams.db.payments.addIncomingPayment(invoice, paymentPreimage, PaymentType.Standard)
invoice
}
}
Loading

0 comments on commit c612e69

Please sign in to comment.