From 9431be2f25fa0bf733e96c2fc6e4ae2aa972be71 Mon Sep 17 00:00:00 2001 From: Chris Stewart Date: Tue, 8 Jun 2021 15:05:40 -0500 Subject: [PATCH] 2021 06 07 dlc wallet pnl (#3229) * WIP * Add profit and loss and rate of return for entire wallet * Fix rebase * Address part 1 of code review from Ben * Add unit test for wallet accounting --- .../commons/serializers/PicklerKeys.scala | 10 +++++ .../commons/serializers/Picklers.scala | 42 +++++++++++++------ .../scala/org/bitcoins/cli/ConsoleCli.scala | 3 ++ .../scala/org/bitcoins/gui/GlobalData.scala | 3 ++ .../scala/org/bitcoins/gui/WalletGUI.scala | 31 ++++++++++---- .../org/bitcoins/gui/WalletGUIModel.scala | 22 +++++++++- .../org/bitcoins/server/RoutesSpec.scala | 23 ++++++++++ .../org/bitcoins/server/WalletRoutes.scala | 7 ++++ .../dlc/accounting/DLCAccountingTest.scala | 16 +++---- .../dlc/accounting/RateOfReturnUtilTest.scala | 16 +++++++ .../core/dlc/accounting/DLCAccounting.scala | 18 +------- .../dlc/accounting/DLCWalletAccounting.scala | 30 +++++++++++++ .../dlc/accounting/PayoutAccounting.scala | 28 +++++++++++++ .../dlc/accounting/RateOfReturnUtil.scala | 4 +- .../core/protocol/dlc/models/DLCStatus.scala | 2 +- .../org/bitcoins/dlc/wallet/DLCWallet.scala | 13 ++++++ .../bitcoins/dlc/wallet/DLCWalletApi.scala | 4 ++ .../wallet/accounting/AccountingUtil.scala | 13 +++--- .../wallet/accounting/DLCAccountingDbs.scala | 17 ++++++++ .../dlc/wallet/util/DLCStatusBuilder.scala | 4 +- 20 files changed, 249 insertions(+), 57 deletions(-) create mode 100644 app-commons/src/main/scala/org/bitcoins/commons/serializers/PicklerKeys.scala create mode 100644 core-test/src/test/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtilTest.scala create mode 100644 core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCWalletAccounting.scala create mode 100644 core/src/main/scala/org/bitcoins/core/dlc/accounting/PayoutAccounting.scala create mode 100644 dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/DLCAccountingDbs.scala diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/PicklerKeys.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/PicklerKeys.scala new file mode 100644 index 0000000000..e947ba6e70 --- /dev/null +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/PicklerKeys.scala @@ -0,0 +1,10 @@ +package org.bitcoins.commons.serializers + +object PicklerKeys { + final val myCollateral: String = "myCollateral" + final val theirCollateral: String = "theirCollateral" + final val myPayout: String = "myPayout" + final val theirPayout: String = "theirPayout" + final val pnl: String = "pnl" + final val rateOfReturn: String = "rateOfReturn" +} diff --git a/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala b/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala index 8c54718e8b..a117ddb1fc 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/serializers/Picklers.scala @@ -9,6 +9,7 @@ import org.bitcoins.core.crypto.{ MnemonicCode } import org.bitcoins.core.currency.{Bitcoins, Satoshis} +import org.bitcoins.core.dlc.accounting.DLCWalletAccounting import org.bitcoins.core.hd.AddressType import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.dlc.models.DLCStatus._ @@ -260,10 +261,7 @@ object Picklers { ) } - private val myPayoutKey: String = "myPayout" private val counterPartyPayoutKey: String = "counterPartyPayout" - private val pnlKey: String = "pnl" - private val rateOfReturnKey: String = "rateOfReturn" implicit val claimedW: Writer[Claimed] = writer[Obj].comap { claimed => import claimed._ @@ -296,11 +294,11 @@ object Picklers { "oracleSigs" -> oracleSigs.map(sig => Str(sig.hex)), "outcomes" -> outcomesJs, "oracles" -> oraclesJs, - myPayoutKey -> Num(claimed.myPayout.satoshis.toLong.toDouble), + PicklerKeys.myPayout -> Num(claimed.myPayout.satoshis.toLong.toDouble), counterPartyPayoutKey -> Num( claimed.counterPartyPayout.satoshis.toLong.toDouble), - pnlKey -> Num(claimed.pnl.satoshis.toLong.toDouble), - rateOfReturnKey -> Num(claimed.rateOfReturn.toDouble) + PicklerKeys.pnl -> Num(claimed.pnl.satoshis.toLong.toDouble), + PicklerKeys.rateOfReturn -> Num(claimed.rateOfReturn.toDouble) ) } @@ -336,11 +334,12 @@ object Picklers { "oracleSigs" -> oracleSigs.map(sig => Str(sig.hex)), "outcomes" -> outcomesJs, "oracles" -> oraclesJs, - myPayoutKey -> Num(remoteClaimed.myPayout.satoshis.toLong.toDouble), + PicklerKeys.myPayout -> Num( + remoteClaimed.myPayout.satoshis.toLong.toDouble), counterPartyPayoutKey -> Num( remoteClaimed.counterPartyPayout.satoshis.toLong.toDouble), - pnlKey -> Num(remoteClaimed.pnl.satoshis.toLong.toDouble), - rateOfReturnKey -> Num(remoteClaimed.rateOfReturn.toDouble) + PicklerKeys.pnl -> Num(remoteClaimed.pnl.satoshis.toLong.toDouble), + PicklerKeys.rateOfReturn -> Num(remoteClaimed.rateOfReturn.toDouble) ) } @@ -363,11 +362,11 @@ object Picklers { "remoteCollateral" -> Num(remoteCollateral.satoshis.toLong.toDouble), "fundingTxId" -> Str(fundingTxId.hex), "closingTxId" -> Str(closingTxId.hex), - myPayoutKey -> Num(refunded.myPayout.satoshis.toLong.toDouble), + PicklerKeys.myPayout -> Num(refunded.myPayout.satoshis.toLong.toDouble), counterPartyPayoutKey -> Num( refunded.counterPartyPayout.satoshis.toLong.toDouble), - pnlKey -> Num(refunded.pnl.satoshis.toLong.toDouble), - rateOfReturnKey -> Num(refunded.rateOfReturn.toDouble) + PicklerKeys.pnl -> Num(refunded.pnl.satoshis.toLong.toDouble), + PicklerKeys.rateOfReturn -> Num(refunded.rateOfReturn.toDouble) ) } @@ -441,7 +440,7 @@ object Picklers { throw new IllegalArgumentException(s"Unexpected outcome $signed") } - lazy val myPayoutJs = obj(myPayoutKey) + lazy val myPayoutJs = obj(PicklerKeys.myPayout) lazy val myPayoutOpt = myPayoutJs.numOpt.map(sats => Satoshis(sats.toLong)) lazy val theirPayoutJs = obj(counterPartyPayoutKey) lazy val theirPayoutOpt = @@ -566,6 +565,23 @@ object Picklers { } } + implicit val dlcWalletAccountingWriter: Writer[DLCWalletAccounting] = { + writer[Obj].comap { walletAccounting: DLCWalletAccounting => + Obj( + PicklerKeys.myCollateral -> Num( + walletAccounting.myCollateral.satoshis.toLong.toDouble), + PicklerKeys.theirCollateral -> Num( + walletAccounting.theirCollateral.satoshis.toLong.toDouble), + PicklerKeys.myPayout -> Num( + walletAccounting.myPayout.satoshis.toLong.toDouble), + PicklerKeys.theirPayout -> Num( + walletAccounting.theirPayout.satoshis.toLong.toDouble), + PicklerKeys.pnl -> Num(walletAccounting.pnl.satoshis.toLong.toDouble), + PicklerKeys.rateOfReturn -> Num(walletAccounting.rateOfReturn.toDouble) + ) + } + } + implicit val mnemonicCodePickler: ReadWriter[MnemonicCode] = readwriter[String].bimap( _.words.mkString(" "), diff --git a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala index f373e9e326..b83f27e8e6 100644 --- a/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala +++ b/app/cli/src/main/scala/org/bitcoins/cli/ConsoleCli.scala @@ -1729,6 +1729,8 @@ object ConsoleCli { case ZipDataDir(path) => RequestParam("zipdatadir", Seq(up.writeJs(path))) + case GetDLCWalletAccounting => + RequestParam("getdlcwalletaccounting") case GetVersion => // skip sending to server and just return version number of cli return Success(EnvUtil.getVersion) @@ -1995,6 +1997,7 @@ object CliCommand { case class GetUnconfirmedBalance(isSats: Boolean) extends AppServerCliCommand case class GetBalances(isSats: Boolean) extends AppServerCliCommand case class GetAddressInfo(address: BitcoinAddress) extends AppServerCliCommand + case object GetDLCWalletAccounting extends AppServerCliCommand case class GetTransaction(txId: DoubleSha256DigestBE) extends AppServerCliCommand diff --git a/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala b/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala index 142368e04d..f53759010b 100644 --- a/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala +++ b/app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala @@ -13,6 +13,9 @@ object GlobalData { val currentReservedBalance: StringProperty = StringProperty("0") val currentTotalBalance: StringProperty = StringProperty("0") + val currentPNL: StringProperty = StringProperty("0") + val rateOfReturn: StringProperty = StringProperty("0%") + val syncHeight: StringProperty = StringProperty("Syncing headers...") var network: BitcoinNetwork = _ diff --git a/app/gui/src/main/scala/org/bitcoins/gui/WalletGUI.scala b/app/gui/src/main/scala/org/bitcoins/gui/WalletGUI.scala index 7f17a0d25b..d8ecdce0f3 100644 --- a/app/gui/src/main/scala/org/bitcoins/gui/WalletGUI.scala +++ b/app/gui/src/main/scala/org/bitcoins/gui/WalletGUI.scala @@ -30,28 +30,35 @@ abstract class WalletGUI { dlcPane.model.setUp() } + private val satsProperty = StringProperty(" sats") + private lazy val confirmedText = new Label() { text <== StringProperty( - "Confirmed balance:\t\t") + GlobalData.currentConfirmedBalance + StringProperty( - " sats") + "Confirmed balance:\t\t") + GlobalData.currentConfirmedBalance + satsProperty } private lazy val unconfirmedText = new Label() { text <== StringProperty( - "Unconfirmed balance:\t") + GlobalData.currentUnconfirmedBalance + StringProperty( - " sats") + "Unconfirmed balance:\t") + GlobalData.currentUnconfirmedBalance + satsProperty } private lazy val reservedText = new Label() { text <== StringProperty( - "Reserved balance:\t\t") + GlobalData.currentReservedBalance + StringProperty( - " sats") + "Reserved balance:\t\t") + GlobalData.currentReservedBalance + satsProperty } private lazy val totalBalanceText = new Label() { text <== StringProperty( - "Total balance:\t\t\t") + GlobalData.currentTotalBalance + StringProperty( - " sats") + "Total balance:\t\t\t") + GlobalData.currentTotalBalance + satsProperty + } + + private lazy val pnlText = new Label() { + text <== StringProperty( + "Profit and Loss:\t\t\t") + GlobalData.currentPNL + satsProperty + } + + private lazy val rateOfReturnText = new Label() { + text <== StringProperty("Rate of Return:\t\t\t") + GlobalData.rateOfReturn } private[gui] lazy val dlcPane = new DLCPane(glassPane) @@ -66,6 +73,11 @@ abstract class WalletGUI { totalBalanceText) } + private lazy val walletAccountingBox = new VBox { + spacing = 10 + children = Vector(pnlText, rateOfReturnText) + } + private lazy val getNewAddressButton = new Button { text = "Get New Address" onAction = _ => model.onGetNewAddress() @@ -84,7 +96,8 @@ abstract class WalletGUI { sendButton.prefWidth <== width getNewAddressButton.maxWidth = 300 sendButton.maxWidth = 300 - children = Vector(balanceBox, getNewAddressButton, sendButton) + children = + Vector(balanceBox, walletAccountingBox, getNewAddressButton, sendButton) } lazy val bottomStack: StackPane = new StackPane() { diff --git a/app/gui/src/main/scala/org/bitcoins/gui/WalletGUIModel.scala b/app/gui/src/main/scala/org/bitcoins/gui/WalletGUIModel.scala index 6bcd6cbe3f..63b5fcfd08 100644 --- a/app/gui/src/main/scala/org/bitcoins/gui/WalletGUIModel.scala +++ b/app/gui/src/main/scala/org/bitcoins/gui/WalletGUIModel.scala @@ -1,9 +1,12 @@ package org.bitcoins.gui import akka.actor.{ActorSystem, Cancellable} +import grizzled.slf4j.Logging import org.bitcoins.cli.CliCommand._ import org.bitcoins.cli.ConsoleCli +import org.bitcoins.commons.serializers.PicklerKeys import org.bitcoins.core.currency.{Bitcoins, Satoshis} +import org.bitcoins.core.dlc.accounting.RateOfReturnUtil import org.bitcoins.core.protocol.BitcoinAddress import org.bitcoins.core.wallet.fee.FeeUnit import org.bitcoins.gui.dialog._ @@ -19,7 +22,8 @@ import scala.concurrent.duration.DurationInt import scala.concurrent.{Await, Promise} import scala.util.{Failure, Success, Try} -class WalletGUIModel(dlcModel: DLCPaneModel)(implicit system: ActorSystem) { +class WalletGUIModel(dlcModel: DLCPaneModel)(implicit system: ActorSystem) + extends Logging { var taskRunner: TaskRunner = _ import system.dispatcher @@ -33,6 +37,7 @@ class WalletGUIModel(dlcModel: DLCPaneModel)(implicit system: ActorSystem) { override def run(): Unit = { Platform.runLater { updateBalance() + updateWalletAccounting() updateWalletInfo() dlcModel.updateDLCs() } @@ -151,4 +156,19 @@ class WalletGUIModel(dlcModel: DLCPaneModel)(implicit system: ActorSystem) { }.showAndWait() } } + + private def updateWalletAccounting(): Unit = { + ConsoleCli.exec(GetDLCWalletAccounting, GlobalData.consoleCliConfig) match { + case Failure(err) => + logger.error(s"Error fetching accounting", err) + case Success(commandReturn) => + val json = ujson.read(commandReturn).obj + val pnl = json(PicklerKeys.pnl).num.toLong.toString + val rateOfReturn = json(PicklerKeys.rateOfReturn).num + val rorPrettyPrint = RateOfReturnUtil.prettyPrint(rateOfReturn) + GlobalData.currentPNL.value = pnl + GlobalData.rateOfReturn.value = rorPrettyPrint + () + } + } } 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 15428c25c4..4349ab7833 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 @@ -11,6 +11,7 @@ import org.bitcoins.core.api.wallet.{AddressInfo, CoinSelectionAlgo} import org.bitcoins.core.config.RegTest import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.currency.{Bitcoins, CurrencyUnit, Satoshis} +import org.bitcoins.core.dlc.accounting.DLCWalletAccounting import org.bitcoins.core.hd._ import org.bitcoins.core.number.{UInt32, UInt64} import org.bitcoins.core.protocol.BlockStamp.{ @@ -1705,5 +1706,27 @@ class RoutesSpec extends AnyWordSpec with ScalatestRouteTest with MockFactory { } } + "get wallet accounting" in { + val accounting = DLCWalletAccounting(myCollateral = Satoshis.one, + theirCollateral = Satoshis.one, + myPayout = Satoshis(2), + theirPayout = Satoshis.zero) + + (mockWalletApi.getWalletAccounting: () => Future[DLCWalletAccounting]) + .expects() + .returning(Future.successful(accounting)) + + val route = walletRoutes.handleCommand( + ServerCommand("getdlcwalletaccounting", Arr())) + + Get() ~> route ~> check { + assert(contentType == `application/json`) + val str = responseAs[String] + val expected = + s"""{"result":{"myCollateral":1,"theirCollateral":1,"myPayout":2,"theirPayout":0,"pnl":1,"rateOfReturn":1},"error":null}""".stripMargin + assert(str == expected) + } + } + } } 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 1123b958cf..4c6cfca8a5 100644 --- a/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala +++ b/app/server/src/main/scala/org/bitcoins/server/WalletRoutes.scala @@ -794,6 +794,13 @@ case class WalletRoutes(wallet: AnyDLCHDWalletApi)(implicit Server.httpSuccess(fee.toString) } } + + case ServerCommand("getdlcwalletaccounting", _) => + complete { + wallet.getWalletAccounting().map { accounting => + Server.httpSuccess(writeJs(accounting)) + } + } } /** Returns information about the state of our wallet */ diff --git a/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/DLCAccountingTest.scala b/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/DLCAccountingTest.scala index d19616805c..e16f989dea 100644 --- a/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/DLCAccountingTest.scala +++ b/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/DLCAccountingTest.scala @@ -21,7 +21,7 @@ class DLCAccountingTest extends BitcoinSUnitTest { //we make 50,000 sats (their collateral) is the profit assert(accounting1.pnl == theirCollateral) - assert(accounting1.rorPrettyPrint == "100%") + assert(accounting1.rorPrettyPrint == "100.00%") } it must "calculate basic pnl where we lose all funds" in { @@ -30,16 +30,16 @@ class DLCAccountingTest extends BitcoinSUnitTest { val accounting1 = DLCAccounting( dlcId = Sha256Digest.empty, - myCollateral = Satoshis(50000), - theirCollateral = Satoshis(50000), + myCollateral = myCollateral, + theirCollateral = theirCollateral, myPayout = Satoshis.zero, theirPayout = myCollateral + theirCollateral ) //we lose 50,000 sats (my collateral) is the loss - assert(accounting1.pnl == Satoshis(-50000)) + assert(accounting1.pnl == -myCollateral) assert(accounting1.rateOfReturn == -1) - assert(accounting1.rorPrettyPrint == "-100%") + assert(accounting1.rorPrettyPrint == "-100.00%") } it must "calculate basic pnl where funds are refunded" in { @@ -48,8 +48,8 @@ class DLCAccountingTest extends BitcoinSUnitTest { val accounting1 = DLCAccounting( dlcId = Sha256Digest.empty, - myCollateral = Satoshis(50000), - theirCollateral = Satoshis(50000), + myCollateral = myCollateral, + theirCollateral = theirCollateral, myPayout = myCollateral, theirPayout = theirCollateral ) @@ -57,6 +57,6 @@ class DLCAccountingTest extends BitcoinSUnitTest { //collateral refunded, so no pnl assert(accounting1.pnl == Satoshis.zero) assert(accounting1.rateOfReturn == 0) - assert(accounting1.rorPrettyPrint == "0%") + assert(accounting1.rorPrettyPrint == "0.00%") } } diff --git a/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtilTest.scala b/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtilTest.scala new file mode 100644 index 0000000000..602450e1c6 --- /dev/null +++ b/core-test/src/test/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtilTest.scala @@ -0,0 +1,16 @@ +package org.bitcoins.core.dlc.accounting + +import org.bitcoins.testkitcore.util.BitcoinSUnitTest + +class RateOfReturnUtilTest extends BitcoinSUnitTest { + behavior of "RateOfReturnUtil" + + it must "pretty print strings with percentages correctly" in { + RateOfReturnUtil.prettyPrint(0) must be("0.00%") + RateOfReturnUtil.prettyPrint(-1) must be("-100.00%") + RateOfReturnUtil.prettyPrint(1) must be("100.00%") + RateOfReturnUtil.prettyPrint(1.23) must be("123.00%") + RateOfReturnUtil.prettyPrint(1.23456) must be("123.46%") + RateOfReturnUtil.prettyPrint(1.23454) must be("123.45%") + } +} diff --git a/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCAccounting.scala b/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCAccounting.scala index ab9b445fdd..19887993ba 100644 --- a/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCAccounting.scala +++ b/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCAccounting.scala @@ -8,19 +8,5 @@ case class DLCAccounting( myCollateral: CurrencyUnit, theirCollateral: CurrencyUnit, myPayout: CurrencyUnit, - theirPayout: CurrencyUnit) { - - /** Profit and loss for the DLC - * @see https://www.investopedia.com/terms/p/plstatement.asp - */ - val pnl: CurrencyUnit = myPayout - myCollateral - - /** Rate of return for the DLC - * @see https://www.investopedia.com/terms/r/rateofreturn.asp - */ - val rateOfReturn: BigDecimal = pnl.toBigDecimal / myCollateral.toBigDecimal - - val rorPrettyPrint: String = { - RateOfReturnUtil.prettyPrint(rateOfReturn) - } -} + theirPayout: CurrencyUnit) + extends PayoutAccounting diff --git a/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCWalletAccounting.scala b/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCWalletAccounting.scala new file mode 100644 index 0000000000..109349b8c2 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/dlc/accounting/DLCWalletAccounting.scala @@ -0,0 +1,30 @@ +package org.bitcoins.core.dlc.accounting + +import org.bitcoins.core.currency.{CurrencyUnit, CurrencyUnits} + +/** Similar to [[org.bitcoins.core.dlc.accounting.DLCAccounting]], but + * represents the entire accounting for the wallet + */ +case class DLCWalletAccounting( + myCollateral: CurrencyUnit, + theirCollateral: CurrencyUnit, + myPayout: CurrencyUnit, + theirPayout: CurrencyUnit) + extends PayoutAccounting + +object DLCWalletAccounting { + + def fromDLCAccounting( + accountings: Vector[DLCAccounting]): DLCWalletAccounting = { + val myCollateral = + accountings.foldLeft(CurrencyUnits.zero)(_ + _.myCollateral) + val theirCollateral = + accountings.foldLeft(CurrencyUnits.zero)(_ + _.theirCollateral) + + val myPayouts = accountings.foldLeft(CurrencyUnits.zero)(_ + _.myPayout) + val theirPayouts = + accountings.foldLeft(CurrencyUnits.zero)(_ + _.theirPayout) + + DLCWalletAccounting(myCollateral, theirCollateral, myPayouts, theirPayouts) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/dlc/accounting/PayoutAccounting.scala b/core/src/main/scala/org/bitcoins/core/dlc/accounting/PayoutAccounting.scala new file mode 100644 index 0000000000..6e11e18f10 --- /dev/null +++ b/core/src/main/scala/org/bitcoins/core/dlc/accounting/PayoutAccounting.scala @@ -0,0 +1,28 @@ +package org.bitcoins.core.dlc.accounting + +import org.bitcoins.core.currency.CurrencyUnit + +/** Utility trait for metrics we need to do accounting */ +trait PayoutAccounting { + def myCollateral: CurrencyUnit + + def theirCollateral: CurrencyUnit + + def myPayout: CurrencyUnit + + def theirPayout: CurrencyUnit + + /** Profit and loss for the DLC + * @see https://www.investopedia.com/terms/p/plstatement.asp + */ + def pnl: CurrencyUnit = myPayout - myCollateral + + /** Rate of return for the DLC + * @see https://www.investopedia.com/terms/r/rateofreturn.asp + */ + def rateOfReturn: BigDecimal = pnl.toBigDecimal / myCollateral.toBigDecimal + + def rorPrettyPrint: String = { + RateOfReturnUtil.prettyPrint(rateOfReturn) + } +} diff --git a/core/src/main/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtil.scala b/core/src/main/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtil.scala index b715c296b8..9c7f14a924 100644 --- a/core/src/main/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtil.scala +++ b/core/src/main/scala/org/bitcoins/core/dlc/accounting/RateOfReturnUtil.scala @@ -2,7 +2,9 @@ package org.bitcoins.core.dlc.accounting object RateOfReturnUtil { + /** @see https://alvinalexander.com/scala/how-to-format-numbers-commas-international-currency-in-scala/ */ def prettyPrint(ror: BigDecimal): String = { - (ror * 100).toString() + "%" + val percent = ror * 100 + f"${percent}%1.2f" + "%" } } diff --git a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCStatus.scala b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCStatus.scala index 6f7da8f757..e29eccd760 100644 --- a/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCStatus.scala +++ b/core/src/main/scala/org/bitcoins/core/protocol/dlc/models/DLCStatus.scala @@ -44,7 +44,7 @@ sealed trait ClosedDLCStatus extends BroadcastedDLCStatus { def myPayout: CurrencyUnit def counterPartyPayout: CurrencyUnit - private def accounting: DLCAccounting = { + def accounting: DLCAccounting = { DLCAccounting(dlcId, localCollateral, remoteCollateral, 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 b78898e762..4d18eb11b7 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 @@ -7,6 +7,7 @@ import org.bitcoins.core.api.wallet.db._ import org.bitcoins.core.config.BitcoinNetwork import org.bitcoins.core.crypto.ExtPublicKey import org.bitcoins.core.currency._ +import org.bitcoins.core.dlc.accounting.DLCWalletAccounting import org.bitcoins.core.hd._ import org.bitcoins.core.number._ import org.bitcoins.core.protocol._ @@ -1213,6 +1214,18 @@ abstract class DLCWallet } yield refundTx } + override def getWalletAccounting(): Future[DLCWalletAccounting] = { + val dlcsF = listDLCs() + for { + dlcs <- dlcsF + closed = dlcs.collect { case c: ClosedDLCStatus => + c + } //only get closed dlcs for accounting + accountings = closed.map(_.accounting) + walletAccounting = DLCWalletAccounting.fromDLCAccounting(accountings) + } yield walletAccounting + } + override def listDLCs(): Future[Vector[DLCStatus]] = { for { ids <- dlcDAO.findAll().map(_.map(_.dlcId)) diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWalletApi.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWalletApi.scala index db7748cd24..8cd9f464bc 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWalletApi.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/DLCWalletApi.scala @@ -2,6 +2,7 @@ package org.bitcoins.dlc.wallet import org.bitcoins.core.api.wallet._ import org.bitcoins.core.currency.Satoshis +import org.bitcoins.core.dlc.accounting.DLCWalletAccounting import org.bitcoins.core.number.UInt32 import org.bitcoins.core.protocol.dlc.models.DLCMessage.{ DLCAccept, @@ -99,6 +100,9 @@ trait DLCWalletApi { self: WalletApi => def findDLC(dlcId: Sha256Digest): Future[Option[DLCStatus]] def cancelDLC(dlcId: Sha256Digest): Future[Unit] + + /** Retrieves accounting and financial metrics for the entire dlc wallet */ + def getWalletAccounting(): Future[DLCWalletAccounting] } /** An HDWallet that supports DLCs and both Neutrino and SPV methods of syncing */ diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/AccountingUtil.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/AccountingUtil.scala index 07da974ef3..8062ab5437 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/AccountingUtil.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/AccountingUtil.scala @@ -1,17 +1,15 @@ package org.bitcoins.dlc.wallet.accounting import org.bitcoins.core.dlc.accounting.DLCAccounting -import org.bitcoins.core.protocol.transaction.Transaction -import org.bitcoins.dlc.wallet.models.{DLCAcceptDb, DLCDb, DLCOfferDb} object AccountingUtil { /** Calculates the profit and loss for the given dlc */ - def calculatePnl( - dlcDb: DLCDb, - offerDb: DLCOfferDb, - acceptDb: DLCAcceptDb, - closingTx: Transaction): DLCAccounting = { + def calculatePnl(financials: DLCAccountingDbs): DLCAccounting = { + val dlcDb = financials.dlcDb + val offerDb = financials.offerDb + val acceptDb = financials.acceptDb + val closingTx = financials.closingTx val (myCollateral, theirCollateral, myPayoutAddress, theirPayoutAddress) = { if (dlcDb.isInitiator) { val myCollateral = offerDb.collateral @@ -45,4 +43,5 @@ object AccountingUtil { theirPayout = theirPayout ) } + } diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/DLCAccountingDbs.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/DLCAccountingDbs.scala new file mode 100644 index 0000000000..271624c114 --- /dev/null +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/accounting/DLCAccountingDbs.scala @@ -0,0 +1,17 @@ +package org.bitcoins.dlc.wallet.accounting + +import org.bitcoins.core.protocol.transaction.Transaction +import org.bitcoins.dlc.wallet.models.{DLCAcceptDb, DLCDb, DLCOfferDb} + +case class DLCAccountingDbs( + dlcDb: DLCDb, + offerDb: DLCOfferDb, + acceptDb: DLCAcceptDb, + closingTx: Transaction) { + require( + dlcDb.dlcId == offerDb.dlcId, + s"dlcDb.dlcId not equal to offerDb.dlcId, got dlcDb.dlcId=${dlcDb.dlcId} offerDb.dlcId=${offerDb.dlcId}") + require( + offerDb.dlcId == acceptDb.dlcId, + s"OfferDb and acceptDb not the same offerDb.dlcId=${offerDb.dlcId}, acceptDb.dlcId=${acceptDb.dlcId}") +} diff --git a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCStatusBuilder.scala b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCStatusBuilder.scala index 0c4c1d1cd1..200ee448a6 100644 --- a/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCStatusBuilder.scala +++ b/dlc-wallet/src/main/scala/org/bitcoins/dlc/wallet/util/DLCStatusBuilder.scala @@ -8,6 +8,7 @@ import org.bitcoins.core.protocol.transaction.Transaction import org.bitcoins.crypto.SchnorrDigitalSignature import org.bitcoins.dlc.wallet.accounting.AccountingUtil import org.bitcoins.dlc.wallet.models._ +import org.bitcoins.dlc.wallet.accounting.DLCAccountingDbs object DLCStatusBuilder { @@ -114,8 +115,9 @@ object DLCStatusBuilder { ) val dlcId = dlcDb.dlcId + val financials = DLCAccountingDbs(dlcDb, offerDb, acceptDb, closingTx) val accounting: DLCAccounting = - AccountingUtil.calculatePnl(dlcDb, offerDb, acceptDb, closingTx) + AccountingUtil.calculatePnl(financials) val totalCollateral = contractData.totalCollateral