offer-send RPC (#4153)

* offer-send RPC

* docs

* change offet-send signature

* change offer_send signature

* add local address parameter

* remove local address parameter

* use temo contract id to send offers

* respond to the PR comments
This commit is contained in:
rorp 2022-03-03 19:00:32 -08:00 committed by GitHub
parent 0bb0d9acdb
commit 56d0ae68ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 299 additions and 44 deletions

View File

@ -104,5 +104,26 @@ case class DLCRoutes(dlcNode: DLCNodeApi)(implicit system: ActorSystem)
}
}
}
case ServerCommand("offer-send", arr) =>
withValidServerCommand(OfferSend.fromJsArr(arr)) {
case OfferSend(remoteAddress, message, offerE) =>
complete {
offerE match {
case Left(offerTLV) =>
dlcNode
.sendDLCOffer(remoteAddress, message, offerTLV)
.map { tempContractId =>
Server.httpSuccess(tempContractId.hex)
}
case Right(tempContractId) =>
dlcNode
.sendDLCOffer(remoteAddress, message, tempContractId)
.map { tempContractId =>
Server.httpSuccess(tempContractId.hex)
}
}
}
}
}
}

View File

@ -15,6 +15,7 @@ import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.{Transaction, TransactionOutPoint}
import org.bitcoins.core.protocol.{BitcoinAddress, BlockStamp}
import org.bitcoins.core.psbt.PSBT
import org.bitcoins.core.util.NetworkUtil
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.AddressLabelTag
import org.bitcoins.crypto._
@ -1384,7 +1385,7 @@ object OfferAdd {
}
case other =>
val exn = new IllegalArgumentException(
s"Bad number or arguments to registerincomingoffer, got=${other.length} expected=3")
s"Bad number or arguments to offer-add, got=${other.length} expected=3")
Failure(exn)
}
}
@ -1403,7 +1404,37 @@ object OfferRemove {
}
case other =>
val exn = new IllegalArgumentException(
s"Bad number or arguments to rejectincomingoffer, got=${other.length} expected=1")
s"Bad number or arguments to offer-remove, got=${other.length} expected=1")
Failure(exn)
}
}
}
case class OfferSend(
remoteAddress: InetSocketAddress,
message: String,
offerE: Either[DLCOfferTLV, Sha256Digest])
object OfferSend {
def fromJsArr(arr: ujson.Arr): Try[OfferSend] = {
arr.arr.toList match {
case offerJs :: peerAddressJs :: messageJs :: Nil =>
Try {
val peerAddress =
NetworkUtil.parseInetSocketAddress(peerAddressJs.str, 2862)
val message = messageJs.str
val offerE =
Try(LnMessageFactory(DLCOfferTLV).fromHex(offerJs.str).tlv)
.orElse(Try(DLCOfferTLV.fromHex(offerJs.str))) match {
case Success(o) => Left(o)
case Failure(_) => Right(Sha256Digest.fromHex(offerJs.str))
}
OfferSend(peerAddress, message, offerE)
}
case other =>
val exn = new IllegalArgumentException(
s"Bad number or arguments to offer-send, got=${other.length} expected=3")
Failure(exn)
}
}

View File

@ -5,6 +5,7 @@ import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.dlc.models.DLCMessage
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.util.StartStopAsync
import org.bitcoins.crypto.Sha256Digest
import java.net.InetSocketAddress
import scala.concurrent.Future
@ -20,5 +21,15 @@ trait DLCNodeApi extends StartStopAsync[Unit] {
externalChangeAddress: Option[BitcoinAddress]): Future[
DLCMessage.DLCAccept]
def sendDLCOffer(
peerAddress: InetSocketAddress,
message: String,
offerTLV: DLCOfferTLV): Future[Sha256Digest]
def sendDLCOffer(
peerAddress: InetSocketAddress,
message: String,
tempContractId: Sha256Digest): Future[Sha256Digest]
def getHostAddress: Future[InetSocketAddress]
}

View File

@ -112,8 +112,13 @@ trait DLCWalletApi { self: WalletApi =>
def findDLC(dlcId: Sha256Digest): Future[Option[DLCStatus]]
def findDLCByTemporaryContractId(
tempContractId: Sha256Digest): Future[Option[DLCStatus]]
def cancelDLC(dlcId: Sha256Digest): Future[Unit]
def getDLCOffer(dlcId: Sha256Digest): Future[Option[DLCOffer]]
/** Retrieves accounting and financial metrics for the entire dlc wallet */
def getWalletAccounting(): Future[DLCWalletAccounting]
@ -125,6 +130,9 @@ trait DLCWalletApi { self: WalletApi =>
def listIncomingDLCOffers(): Future[Vector[IncomingDLCOfferDb]]
def rejectIncomingDLCOffer(offerHash: Sha256Digest): Future[Unit]
def findIncomingDLCOffer(
offerHash: Sha256Digest): Future[Option[IncomingDLCOfferDb]]
}
/** An HDWallet that supports DLCs and both Neutrino and SPV methods of syncing */

View File

@ -1,6 +1,6 @@
package org.bitcoins.core.api.dlc.wallet.db
import org.bitcoins.core.protocol.tlv.DLCOfferTLV
import org.bitcoins.core.protocol.tlv.{DLCOfferTLV, LnMessage, SendOfferTLV}
import org.bitcoins.crypto.Sha256Digest
import java.time.Instant
@ -15,4 +15,15 @@ case class IncomingDLCOfferDb(
"peer length must not exceed 1024 characters")
require(message.forall(_.length <= 1024),
"message length must not exceed 1024 characters")
def toTLV: SendOfferTLV = {
require(peer.nonEmpty)
require(message.nonEmpty)
SendOfferTLV(offer = offerTLV, message = message.get, peer = peer.get)
}
def toMessage: LnMessage[SendOfferTLV] = {
LnMessage(this.toTLV)
}
}

View File

@ -174,7 +174,8 @@ object TLV extends TLVParentFactory[TLV] {
FundingSignaturesV0TLV,
DLCOfferTLV,
DLCAcceptTLV,
DLCSignTLV
DLCSignTLV,
SendOfferTLV
) ++ EventDescriptorTLV.allFactories ++
PayoutCurvePieceTLV.allFactories ++
ContractDescriptorTLV.allFactories ++
@ -1687,6 +1688,38 @@ object FundingSignaturesV0TLV extends TLVFactory[FundingSignaturesV0TLV] {
sealed trait DLCSetupTLV extends TLV
case class SendOfferTLV(
peer: NormalizedString,
message: NormalizedString,
offer: DLCOfferTLV)
extends DLCSetupTLV {
require(peer.length <= 1024, "peer length must not exceed 1024 characters")
require(message.length <= 1024,
"message length must not exceed 1024 characters")
override val tpe: BigSizeUInt = SendOfferTLV.tpe
override val value: ByteVector = {
strBytes(peer) ++ strBytes(message) ++ offer.bytes
}
}
object SendOfferTLV extends TLVFactory[SendOfferTLV] {
override val tpe: BigSizeUInt = BigSizeUInt(65534)
override val typeName: String = "SendOfferTLV"
override def fromTLVValue(value: ByteVector): SendOfferTLV = {
val iter = ValueIterator(value)
val peer = iter.takeString()
val message = iter.takeString()
val offer = iter.take(DLCOfferTLV)
SendOfferTLV(peer, message, offer)
}
}
case class DLCOfferTLV(
protocolVersionOpt: Option[Int],
contractFlags: Byte,

View File

@ -3,6 +3,7 @@ package org.bitcoins.dlc.node
import akka.actor.ActorRef
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.dlc.models.DLCState
import org.bitcoins.core.protocol.tlv.{LnMessage, SendOfferTLV}
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.dlc.node.peer.Peer
import org.bitcoins.rpc.util.RpcUtil
@ -33,17 +34,13 @@ class DLCNegotiationTest extends BitcoinSDualWalletTest {
val connectAddress =
InetSocketAddress.createUnresolved("127.0.0.1", port)
val serverF = DLCServer.bind(walletA, bindAddress, None)
val handlerP = Promise[ActorRef]()
val clientF =
DLCClient.connect(Peer(connectAddress, socks5ProxyParams = None),
walletB,
Some(handlerP))
for {
_ <- serverF
_ <- clientF
_ <- DLCServer.bind(walletA, bindAddress, None)
_ <- DLCClient.connect(Peer(connectAddress, socks5ProxyParams = None),
walletB,
Some(handlerP))
handler <- handlerP.future
@ -79,4 +76,55 @@ class DLCNegotiationTest extends BitcoinSDualWalletTest {
)
} yield succeed
}
it must "receive an offer over" in {
fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) =>
val walletA = fundedDLCWallets._1.wallet
val walletB = fundedDLCWallets._2.wallet
val port = RpcUtil.randomPort
val bindAddress =
new InetSocketAddress("0.0.0.0", port)
val connectAddress =
InetSocketAddress.createUnresolved("127.0.0.1", port)
val handlerP = Promise[ActorRef]()
for {
_ <- DLCServer.bind(walletA, bindAddress, None)
_ <- DLCClient.connect(Peer(connectAddress, socks5ProxyParams = None),
walletB,
Some(handlerP))
handler <- handlerP.future
preA <- walletA.listIncomingDLCOffers()
preB <- walletA.listIncomingDLCOffers()
_ = assert(preA.isEmpty)
_ = assert(preB.isEmpty)
offer <- walletB.createDLCOffer(sampleContractInfo,
half,
Some(SatoshisPerVirtualByte.one),
UInt32.zero,
UInt32.one,
None,
None)
tlv = SendOfferTLV(peer = "peer", message = "msg", offer = offer.toTLV)
_ = handler ! DLCDataHandler.Send(LnMessage(tlv))
_ <- TestAsyncUtil.awaitConditionF { () =>
walletA.listIncomingDLCOffers().map(_.nonEmpty)
}
postA <- walletA.listIncomingDLCOffers()
postB <- walletB.listIncomingDLCOffers()
} yield {
assert(postA.nonEmpty)
assert(postB.isEmpty)
assert(postA.head.peer.get == "peer")
assert(postA.head.message.get == "msg")
assert(postA.head.offerTLV == offer.toTLV)
}
}
}

