Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ trait Eclair {

def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]]

def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Long] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Long] = None, maxFeePct_opt: Option[Double] = None, paymentRequest_opt: Option[PaymentRequest] = None)(implicit timeout: Timeout): Future[UUID]

def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]

Expand Down Expand Up @@ -188,7 +188,8 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.paymentInitiator ? SendPaymentToRoute(amountMsat, paymentHash, route, finalCltvExpiry)).mapTo[UUID]
}

override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThresholdSat_opt: Option[Long], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
override def send(recipientNodeId: PublicKey, amountMsat: Long, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]], minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThresholdSat_opt: Option[Long], maxFeePct_opt: Option[Double], paymentRequest_opt: Option[PaymentRequest])(implicit timeout: Timeout): Future[UUID] = {
require(paymentRequest_opt.map(PaymentRequest.write(_).getBytes).forall(_.length <= 65536))
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)

val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
Expand All @@ -198,8 +199,8 @@ class EclairImpl(appKit: Kit) extends Eclair {
)

val sendPayment = minFinalCltvExpiry_opt match {
case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case None => SendPayment(amountMsat, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case Some(minCltv) => SendPayment(amountMsat, paymentHash, recipientNodeId, paymentRequest_opt, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case None => SendPayment(amountMsat, paymentHash, recipientNodeId, paymentRequest_opt, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams))
}
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
Expand Down
6 changes: 3 additions & 3 deletions eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -219,15 +219,15 @@ trait Service extends ExtraDirectives with Logging {
path("payinvoice") {
formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Long].?, "maxFeePct".as[Double].?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
complete(eclairApi.send(nodeId, amount.toLong, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, None))
case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt, None))
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
}
} ~
path("sendtonode") {
formFields(amountMsatFormParam, paymentHashFormParam, nodeIdFormParam, "maxAttempts".as[Int].?, "feeThresholdSat".as[Long].?, "maxFeePct".as[Double].?) { (amountMsat, paymentHash, nodeId, maxAttempts_opt, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt))
complete(eclairApi.send(nodeId, amountMsat, paymentHash, maxAttempts_opt = maxAttempts_opt, feeThresholdSat_opt = feeThresholdSat_opt, maxFeePct_opt = maxFeePct_opt, paymentRequest_opt = None))
}
} ~
path("sendtoroute") {
Expand Down
33 changes: 24 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/db/PaymentsDb.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package fr.acinq.eclair.db

import java.util.UUID

import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.payment.PaymentRequest

trait PaymentsDb {
Expand Down Expand Up @@ -65,18 +67,31 @@ trait PaymentsDb {
case class IncomingPayment(paymentHash: ByteVector32, amountMsat: Long, receivedAt: Long)

/**
* Sent payment is every payment that is sent by this node, they may not be finalized and
* OutgoingPayment is every payment that is sent by this node, they may not be finalized and
* when is final it can be failed or successful.
*
* @param id internal payment identifier
* @param paymentHash payment_hash
* @param preimage the preimage of the payment_hash, known if the outgoing payment was successful
* @param amountMsat amount of the payment, in milli-satoshis
* @param createdAt absolute time in seconds since UNIX epoch when the payment was created.
* @param completedAt absolute time in seconds since UNIX epoch when the payment succeeded.
* @param status current status of the payment.
* @param id internal payment identifier
* @param paymentHash payment_hash
* @param preimage the preimage of the payment_hash, known if the outgoing payment was successful
* @param targetNodeId the recipient of this payment
* @param amountMsat amount of the payment, in milli-satoshis
* @param createdAt absolute time in seconds since UNIX epoch when the payment was created.
* @param completedAt absolute time in seconds since UNIX epoch when the payment succeeded.
* @param status current status of the payment.
* @param paymentRequest_opt the payment request (serialized) that was associated with this payment
* @param description_opt a custom description
*/
case class OutgoingPayment(id: UUID, paymentHash: ByteVector32, preimage:Option[ByteVector32], amountMsat: Long, createdAt: Long, completedAt: Option[Long], status: OutgoingPaymentStatus.Value)
case class OutgoingPayment(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation is a bit weird, it should be:

case class OutgoingPayment(id: UUID,
                           paymentHash: ByteVector32,
                           preimage: Option[ByteVector32],
                           amountMsat: Long,
                           createdAt: Long,
                           completedAt: Option[Long],
                           status: OutgoingPaymentStatus.Value,
                           paymentRequest_opt: Option[PaymentRequest] = None,
                           description_opt: Option[String] = None,
                           targetNodeId: PublicKey)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7c2e8c9

id: UUID,
paymentHash: ByteVector32,
preimage:Option[ByteVector32],
targetNodeId: Option[PublicKey],
amountMsat: Long,
createdAt: Long,
completedAt: Option[Long],
status: OutgoingPaymentStatus.Value,
paymentRequest_opt: Option[String] = None,
description_opt: Option[String] = None)

object OutgoingPaymentStatus extends Enumeration {
val PENDING = Value(1, "PENDING")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package fr.acinq.eclair.db.sqlite

import java.sql.Connection
import java.sql.{Connection, Statement}
import java.util.UUID
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.eclair.db.sqlite.SqliteUtils._
Expand All @@ -25,6 +25,7 @@ import fr.acinq.eclair.payment.PaymentRequest
import grizzled.slf4j.Logging
import scala.collection.immutable.Queue
import OutgoingPaymentStatus._
import fr.acinq.bitcoin.Crypto.PublicKey
import concurrent.duration._
import scala.compat.Platform

Expand All @@ -33,25 +34,50 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
import SqliteUtils.ExtendedResultSet._

val DB_NAME = "payments"
val CURRENT_VERSION = 2
val CURRENT_VERSION = 3

using(sqlite.createStatement()) { statement =>
require(getVersion(statement, DB_NAME, CURRENT_VERSION) <= CURRENT_VERSION, s"incompatible version of $DB_NAME DB found") // version 2 is "backward compatible" in the sense that it uses separate tables from version 1. There is no migration though
def migration12(statement: Statement) = {
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL)")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS payment_hash_idx ON sent_payments(payment_hash)")
setVersion(statement, DB_NAME, CURRENT_VERSION)
}

def migration23(statement: Statement) = {
statement.executeUpdate("ALTER TABLE sent_payments ADD payment_request TEXT")
statement.executeUpdate("ALTER TABLE sent_payments ADD description BLOB")
statement.executeUpdate(s"ALTER TABLE sent_payments ADD target_node_id BLOB")
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a default node id for the payment destination does not sound right. I think we should either make this field nullable, meaning that OutgoingPayment.targetNodeId would be optional, or have a "" or "unknown"default value for the target_node_id column.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, i've made a fix in 6c71ea2 where the targetNodeId becomes optional (and there is no default for migrated records)


using(sqlite.createStatement()) { statement =>
getVersion(statement, DB_NAME, CURRENT_VERSION) match {
case 1 =>
logger.warn(s"migrating $DB_NAME from version 1 to $CURRENT_VERSION")
migration12(statement)
migration23(statement)
setVersion(statement, DB_NAME, CURRENT_VERSION)
case 2 =>
logger.warn(s"migrating $DB_NAME from version 2 to $CURRENT_VERSION")
migration23(statement)
setVersion(statement, DB_NAME, CURRENT_VERSION)
case CURRENT_VERSION =>
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received_payments (payment_hash BLOB NOT NULL PRIMARY KEY, preimage BLOB NOT NULL, payment_request TEXT NOT NULL, received_msat INTEGER, created_at INTEGER NOT NULL, expire_at INTEGER, received_at INTEGER)")
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent_payments (id TEXT NOT NULL PRIMARY KEY, payment_hash BLOB NOT NULL, preimage BLOB, amount_msat INTEGER NOT NULL, created_at INTEGER NOT NULL, completed_at INTEGER, status VARCHAR NOT NULL, payment_request TEXT, description BLOB, target_node_id BLOB)")
statement.executeUpdate("CREATE INDEX IF NOT EXISTS payment_hash_idx ON sent_payments(payment_hash)")
setVersion(statement, DB_NAME, CURRENT_VERSION)
}
}

override def addOutgoingPayment(sent: OutgoingPayment): Unit = {
using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status) VALUES (?, ?, ?, ?, ?)")) { statement =>
using(sqlite.prepareStatement("INSERT INTO sent_payments (id, payment_hash, amount_msat, created_at, status, payment_request, description, target_node_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement =>
statement.setString(1, sent.id.toString)
statement.setBytes(2, sent.paymentHash.toArray)
statement.setLong(3, sent.amountMsat)
statement.setLong(4, sent.createdAt)
statement.setString(5, sent.status.toString)
val res = statement.executeUpdate()
logger.debug(s"inserted $res payment=${sent.paymentHash} into payment DB")
statement.setString(6, sent.paymentRequest_opt.orNull)
statement.setBytes(7, sent.description_opt.map(_.getBytes).orNull)
statement.setBytes(8, sent.targetNodeId.map(_.value.toArray).orNull)
statement.executeUpdate()
}
}

Expand All @@ -68,18 +94,21 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}

override def getOutgoingPayment(id: UUID): Option[OutgoingPayment] = {
using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments WHERE id = ?")) { statement =>
using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status, payment_request, description, target_node_id FROM sent_payments WHERE id = ?")) { statement =>
statement.setString(1, id.toString)
val rs = statement.executeQuery()
if (rs.next()) {
Some(OutgoingPayment(
UUID.fromString(rs.getString("id")),
rs.getByteVector32("payment_hash"),
rs.getByteVector32Nullable("preimage"),
rs.getByteVectorNullable("target_node_id").map(PublicKey(_)),
rs.getLong("amount_msat"),
rs.getLong("created_at"),
getNullableLong(rs, "completed_at"),
OutgoingPaymentStatus.withName(rs.getString("status"))
rs.getNullableLong("completed_at"),
OutgoingPaymentStatus.withName(rs.getString("status")),
rs.getStringNullable("payment_request"),
rs.getByteVectorNullable("description").map(bytes => new String(bytes.toArray))
))
} else {
None
Expand All @@ -88,7 +117,7 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
}

override def getOutgoingPayments(paymentHash: ByteVector32): Seq[OutgoingPayment] = {
using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments WHERE payment_hash = ?")) { statement =>
using(sqlite.prepareStatement("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status, payment_request, description, target_node_id FROM sent_payments WHERE payment_hash = ?")) { statement =>
statement.setBytes(1, paymentHash.toArray)
val rs = statement.executeQuery()
var q: Queue[OutgoingPayment] = Queue()
Expand All @@ -97,10 +126,13 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {
UUID.fromString(rs.getString("id")),
rs.getByteVector32("payment_hash"),
rs.getByteVector32Nullable("preimage"),
rs.getByteVectorNullable("target_node_id").map(PublicKey(_)),
rs.getLong("amount_msat"),
rs.getLong("created_at"),
getNullableLong(rs, "completed_at"),
OutgoingPaymentStatus.withName(rs.getString("status"))
rs.getNullableLong("completed_at"),
OutgoingPaymentStatus.withName(rs.getString("status")),
rs.getStringNullable("payment_request"),
rs.getByteVectorNullable("description").map(bytes => new String(bytes.toArray))
)
}
q
Expand All @@ -109,17 +141,20 @@ class SqlitePaymentsDb(sqlite: Connection) extends PaymentsDb with Logging {

override def listOutgoingPayments(): Seq[OutgoingPayment] = {
using(sqlite.createStatement()) { statement =>
val rs = statement.executeQuery("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status FROM sent_payments")
val rs = statement.executeQuery("SELECT id, payment_hash, preimage, amount_msat, created_at, completed_at, status, payment_request, description, target_node_id FROM sent_payments")
var q: Queue[OutgoingPayment] = Queue()
while (rs.next()) {
q = q :+ OutgoingPayment(
UUID.fromString(rs.getString("id")),
rs.getByteVector32("payment_hash"),
rs.getByteVector32Nullable("preimage"),
rs.getByteVectorNullable("target_node_id").map(PublicKey(_)),
rs.getLong("amount_msat"),
rs.getLong("created_at"),
getNullableLong(rs, "completed_at"),
OutgoingPaymentStatus.withName(rs.getString("status"))
rs.getNullableLong("completed_at"),
OutgoingPaymentStatus.withName(rs.getString("status")),
rs.getStringNullable("payment_request"),
rs.getByteVectorNullable("description").map(bytes => new String(bytes.toArray))
)
}
q
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
package fr.acinq.eclair.db.sqlite

import java.sql.{Connection, ResultSet, Statement}

import fr.acinq.bitcoin.ByteVector32
import scodec.Codec
import scodec.bits.{BitVector, ByteVector}

import scala.collection.immutable.Queue

object SqliteUtils {
Expand Down Expand Up @@ -92,19 +90,6 @@ object SqliteUtils {
q
}

/**
* This helper retrieves the value from a nullable integer column and interprets it as an option. This is needed
* because `rs.getLong` would return `0` for a null value.
* It is used on Android only
*
* @param label
* @return
*/
def getNullableLong(rs: ResultSet, label: String) : Option[Long] = {
val result = rs.getLong(label)
if (rs.wasNull()) None else Some(result)
}

/**
* Obtain an exclusive lock on a sqlite database. This is useful when we want to make sure that only one process
* accesses the database file (see https://www.sqlite.org/pragma.html).
Expand All @@ -127,9 +112,21 @@ object SqliteUtils {

def getByteVector32(columnLabel: String): ByteVector32 = ByteVector32(ByteVector(rs.getBytes(columnLabel)))

def getByteVector32Nullable(columnLabel: String): Option[ByteVector32] = {
def getByteVectorNullable(columnLabel: String): Option[ByteVector] = {
val bytes = rs.getBytes(columnLabel)
if(rs.wasNull()) None else Some(ByteVector32(ByteVector(bytes)))
if(rs.wasNull()) None else Some(ByteVector(bytes))
}

def getByteVector32Nullable(columnLabel: String): Option[ByteVector32] = getByteVectorNullable(columnLabel).map(ByteVector32(_))

def getStringNullable(columnLabel: String): Option[String] = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is getStringNullable not alongside getNullableLong (see above)?

We should move getNullableLong into ExtendedResultSet for consistency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done in 7c2e8c9

val str = rs.getString(columnLabel)
if(rs.wasNull()) None else Some(str)
}

def getNullableLong(label: String) : Option[Long] = {
val result = rs.getLong(label)
if (rs.wasNull()) None else Some(result)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, R
import fr.acinq.eclair.router.{Announcements, Data}
import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails}
import fr.acinq.eclair.{NodeParams, randomBytes32, secureRandom}

import scala.concurrent.duration._

/**
Expand Down Expand Up @@ -54,7 +53,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
case Some(targetNodeId) =>
val paymentHash = randomBytes32 // we don't even know the preimage (this needs to be a secure random!)
log.info(s"sending payment probe to node=$targetNodeId payment_hash=$paymentHash")
paymentInitiator ! SendPayment(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1)
paymentInitiator ! SendPayment(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1, paymentRequest_opt = None)
case None =>
log.info(s"could not find a destination, re-scheduling")
scheduleProbe()
Expand Down
Loading