Add an ability to set custom payout and change addresses (#4101)

* Add an ability to set custom payout and change addresses

* config changes

* formatting

* respond to the comments
This commit is contained in:
rorp 2022-02-18 07:29:00 -08:00 committed by GitHub
parent 5b1b1ee149
commit 5777ec1c31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 596 additions and 297 deletions

View file

@ -952,18 +952,26 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
)
"create a dlc offer" in {
(mockWalletApi
.createDLCOffer(_: ContractInfoTLV,
_: Satoshis,
_: Option[SatoshisPerVirtualByte],
_: UInt32,
_: UInt32))
(
mockWalletApi
.createDLCOffer(
_: ContractInfoTLV,
_: Satoshis,
_: Option[SatoshisPerVirtualByte],
_: UInt32,
_: UInt32,
_: Option[BitcoinAddress],
_: Option[BitcoinAddress]
)
)
.expects(
contractInfoTLV,
Satoshis(2500),
Some(SatoshisPerVirtualByte(Satoshis.one)),
UInt32(contractMaturity),
UInt32(contractTimeout)
UInt32(contractTimeout),
None,
None
)
.returning(Future.successful(offer))
@ -1021,8 +1029,10 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory {
"accept a dlc offer" in {
(mockWalletApi
.acceptDLCOffer(_: DLCOfferTLV))
.expects(offer.toTLV)
.acceptDLCOffer(_: DLCOfferTLV,
_: Option[BitcoinAddress],
_: Option[BitcoinAddress]))
.expects(offer.toTLV, None, None)
.returning(Future.successful(accept))
val route = walletRoutes.handleCommand(

View file

@ -2,7 +2,7 @@ package org.bitcoins.server
import org.bitcoins.commons.jsonmodels.bitcoind.RpcOpts.LockUnspentOutputParameter
import org.bitcoins.commons.jsonmodels.cli.ContractDescriptorParser
import org.bitcoins.commons.serializers.{JsonReaders}
import org.bitcoins.commons.serializers.JsonReaders
import org.bitcoins.core.api.wallet.CoinSelectionAlgo
import org.bitcoins.core.crypto._
import org.bitcoins.core.currency.{Bitcoins, Satoshis}
@ -655,26 +655,67 @@ case class CreateDLCOffer(
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,
refundLocktime: UInt32)
refundLocktime: UInt32,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress])
object CreateDLCOffer extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[CreateDLCOffer] = {
def parseParameters(
contractInfoJs: Value,
collateralJs: Value,
feeRateOptJs: Value,
locktimeJs: Value,
refundLTJs: Value,
payoutAddressJs: Value,
changeAddressJs: Value) = Try {
val contractInfoTLV = jsToContractInfoTLV(contractInfoJs)
val collateral = jsToSatoshis(collateralJs)
val feeRate = jsToSatoshisPerVirtualByteOpt(feeRateOptJs)
val locktime = jsToUInt32(locktimeJs)
val refundLT = jsToUInt32(refundLTJs)
val payoutAddressJsOpt = nullToOpt(payoutAddressJs)
val payoutAddressOpt =
payoutAddressJsOpt.map(js => jsToBitcoinAddress(js))
val changeAddressJsOpt = nullToOpt(changeAddressJs)
val changeAddressOpt =
changeAddressJsOpt.map(js => jsToBitcoinAddress(js))
CreateDLCOffer(contractInfoTLV,
collateral,
feeRate,
locktime,
refundLT,
payoutAddressOpt,
changeAddressOpt)
}
jsArr.arr.toList match {
case contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: Nil =>
Try {
val contractInfoTLV = jsToContractInfoTLV(contractInfoJs)
val collateral = jsToSatoshis(collateralJs)
val feeRate = jsToSatoshisPerVirtualByteOpt(feeRateOptJs)
val locktime = jsToUInt32(locktimeJs)
val refundLT = jsToUInt32(refundLTJs)
CreateDLCOffer(contractInfoTLV,
collateral,
feeRate,
locktime,
refundLT)
}
parseParameters(contractInfoJs,
collateralJs,
feeRateOptJs,
locktimeJs,
refundLTJs,
Null,
Null)
case contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: payoutAddressJs :: Nil =>
parseParameters(contractInfoJs,
collateralJs,
feeRateOptJs,
locktimeJs,
refundLTJs,
payoutAddressJs,
Null)
case contractInfoJs :: collateralJs :: feeRateOptJs :: locktimeJs :: refundLTJs :: payoutAddressJs :: changeAddressJs :: Nil =>
parseParameters(contractInfoJs,
collateralJs,
feeRateOptJs,
locktimeJs,
refundLTJs,
payoutAddressJs,
changeAddressJs)
case other =>
Failure(
new IllegalArgumentException(
@ -839,17 +880,35 @@ object DecodeAttestations extends ServerJsonModels {
}
}
case class AcceptDLCOffer(offer: LnMessage[DLCOfferTLV])
case class AcceptDLCOffer(
offer: LnMessage[DLCOfferTLV],
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress])
object AcceptDLCOffer extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[AcceptDLCOffer] = {
def parseParameters(
offerJs: Value,
payoutAddressJs: Value,
changeAddressJs: Value) = Try {
val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerJs.str)
val payoutAddressJsOpt = nullToOpt(payoutAddressJs)
val payoutAddressOpt =
payoutAddressJsOpt.map(js => jsToBitcoinAddress(js))
val changeAddressJsOpt = nullToOpt(changeAddressJs)
val changeAddressOpt =
changeAddressJsOpt.map(js => jsToBitcoinAddress(js))
AcceptDLCOffer(offer, payoutAddressOpt, changeAddressOpt)
}
jsArr.arr.toList match {
case offerJs :: Nil =>
Try {
val offer = LnMessageFactory(DLCOfferTLV).fromHex(offerJs.str)
AcceptDLCOffer(offer)
}
parseParameters(offerJs, Null, Null)
case offerJs :: payoutAddressJs :: Nil =>
parseParameters(offerJs, payoutAddressJs, Null)
case offerJs :: payoutAddressJs :: changeAddressJs :: Nil =>
parseParameters(offerJs, payoutAddressJs, changeAddressJs)
case Nil =>
Failure(new IllegalArgumentException("Missing offer argument"))
@ -930,24 +989,42 @@ object AddDLCSigs extends ServerJsonModels {
}
}
case class DLCDataFromFile(path: Path, destinationOpt: Option[Path])
case class DLCDataFromFile(
path: Path,
destinationOpt: Option[Path],
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress])
object DLCDataFromFile extends ServerJsonModels {
def fromJsArr(jsArr: ujson.Arr): Try[DLCDataFromFile] = {
def parseParameters(
pathJs: Value,
destJs: Value,
payoutAddressJs: Value,
changeAddressJs: Value) = Try {
val path = new File(pathJs.str).toPath
val destJsOpt = nullToOpt(destJs)
val destOpt = destJsOpt.map(js => new File(js.str).toPath)
val payoutAddressJsOpt = nullToOpt(payoutAddressJs)
val payoutAddressOpt =
payoutAddressJsOpt.map(js => jsToBitcoinAddress(js))
val changeAddressJsOpt = nullToOpt(changeAddressJs)
val changeAddressOpt =
changeAddressJsOpt.map(js => jsToBitcoinAddress(js))
DLCDataFromFile(path, destOpt, payoutAddressOpt, changeAddressOpt)
}
jsArr.arr.toList match {
case pathJs :: Nil =>
Try {
val path = new File(pathJs.str).toPath
DLCDataFromFile(path, None)
}
parseParameters(pathJs, Null, Null, Null)
case pathJs :: destJs :: Nil =>
Try {
val path = new File(pathJs.str).toPath
val destJsOpt = nullToOpt(destJs)
val destOpt = destJsOpt.map(js => new File(js.str).toPath)
DLCDataFromFile(path, destOpt)
}
parseParameters(pathJs, destJs, Null, Null)
case pathJs :: destJs :: payoutAddressJs :: Nil =>
parseParameters(pathJs, destJs, payoutAddressJs, Null)
case pathJs :: destJs :: payoutAddressJs :: changeAddressJs :: Nil =>
parseParameters(pathJs, destJs, payoutAddressJs, changeAddressJs)
case Nil =>
Failure(new IllegalArgumentException("Missing path argument"))
case other =>

View file

@ -6,14 +6,14 @@ import akka.http.scaladsl.server._
import akka.stream.Materializer
import grizzled.slf4j.Logging
import org.bitcoins.commons.serializers.Picklers._
import org.bitcoins.core.api.dlc.wallet.AnyDLCHDWalletApi
import org.bitcoins.core.api.wallet.db.SpendingInfoDb
import org.bitcoins.core.currency._
import org.bitcoins.core.protocol.tlv._
import org.bitcoins.core.protocol.transaction.Transaction
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.core.wallet.utxo.{AddressLabelTagType, TxoState}
import org.bitcoins.crypto.NetworkElement
import org.bitcoins.core.api.dlc.wallet.AnyDLCHDWalletApi
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
import org.bitcoins.keymanager._
import org.bitcoins.keymanager.config.KeyManagerAppConfig
import org.bitcoins.server.routes.{Server, ServerCommand, ServerRoute}
@ -298,7 +298,9 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
collateral,
feeRateOpt,
locktime,
refundLT)) =>
refundLT,
payoutAddressOpt,
changeAddressOpt)) =>
complete {
val announcements = contractInfo.oracleInfo match {
case OracleInfoV0TLV(announcement) => Vector(announcement)
@ -316,7 +318,9 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
collateral,
feeRateOpt,
locktime,
refundLT)
refundLT,
payoutAddressOpt,
changeAddressOpt)
.map { offer =>
Server.httpSuccess(offer.toMessage.hex)
}
@ -327,10 +331,11 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
AcceptDLCOffer.fromJsArr(arr) match {
case Failure(exception) =>
complete(Server.httpBadRequest(exception))
case Success(AcceptDLCOffer(offer)) =>
case Success(
AcceptDLCOffer(offer, payoutAddressOpt, changeAddressOpt)) =>
complete {
wallet
.acceptDLCOffer(offer.tlv)
.acceptDLCOffer(offer.tlv, payoutAddressOpt, changeAddressOpt)
.map { accept =>
Server.httpSuccess(accept.toMessage.hex)
}
@ -341,7 +346,11 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
complete(Server.httpBadRequest(exception))
case Success(DLCDataFromFile(path, destOpt)) =>
case Success(
DLCDataFromFile(path,
destOpt,
payoutAddressOpt,
changeAddressOpt)) =>
complete {
val hex = Files.readAllLines(path).get(0)
@ -349,7 +358,9 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
val offerMessage = LnMessageFactory(DLCOfferTLV).fromHex(hex)
wallet
.acceptDLCOffer(offerMessage.tlv)
.acceptDLCOffer(offerMessage.tlv,
payoutAddressOpt,
changeAddressOpt)
.map { accept =>
val ret = handleDestinationOpt(accept.toMessage.hex, destOpt)
Server.httpSuccess(ret)
@ -375,7 +386,7 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
complete(Server.httpBadRequest(exception))
case Success(DLCDataFromFile(path, destOpt)) =>
case Success(DLCDataFromFile(path, destOpt, _, _)) =>
complete {
val hex = Files.readAllLines(path).get(0)
@ -408,7 +419,7 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
complete(Server.httpBadRequest(exception))
case Success(DLCDataFromFile(path, _)) =>
case Success(DLCDataFromFile(path, _, _, _)) =>
complete {
val hex = Files.readAllLines(path).get(0)
@ -439,7 +450,7 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit
DLCDataFromFile.fromJsArr(arr) match {
case Failure(exception) =>
complete(Server.httpBadRequest(exception))
case Success(DLCDataFromFile(path, _)) =>
case Success(DLCDataFromFile(path, _, _, _)) =>
val hex = Files.readAllLines(path).get(0)
val signMessage = LnMessageFactory(DLCSignTLV).fromHex(hex)

View file

@ -32,5 +32,8 @@ bitcoin-s.dlcnode.proxy.socks5 = ${?BITCOIN_S_DLCNODE_PROXY_SOCKS5}
bitcoin-s.dlcnode.tor.enabled = ${?BITCOIN_S_DLCNODE_TOR_ENABLED}
bitcoin-s.dlcnode.external-ip = ${?BITCOIN_S_DLCNODE_EXTERNAL_IP}
bitcoin-s.wallet.allowExternalDLCAddresses = false
bitcoin-s.wallet.allowExternalDLCAddresses = ${?BITCOIN_S_ALLOW_EXT_DLC_ADDRESSES}
bitcoin-s.tor.enabled = ${?BITCOIN_S_TOR_ENABLED}
bitcoin-s.tor.provided = ${?BITCOIN_S_TOR_PROVIDED}

View file

@ -5,6 +5,7 @@ import org.bitcoins.core.api.wallet._
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.dlc.accounting._
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.dlc.models.DLCMessage._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.tlv._
@ -22,9 +23,17 @@ trait DLCWalletApi { self: WalletApi =>
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,
refundLT: UInt32): Future[DLCOffer] = {
refundLT: UInt32,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCOffer] = {
val contractInfo = ContractInfo.fromTLV(contractInfoTLV)
createDLCOffer(contractInfo, collateral, feeRateOpt, locktime, refundLT)
createDLCOffer(contractInfo,
collateral,
feeRateOpt,
locktime,
refundLT,
externalPayoutAddressOpt,
externalChangeAddressOpt)
}
def createDLCOffer(
@ -32,7 +41,9 @@ trait DLCWalletApi { self: WalletApi =>
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,
refundLT: UInt32): Future[DLCOffer]
refundLT: UInt32,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCOffer]
def registerDLCOffer(dlcOffer: DLCOffer): Future[DLCOffer] = {
createDLCOffer(
@ -40,15 +51,25 @@ trait DLCWalletApi { self: WalletApi =>
dlcOffer.totalCollateral,
Some(dlcOffer.feeRate),
dlcOffer.timeouts.contractMaturity.toUInt32,
dlcOffer.timeouts.contractTimeout.toUInt32
dlcOffer.timeouts.contractTimeout.toUInt32,
None,
None
)
}
def acceptDLCOffer(dlcOfferTLV: DLCOfferTLV): Future[DLCAccept] = {
acceptDLCOffer(DLCOffer.fromTLV(dlcOfferTLV))
def acceptDLCOffer(
dlcOfferTLV: DLCOfferTLV,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] = {
acceptDLCOffer(DLCOffer.fromTLV(dlcOfferTLV),
externalPayoutAddressOpt,
externalChangeAddressOpt)
}
def acceptDLCOffer(dlcOffer: DLCOffer): Future[DLCAccept]
def acceptDLCOffer(
dlcOffer: DLCOffer,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept]
def signDLC(acceptTLV: DLCAcceptTLV): Future[DLCSign]

View file

@ -5,6 +5,7 @@ import org.bitcoins.core.crypto.ExtPublicKey
import org.bitcoins.core.hd.{BIP32Path, HDChainType}
import org.bitcoins.core.number.UInt16
import org.bitcoins.core.policy.Policy
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.dlc.build.DLCTxBuilder
import org.bitcoins.core.protocol.dlc.models.DLCMessage.{
DLCAccept,
@ -324,7 +325,8 @@ object DLCUtil {
xpub: ExtPublicKey,
chainType: HDChainType,
keyIndex: Int,
networkParameters: NetworkParameters): DLCPublicKeys = {
networkParameters: NetworkParameters,
externalPayoutAddressOpt: Option[BitcoinAddress]): DLCPublicKeys = {
val chainIndex = chainType.index
val fundingKey =
xpub
@ -332,16 +334,20 @@ object DLCUtil {
.get
.key
val payoutKey =
xpub
.deriveChildPubKey(
BIP32Path.fromString(s"m/$chainIndex/${keyIndex + 1}"))
.get
.key
networkParameters match {
case bitcoinNetwork: BitcoinNetwork =>
DLCPublicKeys.fromPubKeys(fundingKey, payoutKey, bitcoinNetwork)
externalPayoutAddressOpt match {
case None =>
val payoutKey =
xpub
.deriveChildPubKey(
BIP32Path.fromString(s"m/$chainIndex/${keyIndex + 1}"))
.get
.key
DLCPublicKeys.fromPubKeys(fundingKey, payoutKey, bitcoinNetwork)
case Some(externalPayoutAddress) =>
DLCPublicKeys(fundingKey, externalPayoutAddress)
}
}
}
}

View file

@ -134,6 +134,10 @@ bitcoin-s {
# before we timeout
addressQueueTimeout = 5 seconds
# Allow external payout and change addresses in DLCs
# By default all DLC addresses are generated by the wallet itself
allowExternalDLCAddresses = false
# this config key is read by Slick
db {
name = walletdb

View file

@ -57,8 +57,10 @@ class DLCNegotiationTest extends BitcoinSDualWalletTest {
half,
Some(SatoshisPerVirtualByte.one),
UInt32.zero,
UInt32.one)
accept <- walletA.acceptDLCOffer(offer)
UInt32.one,
None,
None)
accept <- walletA.acceptDLCOffer(offer, None, None)
// Send accept message to begin p2p
_ = handler ! accept.toMessage

View file

@ -35,7 +35,9 @@ class DLCNodeTest extends BitcoinSDLCNodeTest {
half,
Some(SatoshisPerVirtualByte.one),
UInt32.zero,
UInt32.one)
UInt32.one,
None,
None)
_ <- nodeB.acceptDLCOffer(addrA, offer.toMessage)

View file

@ -47,7 +47,7 @@ class DLCDataHandler(dlcWalletApi: DLCWalletApi, connectionHandler: ActorRef)
Future.unit
case dlcOffer: DLCOfferTLV =>
val f = for {
accept <- dlcWalletApi.acceptDLCOffer(dlcOffer)
accept <- dlcWalletApi.acceptDLCOffer(dlcOffer, None, None)
_ = connectionHandler ! accept.toMessage
} yield ()
f

View file

@ -362,11 +362,15 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
//helper method to make an offer
def makeOffer(): Future[DLCOffer] = {
walletA.createDLCOffer(contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one)
walletA.createDLCOffer(
contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
}
//simply try to make 2 offers with the same contract info
@ -414,7 +418,9 @@ class DLCExecutionTest extends BitcoinSDualWalletTest {
status.localCollateral.satoshis,
None,
UInt32.zero,
UInt32.one)
UInt32.one,
None,
None)
_ <- walletA.listDLCs()
} yield succeed

View file

@ -51,7 +51,9 @@ class MultiWalletDLCTest extends BitcoinSWalletTest {
half,
Some(SatoshisPerVirtualByte.one),
UInt32.zero,
UInt32.one)
UInt32.one,
None,
None)
dlcsA <- walletA.listDLCs()
dlcsB <- walletB.listDLCs()
@ -68,12 +70,15 @@ class MultiWalletDLCTest extends BitcoinSWalletTest {
fundedWallet: FundedDLCWallet =>
//see: https://github.com/bitcoin-s/bitcoin-s/issues/3813#issue-1051117559
val wallet = fundedWallet.wallet
val offerF = wallet.createDLCOffer(contractInfo = sampleContractInfo,
collateral = half,
feeRateOpt =
Some(SatoshisPerVirtualByte.one),
locktime = UInt32.zero,
refundLocktime = UInt32.one)
val offerF = wallet.createDLCOffer(
contractInfo = sampleContractInfo,
collateral = half,
feeRateOpt = Some(SatoshisPerVirtualByte.one),
locktime = UInt32.zero,
refundLocktime = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
//now unreserve the utxo
val reservedUtxoF = for {

View file

@ -2,6 +2,7 @@ package org.bitcoins.dlc.wallet
import org.bitcoins.core.currency._
import org.bitcoins.core.number.{UInt32, UInt64}
import org.bitcoins.core.protocol.BitcoinAddress
import org.bitcoins.core.protocol.dlc.models.DLCMessage._
import org.bitcoins.core.protocol.dlc.models._
import org.bitcoins.core.protocol.script.P2WPKHWitnessV0
@ -44,7 +45,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
dlcA1Opt <- walletA.dlcDAO.read(dlcId)
@ -61,7 +64,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
assert(offer.changeAddress.value.nonEmpty)
}
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
dlcB1Opt <- walletB.dlcDAO.read(dlcId)
_ = {
assert(dlcB1Opt.isDefined)
@ -185,11 +188,13 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
// reorder dlc inputs in wallets
_ <- reorderInputDbs(walletA, dlcId)
@ -242,11 +247,13 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
accept <- walletB.acceptDLCOffer(offer.toTLV)
accept <- walletB.acceptDLCOffer(offer.toTLV, None, None)
// reorder dlc inputs in wallets
_ <- reorderInputDbs(walletA, dlcId)
@ -271,7 +278,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
dlcA1Opt <- walletA.dlcDAO.read(dlcId)
@ -287,7 +296,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
assert(offer.changeAddress.value.nonEmpty)
}
accept <- walletB.acceptDLCOffer(offer.toTLV)
accept <- walletB.acceptDLCOffer(offer.toTLV, None, None)
dlcB1Opt <- walletB.dlcDAO.read(dlcId)
_ = {
assert(dlcB1Opt.isDefined)
@ -365,10 +374,12 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
} yield accept
}
@ -509,7 +520,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
@ -552,9 +565,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
_ <- walletB.acceptDLCOffer(offer)
_ <- walletB.acceptDLCOffer(offer, None, None)
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
@ -593,9 +608,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
sign <- walletA.signDLC(accept)
_ <- walletB.addDLCSigs(sign)
@ -635,9 +652,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
sign <- walletA.signDLC(accept)
_ <- walletB.addDLCSigs(sign)
@ -668,9 +687,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
UInt32.max
UInt32.max,
None,
None
)
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
sign <- walletA.signDLC(accept)
_ <- walletB.addDLCSigs(sign)
@ -733,7 +754,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
offerData.timeouts.contractTimeout.toUInt32
offerData.timeouts.contractTimeout.toUInt32,
None,
None
)
_ = {
assert(offer.oracleInfos == offerData.oracleInfos)
@ -747,7 +770,7 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
dlcId = calcDLCId(offer.fundingInputs.map(_.outPoint))
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
_ = {
assert(accept.fundingInputs.nonEmpty)
assert(
@ -824,19 +847,23 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
val totalCollateral = Satoshis(5000)
def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = {
walletA.createDLCOffer(contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one)
walletA.createDLCOffer(
contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
}
for {
offerA <- makeOffer(contractInfoA)
offerB <- makeOffer(contractInfoB)
_ <- walletB.acceptDLCOffer(offerA)
_ <- walletB.acceptDLCOffer(offerB)
_ <- walletB.acceptDLCOffer(offerA, None, None)
_ <- walletB.acceptDLCOffer(offerB, None, None)
} yield succeed
}
@ -868,17 +895,21 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
val totalCollateral = Satoshis(100000)
def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = {
walletA.createDLCOffer(contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one)
walletA.createDLCOffer(
contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
}
for {
offer <- makeOffer(contractInfoA)
accept1F = walletB.acceptDLCOffer(offer)
accept2F = walletB.acceptDLCOffer(offer)
accept1F = walletB.acceptDLCOffer(offer, None, None)
accept2F = walletB.acceptDLCOffer(offer, None, None)
_ <- recoverToSucceededIf[DuplicateOfferException](
Future.sequence(Seq(accept1F, accept2F)))
} yield {
@ -895,17 +926,21 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
val totalCollateral = Satoshis(100000)
def makeOffer(contractInfo: ContractInfoV0TLV): Future[DLCOffer] = {
walletA.createDLCOffer(contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one)
walletA.createDLCOffer(
contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
}
for {
offer <- makeOffer(contractInfoA)
accept1 <- walletB.acceptDLCOffer(offer)
accept2 <- walletB.acceptDLCOffer(offer)
accept1 <- walletB.acceptDLCOffer(offer, None, None)
accept2 <- walletB.acceptDLCOffer(offer, None, None)
} yield {
assert(accept1 == accept2)
}
@ -923,9 +958,11 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
UInt32.max
UInt32.max,
None,
None
)
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer, None, None)
res <- recoverToSucceededIf[IllegalArgumentException](
walletB.signDLC(accept))
} yield res
@ -944,7 +981,9 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
offerData.totalCollateral,
Some(offerData.feeRate),
offerData.timeouts.contractMaturity.toUInt32,
UInt32.max
UInt32.max,
None,
None
))
} yield {
res
@ -963,18 +1002,82 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest {
val totalCollateral = Satoshis(5000)
for {
offer <- walletA.createDLCOffer(contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one)
offer <- walletA.createDLCOffer(
contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
invalidOffer = offer.copy(contractInfo = invalidContractInfo)
res <- recoverToSucceededIf[InvalidAnnouncementSignature](
walletB.acceptDLCOffer(invalidOffer))
walletB.acceptDLCOffer(invalidOffer, None, None))
} yield {
res
}
}
it must "use external payout and change addresses when they are provided" in {
wallets =>
val walletA = wallets._1.wallet
val walletB = wallets._2.wallet
//https://test.oracle.suredbits.com/contract/enum/75b08299654dca23b80cf359db6afb6cfd6e55bc898b5397d3c0fe796dfc13f0/12fb3e5f091086329ed0d2a12c3fcfa80111a36ef3fc1ac9c2567076a57d6a73
val contractInfo = ContractInfoV0TLV.fromHex(
"fdd82eeb00000000000186a0fda71026030359455300000000000186a0024e4f0000000000000000056f746865720000000000000000fda712b5fdd824b1596ec40d0dae3fdf54d9795ad51ec069970c6863a02d244663d39fd6bedadc0070349e1ba2e17583ee2d1cb3ae6fffaaa1c45039b61c5c4f1d0d864221c461745d1bcfab252c6dd9edd7aea4c5eeeef138f7ff7346061ea40143a9f5ae80baa9fdd8224d0001fa5b84283852400b21a840d5d5ca1cc31867c37326ad521aa50bebf3df4eea1a60b03280fdd8060f000303594553024e4f056f74686572135465746865722d52657365727665732d363342")
val contractInfo1 = DLCWalletUtil.sampleDLCOffer.contractInfo.toTLV
val feeRateOpt = Some(SatoshisPerVirtualByte(Satoshis.one))
val totalCollateral = Satoshis(5000)
val feeRateOpt1 = Some(SatoshisPerVirtualByte(Satoshis(2)))
val totalCollateral1 = Satoshis(10000)
// random testnet addresses
val payoutAddressAOpt = Some(
BitcoinAddress.fromString("tb1qw98mrsxpqtz25xe332khnvlapvl09ejnzk7c3f"))
val changeAddressAOpt = Some(
BitcoinAddress.fromString("tb1qkfaglsvpcwe5pm9ktqs80u9d9jd0qzgqjqd240"))
val payoutAddressBOpt =
Some(BitcoinAddress.fromString("2MsM67NLa71fHvTUBqNENW15P68nHB2vVXb"))
val changeAddressBOpt =
Some(BitcoinAddress.fromString("2N4YXTxKEso3yeYXNn5h42Vqu3FzTTQ8Lq5"))
for {
offer <- walletA.createDLCOffer(
contractInfoTLV = contractInfo,
collateral = totalCollateral,
feeRateOpt = feeRateOpt,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = payoutAddressAOpt,
externalChangeAddressOpt = changeAddressAOpt
)
accept <- walletB.acceptDLCOffer(offer,
payoutAddressBOpt,
changeAddressBOpt)
offer1 <- walletA.createDLCOffer(
contractInfoTLV = contractInfo1,
collateral = totalCollateral1,
feeRateOpt = feeRateOpt1,
locktime = UInt32.zero,
refundLT = UInt32.one,
externalPayoutAddressOpt = None,
externalChangeAddressOpt = None
)
accept1 <- walletB.acceptDLCOffer(offer1, None, None)
} yield {
assert(offer.pubKeys.payoutAddress == payoutAddressAOpt.get)
assert(offer.changeAddress == changeAddressAOpt.get)
assert(accept.pubKeys.payoutAddress == payoutAddressBOpt.get)
assert(accept.changeAddress == changeAddressBOpt.get)
assert(offer1.pubKeys.payoutAddress != payoutAddressAOpt.get)
assert(offer1.changeAddress != changeAddressAOpt.get)
assert(accept1.pubKeys.payoutAddress != payoutAddressBOpt.get)
assert(accept1.changeAddress != changeAddressBOpt.get)
}
}
}

View file

@ -268,8 +268,16 @@ abstract class DLCWallet
collateral: Satoshis,
feeRateOpt: Option[SatoshisPerVirtualByte],
locktime: UInt32,
refundLocktime: UInt32): Future[DLCOffer] = {
refundLocktime: UInt32,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCOffer] = {
logger.info("Creating DLC Offer")
if (
!walletConfig.allowExternalDLCAddresses && (externalPayoutAddressOpt.nonEmpty || externalChangeAddressOpt.nonEmpty)
) {
return Future.failed(
new IllegalArgumentException("External DLC addresses are not allowed"))
}
if (!validateAnnouncementSignatures(contractInfo.oracleInfos)) {
return Future.failed(
InvalidAnnouncementSignature(
@ -279,19 +287,15 @@ abstract class DLCWallet
val announcements =
contractInfo.oracleInfos.head.singleOracleInfos.map(_.announcement)
//hack for now to get around https://github.com/bitcoin-s/bitcoin-s/issues/3127
//filter announcements that we already have in the db
val groupedAnnouncementsF: Future[AnnouncementGrouping] = {
groupByExistingAnnouncements(announcements)
}
val feeRateF = determineFeeRate(feeRateOpt).map { fee =>
SatoshisPerVirtualByte(fee.currencyUnit)
}
for {
feeRate <- feeRateF
groupedAnnouncements <- groupedAnnouncementsF
//hack for now to get around https://github.com/bitcoin-s/bitcoin-s/issues/3127
//filter announcements that we already have in the db
groupedAnnouncements <- groupByExistingAnnouncements(announcements)
announcementDataDbs <- announcementDAO.createAll(
groupedAnnouncements.newAnnouncements)
allAnnouncementDbs =
@ -341,14 +345,17 @@ abstract class DLCWallet
used = false)
}
changeSPK =
txBuilder.finalizer.changeSPK
changeAddr = BitcoinAddress.fromScriptPubKey(changeSPK, networkParameters)
changeAddr = externalChangeAddressOpt.getOrElse {
val changeSPK = txBuilder.finalizer.changeSPK
BitcoinAddress.fromScriptPubKey(changeSPK, networkParameters)
}
dlcPubKeys = DLCUtil.calcDLCPubKeys(xpub = account.xpub,
chainType = chainType,
keyIndex = nextIndex,
networkParameters = networkParameters)
networkParameters = networkParameters,
externalPayoutAddressOpt =
externalPayoutAddressOpt)
_ = logger.debug(
s"DLC Offer data collected, creating database entry, ${dlcId.hex}")
@ -552,8 +559,17 @@ abstract class DLCWallet
*
* This is the first step of the recipient
*/
override def acceptDLCOffer(offer: DLCOffer): Future[DLCAccept] = {
override def acceptDLCOffer(
offer: DLCOffer,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] = {
logger.debug("Calculating relevant wallet data for DLC Accept")
if (
!walletConfig.allowExternalDLCAddresses && (externalPayoutAddressOpt.nonEmpty || externalChangeAddressOpt.nonEmpty)
) {
return Future.failed(
new IllegalArgumentException("External DLC addresses are not allowed"))
}
if (!validateAnnouncementSignatures(offer.oracleInfos)) {
return Future.failed(InvalidAnnouncementSignature(
s"Offer ${offer.tempContractId.hex} contains invalid announcement signature(s)"))
@ -573,7 +589,11 @@ abstract class DLCWallet
dlcAccept <- {
dlcAcceptOpt match {
case Some(accept) => Future.successful(accept)
case None => createNewDLCAccept(collateral, offer)
case None =>
createNewDLCAccept(collateral,
offer,
externalPayoutAddressOpt,
externalChangeAddressOpt)
}
}
status <- findDLC(dlcId)
@ -612,155 +632,161 @@ abstract class DLCWallet
private def createNewDLCAccept(
collateral: CurrencyUnit,
offer: DLCOffer): Future[DLCAccept] = Future {
DLCWallet.AcceptingOffersLatch.startAccepting(offer.tempContractId)
offer: DLCOffer,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): Future[DLCAccept] =
Future {
DLCWallet.AcceptingOffersLatch.startAccepting(offer.tempContractId)
logger.info(
s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}")
logger.info(
s"Creating DLC Accept for tempContractId ${offer.tempContractId.hex}")
def getFundingPrivKey(account: AccountDb, dlc: DLCDb): AdaptorSign = {
val bip32Path = BIP32Path(
account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false),
BIP32Node(dlc.keyIndex,
hardened = false)))
val privKeyPath = HDPath.fromString(bip32Path.toString)
keyManager.toSign(privKeyPath)
}
val result = for {
account <- getDefaultAccountForType(AddressType.SegWit)
(dlc, offerDb, contractDataDb) <- initDLCForAccept(offer, account)
(txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer,
collateral = collateral,
account = account)
fundingPrivKey = getFundingPrivKey(account, dlc)
(acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs(
dlc = dlc,
offer = offer,
txBuilder = txBuilder,
spendingInfos = spendingInfos,
account = account,
fundingPrivKey = fundingPrivKey,
collateral = collateral,
networkParameters = networkParameters
)
builder = DLCTxBuilder(offer, acceptWithoutSigs)
contractId = builder.calcContractId
dlcDbWithContractId = dlc.copy(contractIdOpt = Some(contractId))
signer = DLCTxSigner(builder = builder,
isInitiator = false,
fundingKey = fundingPrivKey,
finalAddress = dlcPubKeys.payoutAddress,
fundingUtxos = spendingInfos)
spkDb = ScriptPubKeyDb(builder.fundingSPK)
// only update spk db if we don't have it
_ <- scriptPubKeyDAO.createIfNotExists(spkDb)
_ = logger.info(s"Creating CET Sigs for ${contractId.toHex}")
//emit websocket event that we are now computing adaptor signatures
status = DLCStatusBuilder.buildInProgressDLCStatus(
dlcDb = dlcDbWithContractId,
contractInfo = offer.contractInfo,
contractData = contractDataDb,
offerDb = offerDb)
_ = dlcConfig.walletCallbacks.executeOnDLCStateChange(logger, status)
cetSigs <- signer.createCETSigsAsync()
refundSig = signer.signRefundTx
_ = logger.debug(
s"DLC Accept data collected, creating database entry, ${dlc.dlcId.hex}")
dlcAcceptDb = DLCAcceptDb(
dlcId = dlc.dlcId,
fundingKey = dlcPubKeys.fundingKey,
payoutAddress = dlcPubKeys.payoutAddress,
payoutSerialId = acceptWithoutSigs.payoutSerialId,
collateral = collateral,
changeAddress = acceptWithoutSigs.changeAddress,
changeSerialId = acceptWithoutSigs.changeSerialId,
negotiationFieldsTLV = NoNegotiationFields.toTLV
)
sigsDbs = cetSigs.outcomeSigs.zipWithIndex.map { case (sig, index) =>
DLCCETSignaturesDb(dlc.dlcId, index = index, sig._1, sig._2, None)
def getFundingPrivKey(account: AccountDb, dlc: DLCDb): AdaptorSign = {
val bip32Path = BIP32Path(
account.hdAccount.path ++ Vector(BIP32Node(0, hardened = false),
BIP32Node(dlc.keyIndex,
hardened = false)))
val privKeyPath = HDPath.fromString(bip32Path.toString)
keyManager.toSign(privKeyPath)
}
refundSigsDb =
DLCRefundSigsDb(dlc.dlcId, refundSig, None)
val result = for {
account <- getDefaultAccountForType(AddressType.SegWit)
(dlc, offerDb, contractDataDb) <- initDLCForAccept(offer, account)
(txBuilder, spendingInfos) <- fundDLCAcceptMsg(offer = offer,
collateral = collateral,
account = account)
fundingPrivKey = getFundingPrivKey(account, dlc)
(acceptWithoutSigs, dlcPubKeys) = DLCAcceptUtil.buildAcceptWithoutSigs(
dlc = dlc,
offer = offer,
txBuilder = txBuilder,
spendingInfos = spendingInfos,
account = account,
fundingPrivKey = fundingPrivKey,
collateral = collateral,
networkParameters = networkParameters,
externalPayoutAddressOpt = externalPayoutAddressOpt,
externalChangeAddressOpt = externalChangeAddressOpt
)
builder = DLCTxBuilder(offer, acceptWithoutSigs)
offerInputs = offer.fundingInputs.zipWithIndex.map {
case (funding, idx) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = true,
index = idx,
inputSerialId = funding.inputSerialId,
outPoint = funding.outPoint,
output = funding.output,
nSequence = funding.sequence,
maxWitnessLength = funding.maxWitnessLen.toLong,
redeemScriptOpt = funding.redeemScriptOpt,
witnessScriptOpt = None
)
}
contractId = builder.calcContractId
offerPrevTxs = offer.fundingInputs.map(funding =>
TransactionDbHelper.fromTransaction(funding.prevTx,
blockHashOpt = None))
dlcDbWithContractId = dlc.copy(contractIdOpt = Some(contractId))
acceptInputs = spendingInfos
.zip(acceptWithoutSigs.fundingInputs)
.zipWithIndex
.map { case ((utxo, fundingInput), idx) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = false,
index = idx,
inputSerialId = fundingInput.inputSerialId,
outPoint = utxo.outPoint,
output = utxo.output,
nSequence = fundingInput.sequence,
maxWitnessLength = fundingInput.maxWitnessLen.toLong,
redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo),
witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo)
)
signer = DLCTxSigner(builder = builder,
isInitiator = false,
fundingKey = fundingPrivKey,
finalAddress = dlcPubKeys.payoutAddress,
fundingUtxos = spendingInfos)
spkDb = ScriptPubKeyDb(builder.fundingSPK)
// only update spk db if we don't have it
_ <- scriptPubKeyDAO.createIfNotExists(spkDb)
_ = logger.info(s"Creating CET Sigs for ${contractId.toHex}")
//emit websocket event that we are now computing adaptor signatures
status = DLCStatusBuilder.buildInProgressDLCStatus(
dlcDb = dlcDbWithContractId,
contractInfo = offer.contractInfo,
contractData = contractDataDb,
offerDb = offerDb)
_ = dlcConfig.walletCallbacks.executeOnDLCStateChange(logger, status)
cetSigs <- signer.createCETSigsAsync()
refundSig = signer.signRefundTx
_ = logger.debug(
s"DLC Accept data collected, creating database entry, ${dlc.dlcId.hex}")
dlcAcceptDb = DLCAcceptDb(
dlcId = dlc.dlcId,
fundingKey = dlcPubKeys.fundingKey,
payoutAddress = dlcPubKeys.payoutAddress,
payoutSerialId = acceptWithoutSigs.payoutSerialId,
collateral = collateral,
changeAddress = acceptWithoutSigs.changeAddress,
changeSerialId = acceptWithoutSigs.changeSerialId,
negotiationFieldsTLV = NoNegotiationFields.toTLV
)
sigsDbs = cetSigs.outcomeSigs.zipWithIndex.map { case (sig, index) =>
DLCCETSignaturesDb(dlc.dlcId, index = index, sig._1, sig._2, None)
}
accept =
dlcAcceptDb.toDLCAccept(tempContractId = dlc.tempContractId,
fundingInputs = acceptWithoutSigs.fundingInputs,
outcomeSigs = cetSigs.outcomeSigs,
refundSig = refundSig)
refundSigsDb =
DLCRefundSigsDb(dlc.dlcId, refundSig, None)
_ = require(accept.tempContractId == offer.tempContractId,
"Offer and Accept have differing tempContractIds!")
offerInputs = offer.fundingInputs.zipWithIndex.map {
case (funding, idx) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = true,
index = idx,
inputSerialId = funding.inputSerialId,
outPoint = funding.outPoint,
output = funding.output,
nSequence = funding.sequence,
maxWitnessLength = funding.maxWitnessLen.toLong,
redeemScriptOpt = funding.redeemScriptOpt,
witnessScriptOpt = None
)
}
_ <- remoteTxDAO.upsertAll(offerPrevTxs)
actions = actionBuilder.buildCreateAcceptAction(
dlcDb = dlcDbWithContractId.updateState(DLCState.Accepted),
dlcAcceptDb = dlcAcceptDb,
offerInputs = offerInputs,
acceptInputs = acceptInputs,
cetSigsDb = sigsDbs,
refundSigsDb = refundSigsDb
)
_ <- safeDatabase.run(actions)
dlcDb <- updateDLCContractIds(offer, accept)
_ = logger.info(
s"Created DLCAccept for tempContractId ${offer.tempContractId.hex} with contract Id ${contractId.toHex}")
offerPrevTxs = offer.fundingInputs.map(funding =>
TransactionDbHelper.fromTransaction(funding.prevTx,
blockHashOpt = None))
fundingTx = builder.buildFundingTx
outPoint = TransactionOutPoint(fundingTx.txId,
UInt32(builder.fundOutputIndex))
_ <- updateFundingOutPoint(dlcDb.contractIdOpt.get, outPoint)
} yield accept
result.onComplete(_ =>
DLCWallet.AcceptingOffersLatch.doneAccepting(offer.tempContractId))
result
}.flatten
acceptInputs = spendingInfos
.zip(acceptWithoutSigs.fundingInputs)
.zipWithIndex
.map { case ((utxo, fundingInput), idx) =>
DLCFundingInputDb(
dlcId = dlc.dlcId,
isInitiator = false,
index = idx,
inputSerialId = fundingInput.inputSerialId,
outPoint = utxo.outPoint,
output = utxo.output,
nSequence = fundingInput.sequence,
maxWitnessLength = fundingInput.maxWitnessLen.toLong,
redeemScriptOpt = InputInfo.getRedeemScript(utxo.inputInfo),
witnessScriptOpt = InputInfo.getScriptWitness(utxo.inputInfo)
)
}
accept =
dlcAcceptDb.toDLCAccept(tempContractId = dlc.tempContractId,
fundingInputs =
acceptWithoutSigs.fundingInputs,
outcomeSigs = cetSigs.outcomeSigs,
refundSig = refundSig)
_ = require(accept.tempContractId == offer.tempContractId,
"Offer and Accept have differing tempContractIds!")
_ <- remoteTxDAO.upsertAll(offerPrevTxs)
actions = actionBuilder.buildCreateAcceptAction(
dlcDb = dlcDbWithContractId.updateState(DLCState.Accepted),
dlcAcceptDb = dlcAcceptDb,
offerInputs = offerInputs,
acceptInputs = acceptInputs,
cetSigsDb = sigsDbs,
refundSigsDb = refundSigsDb
)
_ <- safeDatabase.run(actions)
dlcDb <- updateDLCContractIds(offer, accept)
_ = logger.info(
s"Created DLCAccept for tempContractId ${offer.tempContractId.hex} with contract Id ${contractId.toHex}")
fundingTx = builder.buildFundingTx
outPoint = TransactionOutPoint(fundingTx.txId,
UInt32(builder.fundOutputIndex))
_ <- updateFundingOutPoint(dlcDb.contractIdOpt.get, outPoint)
} yield accept
result.onComplete(_ =>
DLCWallet.AcceptingOffersLatch.doneAccepting(offer.tempContractId))
result
}.flatten
def registerDLCAccept(
accept: DLCAccept): Future[(DLCDb, Vector[DLCCETSignaturesDb])] = {

View file

@ -38,7 +38,9 @@ object DLCAcceptUtil extends Logging {
account: AccountDb,
fundingPrivKey: AdaptorSign,
collateral: CurrencyUnit,
networkParameters: NetworkParameters): (
networkParameters: NetworkParameters,
externalPayoutAddressOpt: Option[BitcoinAddress],
externalChangeAddressOpt: Option[BitcoinAddress]): (
DLCAcceptWithoutSigs,
DLCPublicKeys) = {
val serialIds = DLCMessage.genSerialIds(
@ -49,15 +51,18 @@ object DLCAcceptUtil extends Logging {
.fromInputSigningInfo(utxo, id, TransactionConstants.enableRBFSequence)
}
val changeSPK = txBuilder.finalizer.changeSPK
val changeAddr =
val changeAddr = externalChangeAddressOpt.getOrElse {
val changeSPK = txBuilder.finalizer.changeSPK
BitcoinAddress.fromScriptPubKey(changeSPK, networkParameters)
}
val dlcPubKeys = DLCUtil.calcDLCPubKeys(xpub = account.xpub,
chainType = dlc.changeIndex,
keyIndex = dlc.keyIndex,
networkParameters =
networkParameters)
val dlcPubKeys = DLCUtil.calcDLCPubKeys(
xpub = account.xpub,
chainType = dlc.changeIndex,
keyIndex = dlc.keyIndex,
networkParameters = networkParameters,
externalPayoutAddressOpt = externalPayoutAddressOpt
)
require(dlcPubKeys.fundingKey == fundingPrivKey.publicKey,
"Did not derive the same funding private and public key")

View file

@ -242,6 +242,10 @@ bitcoin-s {
# before we timeout
addressQueueTimeout = 5 seconds
# Allow external payout and change addresses in DLCs
# By default all DLC addresses are generated by the wallet itself
allowExternalDLCAddresses = false
# How often the wallet will rebroadcast unconfirmed transactions
rebroadcastFrequency = 4 hours

View file

@ -46,7 +46,9 @@ object BitcoinSTestAppConfig {
| node {
| mode = spv
| }
|
| wallet {
| allowExternalDLCAddresses = true
| }
| proxy.enabled = $torEnabled
| tor.enabled = $torEnabled
| tor.use-random-ports = false

View file

@ -274,7 +274,12 @@ object DLCWalletUtil extends Logging {
def initDLC(
fundedWalletA: FundedDLCWallet,
fundedWalletB: FundedDLCWallet,
contractInfo: ContractInfo)(implicit ec: ExecutionContext): Future[
contractInfo: ContractInfo,
payoutAddressAOpt: Option[BitcoinAddress] = None,
changeAddressAOpt: Option[BitcoinAddress] = None,
payoutAddressBOpt: Option[BitcoinAddress] = None,
changeAddressBOpt: Option[BitcoinAddress] = None)(implicit
ec: ExecutionContext): Future[
(InitializedDLCWallet, InitializedDLCWallet)] = {
val walletA = fundedWalletA.wallet
val walletB = fundedWalletB.wallet
@ -285,9 +290,13 @@ object DLCWalletUtil extends Logging {
collateral = half,
feeRateOpt = Some(SatoshisPerVirtualByte.fromLong(10)),
locktime = dummyTimeouts.contractMaturity.toUInt32,
refundLocktime = dummyTimeouts.contractTimeout.toUInt32
refundLocktime = dummyTimeouts.contractTimeout.toUInt32,
externalPayoutAddressOpt = payoutAddressAOpt,
externalChangeAddressOpt = changeAddressAOpt
)
accept <- walletB.acceptDLCOffer(offer)
accept <- walletB.acceptDLCOffer(offer,
payoutAddressBOpt,
changeAddressBOpt)
sigs <- walletA.signDLC(accept)
_ <- walletB.addDLCSigs(sigs)
tx <- walletB.broadcastDLCFundingTx(sigs.contractId)

View file

@ -145,6 +145,9 @@ case class WalletAppConfig(baseDatadir: Path, configOverrides: Vector[Config])(
lazy val feeProviderTargetOpt: Option[Int] =
config.getIntOpt("bitcoin-s.fee-provider.target")
lazy val allowExternalDLCAddresses: Boolean =
config.getBoolean("bitcoin-s.wallet.allowExternalDLCAddresses")
lazy val bip39PasswordOpt: Option[String] = kmConf.bip39PasswordOpt
lazy val aesPasswordOpt: Option[AesPassword] = kmConf.aesPasswordOpt