View File

@ -51,6 +51,13 @@ class DLCDataHandler(dlcWalletApi: DLCWalletApi, connectionHandler: ActorRef)
for {
_ <- dlcWalletApi.registerIncomingDLCOffer(dlcOffer, None, None)
} yield ()
case dlcOfferMessage: SendOfferTLV =>
for {
_ <- dlcWalletApi.registerIncomingDLCOffer(
offerTLV = dlcOfferMessage.offer,
peer = Some(dlcOfferMessage.peer),
message = Some(dlcOfferMessage.message))
} yield ()
case dlcAccept: DLCAcceptTLV =>
val f = for {
sign <- dlcWalletApi.signDLC(dlcAccept)

View File

@ -7,6 +7,7 @@ import org.bitcoins.core.api.dlc.wallet.DLCWalletApi
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.dlc.models.DLCMessage
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.crypto.Sha256Digest
import org.bitcoins.dlc.node.config._
import org.bitcoins.dlc.node.peer.Peer
@ -56,21 +57,14 @@ case class DLCNode(wallet: DLCWalletApi)(implicit
}
}
def acceptDLCOffer(
override def acceptDLCOffer(
peerAddress: InetSocketAddress,
dlcOffer: LnMessage[DLCOfferTLV],
externalPayoutAddress: Option[BitcoinAddress],
externalChangeAddress: Option[BitcoinAddress]): Future[
DLCMessage.DLCAccept] = {
val peer =
Peer(socket = peerAddress, socks5ProxyParams = config.socks5ProxyParams)
val handlerP = Promise[ActorRef]()
for {
_ <- DLCClient.connect(peer, wallet, Some(handlerP))
handler <- handlerP.future
handler <- connectToPeer(peerAddress)
accept <- wallet.acceptDLCOffer(dlcOffer.tlv,
externalPayoutAddress,
externalChangeAddress)
@ -79,4 +73,51 @@ case class DLCNode(wallet: DLCWalletApi)(implicit
accept
}
}
override def sendDLCOffer(
peerAddress: InetSocketAddress,
message: String,
offerTLV: DLCOfferTLV): Future[Sha256Digest] = {
for {
handler <- connectToPeer(peerAddress)
localAddress <- getHostAddress
} yield {
val peer = NormalizedString(
localAddress.getHostString + ":" + peerAddress.getPort)
val msg = NormalizedString(message)
val lnMessage = LnMessage(
SendOfferTLV(peer = peer, message = msg, offer = offerTLV))
handler ! DLCDataHandler.Send(lnMessage)
offerTLV.tempContractId
}
}
override def sendDLCOffer(
peerAddress: InetSocketAddress,
message: String,
temporaryContractId: Sha256Digest): Future[Sha256Digest] = {
for {
dlcOpt <- wallet.findDLCByTemporaryContractId(temporaryContractId)
dlc = dlcOpt.getOrElse(
throw new IllegalArgumentException(
s"Cannot find a DLC with temp contact ID $temporaryContractId"))
offerOpt <- wallet.getDLCOffer(dlc.dlcId)
offer = offerOpt.getOrElse(
throw new IllegalArgumentException(
s"Cannot find an offer with for DLC ID ${dlc.dlcId}"))
res <- sendDLCOffer(peerAddress, message, offer.toTLV)
} yield res
}
private def connectToPeer(
peerAddress: InetSocketAddress): Future[ActorRef] = {
val peer =
Peer(socket = peerAddress, socks5ProxyParams = config.socks5ProxyParams)
val handlerP = Promise[ActorRef]()
for {
_ <- DLCClient.connect(peer, wallet, Some(handlerP))
handler <- handlerP.future
} yield handler
}
}

View File

@ -550,7 +550,8 @@ object DLCParsingTestVector extends TestVectorParser[DLCParsingTestVector] {
)
DLCMessageTestVector(LnMessage(tlv), "oracle_attestment_v0", fields)
case _: UnknownTLV | _: ErrorTLV | _: PingTLV | _: PongTLV | _: InitTLV =>
case _: UnknownTLV | _: ErrorTLV | _: PingTLV | _: PongTLV | _: InitTLV |
_: SendOfferTLV =>
throw new IllegalArgumentException(
s"DLCParsingTestVector is only defined for DLC messages and TLVs, got $tlv")
}

