diff --git a/app/server-routes/src/main/scala/org/bitcoins/server/routes/Server.scala b/app/server-routes/src/main/scala/org/bitcoins/server/routes/Server.scala index 91a3cf817c..983e01b2ea 100644 --- a/app/server-routes/src/main/scala/org/bitcoins/server/routes/Server.scala +++ b/app/server-routes/src/main/scala/org/bitcoins/server/routes/Server.scala @@ -211,12 +211,7 @@ object Server { ) } - def httpBadRequest(ex: Throwable): HttpResponse = { - httpBadRequest(ex.getMessage) - } - - def httpBadRequest(msg: String): HttpResponse = { - + def httpError(msg: String): HttpEntity.Strict = { val entity = { val response = Response(error = Some(msg)) HttpEntity( @@ -224,6 +219,15 @@ object Server { up.write(response.toJsonMap) ) } + entity + } + + def httpBadRequest(ex: Throwable): HttpResponse = { + httpBadRequest(ex.getMessage) + } + + def httpBadRequest(msg: String): HttpResponse = { + val entity = httpError(msg) HttpResponse(status = StatusCodes.BadRequest, entity = entity) } diff --git a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala index 23e30ee25e..cb21237125 100644 --- a/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala +++ b/app/server-test/src/test/scala/org/bitcoins/server/RoutesSpec.scala @@ -1175,7 +1175,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { (mockWalletApi .executeDLC(_: ByteVector, _: Seq[OracleAttestmentTLV])) .expects(contractId, Vector(dummyOracleAttestment)) - .returning(Future.successful(EmptyTransaction)) + .returning(Future.successful(Some(EmptyTransaction))) val route = walletRoutes.handleCommand( ServerCommand("executedlc", @@ -1191,12 +1191,31 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } } + "execute a dlc that is not in the wallet and handle gracefully" in { + (mockWalletApi + .executeDLC(_: ByteVector, _: Seq[OracleAttestmentTLV])) + .expects(contractId, Vector(dummyOracleAttestment)) + .returning(Future.successful(None)) + + val route = walletRoutes.handleCommand( + ServerCommand("executedlc", + Arr(Str(contractId.toHex), + Arr(Str(dummyOracleAttestment.hex)), + Bool(true)))) + + Post() ~> route ~> check { + assert(contentType == `application/json`) + assert( + responseAs[String] == s"""{"result":null,"error":"Cannot execute DLC with contractId=${contractId.toHex}"}""") + } + } + "execute a dlc with multiple sigs" in { (mockWalletApi .executeDLC(_: ByteVector, _: Seq[OracleAttestmentTLV])) .expects(contractId, Vector(dummyOracleAttestment, dummyOracleAttestment)) - .returning(Future.successful(EmptyTransaction)) + .returning(Future.successful(Some(EmptyTransaction))) val route = walletRoutes.handleCommand( ServerCommand("executedlc", @@ -1217,7 +1236,7 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { (mockWalletApi .executeDLC(_: ByteVector, _: Seq[OracleAttestmentTLV])) .expects(contractId, Vector(dummyOracleAttestment)) - .returning(Future.successful(EmptyTransaction)) + .returning(Future.successful(Some(EmptyTransaction))) (mockWalletApi.broadcastTransaction _) .expects(EmptyTransaction) diff --git a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala index 16d206570b..c98d5bfff5 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -537,10 +537,25 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit case Success(ExecuteDLC(contractId, sigs, noBroadcast)) => complete { for { - tx <- wallet.executeDLC(contractId, sigs) - ret <- handleBroadcastable(tx, noBroadcast) + txOpt <- wallet.executeDLC(contractId, sigs) + retOpt <- { + txOpt match { + case Some(tx) => + handleBroadcastable(tx, noBroadcast) + .map(_.hex) + .map(Some(_)) + case None => + Future.successful(None) + } + } } yield { - Server.httpSuccess(ret.hex) + retOpt match { + case Some(ret) => + Server.httpSuccess(ret) + case None => + Server.httpError( + s"Cannot execute DLC with contractId=${contractId.toHex}") + } } } } diff --git a/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala b/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala index 98ec9af47d..def4171efc 100644 --- a/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala +++ b/core/src/main/scala/org/bitcoins/core/api/dlc/wallet/DLCWalletApi.scala @@ -86,24 +86,24 @@ trait DLCWalletApi { self: WalletApi => /** Creates the CET for the given contractId and oracle signature, does not broadcast it */ def executeDLC( contractId: ByteVector, - oracleSig: OracleAttestmentTLV): Future[Transaction] = + oracleSig: OracleAttestmentTLV): Future[Option[Transaction]] = executeDLC(contractId, Vector(oracleSig)) /** Creates the CET for the given contractId and oracle signature, does not broadcast it */ def executeDLC( contractId: ByteVector, - oracleSigs: Seq[OracleAttestmentTLV]): Future[Transaction] + oracleSigs: Seq[OracleAttestmentTLV]): Future[Option[Transaction]] /** Creates the CET for the given contractId and oracle signature, does not broadcast it */ def executeDLC( contractId: ByteVector, - oracleSig: OracleSignatures): Future[Transaction] = + oracleSig: OracleSignatures): Future[Option[Transaction]] = executeDLC(contractId, Vector(oracleSig)) /** Creates the CET for the given contractId and oracle signature, does not broadcast it */ def executeDLC( contractId: ByteVector, - oracleSigs: Vector[OracleSignatures]): Future[Transaction] + oracleSigs: Vector[OracleSignatures]): Future[Option[Transaction]] /** Creates the refund transaction for the given contractId, does not broadcast it */ def executeDLCRefund(contractId: ByteVector): Future[Transaction] diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionBitcoindBackendTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionBitcoindBackendTest.scala index 251caada88..7dc2bd723c 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionBitcoindBackendTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionBitcoindBackendTest.scala @@ -74,7 +74,7 @@ class DLCExecutionBitcoindBackendTest s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - closingTx <- dlcB.executeDLC(contractId, oracleSigs) + closingTx <- dlcB.executeDLC(contractId, oracleSigs).map(_.get) //broadcast the closing tx _ <- dlcB.broadcastTransaction(closingTx) dlcs <- dlcB diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala index 69ed30ba8f..84044acaa9 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCExecutionTest.scala @@ -108,7 +108,8 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -153,7 +154,8 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = false, @@ -225,7 +227,8 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { } } - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -406,7 +409,8 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -452,7 +456,7 @@ class DLCExecutionTest extends BitcoinSDualWalletTest { sigs = badSigs, outcomes = badOutcomes) func = (wallet: DLCWallet) => - wallet.executeDLC(contractId, badAttestment) + wallet.executeDLC(contractId, badAttestment).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleEnumExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleEnumExecutionTest.scala index 0e61a88b50..92a8dba781 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleEnumExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleEnumExecutionTest.scala @@ -112,7 +112,8 @@ class DLCMultiOracleEnumExecutionTest extends BitcoinSDualWalletTest { contractId <- getContractId(wallets._1.wallet) (sig, _) = getSigs status <- getDLCStatus(wallets._2.wallet) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -151,7 +152,8 @@ class DLCMultiOracleEnumExecutionTest extends BitcoinSDualWalletTest { contractId <- getContractId(wallets._1.wallet) (_, sig) = getSigs status <- getDLCStatus(wallets._2.wallet) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = false, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala index 85f6cff618..fe5152adbb 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleExactNumericExecutionTest.scala @@ -105,7 +105,8 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest { contractId <- getContractId(wallets._1.wallet) status <- getDLCStatus(wallets._1.wallet) (sigs, _) = getSigs(status.contractInfo) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sigs).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -144,7 +145,8 @@ class DLCMultiOracleExactNumericExecutionTest extends BitcoinSDualWalletTest { contractId <- getContractId(wallets._1.wallet) status <- getDLCStatus(wallets._2.wallet) (_, sigs) = getSigs(status.contractInfo) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sigs).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = false, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala index 7ed5915526..a868ca171d 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCMultiOracleNumericExecutionTest.scala @@ -114,7 +114,8 @@ class DLCMultiOracleNumericExecutionTest contractId <- getContractId(wallets._1.wallet) status <- getDLCStatus(wallets._1.wallet) (sigs, _) = getSigs(status.contractInfo) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sigs).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -153,7 +154,8 @@ class DLCMultiOracleNumericExecutionTest contractId <- getContractId(wallets._1.wallet) status <- getDLCStatus(wallets._2.wallet) (_, sigs) = getSigs(status.contractInfo) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sigs).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = false, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala index 6e32e2db45..c9f92af136 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCNumericExecutionTest.scala @@ -91,7 +91,8 @@ class DLCNumericExecutionTest extends BitcoinSDualWalletTest { contractId <- getContractId(wallets._1.wallet) status <- getDLCStatus(wallets._1.wallet) (sigs, _) = getSigs(status.contractInfo) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sigs).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = true, @@ -130,7 +131,8 @@ class DLCNumericExecutionTest extends BitcoinSDualWalletTest { contractId <- getContractId(wallets._1.wallet) status <- getDLCStatus(wallets._2.wallet) (_, sigs) = getSigs(status.contractInfo) - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sigs) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sigs).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = false, @@ -181,7 +183,7 @@ class DLCNumericExecutionTest extends BitcoinSDualWalletTest { sigs = badSigs, outcomes = badOutcomes) func = (wallet: DLCWallet) => - wallet.executeDLC(contractId, badAttestment) + wallet.executeDLC(contractId, badAttestment).map(_.get) result <- dlcExecutionTest(wallets = wallets, asInitiator = false, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCWalletCallbackTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCWalletCallbackTest.scala index 632cf52bae..9a17a125b4 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCWalletCallbackTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/DLCWalletCallbackTest.scala @@ -106,7 +106,7 @@ class DLCWalletCallbackTest extends BitcoinSDualWalletTest { s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - transaction <- walletA.executeDLC(contractId, sigs._1) + transaction <- walletA.executeDLC(contractId, sigs._1).map(_.get) _ <- walletB.processTransaction(transaction, None) } yield () diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/RescanDLCTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/RescanDLCTest.scala index c18854721f..c78cc600a0 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/RescanDLCTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/RescanDLCTest.scala @@ -40,7 +40,8 @@ class RescanDLCTest extends DualWalletTestCachedBitcoind { s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = (walletA, walletB), asInitiator = true, @@ -79,7 +80,8 @@ class RescanDLCTest extends DualWalletTestCachedBitcoind { s"Cannot retrieve sigs for disjoint union contract, got=$disjoint") } } - func = (wallet: DLCWallet) => wallet.executeDLC(contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(contractId, sig).map(_.get) result <- dlcExecutionTest(wallets = (walletA, walletB), asInitiator = true, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala index 62dd8d8b49..530f3100da 100644 --- a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/WalletDLCSetupTest.scala @@ -816,7 +816,8 @@ class WalletDLCSetupTest extends BitcoinSDualWalletTest { tx <- walletB.broadcastDLCFundingTx(sign.contractId) _ <- walletA.processTransaction(tx, None) - func = (wallet: DLCWallet) => wallet.executeDLC(sign.contractId, sig) + func = (wallet: DLCWallet) => + wallet.executeDLC(sign.contractId, sig).map(_.get) result <- dlcExecutionTest(dlcA = walletA, dlcB = walletB, asInitiator = true, diff --git a/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagementTest.scala b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagementTest.scala new file mode 100644 index 0000000000..566768339a --- /dev/null +++ b/dlc-wallet-test/src/test/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagementTest.scala @@ -0,0 +1,79 @@ +package org.bitcoins.dlc.wallet.internal + +import org.bitcoins.core.protocol.dlc.compute.DLCUtil +import org.bitcoins.core.protocol.dlc.models.DLCMessage.DLCOffer +import org.bitcoins.core.protocol.dlc.models.DLCState +import org.bitcoins.dlc.wallet.models.{AcceptDbState, SignDbState} +import org.bitcoins.testkit.wallet.{BitcoinSDualWalletTest, DLCWalletUtil} +import org.bitcoins.testkit.wallet.FundWalletUtil.FundedDLCWallet +import org.scalatest.FutureOutcome + +class DLCDataManagementTest extends BitcoinSDualWalletTest { + type FixtureParam = (FundedDLCWallet, FundedDLCWallet) + + override def withFixture(test: OneArgAsyncTest): FutureOutcome = { + withDualFundedDLCWallets(test) + } + + behavior of "DLCDataManagement" + + it must "retrieve a acceptdb state from getDLCFundingData" in { + fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) => + val walletA = fundedDLCWallets._1.wallet + val walletB = fundedDLCWallets._2.wallet + + val offerData: DLCOffer = + DLCWalletUtil.sampleDLCOffer + + for { + offer1 <- walletA.createDLCOffer( + offerData.contractInfo, + offerData.collateral, + Some(offerData.feeRate), + offerData.timeouts.contractMaturity.toUInt32, + offerData.timeouts.contractTimeout.toUInt32, + None, + None + ) + accept <- walletB.acceptDLCOffer(offer1, None, None) + contractId = DLCUtil.calcContractId(offer1, accept) + acceptDbStateOpt <- walletB.dlcDataManagement.getDLCFundingData( + contractId, + walletA.transactionDAO) + } yield { + assert(acceptDbStateOpt.isDefined) + assert(acceptDbStateOpt.get.isInstanceOf[AcceptDbState]) + assert(acceptDbStateOpt.get.state == DLCState.Accepted) + } + } + it must "retrieve a signdb state from getDLCFundingData" in { + fundedDLCWallets: (FundedDLCWallet, FundedDLCWallet) => + val walletA = fundedDLCWallets._1.wallet + val walletB = fundedDLCWallets._2.wallet + + val offerData: DLCOffer = + DLCWalletUtil.sampleDLCOffer + + for { + offer1 <- walletA.createDLCOffer( + offerData.contractInfo, + offerData.collateral, + Some(offerData.feeRate), + offerData.timeouts.contractMaturity.toUInt32, + offerData.timeouts.contractTimeout.toUInt32, + None, + None + ) + accept <- walletB.acceptDLCOffer(offer1, None, None) + + sign <- walletA.signDLC(accept) + signDbStateOpt <- walletA.dlcDataManagement.getDLCFundingData( + sign.contractId, + walletA.transactionDAO) + } yield { + assert(signDbStateOpt.isDefined) + assert(signDbStateOpt.get.isInstanceOf[SignDbState]) + assert(signDbStateOpt.get.state == DLCState.Signed) + } + } +} diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala index 38f5b39ad1..58b4f6fecf 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCAppConfig.scala @@ -13,9 +13,9 @@ import org.bitcoins.db.DatabaseDriver._ import org.bitcoins.db._ import org.bitcoins.dlc.wallet.internal.DLCDataManagement import org.bitcoins.dlc.wallet.models.{ - AcceptDbState, DLCSetupDbState, - OfferedDbState + OfferedDbState, + SetupCompleteDLCDbState } import org.bitcoins.wallet.config.WalletAppConfig import org.bitcoins.wallet.models.TransactionDAO @@ -176,7 +176,7 @@ case class DLCAppConfig(baseDatadir: Path, configOverrides: Vector[Config])( vec: Vector[DLCSetupDbState]): Vector[DLCDb] = { vec.map { case state: DLCSetupDbState => val updatedDlcDb: DLCDb = state match { - case acceptDbState: AcceptDbState => + case acceptDbState: SetupCompleteDLCDbState => val offer = acceptDbState.offer val acceptWithoutSigs = acceptDbState.acceptWithoutSigs val dlcDb = acceptDbState.dlcDb diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala index 3d2352975e..13f4aef16c 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWallet.scala @@ -89,7 +89,7 @@ abstract class DLCWallet incomingOfferDAO ) - private val dlcDataManagement = DLCDataManagement(dlcWalletDAOs) + private[wallet] val dlcDataManagement = DLCDataManagement(dlcWalletDAOs) protected lazy val actionBuilder: DLCActionBuilder = { DLCActionBuilder(dlcWalletDAOs) @@ -482,6 +482,9 @@ abstract class DLCWallet Future.failed( new RuntimeException( s"We cannot accept a DLC we offered, dlcId=${dlcId.hex}")) + case _: SignDbState => + Future.failed(new RuntimeException( + s"We cannot accept a DLC we have already signed, dlcId=${dlcId.hex}")) case a: AcceptDbState => val dlcPubKeys = DLCUtil.calcDLCPubKeys( xpub = account.xpub, @@ -1327,17 +1330,17 @@ abstract class DLCWallet setupStateOpt <- dlcDataManagement.getDLCFundingData(contractId, txDAO = transactionDAO) - acceptState = { + complete = { setupStateOpt.map { case _: OfferedDbState => sys.error( s"Cannot retrieve funding transaction when DLC is in offered state") - case accept: AcceptDbState => accept + case complete: SetupCompleteDLCDbState => complete }.get //bad but going to have to save this refactor for future } - dlcDb = acceptState.dlcDb + dlcDb = complete.dlcDb //is this right? We don't have counterpart scriptSigParams - fundingInputs = acceptState.allFundingInputs + fundingInputs = complete.allFundingInputs scriptSigParams <- getScriptSigParams(dlcDb, fundingInputs) signerOpt <- dlcDataManagement.signerFromDb( dlcDb = dlcDb, @@ -1423,21 +1426,37 @@ abstract class DLCWallet override def executeDLC( contractId: ByteVector, - sigs: Seq[OracleAttestmentTLV]): Future[Transaction] = { + sigs: Seq[OracleAttestmentTLV]): Future[Option[Transaction]] = { logger.info( s"Executing dlc with contractId=${contractId.toHex} sigs=${sigs.map(_.hex)}") require(sigs.nonEmpty, "Must provide at least one oracle signature") - + val dlcDbOpt = dlcDAO.findByContractId(contractId) for { - dlcDb <- dlcDAO.findByContractId(contractId).map(_.get) - _ = dlcDb.state match { - case state @ (Offered | AcceptComputingAdaptorSigs | Accepted | - SignComputingAdaptorSigs | Signed) => - sys.error( - s"Cannot execute DLC before the DLC is broadcast to the blockchain, state=$state") - case Broadcasted | Confirmed | _: ClosedState => - //can continue executing, do nothing + dlcDbOpt <- dlcDbOpt + txOpt <- { + dlcDbOpt match { + case Some(dlcDb) => + executeDLC(dlcDb, sigs) + case None => + Future.successful(None) + } + } + } yield txOpt + } + + def executeDLC( + dlcDb: DLCDb, + sigs: Seq[OracleAttestmentTLV]): Future[Option[Transaction]] = { + val _ = dlcDb.state match { + case state @ (Offered | AcceptComputingAdaptorSigs | Accepted | + SignComputingAdaptorSigs | Signed) => + sys.error( + s"Cannot execute DLC before the DLC is broadcast to the blockchain, state=$state") + case Broadcasted | Confirmed | _: ClosedState => + //can continue executing, do nothing + } + for { (announcements, announcementData, nonceDbs) <- dlcDataManagement .getDLCAnnouncementDbs(dlcDb.dlcId) @@ -1450,13 +1469,13 @@ abstract class DLCWallet announcements = announcementTLVs, attestments = sigs.toVector) - tx <- executeDLC(contractId, oracleSigs) + tx <- executeDLC(dlcDb.contractIdOpt.get, oracleSigs) } yield tx } override def executeDLC( contractId: ByteVector, - oracleSigs: Vector[OracleSignatures]): Future[Transaction] = { + oracleSigs: Vector[OracleSignatures]): Future[Option[Transaction]] = { require(oracleSigs.nonEmpty, "Must provide at least one oracle signature") dlcDAO.findByContractId(contractId).flatMap { case None => @@ -1467,7 +1486,7 @@ abstract class DLCWallet db.closingTxIdOpt match { case Some(txId) => transactionDAO.findByTxId(txId).flatMap { - case Some(tx) => Future.successful(tx.transaction) + case Some(tx) => Future.successful(Some(tx.transaction)) case None => createDLCExecutionTx(contractId, oracleSigs) } case None => @@ -1476,60 +1495,69 @@ abstract class DLCWallet } } + /** Returns a execution transaction if we can build one. Returns None + * if the dlc db state is incorrect, or we have pruned CET signatures + * from the database + */ private def createDLCExecutionTx( contractId: ByteVector, - oracleSigs: Vector[OracleSignatures]): Future[Transaction] = { + oracleSigs: Vector[OracleSignatures]): Future[Option[Transaction]] = { require(oracleSigs.nonEmpty, "Must provide at least one oracle signature") - for { - setupStateOpt <- - dlcDataManagement.getDLCFundingData(contractId, txDAO = transactionDAO) - _ = require(setupStateOpt.isDefined, - s"Must have setup state defined to create execution tx") - _ = require( - setupStateOpt.get.isInstanceOf[AcceptDbState], - s"Setup state must be accept to create dlc execution tx, got=${setupStateOpt.get.state}") - setupState = setupStateOpt.get.asInstanceOf[AcceptDbState] - dlcDb = setupState.dlcDb - fundingInputs = setupState.allFundingInputs - scriptSigParams <- getScriptSigParams(dlcDb, fundingInputs) - executorWithSetupOpt <- dlcDataManagement.executorAndSetupFromDb( - contractId = contractId, - txDAO = transactionDAO, - fundingUtxoScriptSigParams = scriptSigParams, - keyManager = keyManager) - tx <- { - executorWithSetupOpt match { - case Some(executorWithSetup) => - buildExecutionTxWithExecutor(executorWithSetup, - oracleSigs, - contractId) - case None => - //means we don't have cet sigs in the db anymore - //can we retrieve the CET some other way? + val setupStateOptF = + dlcDataManagement.getDLCFundingData(contractId, txDAO = transactionDAO) + setupStateOptF.flatMap { + case None => Future.successful(None) + case Some(setupDbState) => + setupDbState match { + case o: OfferedDbState => + logger.info( + s"Cannot create execution tx for dlc in state=${o.state}") + Future.successful(None) + case c: SetupCompleteDLCDbState => + val dlcDb = c.dlcDb + val fundingInputs = c.allFundingInputs + val scriptSigParamsF = getScriptSigParams(dlcDb, fundingInputs) + val executorWithSetupOptF = scriptSigParamsF.flatMap { + scriptSigParams => + dlcDataManagement.executorAndSetupFromDb( + contractId = contractId, + txDAO = transactionDAO, + fundingUtxoScriptSigParams = scriptSigParams, + keyManager = keyManager) + } + executorWithSetupOptF.flatMap { + case Some(executorWithSetup) => + buildExecutionTxWithExecutor(executorWithSetup, + oracleSigs, + contractId).map(Some(_)) + case None => + //means we don't have cet sigs in the db anymore + //can we retrieve the CET some other way? - //lets try to retrieve it from our transactionDAO - val dlcDbOptF = dlcDAO.findByContractId(contractId) + //lets try to retrieve it from our transactionDAO + val dlcDbOptF = dlcDAO.findByContractId(contractId) - for { - dlcDbOpt <- dlcDbOptF - _ = require( - dlcDbOpt.isDefined, - s"Could not find dlc associated with this contractId=${contractId.toHex}") - dlcDb = dlcDbOpt.get - _ = require( - dlcDb.closingTxIdOpt.isDefined, - s"If we don't have CET signatures, the closing tx must be defined, contractId=${contractId.toHex}") - closingTxId = dlcDb.closingTxIdOpt.get - closingTxOpt <- transactionDAO.findByTxId(closingTxId) - } yield { - require( - closingTxOpt.isDefined, - s"Could not find closing tx for DLC in db, contactId=${contractId.toHex} closingTxId=${closingTxId.hex}") - closingTxOpt.get.transaction + for { + dlcDbOpt <- dlcDbOptF + _ = require( + dlcDbOpt.isDefined, + s"Could not find dlc associated with this contractId=${contractId.toHex}") + dlcDb = dlcDbOpt.get + _ = require( + dlcDb.closingTxIdOpt.isDefined, + s"If we don't have CET signatures, the closing tx must be defined, contractId=${contractId.toHex}, state=${dlcDb.state}" + ) + closingTxId = dlcDb.closingTxIdOpt.get + closingTxOpt <- transactionDAO.findByTxId(closingTxId) + } yield { + require( + closingTxOpt.isDefined, + s"Could not find closing tx for DLC in db, contactId=${contractId.toHex} closingTxId=${closingTxId.hex}") + Some(closingTxOpt.get.transaction) + } } } - } - } yield tx + } } private def buildExecutionTxWithExecutor( diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala index 0ca274b2b7..6d26933f43 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCDataManagement.scala @@ -269,6 +269,37 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit } } + private def buildAcceptDbState( + offerDbState: OfferedDbState, + dlcAccept: DLCAcceptDb, + acceptInputs: Vector[DLCFundingInputDb], + cetSignatures: Vector[DLCCETSignaturesDb], + refundSigDb: DLCRefundSigsDb, + txDAO: TransactionDAO): Future[AcceptDbState] = { + + val signaturesOpt = { + if (cetSignatures.isEmpty) { + //means we have pruned signatures from the database + //we have to return None + None + } else { + Some(cetSignatures) + } + } + val acceptPrevTxsDbF = + getAcceptPrevTxs(offerDbState.dlcDb, acceptInputs, txDAO) + + acceptPrevTxsDbF.map { case acceptPrevTxs => + offerDbState.toAcceptDb( + acceptDb = dlcAccept, + acceptFundingInputsDb = acceptInputs, + acceptPrevTxsDb = acceptPrevTxs, + cetSigsOpt = signaturesOpt, + refundSigDb = refundSigDb + ) + } + } + private[wallet] def getDLCFundingData( dlcId: Sha256Digest, txDAO: TransactionDAO): Future[Option[DLCSetupDbState]] = { @@ -282,40 +313,54 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit for { offerDbState <- offerDbStateOpt } yield { - //if the accept message is defined we must have refund sigs - dlcAcceptOpt.zip(refundSigsOpt).headOption match { - case Some((dlcAccept, refundSigDb)) => - require( - refundSigsOpt.isDefined, - s"Cannot have accept in the database if we do not have refund signatures, dlcId=${dlcId.hex}") - val outcomeSigs = cetSignatures.map { dbSig => - dbSig.sigPoint -> dbSig.accepterSig - } - val signaturesOpt = { - if (cetSignatures.isEmpty) { - //means we have pruned signatures from the database - //we have to return None - None - } else { - val sigs = CETSignatures(outcomeSigs) - Some(sigs) - } - } - val acceptPrevTxsDbF = - getAcceptPrevTxs(offerDbState.dlcDb, acceptInputs, txDAO) - - acceptPrevTxsDbF.map { case acceptPrevTxs => - offerDbState.toAcceptDb( - acceptDb = dlcAccept, - acceptFundingInputsDb = acceptInputs, - acceptPrevTxsDb = acceptPrevTxs, - cetSignaturesOpt = signaturesOpt, - refundSigDb = refundSigDb - ) - } - case None => - //just return the offerDbState if we don't have an accept + offerDbState.dlcDb.state match { + case DLCState.Offered | DLCState.AcceptComputingAdaptorSigs => Future.successful(offerDbState) + case DLCState.Accepted | DLCState.SignComputingAdaptorSigs => + //if the accept message is defined we must have refund sigs + dlcAcceptOpt.zip(refundSigsOpt).headOption match { + case Some((dlcAccept, refundSigDb)) => + require( + refundSigsOpt.isDefined, + s"Cannot have accept in the database if we do not have refund signatures, dlcId=${dlcId.hex}") + + buildAcceptDbState(offerDbState = offerDbState, + dlcAccept = dlcAccept, + acceptInputs = acceptInputs, + cetSignatures = cetSignatures, + refundSigDb = refundSigDb, + txDAO = txDAO) + case None => + //just return the offerDbState if we don't have an accept + Future.successful(offerDbState) + } + + case DLCState.Signed | DLCState.Confirmed | DLCState.Broadcasted | + _: DLCState.ClosedState => + //if the accept message is defined we must have refund sigs + dlcAcceptOpt.zip(refundSigsOpt).headOption match { + case Some((dlcAccept, refundSigDb)) => + require( + refundSigsOpt.isDefined, + s"Cannot have accept in the database if we do not have refund signatures, dlcId=${dlcId.hex}") + + val acceptDbStateF = buildAcceptDbState( + offerDbState = offerDbState, + dlcAccept = dlcAccept, + acceptInputs = acceptInputs, + cetSignatures = cetSignatures, + refundSigDb = refundSigDb, + txDAO = txDAO) + val signDbF = for { + acceptDbState <- acceptDbStateF + } yield { + acceptDbState.toSignDbOpt.get + } + signDbF + case None => + //just return the offerDbState if we don't have an accept + Future.successful(offerDbState) + } } } } @@ -330,7 +375,7 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit private[wallet] def getAllDLCData( contractId: ByteVector, - txDAO: TransactionDAO): Future[Option[DLCClosedDbState]] = { + txDAO: TransactionDAO): Future[Option[DLCDbState]] = { val resultF = for { dlcDbOpt <- dlcDAO.findByContractId(contractId) closedDbStateOptNested = dlcDbOpt.map(d => getAllDLCData(d.dlcId, txDAO)) @@ -345,7 +390,7 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit private[wallet] def getAllDLCData( dlcId: Sha256Digest, - txDAO: TransactionDAO): Future[Option[DLCClosedDbState]] = { + txDAO: TransactionDAO): Future[Option[DLCDbState]] = { val sigDLCsF = dlcSigsDAO.findByDLCId(dlcId) for { @@ -356,12 +401,27 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit val sigsOpt = if (sigs.isEmpty) None else Some(sigs) val closedState = setupStateOpt.flatMap { case acceptState: AcceptDbState => - val closedState = - DLCClosedDbState.fromSetupState(acceptState, sigsOpt) - Some(closedState) - case _: OfferedDbState => - //cannot return a closed state because we haven't seen the accept message - None + acceptState.state match { + case _: DLCState.ClosedState => + val closedState = + DLCClosedDbState.fromCompleteSetupState(acceptState, sigsOpt) + Some(closedState) + case _: DLCState.InProgressState => + Some(acceptState) + } + + case signState: SignDbState => + signState.state match { + case _: DLCState.ClosedState => + val closedState = + DLCClosedDbState.fromCompleteSetupState(signState, sigsOpt) + Some(closedState) + case _: DLCState.InProgressState => + Some(signState) + } + + case o: OfferedDbState => + Some(o) } closedState } @@ -409,12 +469,13 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit def getOfferAndAcceptWithoutSigs( dlcId: Sha256Digest, - txDAO: TransactionDAO): Future[Option[AcceptDbState]] = { + txDAO: TransactionDAO): Future[Option[SetupCompleteDLCDbState]] = { val dataF: Future[Option[DLCSetupDbState]] = getDLCFundingData(dlcId, txDAO) dataF.map { case Some(setupDbState) => setupDbState match { case a: AcceptDbState => Some(a) + case s: SignDbState => Some(s) case _: OfferedDbState => None } case None => None @@ -427,9 +488,10 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit for { setupStateOpt <- getOfferAndAcceptWithoutSigs(dlcDb.dlcId, transactionDAO) } yield { - setupStateOpt.map { acceptDbState => - val txBuilder = DLCTxBuilder(offer = acceptDbState.offer, - accept = acceptDbState.acceptWithoutSigs) + setupStateOpt.map { completeSetupDLCDbState => + val txBuilder = + DLCTxBuilder(offer = completeSetupDLCDbState.offer, + accept = completeSetupDLCDbState.acceptWithoutSigs) txBuilder } } @@ -573,10 +635,32 @@ case class DLCDataManagement(dlcWalletDAOs: DLCWalletDAOs)(implicit fundingUtxoScriptSigParams = fundingUtxoScriptSigParams, keyManager = keyManager ) + case c: SetupCompleteDLCDbState => + c.cetSigsOpt match { + case Some(cetSigs) => + executorAndSetupFromDb( + dlcDb = c.dlcDb, + refundSigsDb = c.refundSigDb, + fundingInputs = c.allFundingInputs, + outcomeSigsDbs = cetSigs, + transactionDAO = txDAO, + fundingUtxoScriptSigParams = fundingUtxoScriptSigParams, + keyManager = keyManager + ) + case None => + //we don't have cet signatures for the + //sign message any more + Future.successful(None) + } + case _: ClosedDbStateNoCETSigs => //means we cannot re-create messages because //we don't have the cets in the database anymore Future.successful(None) + case _: OfferedDbState => + //means we cannot recreate messages because + //we don't have an accept or sign message in the database + Future.successful(None) } case None => Future.successful(None) } diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala index 53b38b76a3..8754638da3 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/internal/DLCTransactionProcessing.scala @@ -30,9 +30,6 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing { self: DLCWallet => private lazy val safeDatabase: SafeDatabase = dlcDAO.safeDatabase - private lazy val dlcDataManagement: DLCDataManagement = DLCDataManagement( - dlcWalletDAOs) - /** Calculates the new state of the DLCDb based on the closing transaction, * will delete old CET sigs that are no longer needed after execution * @return a DLCDb if we can calculate the state, else None if we cannot calculate the state @@ -147,9 +144,9 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing { setupStateOpt <- dlcDataManagement.getDLCFundingData(dlcId, txDAO = transactionDAO) - acceptDbState = { + completeDbState = { setupStateOpt.get match { - case accept: AcceptDbState => accept + case c: SetupCompleteDLCDbState => c case offered: OfferedDbState => sys.error( s"Cannot calculate and set outcome of dlc that is only offered, id=${offered.dlcDb.dlcId.hex}") @@ -165,7 +162,7 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing { .map(_.get.transaction.asInstanceOf[WitnessTransaction]) sigAndOutcome = recoverSigAndOutcomeForRemoteClaimed( - acceptDbState = acceptDbState, + completeDbState = completeDbState, cet = cet, sigDbs = sigDbs, refundSigsDbOpt = refundSigOpt) @@ -384,25 +381,25 @@ private[bitcoins] trait DLCTransactionProcessing extends TransactionProcessing { * so we do not necessarily have access to what the [[OracleAttestment]] is */ private def recoverSigAndOutcomeForRemoteClaimed( - acceptDbState: AcceptDbState, + completeDbState: SetupCompleteDLCDbState, cet: WitnessTransaction, sigDbs: Vector[DLCCETSignaturesDb], refundSigsDbOpt: Option[DLCRefundSigsDb]): ( SchnorrDigitalSignature, OracleOutcome) = { - val dlcDb = acceptDbState.dlcDb + val dlcDb = completeDbState.dlcDb val dlcId = dlcDb.dlcId val isInit = dlcDb.isInitiator - val offer = acceptDbState.offer + val offer = completeDbState.offer - val acceptOpt = acceptDbState.acceptOpt + val acceptOpt = completeDbState.acceptOpt require( acceptOpt.isDefined, s"Accept message must still have CET signatures to recover an outcome on chain, dlcId=${dlcId.hex}") val accept = acceptOpt.get - val fundingInputDbs = acceptDbState.allFundingInputs + val fundingInputDbs = completeDbState.allFundingInputs val offerRefundSigOpt = refundSigsDbOpt.flatMap(_.initiatorSig) val signOpt: Option[DLCSign] = offerRefundSigOpt.map { refundSig => diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCDbState.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCDbState.scala index 3110f4b4b8..b70e268703 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCDbState.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/models/DLCDbState.scala @@ -7,10 +7,6 @@ import org.bitcoins.core.protocol.dlc.models.DLCMessage.{ DLCAcceptWithoutSigs, DLCOffer } -import org.bitcoins.core.protocol.dlc.models.DLCState.{ - ClosedState, - InProgressState -} import org.bitcoins.core.protocol.dlc.models.{ CETSignatures, ContractInfo, @@ -39,19 +35,17 @@ sealed trait DLCDbState { contractDataDb = contractDataDb) } - def state: DLCState + final def state: DLCState = dlcDb.state } /** Represents a DLC in the database that * has not had its funding transaction published. * This means we are still setting up the DLC */ -sealed trait DLCSetupDbState extends DLCDbState { - override def state: InProgressState -} +sealed trait DLCSetupDbState extends DLCDbState /** Represents a DLC in the database that has - * been fully setup and is in progress + * been fully setup and settled */ sealed trait DLCClosedDbState extends DLCDbState @@ -63,10 +57,6 @@ case class OfferedDbState( offerFundingInputsDb: Vector[DLCFundingInputDb], offerPrevTxs: Vector[TransactionDb]) extends DLCSetupDbState { - //require(dlcDb.state == DLCState.Offered, - // s"OfferedDbState requires state offered, got=${dlcDb.state}") - - override val state: DLCState.Offered.type = DLCState.Offered /** Converts a [[OfferedDbState]] to an [[AcceptDbState]] * @param acceptDb @@ -78,7 +68,7 @@ case class OfferedDbState( acceptDb: DLCAcceptDb, acceptFundingInputsDb: Vector[DLCFundingInputDb], acceptPrevTxsDb: Vector[TransactionDb], - cetSignaturesOpt: Option[CETSignatures], + cetSigsOpt: Option[Vector[DLCCETSignaturesDb]], refundSigDb: DLCRefundSigsDb): AcceptDbState = { AcceptDbState( dlcDb = dlcDb, @@ -90,12 +80,78 @@ case class OfferedDbState( offerPrevTxs = offerPrevTxs, acceptFundingInputsDb = acceptFundingInputsDb, acceptPrevTxs = acceptPrevTxsDb, - cetSignaturesOpt = cetSignaturesOpt, + cetSigsOpt = cetSigsOpt, refundSigDb = refundSigDb ) } } +/** Shared data structured when we have all information to build a funding + * transaction for a discreet log contract + */ +sealed trait SetupCompleteDLCDbState extends DLCSetupDbState { + def dlcDb: DLCDb + def contractDataDb: DLCContractDataDb + def contractInfo: ContractInfo + def offerDb: DLCOfferDb + def acceptDb: DLCAcceptDb + def offerFundingInputsDb: Vector[DLCFundingInputDb] + def offerPrevTxs: Vector[TransactionDb] + def acceptFundingInputsDb: Vector[DLCFundingInputDb] + def acceptPrevTxs: Vector[TransactionDb] + def refundSigDb: DLCRefundSigsDb + def cetSigsOpt: Option[Vector[DLCCETSignaturesDb]] + + def allFundingInputs: Vector[DLCFundingInputDb] + + def acceptFundingInputs: Vector[DLCFundingInput] = { + DLCTxUtil.matchPrevTxsWithInputs(acceptFundingInputsDb, acceptPrevTxs) + } + + def acceptWithoutSigs: DLCAcceptWithoutSigs = { + acceptDb.toDLCAcceptWithoutSigs( + tempContractId = dlcDb.tempContractId, + fundingInputs = acceptFundingInputs + ) + } + + def cetSignaturesOpt: Option[CETSignatures] = { + cetSigsOpt.map { cetSigs => + this match { + case _: AcceptDbState => + acceptCETSigsOpt.get + case _: SignDbState => + CETSignatures(cetSigs.map(c => (c.sigPoint, c.initiatorSig.get))) + } + } + } + + def acceptCETSigsOpt: Option[CETSignatures] = { + cetSigsOpt.map { cetSigs => + CETSignatures(cetSigs.map(c => (c.sigPoint, c.accepterSig))) + } + } + + def offererCETSigsOpt: Option[CETSignatures] = { + cetSigsOpt.map { cetSigs => + CETSignatures(cetSigs.map(c => (c.sigPoint, c.initiatorSig.get))) + } + } + + /** Reconstructs the [[DLCAccept]] message if we have [[CETSignatures]] + * in the database. If we don't have the signatures because we have pruned + * them we return None as we can't reconstruct the message + */ + def acceptOpt: Option[DLCAccept] = { + acceptCETSigsOpt.map { cetSignatures => + acceptDb.toDLCAccept(dlcDb.tempContractId, + acceptFundingInputs, + outcomeSigs = cetSignatures.outcomeSigs, + refundSig = refundSigDb.accepterSig) + } + } +} + case class AcceptDbState( dlcDb: DLCDb, contractDataDb: DLCContractDataDb, @@ -106,26 +162,11 @@ case class AcceptDbState( offerPrevTxs: Vector[TransactionDb], acceptFundingInputsDb: Vector[DLCFundingInputDb], acceptPrevTxs: Vector[TransactionDb], - cetSignaturesOpt: Option[CETSignatures], + cetSigsOpt: Option[Vector[DLCCETSignaturesDb]], refundSigDb: DLCRefundSigsDb) - extends DLCSetupDbState { - //require(dlcDb.state == DLCState.Accepted, - // s"OfferedDbState requires state accepted, got=${dlcDb.state}") + extends SetupCompleteDLCDbState { - override val state: DLCState.Accepted.type = DLCState.Accepted - - val acceptFundingInputs: Vector[DLCFundingInput] = { - DLCTxUtil.matchPrevTxsWithInputs(acceptFundingInputsDb, acceptPrevTxs) - } - - val acceptWithoutSigs: DLCAcceptWithoutSigs = { - acceptDb.toDLCAcceptWithoutSigs( - tempContractId = dlcDb.tempContractId, - fundingInputs = acceptFundingInputs - ) - } - - val allFundingInputs: Vector[DLCFundingInputDb] = + override val allFundingInputs: Vector[DLCFundingInputDb] = offerFundingInputsDb ++ acceptFundingInputsDb val remotePrevTxs: Vector[TransactionDb] = { @@ -138,20 +179,64 @@ case class AcceptDbState( else acceptPrevTxs } - /** Reconstructs the [[DLCAccept]] message if we have [[CETSignatures]] - * in the database. If we don't have the signatures because we have pruned - * them we return None as we can't reconstruct the message + /** Converts the AcceptDbState -> SignDbState if we have + * all parties CET signatures and refund signatures */ - def acceptOpt: Option[DLCAccept] = { - cetSignaturesOpt.map { cetSignatures => - acceptDb.toDLCAccept(dlcDb.tempContractId, - acceptFundingInputs, - outcomeSigs = cetSignatures.outcomeSigs, - refundSig = refundSigDb.accepterSig) + def toSignDbOpt: Option[SignDbState] = { + + //if we haven't pruned CET signatures from the db + //they must have the offerer's CET signatures defined + cetSigsOpt.map { cetSigs => + require(cetSigs.forall(_.initiatorSig.isDefined), + s"CET signatures must be defined for the offerer") + } + + //if we don't have a refund signature from the offerer + //yet we haven't completed the sign message + refundSigDb.initiatorSig.map { _ => + val sign = SignDbState( + dlcDb, + contractDataDb, + contractInfo, + offerDb, + acceptDb = acceptDb, + offerFundingInputsDb = offerFundingInputsDb, + offerPrevTxs = offerPrevTxs, + acceptFundingInputsDb = acceptFundingInputsDb, + acceptPrevTxs = acceptPrevTxs, + refundSigDb = refundSigDb, + cetSigsOpt = cetSigsOpt + ) + sign } } } +case class SignDbState( + dlcDb: DLCDb, + contractDataDb: DLCContractDataDb, + contractInfo: ContractInfo, + offerDb: DLCOfferDb, + acceptDb: DLCAcceptDb, + offerFundingInputsDb: Vector[DLCFundingInputDb], + offerPrevTxs: Vector[TransactionDb], + acceptFundingInputsDb: Vector[DLCFundingInputDb], + acceptPrevTxs: Vector[TransactionDb], + refundSigDb: DLCRefundSigsDb, + cetSigsOpt: Option[Vector[DLCCETSignaturesDb]] +) extends SetupCompleteDLCDbState { + require(refundSigDb.initiatorSig.isDefined, + s"Refund signature for offerer must be defined") + + //If we have not prune CET signatures, the offerer CET signatures must be defined + cetSigsOpt.map(cetSigs => + require(cetSigs.forall(_.initiatorSig.isDefined), + s"Offerer CET signatures must be defined when in SignDbState")) + + override val allFundingInputs: Vector[DLCFundingInputDb] = + offerFundingInputsDb ++ acceptFundingInputsDb +} + case class ClosedDbStateWithCETSigs( dlcDb: DLCDb, contractDataDb: DLCContractDataDb, @@ -165,11 +250,6 @@ case class ClosedDbStateWithCETSigs( refundSigsDb: DLCRefundSigsDb, cetSigs: Vector[DLCCETSignaturesDb] ) extends DLCClosedDbState { - //require( - // dlcDb.state.isInstanceOf[DLCState.ClosedState], - // s"ClosedDbStateWithCETSigs dlc state must be closed, got=${dlcDb.state}") - - override val state: DLCState = dlcDb.state val allFundingInputs: Vector[DLCFundingInputDb] = offerFundingInputsDb ++ acceptFundingInputsDb @@ -189,47 +269,40 @@ case class ClosedDbStateNoCETSigs( acceptFundingInputsDb: Vector[DLCFundingInputDb], acceptPrevTxs: Vector[TransactionDb], refundSigsDb: DLCRefundSigsDb) - extends DLCClosedDbState { - //require( - // dlcDb.state.isInstanceOf[DLCState.ClosedState], - // s"ClosedDbStateWithCETSigs dlc state must be closed, got=${dlcDb.state}") - - override val state: ClosedState = - dlcDb.state.asInstanceOf[DLCState.ClosedState] -} + extends DLCClosedDbState object DLCClosedDbState { - def fromSetupState( - acceptState: AcceptDbState, + def fromCompleteSetupState( + completeState: SetupCompleteDLCDbState, cetSigsOpt: Option[Vector[DLCCETSignaturesDb]]): DLCClosedDbState = { cetSigsOpt match { case Some(cetSigs) => ClosedDbStateWithCETSigs( - acceptState.dlcDb, - acceptState.contractDataDb, - acceptState.contractInfo, - acceptState.offerDb, - acceptState.acceptDb, - acceptState.offerFundingInputsDb, - acceptState.offerPrevTxs, - acceptState.acceptFundingInputsDb, - acceptState.acceptPrevTxs, - acceptState.refundSigDb, + completeState.dlcDb, + completeState.contractDataDb, + completeState.contractInfo, + completeState.offerDb, + completeState.acceptDb, + completeState.offerFundingInputsDb, + completeState.offerPrevTxs, + completeState.acceptFundingInputsDb, + completeState.acceptPrevTxs, + completeState.refundSigDb, cetSigs ) case None => ClosedDbStateNoCETSigs( - acceptState.dlcDb, - acceptState.contractDataDb, - acceptState.contractInfo, - acceptState.offerDb, - acceptState.acceptDb, - acceptState.offerFundingInputsDb, - acceptState.offerPrevTxs, - acceptState.acceptFundingInputsDb, - acceptState.acceptPrevTxs, - acceptState.refundSigDb + completeState.dlcDb, + completeState.contractDataDb, + completeState.contractInfo, + completeState.offerDb, + completeState.acceptDb, + completeState.offerFundingInputsDb, + completeState.offerPrevTxs, + completeState.acceptFundingInputsDb, + completeState.acceptPrevTxs, + completeState.refundSigDb ) } }