Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

### API changes

<insert changes>
- `listoffers` now returns more details about each offer.

### Miscellaneous improvements and bug fixes

Expand Down
17 changes: 9 additions & 8 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerByte, Feera
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats}
import fr.acinq.eclair.db.{IncomingPayment, OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.db.{IncomingPayment, OfferData, OutgoingPayment, OutgoingPaymentStatus}
import fr.acinq.eclair.io.Peer.{GetPeerInfo, OpenChannelResponse, PeerInfo}
import fr.acinq.eclair.io._
import fr.acinq.eclair.message.{OnionMessages, Postman}
Expand Down Expand Up @@ -126,11 +126,11 @@ trait Eclair {

def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[Bolt11Invoice]

def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], issuer_opt: Option[String], blindedPathsFirstNodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Offer]
def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], issuer_opt: Option[String], blindedPathsFirstNodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[OfferData]

def disableOffer(offer: Offer)(implicit timeout: Timeout): Future[Unit]
def disableOffer(offer: Offer)(implicit timeout: Timeout): Future[Map[ByteVector32, Boolean]]

def listOffers(onlyActive: Boolean = true)(implicit timeout: Timeout): Future[Seq[Offer]]
def listOffers(onlyActive: Boolean = true)(implicit timeout: Timeout): Future[Seq[OfferData]]

def newAddress(): Future[String]

Expand Down Expand Up @@ -394,7 +394,7 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
}
}

override def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expireInSeconds_opt: Option[Long], issuer_opt: Option[String], blindedPathsFirstNodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Offer] = {
override def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expireInSeconds_opt: Option[Long], issuer_opt: Option[String], blindedPathsFirstNodeId_opt: Option[PublicKey])(implicit timeout: Timeout): Future[OfferData] = {
val offerCreator = appKit.system.spawnAnonymous(OfferCreator(appKit.nodeParams, appKit.router, appKit.offerManager, appKit.defaultOfferHandler))
val expiry_opt = expireInSeconds_opt.map(TimestampSecond.now() + _)
offerCreator.ask[OfferCreator.CreateOfferResult](replyTo => OfferCreator.Create(replyTo, description_opt, amount_opt, expiry_opt, issuer_opt, blindedPathsFirstNodeId_opt))
Expand All @@ -404,12 +404,13 @@ class EclairImpl(val appKit: Kit) extends Eclair with Logging with SpendFromChan
}
}

override def disableOffer(offer: Offer)(implicit timeout: Timeout): Future[Unit] = Future {
override def disableOffer(offer: Offer)(implicit timeout: Timeout): Future[Map[ByteVector32, Boolean]] = Future {
appKit.offerManager ! OfferManager.DisableOffer(offer)
Map(offer.offerId -> true)
}

override def listOffers(onlyActive: Boolean = true)(implicit timeout: Timeout): Future[Seq[Offer]] = Future {
appKit.nodeParams.db.offers.listOffers(onlyActive).map(_.offer)
override def listOffers(onlyActive: Boolean = true)(implicit timeout: Timeout): Future[Seq[OfferData]] = Future {
appKit.nodeParams.db.offers.listOffers(onlyActive)
}

override def newAddress(): Future[String] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ case class DualOffersDb(primary: OffersDb, secondary: OffersDb) extends OffersDb

private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-offers").build()))

override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Unit = {
override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Option[OfferData] = {
runAsync(secondary.addOffer(offer, pathId_opt, createdAt))
primary.addOffer(offer, pathId_opt, createdAt)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ trait OffersDb {
*
* @param pathId_opt If the offer uses a blinded path, this is the corresponding pathId.
*/
def addOffer(offer: Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Unit
def addOffer(offer: Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Option[OfferData]

/**
* Disable an offer. The offer is still stored but new invoice requests and new payment attempts for already emitted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,18 @@ class PgOffersDb(implicit ds: DataSource, lock: PgLock) extends OffersDb with Lo
}
}

override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Unit = withMetrics("offers/add", DbBackends.Postgres) {
override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Option[OfferData] = withMetrics("offers/add", DbBackends.Postgres) {
withLock { pg =>
using(pg.prepareStatement("INSERT INTO payments.offers (offer_id, offer, path_id, created_at, is_active, disabled_at) VALUES (?, ?, ?, ?, TRUE, NULL)")) { statement =>
using(pg.prepareStatement("INSERT INTO payments.offers (offer_id, offer, path_id, created_at, is_active, disabled_at) VALUES (?, ?, ?, ?, TRUE, NULL) ON CONFLICT DO NOTHING")) { statement =>
statement.setString(1, offer.offerId.toHex)
statement.setString(2, offer.toString)
statement.setString(3, pathId_opt.map(_.toHex).orNull)
statement.setTimestamp(4, createdAt.toSqlTimestamp)
statement.executeUpdate()
if (statement.executeUpdate() == 1) {
Some(OfferData(offer, pathId_opt, createdAt, disabledAt_opt = None))
} else {
None
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,19 @@ class SqliteOffersDb(val sqlite: Connection) extends OffersDb with Logging {
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
}
setVersion(statement, DB_NAME, CURRENT_VERSION)

}

override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Unit = withMetrics("offers/add", DbBackends.Sqlite) {
using(sqlite.prepareStatement("INSERT INTO offers (offer_id, offer, path_id, created_at, is_active, disabled_at) VALUES (?, ?, ?, ?, TRUE, NULL)")) { statement =>
override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], createdAt: TimestampMilli = TimestampMilli.now()): Option[OfferData] = withMetrics("offers/add", DbBackends.Sqlite) {
using(sqlite.prepareStatement("INSERT OR IGNORE INTO offers (offer_id, offer, path_id, created_at, is_active, disabled_at) VALUES (?, ?, ?, ?, TRUE, NULL)")) { statement =>
statement.setBytes(1, offer.offerId.toArray)
statement.setString(2, offer.toString)
statement.setBytes(3, pathId_opt.map(_.toArray).orNull)
statement.setLong(4, createdAt.toLong)
statement.executeUpdate()
if (statement.executeUpdate() == 1) {
Some(OfferData(offer, pathId_opt, createdAt, disabledAt_opt = None))
} else {
None
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.crypto.{ShaChain, Sphinx}
import fr.acinq.eclair.db.FailureType.FailureType
import fr.acinq.eclair.db.{IncomingPaymentStatus, OutgoingPaymentStatus}
import fr.acinq.eclair.db.{IncomingPaymentStatus, OfferData, OutgoingPaymentStatus}
import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.io.Peer.OpenChannelResponse
import fr.acinq.eclair.message.OnionMessages
Expand All @@ -35,7 +35,7 @@ import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, EncodedNodeId, Feature, FeatureSupport, MilliSatoshi, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
import org.json4s
import org.json4s.JsonAST._
import org.json4s.jackson.Serialization
Expand Down Expand Up @@ -492,6 +492,29 @@ object InvoiceSerializer extends MinimalSerializer({
JObject(fieldList)
})

private case class OfferDataJson(amountMsat: Option[MilliSatoshi],
description: Option[String],
issuer: Option[String],
nodeId: Option[PublicKey],
blindedPathFirstNodeId: Option[PublicKey],
createdAt: TimestampMilli,
expiry: Option[TimestampSecond],
disabled: Boolean,
disabledAt: Option[TimestampMilli],
encoded: String)
object OfferDataSerializer extends ConvertClassSerializer[OfferData](o => OfferDataJson(
amountMsat = o.offer.amount,
description = o.offer.description,
issuer = o.offer.issuer,
nodeId = o.offer.nodeId,
blindedPathFirstNodeId = o.offer.contactInfos.collect { case OfferTypes.BlindedPath(path) => path.firstNodeId }.collectFirst { case p: EncodedNodeId.WithPublicKey => p.publicKey },
createdAt = o.createdAt,
expiry = o.offer.expiry,
disabled = o.disabled,
disabledAt = o.disabledAt_opt,
encoded = o.offer.encode()
))

object JavaUUIDSerializer extends MinimalSerializer({
case id: UUID => JString(id.toString)
})
Expand Down Expand Up @@ -726,6 +749,7 @@ object JsonSerializers {
NodeAddressSerializer +
DirectedHtlcSerializer +
InvoiceSerializer +
OfferDataSerializer +
JavaUUIDSerializer +
OriginSerializer +
ByteVector32KeySerializer +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import akka.actor.typed.scaladsl.{ActorContext, Behaviors}
import akka.actor.{ActorRef, typed}
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32}
import fr.acinq.eclair.db.OfferData
import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.message.OnionMessages.{IntermediateNode, Recipient}
import fr.acinq.eclair.payment.offer.OfferCreator.CreateOfferResult
Expand Down Expand Up @@ -48,7 +49,7 @@ object OfferCreator {

// @formatter:off
sealed trait CreateOfferResult
case class CreatedOffer(offer: Offer) extends CreateOfferResult
case class CreatedOffer(offerData: OfferData) extends CreateOfferResult
case class CreateOfferError(reason: String) extends CreateOfferResult
// @formatter:on

Expand Down Expand Up @@ -115,9 +116,13 @@ private class OfferCreator(context: ActorContext[OfferCreator.Command],
}

private def registerOffer(offer: Offer, nodeKey_opt: Option[PrivateKey], pathId_opt: Option[ByteVector32]): Behavior[Command] = {
nodeParams.db.offers.addOffer(offer, pathId_opt)
offerManager ! OfferManager.RegisterOffer(offer, nodeKey_opt, pathId_opt, defaultOfferHandler)
replyTo ! CreatedOffer(offer)
nodeParams.db.offers.addOffer(offer, pathId_opt) match {
case Some(offerData) =>
offerManager ! OfferManager.RegisterOffer(offer, nodeKey_opt, pathId_opt, defaultOfferHandler)
replyTo ! CreatedOffer(offerData)
case None =>
replyTo ! CreateOfferError("This offer is already registered")
}
Behaviors.stopped
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ class OffersDbSpec extends AnyFunSuite {

assert(db.listOffers(onlyActive = false).isEmpty)
val offer1 = OfferData(Offer(None, Some("test 1"), randomKey().publicKey, Features(), Block.LivenetGenesisBlock.hash), None, TimestampMilli(100), None)
db.addOffer(offer1.offer, None, offer1.createdAt)
assert(db.addOffer(offer1.offer, None, offer1.createdAt).nonEmpty)
assert(db.addOffer(offer1.offer, None, TimestampMilli(150)).isEmpty)
assert(db.listOffers(onlyActive = true) == Seq(offer1))
val pathId = randomBytes32()
val offer2 = OfferData(Offer(Some(15_000 msat), Some("test 2"), randomKey().publicKey, Features(), Block.LivenetGenesisBlock.hash), Some(pathId), TimestampMilli(200), None)
db.addOffer(offer2.offer, Some(pathId), offer2.createdAt)
assert(db.addOffer(offer2.offer, Some(pathId), offer2.createdAt).nonEmpty)
assert(db.listOffers(onlyActive = true) == Seq(offer2, offer1))
db.disableOffer(offer1.offer, disabledAt = TimestampMilli(250))
assert(db.listOffers(onlyActive = true) == Seq(offer2))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.scaladsl.adapter.{ClassicActorRefOps, ClassicActorSystemOps}
import akka.testkit.TestProbe
import akka.util.Timeout
import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong}
Expand All @@ -33,8 +32,8 @@ import fr.acinq.eclair.integration.basic.fixtures.composite.ThreeNodesFixture
import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.message.OnionMessages.{IntermediateNode, Recipient, buildRoute}
import fr.acinq.eclair.payment._
import fr.acinq.eclair.payment.offer.{OfferCreator, OfferManager}
import fr.acinq.eclair.payment.offer.OfferManager.InvoiceRequestActor
import fr.acinq.eclair.payment.offer.{OfferCreator, OfferManager}
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
import fr.acinq.eclair.payment.send.OfferPayment
import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPaymentToNode, SendSpontaneousPayment}
Expand All @@ -49,7 +48,6 @@ import org.scalatest.{Tag, TestData}
import scodec.bits.HexStringSyntax

import java.util.UUID
import scala.concurrent.Await
import scala.concurrent.duration.DurationInt

class OfferPaymentSpec extends FixtureSpec with IntegrationPatience {
Expand Down Expand Up @@ -171,7 +169,7 @@ class OfferPaymentSpec extends FixtureSpec with IntegrationPatience {
val sender = TestProbe("sender")(recipient.system)
val offerCreator = recipient.system.spawnAnonymous(OfferCreator(recipient.nodeParams, recipient.router, recipient.offerManager, recipient.defaultOfferHandler))
offerCreator ! OfferCreator.Create(sender.ref.toTyped, description_opt, amount_opt, None, issuer_opt, blindedPathsFirstNodeId_opt)
sender.expectMsgType[OfferCreator.CreatedOffer].offer
sender.expectMsgType[OfferCreator.CreatedOffer].offerData.offer
}

def payOffer(payer: MinimalNodeFixture, offer: Offer, amount: MilliSatoshi, maxAttempts: Int = 1): PaymentEvent = {
Expand Down
Loading