View File

@ -1598,44 +1598,69 @@ abstract class DLCWallet
}
}
override def getDLCOffer(dlcId: Sha256Digest): Future[Option[DLCOffer]] =
dlcDataManagement.getOffer(dlcId, transactionDAO)
override def findDLCByTemporaryContractId(
tempContractId: Sha256Digest): Future[Option[DLCStatus]] = {
val start = System.currentTimeMillis()
val dlcOptF = for {
dlcDbOpt <- dlcDAO.findByTempContractId(tempContractId)
dlcStatusOpt <- dlcDbOpt match {
case None => Future.successful(None)
case Some(dlcDb) => findDLCStatus(dlcDb)
}
} yield dlcStatusOpt
dlcOptF.foreach(_ =>
logger.debug(
s"Done finding tempContractId=$tempContractId, it took=${System
.currentTimeMillis() - start}ms"))
dlcOptF
}
override def findDLC(dlcId: Sha256Digest): Future[Option[DLCStatus]] = {
val start = System.currentTimeMillis()
val dlcDbOptF = dlcDAO.read(dlcId)
val dlcOptF = for {
dlcDbOpt <- dlcDAO.read(dlcId)
dlcStatusOpt <- dlcDbOpt match {
case None => Future.successful(None)
case Some(dlcDb) => findDLCStatus(dlcDb)
}
} yield dlcStatusOpt
dlcOptF.foreach(_ =>
logger.debug(
s"Done finding dlc=$dlcId, it took=${System.currentTimeMillis() - start}ms"))
dlcOptF
}
private def findDLCStatus(dlcDb: DLCDb) = {
val dlcId = dlcDb.dlcId
val contractDataOptF = contractDataDAO.read(dlcId)
val offerDbOptF = dlcOfferDAO.read(dlcId)
val acceptDbOptF = dlcAcceptDAO.read(dlcId)
val closingTxOptF: Future[Option[TransactionDb]] = for {
dlcDbOpt <- dlcDbOptF
closingTxFOpt <- {
dlcDbOpt.map(dlcDb => getClosingTxOpt(dlcDb)) match {
case None => Future.successful(None)
case Some(closingTxIdOpt) => closingTxIdOpt
}
}
} yield closingTxFOpt
val closingTxOptF: Future[Option[TransactionDb]] = getClosingTxOpt(dlcDb)
val dlcOptF: Future[Option[DLCStatus]] = for {
dlcDbOpt <- dlcDbOptF
contractDataOpt <- contractDataOptF
offerDbOpt <- offerDbOptF
acceptDbOpt <- acceptDbOptF
closingTxOpt <- closingTxOptF
result <- {
(dlcDbOpt, contractDataOpt, offerDbOpt) match {
case (Some(dlcDb), Some(contractData), Some(offerDb)) =>
(contractDataOpt, offerDbOpt) match {
case (Some(contractData), Some(offerDb)) =>
buildDLCStatus(dlcDb,
contractData,
offerDb,
acceptDbOpt,
closingTxOpt)
case (_, _, _) => Future.successful(None)
case (_, _) => Future.successful(None)
}
}
} yield result
dlcOptF.foreach(_ =>
logger.debug(
s"Done finding dlc=$dlcId, it took=${System.currentTimeMillis() - start}ms"))
dlcOptF
}

View File

@ -48,6 +48,13 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit
}
private val safeDatabase: SafeDatabase = dlcDAO.safeDatabase
private[wallet] def getOffer(
dlcId: Sha256Digest,
txDAO: TransactionDAO): Future[Option[DLCOffer]] = {
val dataF = getDLCFundingData(dlcId, txDAO)
dataF.map(data => data.map(_.offer))
}
private[wallet] def getDLCAnnouncementDbs(dlcId: Sha256Digest): Future[(
Vector[DLCAnnouncementDb],
Vector[OracleAnnouncementDataDb],
@ -203,7 +210,7 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit
}
}
private def getDLCOfferData(
private[wallet] def getDLCOfferData(
dlcId: Sha256Digest,
transactionDAO: TransactionDAO): Future[Option[OfferedDbState]] = {

View File

@ -33,4 +33,9 @@ trait IncomingDLCOffersHandling { self: DLCWallet =>
def listIncomingDLCOffers(): Future[Vector[IncomingDLCOfferDb]] = {
dlcWalletDAOs.incomingDLCOfferDAO.findAll()
}
def findIncomingDLCOffer(
offerHash: Sha256Digest): Future[Option[IncomingDLCOfferDb]] = {
dlcWalletDAOs.incomingDLCOfferDAO.find(offerHash)
}
}

View File

@ -48,6 +48,11 @@ case class IncomingDLCOfferDAO()(implicit
safeDatabase.run(query.delete)
}
def find(pk: Sha256Digest): Future[Option[IncomingDLCOfferDb]] = {
val query = table.filter(_.hash === pk)
safeDatabase.run(query.result).map(_.headOption)
}
class IncomingDLCOfferTable(tag: Tag)
extends Table[IncomingDLCOfferDb](tag, schemaName, "incoming_offers") {

View File

@ -300,13 +300,14 @@ the `-p 9999:9999` port mapping on the docker container to adjust for this.
- `getdlcs` - Returns all dlcs in the wallet
- `getdlc` `dlcId` - Gets a specific dlc in the wallet
- `dlcId` - Internal id of the DLC
- `offer-add` `offerTLV` `peer` `message` - Puts an incoming offer into the inbox
- `offer-add` `offerTLV` `peerAddress` `message` - Puts an incoming offer into the inbox
- `offerTLV` - Offer TLV
- `peer` - Peer URI (optional)
- `message` - Peer's message or note (optional)
- `"offer-remove` `hash` - remove an incoming offer from inbox
- `"offer-remove` `hash` - Remove an incoming offer from inbox
- `hash` - Hash of the offer TLV
- `offers-list` - List all incoming offers from the inbox
- `offer-send` `offerOrTempContractId` `peerAddress` `message` - Sends an offer to a peer. `offerOrTempContractId` is either an offer TLV or a temporary contract ID.
- `offers-list` - List all incoming offers from the inbox
### Network
- `getpeers` - List the connected peers