All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fr.acinq.eclair.payment.PaymentEvents.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 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.payment

import java.util.UUID

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.router.Hop

import scala.compat.Platform

/**
 * Created by PM on 01/02/2017.
 */

sealed trait PaymentEvent {
  val paymentHash: ByteVector32
  val timestamp: Long
}

/**
 * A payment was successfully sent and fulfilled.
 *
 * @param id              id of the whole payment attempt (if using multi-part, there will be multiple parts, each with
 *                        a different id).
 * @param paymentHash     payment hash.
 * @param paymentPreimage payment preimage (proof of payment).
 * @param recipientAmount amount that has been received by the final recipient.
 * @param recipientNodeId id of the final recipient.
 * @param parts           child payments (actual outgoing HTLCs).
 */
case class PaymentSent(id: UUID, paymentHash: ByteVector32, paymentPreimage: ByteVector32, recipientAmount: MilliSatoshi, recipientNodeId: PublicKey, parts: Seq[PaymentSent.PartialPayment]) extends PaymentEvent {
  require(parts.nonEmpty, "must have at least one payment part")
  val amountWithFees: MilliSatoshi = parts.map(_.amountWithFees).sum
  val feesPaid: MilliSatoshi = amountWithFees - recipientAmount // overall fees for this payment (routing + trampoline)
  val trampolineFees: MilliSatoshi = parts.map(_.amount).sum - recipientAmount
  val nonTrampolineFees: MilliSatoshi = feesPaid - trampolineFees // routing fees to reach the first trampoline node, or the recipient if not using trampoline
  val timestamp: Long = parts.map(_.timestamp).min // we use min here because we receive the proof of payment as soon as the first partial payment is fulfilled
}

object PaymentSent {

  /**
   * A successfully sent partial payment (single outgoing HTLC).
   *
   * @param id          id of the outgoing payment.
   * @param amount      amount received by the target node.
   * @param feesPaid    fees paid to route to the target node.
   * @param toChannelId id of the channel used.
   * @param route       payment route used.
   * @param timestamp   absolute time in milli-seconds since UNIX epoch when the payment was fulfilled.
   */
  case class PartialPayment(id: UUID, amount: MilliSatoshi, feesPaid: MilliSatoshi, toChannelId: ByteVector32, route: Option[Seq[Hop]], timestamp: Long = Platform.currentTime) {
    require(route.isEmpty || route.get.nonEmpty, "route must be None or contain at least one hop")
    val amountWithFees: MilliSatoshi = amount + feesPaid
  }

}

case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure], timestamp: Long = Platform.currentTime) extends PaymentEvent

sealed trait PaymentRelayed extends PaymentEvent {
  val amountIn: MilliSatoshi
  val amountOut: MilliSatoshi
  val timestamp: Long
}

case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentRelayed

case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, timestamp: Long = Platform.currentTime) extends PaymentRelayed {
  override val amountIn: MilliSatoshi = incoming.map(_.amount).sum
  override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum
}

object PaymentRelayed {

  case class Part(amount: MilliSatoshi, channelId: ByteVector32)

  type Incoming = Seq[Part]
  type Outgoing = Seq[Part]

}

case class PaymentReceived(paymentHash: ByteVector32, parts: Seq[PaymentReceived.PartialPayment]) extends PaymentEvent {
  require(parts.nonEmpty, "must have at least one payment part")
  val amount: MilliSatoshi = parts.map(_.amount).sum
  val timestamp: Long = parts.map(_.timestamp).max // we use max here because we fulfill the payment only once we received all the parts
}

object PaymentReceived {

  case class PartialPayment(amount: MilliSatoshi, fromChannelId: ByteVector32, timestamp: Long = Platform.currentTime)

}

case class PaymentSettlingOnChain(id: UUID, amount: MilliSatoshi, paymentHash: ByteVector32, timestamp: Long = Platform.currentTime) extends PaymentEvent

sealed trait PaymentFailure

/** A failure happened locally, preventing the payment from being sent (e.g. no route found). */
case class LocalFailure(t: Throwable) extends PaymentFailure

/** A remote node failed the payment and we were able to decrypt the onion failure packet. */
case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure

/** A remote node failed the payment but we couldn't decrypt the failure (e.g. a malicious node tampered with the message). */
case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure

object PaymentFailure {

  import fr.acinq.eclair.channel.AddHtlcFailed
  import fr.acinq.eclair.router.RouteNotFound
  import fr.acinq.eclair.wire.Update

  /**
   * Rewrites a list of failures to retrieve the meaningful part.
   *
   * If a list of failures with many elements ends up with a LocalFailure RouteNotFound, this RouteNotFound failure
   * should be removed. This last failure is irrelevant information. In such a case only the n-1 attempts were rejected
   * with a **significant reason**; the final RouteNotFound error provides no meaningful insight.
   *
   * This method should be used by the user interface to provide a non-exhaustive but more useful feedback.
   *
   * @param failures a list of payment failures for a payment
   */
  def transformForUser(failures: Seq[PaymentFailure]): Seq[PaymentFailure] = {
    failures.map {
      case LocalFailure(AddHtlcFailed(_, _, t, _, _, _)) => LocalFailure(t) // we're interested in the error which caused the add-htlc to fail
      case other => other
    } match {
      case previousFailures :+ LocalFailure(RouteNotFound) if previousFailures.nonEmpty => previousFailures
      case other => other
    }
  }

  /**
   * This allows us to detect if a bad node always answers with a new update (e.g. with a slightly different expiry or fee)
   * in order to mess with us.
   */
  def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean =
    failures
      .collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update }
      .isDefined

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy