diff --git a/build.gradle b/build.gradle index 561f81ac432..ae389ac9404 100644 --- a/build.gradle +++ b/build.gradle @@ -79,6 +79,7 @@ installDist.destinationDir = file('build/app') // // 3. Run `git diff` to verify that expected hashes have changed // 4. Commit the changes + dependencyVerification { verify = [ 'org.controlsfx:controlsfx:b98f1c9507c05600f80323674b33d15674926c71b0116f70085b62bdacf1e573', @@ -88,15 +89,6 @@ dependencyVerification { 'de.jensd:fontawesomefx-materialdesignfont:8f700556bbfdc4a581224d3bd6ff869b8a03f6670bd7e0fc78884bd2f31fdb64', 'de.jensd:fontawesomefx-commons:e1505a31433f1b2902478217651afc78dae5ab09670336afc46e582a1dea1e4d', 'com.googlecode.jcsv:jcsv:73ca7d715e90c8d2c2635cc284543b038245a34f70790660ed590e157b8714a2', - 'net.sf.jopt-simple:jopt-simple:6f45c00908265947c39221035250024f2caec9a15c1c8cf553ebeecee289f342', - 'network.bisq.btcd-cli4j:btcd-cli4j-daemon:300e2fb9500398f4c5cec6ae07d16d07570dbe0eb9944061eb67ab7db18fa72c', - 'network.bisq.btcd-cli4j:btcd-cli4j-core:63baeecea21940670e054bba6b041959e49fa094a496c141b9da55401ee4a8e7', - 'com.fasterxml.jackson.core:jackson-databind:fcf3c2b0c332f5f54604f7e27fa7ee502378a2cc5df6a944bbfae391872c32ff', - 'com.fasterxml.jackson.core:jackson-core:39a74610521d7fb9eb3f437bb8739bbf47f6435be12d17bf954c731a0c6352bb', - 'com.fasterxml.jackson.core:jackson-annotations:2566b3a6662afa3c6af4f5b25006cb46be2efc68f1b5116291d6998a8cdf7ed3', - 'com.github.JesusMcCloud.netlayer:tor.native:de44e782b21838d3426dbff99abbfd1cbb8e5d3f6d5e997441ff4fd8354934fa', - 'org.apache.httpcomponents:httpclient:db3d1b6c2d6a5e5ad47577ad61854e2f0e0936199b8e05eb541ed52349263135', - 'org.fxmisc.easybind:easybind:666af296dda6de68751668a62661571b5238ac6f1c07c8a204fc6f902b222aaf', 'com.google.protobuf:protobuf-java:b5e2d91812d183c9f053ffeebcbcda034d4de6679521940a19064714966c2cd4', 'com.google.code.gson:gson:2d43eb5ea9e133d2ee2405cc14f5ee08951b8361302fdd93494a3a997b508d32', 'com.googlecode.json-simple:json-simple:4e69696892b88b41c55d49ab2fdcc21eead92bf54acc588c0050596c3b75199c', @@ -108,6 +100,7 @@ dependencyVerification { 'com.google.guava:guava:36a666e3b71ae7f0f0dca23654b67e086e6c93d192f60ba5dfd5519db6c288c8', 'com.google.inject:guice:9b9df27a5b8c7864112b4137fd92b36c3f1395bfe57be42fedf2f520ead1a93e', 'network.bisq.libdohj:libdohj-core:cef7db8a2032ffbc229ccacc29b7d13e8ee1daf1cd60ee8f988cdfa60a041d8e', + 'com.github.JesusMcCloud.netlayer:tor.native:de44e782b21838d3426dbff99abbfd1cbb8e5d3f6d5e997441ff4fd8354934fa', 'com.github.JesusMcCloud.netlayer:tor:3896950c56a41985f901ff9475524ac162cba18b2d5a0ed39810b20ddaf5128a', 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:841b021d62fc007ce2883963ff9440d5393fb1f6a0604ed68cd016afcaf02967', 'com.github.MicroUtils:kotlin-logging:7dbd501cc210d721f730d480c53ee2a6e3c154ae89b07dc7dee224b9c5aca9eb', @@ -118,7 +111,20 @@ dependencyVerification { 'commons-io:commons-io:cc6a41dc3eaacc9e440a6bd0d2890b20d36b4ee408fe2d67122f328bb6e01581', 'org.apache.commons:commons-lang3:734c8356420cc8e30c795d64fd1fcd5d44ea9d90342a2cc3262c5158fbc6d98b', 'org.bouncycastle:bcprov-jdk15on:963e1ee14f808ffb99897d848ddcdb28fa91ddda867eb18d303e82728f878349', + 'net.sf.jopt-simple:jopt-simple:6f45c00908265947c39221035250024f2caec9a15c1c8cf553ebeecee289f342', + //'network.bisq.btcd-cli4j:btcd-cli4j-daemon:cf19fddccfd9c5302498dfdb29a7fead8faff04912285bfdaacad1fd7a27f646', + //'network.bisq.btcd-cli4j:btcd-cli4j-core:cc063c3a501ee6dd6f2e2e98ce59e82d720a52522acc1ec0751434dece4851e4', + 'com.fasterxml.jackson.core:jackson-databind:fcf3c2b0c332f5f54604f7e27fa7ee502378a2cc5df6a944bbfae391872c32ff', + 'com.fasterxml.jackson.core:jackson-core:39a74610521d7fb9eb3f437bb8739bbf47f6435be12d17bf954c731a0c6352bb', + 'com.fasterxml.jackson.core:jackson-annotations:2566b3a6662afa3c6af4f5b25006cb46be2efc68f1b5116291d6998a8cdf7ed3', + 'org.apache.httpcomponents:httpclient:db3d1b6c2d6a5e5ad47577ad61854e2f0e0936199b8e05eb541ed52349263135', + 'org.fxmisc.easybind:easybind:666af296dda6de68751668a62661571b5238ac6f1c07c8a204fc6f902b222aaf', 'com.google.zxing:javase:0ec23e2ec12664ddd6347c8920ad647bb3b9da290f897a88516014b56cc77eb9', + 'commons-logging:commons-logging:daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636', + 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', + 'aopalliance:aopalliance:0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08', + 'com.github.bisq-network.bitcoinj:bitcoinj-core:05c94cc68f1524ed08f5aa815f5f09d99a3aae5ab277527d7729ee74d0aece13', + 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'commons-codec:commons-codec:ad19d2601c3abf0b946b5c3a4113e226a8c1e3305e395b90013b78dd94a723ce', 'com.cedricwalter:tor-binary-macos:87790e9eade1e44eeadc81f92670f338cd47ef1b39b46a4b022c75d0cf6465fd', 'com.cedricwalter:tor-binary-linux32:814f6da3b662c96490bcb09781764dd31dfe497ea9c25c73fe61170d2a78086f', @@ -126,21 +132,16 @@ dependencyVerification { 'com.cedricwalter:tor-binary-windows:9487a735dadcadc6ede5ffad36a911c2d4a484f996be93d71094f26591b8c29e', 'com.github.ravn:jsocks:3c71600af027b2b6d4244e4ad14d98ff2352a379410daebefff5d8cd48d742a4', 'org.apache.httpcomponents:httpcore:d7f853dee87680b07293d30855b39b9eb56c1297bd16ff1cd6f19ddb8fa745fb', - 'commons-logging:commons-logging:daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636', - 'javax.inject:javax.inject:91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff', - 'aopalliance:aopalliance:0addec670fedcd3f113c5c8091d783280d23f75e3acb841b61a9cdb079376a08', - 'com.github.bisq-network.bitcoinj:bitcoinj-core:05c94cc68f1524ed08f5aa815f5f09d99a3aae5ab277527d7729ee74d0aece13', - 'com.lambdaworks:scrypt:9a82d218099fb14c10c0e86e7eefeebd8c104de920acdc47b8b4b7a686fb73b4', 'com.google.zxing:core:11aae8fd974ab25faa8208be50468eb12349cd239e93e7c797377fa13e381729', - 'com.cedricwalter:tor-binary-geoip:7fc7b5ebf80d65ec53d97dd8d3878b8d2c85dc04f3943e5e85e7ba641655492b', - 'com.github.JesusMcCloud:jtorctl:c6ef92e46074d8d26db718ce0fe4b64b8cf7b934b7377d164c5d613b4cd7b847', - 'org.apache.commons:commons-compress:a778bbd659722889245fc52a0ec2873fbbb89ec661bc1ad3dc043c0757c784c4', - 'org.tukaani:xz:a594643d73cc01928cf6ca5ce100e094ea9d73af760a5d4fb6b75fa673ecec96', 'com.madgag.spongycastle:core:8d6240b974b0aca4d3da9c7dd44d42339d8a374358aca5fc98e50a995764511f', 'net.jcip:jcip-annotations:be5805392060c71474bf6c9a67a099471274d30b83eef84bfc4e0889a4f1dcc0', 'org.bitcoinj:orchid:f836325cfa0466a011cb755c9b0fee6368487a2352eb45f4306ad9e4c18de080', 'com.squareup.okhttp:okhttp:b4c943138fcef2bcc9d2006b2250c4aabbedeafc5947ed7c0af7fd103ceb2707', - 'org.objenesis:objenesis:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d', + 'com.cedricwalter:tor-binary-geoip:7fc7b5ebf80d65ec53d97dd8d3878b8d2c85dc04f3943e5e85e7ba641655492b', + 'com.github.JesusMcCloud:jtorctl:c6ef92e46074d8d26db718ce0fe4b64b8cf7b934b7377d164c5d613b4cd7b847', + 'org.apache.commons:commons-compress:a778bbd659722889245fc52a0ec2873fbbb89ec661bc1ad3dc043c0757c784c4', + 'org.tukaani:xz:a594643d73cc01928cf6ca5ce100e094ea9d73af760a5d4fb6b75fa673ecec96', 'com.squareup.okio:okio:114bdc1f47338a68bcbc95abf2f5cdc72beeec91812f2fcd7b521c1937876266', + 'org.objenesis:objenesis:5e168368fbc250af3c79aa5fef0c3467a2d64e5a7bd74005f25d8399aeb0708d', ] } diff --git a/doc/rpc.md b/doc/rpc.md index 27219a2eed8..27faa81d19f 100644 --- a/doc/rpc.md +++ b/doc/rpc.md @@ -32,5 +32,8 @@ If you use RegTest in development environment you need to create the genesis tra Create one Bitcoin transaction from Bitcoin Core to one or 2 Bisq instances using the BSQ receive addresses from those apps (1 tx with 2 or more outputs to the Bisq app). If you copy the BSQ address and use that in Bitcoin Core you need to remove the "B" at the beginning. This is only for protection to mix up BTC and BSQ addresses but without the B it is a native Bitcoin address. Create one block with the debug command line inside Bitcoin Core (generate 1). Look up the block height in the info screen in the debug window. -Set the block height and transaction ID at BsqBlockChain.BTC_REG_TEST_GENESIS_TX_ID and BsqBlockChain.BTC_REG_TEST_GENESIS_BLOCK_HEIGHT. +Set the block height and transaction ID at with options genesisBlockHeight and genesisTxId. Restart the Bisq apps. After that the app will recognize the received Bitcoin as BSQ. + +Here are example options for regtest mode: +--daoActivated=true --genesisBlockHeight=111 --genesisTxId=aa92a8d56be3aaafc6b1a8d248ae67c221d78a31de8867a9564e7ae24340b495 --useDevPrivilegeKeys=true --useDevMode=true--baseCurrencyNetwork=BTC_REGTEST --useLocalhostForP2P=true --nodePort=3612 --appName=bisq-BTC_REGTEST_Bob_dao --fullDaoNode=true --rpcUser=bisq --rpcPassword=bisqPW --rpcPort=18332 --rpcBlockNotificationPort=5159 diff --git a/src/main/java/bisq/desktop/DesktopModule.java b/src/main/java/bisq/desktop/DesktopModule.java index a59cedc4b3d..aeef737f3b5 100644 --- a/src/main/java/bisq/desktop/DesktopModule.java +++ b/src/main/java/bisq/desktop/DesktopModule.java @@ -23,6 +23,7 @@ import bisq.desktop.common.view.ViewLoader; import bisq.desktop.common.view.guice.InjectorViewFactory; import bisq.desktop.main.MarketPricePresentation; +import bisq.desktop.main.dao.bonding.BondingViewUtils; import bisq.desktop.main.funds.transactions.DisplayedTransactionsFactory; import bisq.desktop.main.funds.transactions.TradableRepository; import bisq.desktop.main.funds.transactions.TransactionAwareTradableFactory; @@ -79,6 +80,8 @@ protected void configure() { bind(TransactionAwareTradableFactory.class).in(Singleton.class); bind(DisplayedTransactionsFactory.class).in(Singleton.class); + bind(BondingViewUtils.class).in(Singleton.class); + bindConstant().annotatedWith(Names.named(AppOptionKeys.APP_NAME_KEY)).to(environment.getRequiredProperty(AppOptionKeys.APP_NAME_KEY)); } } diff --git a/src/main/java/bisq/desktop/app/BisqApp.java b/src/main/java/bisq/desktop/app/BisqApp.java index 3eca300e7df..f1380916155 100644 --- a/src/main/java/bisq/desktop/app/BisqApp.java +++ b/src/main/java/bisq/desktop/app/BisqApp.java @@ -34,9 +34,7 @@ import bisq.core.alert.AlertManager; import bisq.core.app.AppOptionKeys; -import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.WalletService; import bisq.core.btc.wallet.WalletsManager; import bisq.core.filter.FilterManager; import bisq.core.locale.Res; @@ -249,7 +247,9 @@ private void addSceneKeyEventHandler(Scene scene, Injector injector) { stop(); } else { if (Utilities.isAltOrCtrlPressed(KeyCode.E, keyEvent)) { - showEmptyWalletPopup(injector.getInstance(BtcWalletService.class), injector); + showBtcEmergencyWalletPopup(injector); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.B, keyEvent)) { + showBsqEmergencyWalletPopup(injector); } else if (Utilities.isAltOrCtrlPressed(KeyCode.M, keyEvent)) { showSendAlertMessagePopup(injector); } else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) { @@ -267,10 +267,7 @@ private void addSceneKeyEventHandler(Scene scene, Injector injector) { new Popup<>().warning(Res.get("popup.warning.walletNotInitialized")).show(); } else if (DevEnv.isDevMode()) { // dev ode only - if (Utilities.isAltOrCtrlPressed(KeyCode.B, keyEvent)) { - // BSQ empty wallet not public yet - showEmptyWalletPopup(injector.getInstance(BsqWalletService.class), injector); - } else if (Utilities.isAltOrCtrlPressed(KeyCode.P, keyEvent)) { + if (Utilities.isAltOrCtrlPressed(KeyCode.P, keyEvent)) { showFPSWindow(scene); } else if (Utilities.isAltOrCtrlPressed(KeyCode.Z, keyEvent)) { showDebugWindow(scene, injector); @@ -298,9 +295,15 @@ private void showFilterPopup(Injector injector) { .show(); } - private void showEmptyWalletPopup(WalletService walletService, Injector injector) { + private void showBtcEmergencyWalletPopup(Injector injector) { EmptyWalletWindow emptyWalletWindow = injector.getInstance(EmptyWalletWindow.class); - emptyWalletWindow.setWalletService(walletService); + emptyWalletWindow.setIsBtc(true); + emptyWalletWindow.show(); + } + + private void showBsqEmergencyWalletPopup(Injector injector) { + EmptyWalletWindow emptyWalletWindow = injector.getInstance(EmptyWalletWindow.class); + emptyWalletWindow.setIsBtc(false); emptyWalletWindow.show(); } diff --git a/src/main/java/bisq/desktop/app/BisqAppMain.java b/src/main/java/bisq/desktop/app/BisqAppMain.java index 348e8f560c4..1d519806758 100644 --- a/src/main/java/bisq/desktop/app/BisqAppMain.java +++ b/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -39,6 +39,11 @@ public class BisqAppMain extends BisqExecutable { private BisqApp application; + /* @Nullable + private BisqHttpApiServer bisqHttpApiServer;*/ + /* @Nullable + private BisqGrpcServer bisqGrpcServer; + */ public static void main(String[] args) throws Exception { if (BisqExecutable.setupInitialOptionParser(args)) { // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. @@ -119,5 +124,23 @@ protected void startApplication() { @Override protected void onApplicationStarted() { super.onApplicationStarted(); + + /* if (runWithHttpApi()) { + final BisqFacade bisqFacade = injector.getInstance(BisqFacade.class); + bisqHttpApiServer = new BisqHttpApiServer(bisqFacade); + }*/ + /* + if (runWithGrpcApi()) { + final BisqFacade bisqFacade = injector.getInstance(BisqFacade.class); + bisqGrpcServer = new BisqGrpcServer(bisqFacade); + }*/ + } + + private boolean runWithHttpApi() { + return bisqEnvironment.getDesktopWithHttpApi().toLowerCase().equals("true"); + } + + private boolean runWithGrpcApi() { + return bisqEnvironment.getDesktopWithGrpcApi().toLowerCase().equals("true"); } } diff --git a/src/main/java/bisq/desktop/bisq.css b/src/main/java/bisq/desktop/bisq.css index 5f2aedf9c25..05b72669796 100644 --- a/src/main/java/bisq/desktop/bisq.css +++ b/src/main/java/bisq/desktop/bisq.css @@ -327,57 +327,6 @@ bg color of non edit textFields: fafafa -fx-text-fill: -bs-red-soft; } -.dao-tx-type-fee-icon, -.dao-tx-type-fee-icon:hover { - -fx-text-fill: #5ab9ea; - -fx-cursor: hand; -} - -.dao-tx-type-unverified-icon, -.dao-tx-type-unverified-icon:hover { - -fx-text-fill: -bs-yellow; -} - -.dao-tx-type-invalid-icon, -.dao-tx-type-invalid-icon:hover { - -fx-text-fill: -bs-error-red; -} - -.dao-tx-type-default-icon, -.dao-tx-type-default-icon:hover { - -fx-text-fill: -bs-grey; -} - -.dao-tx-type-genesis-icon, -.dao-tx-type-genesis-icon:hover { - -fx-text-fill: -fx-accent; -} - -.dao-tx-type-received-funds-icon, -.dao-tx-type-received-funds-icon:hover { - -fx-text-fill: -bs-green-soft; -} - -.dao-tx-type-sent-funds-icon, -.dao-tx-type-sent-funds-icon:hover { - -fx-text-fill: -bs-red-soft; -} - -.dao-tx-type-vote-icon, -.dao-tx-type-vote-icon:hover { - -fx-text-fill: -bs-bg-blue1; -} - -.dao-tx-type-vote-reveal-icon, -.dao-tx-type-vote-reveal-icon:hover { - -fx-text-fill: -bs-bg-green2; -} - -.dao-tx-type-issuance-icon, -.dao-tx-type-issuance-icon:hover { - -fx-text-fill: -bs-green; -} - .version { -fx-text-fill: black; -fx-underline: false; @@ -1327,17 +1276,106 @@ textfield */ -fx-font-size: 0.846em; } - /******************************************************************************************************************** * * * DAO * * * ********************************************************************************************************************/ +.dao-tx-type-trade-fee-icon, +.dao-tx-type-trade-fee-icon:hover { + -fx-text-fill: #689f43; +} + +.dao-tx-type-unverified-icon, +.dao-tx-type-unverified-icon:hover { + -fx-text-fill: #ffac00; +} + +.dao-tx-type-invalid-icon, +.dao-tx-type-invalid-icon:hover { + -fx-text-fill: #ff4500; +} + +.dao-tx-type-self-icon, +.dao-tx-type-self-icon:hover { + -fx-text-fill: #818181; +} + +.dao-tx-type-proposal-fee-icon, +.dao-tx-type-proposal-fee-icon:hover { + -fx-text-fill: #6c8b3b; +} + +.dao-tx-type-genesis-icon, +.dao-tx-type-genesis-icon:hover { + -fx-text-fill: -fx-accent; +} + +.dao-tx-type-received-funds-icon, +.dao-tx-type-received-funds-icon:hover { + -fx-text-fill: -bs-green-soft; +} + +.dao-tx-type-sent-funds-icon, +.dao-tx-type-sent-funds-icon:hover { + -fx-text-fill: -bs-red-soft; +} + +.dao-tx-type-vote-icon, +.dao-tx-type-vote-icon:hover { + -fx-text-fill: #0a4576; +} + +.dao-tx-type-vote-reveal-icon, +.dao-tx-type-vote-reveal-icon:hover { + -fx-text-fill: #4AC5FF; +} + +.dao-tx-type-issuance-icon, +.dao-tx-type-issuance-icon:hover { + -fx-text-fill: #04a908; +} + +.dao-tx-type-lockup-icon, +.dao-tx-type-lockup-icon:hover { + -fx-text-fill: #203e7f; +} + +.dao-tx-type-unlock-icon, +.dao-tx-type-unlock-icon:hover { + -fx-text-fill: #438e6c; +} + +.dao-accepted-icon { + -fx-text-fill: -bs-green; +} + +.dao-rejected-icon { + -fx-text-fill: -bs-error-red; +} + +.dao-ignored-icon { + -fx-text-fill: -bs-medium-grey; +} + .compensation-root { -fx-background-insets: 0, 0 0 0 0 } +.info-icon { + -fx-text-fill: -fx-accent; +} + +.info-icon-button { + -fx-cursor: hand; + -fx-background-color: transparent; +} + +.dao-remove-proposal-icon { + -fx-text-fill: -fx-accent; +} + /******************************************************************************************************************** * * * Notifications * diff --git a/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java b/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java index 2a6a1963b1b..be5e5004f33 100644 --- a/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java +++ b/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java @@ -26,7 +26,11 @@ import javafx.geometry.Insets; +import lombok.Getter; + public class HyperlinkWithIcon extends Hyperlink { + @Getter + private final Label icon; public HyperlinkWithIcon(String text) { this(text, AwesomeIcon.INFO_SIGN); @@ -36,7 +40,7 @@ public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) { super(text); - Label icon = new Label(); + icon = new Label(); AwesomeDude.setIcon(icon, awesomeIcon); icon.setMinWidth(20); icon.setOpacity(0.7); diff --git a/src/main/java/bisq/desktop/components/MenuItem.java b/src/main/java/bisq/desktop/components/MenuItem.java index 353df7e9635..1cb87270aee 100644 --- a/src/main/java/bisq/desktop/components/MenuItem.java +++ b/src/main/java/bisq/desktop/components/MenuItem.java @@ -57,7 +57,7 @@ public MenuItem(Navigation navigation, this.baseNavPath = baseNavPath; setToggleGroup(toggleGroup); - setText(title); + setLabelText(title); setId("account-settings-item-background-active"); setPrefHeight(40); setPrefWidth(240); @@ -109,9 +109,14 @@ private Class[] getNavPathClasses() { list.toArray(array); return array; } + public void deactivate() { setOnAction(null); selectedProperty().removeListener(selectedPropertyChangeListener); disableProperty().removeListener(disablePropertyChangeListener); } + + public void setLabelText(String value) { + setText(value); + } } diff --git a/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java b/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java index e68b155d851..5b17bdbbfb3 100644 --- a/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java +++ b/src/main/java/bisq/desktop/components/SeparatedPhaseBars.java @@ -17,18 +17,14 @@ package bisq.desktop.components; -import bisq.core.dao.vote.PeriodService; +import bisq.core.dao.state.period.DaoPhase; import bisq.core.locale.Res; -import bisq.common.UserThread; - import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; -import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.beans.property.DoubleProperty; @@ -37,120 +33,115 @@ import javafx.beans.property.SimpleIntegerProperty; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @Slf4j -public class SeparatedPhaseBars extends HBox { - +public class SeparatedPhaseBars extends VBox { + private double labelMinWidth = 150; + private double breakMinWidth = 20; private int totalDuration; private List items; - private VBox vBoxLabels; public SeparatedPhaseBars(List items) { this.items = items; - setSpacing(0); + setSpacing(10); - addLabels(); + HBox titlesBars = new HBox(); + titlesBars.setSpacing(5); + getChildren().add(titlesBars); - items.stream().forEach(item -> { - Label titleLabel = new Label(Res.get("dao.phase.short." + item.phase)); - item.setTitleLabel(titleLabel); + HBox progressBars = new HBox(); + progressBars.setSpacing(5); + getChildren().add(progressBars); - Label startLabel = new Label(); - item.startValueProperty.addListener((observable, oldValue, newValue) -> { - startLabel.setText(String.valueOf((int) newValue)); - }); - startLabel.setVisible(item.isShowBlocks()); - AnchorPane startLabelPane = new AnchorPane(); - AnchorPane.setLeftAnchor(startLabel, 0d); - startLabelPane.getChildren().add(startLabel); - - Label endLabel = new Label(); - item.endValueProperty.addListener((observable, oldValue, newValue) -> { - endLabel.setText(String.valueOf((int) newValue)); - }); - endLabel.setVisible(item.isShowBlocks()); - AnchorPane endLabelPane = new AnchorPane(); - AnchorPane.setRightAnchor(endLabel, 0d); - endLabelPane.getChildren().add(endLabel); + items.forEach(item -> { + String text = item.phase.name().startsWith("BREAK") ? "" : Res.get("dao.phase.separatedPhaseBar." + item.phase); + Label titleLabel = new Label(text); + titleLabel.setEllipsisString(""); + titleLabel.setAlignment(Pos.CENTER); + item.setTitleLabel(titleLabel); + titlesBars.getChildren().addAll(titleLabel); ProgressBar progressBar = new ProgressBar(); progressBar.setMinHeight(9); progressBar.setMaxHeight(9); - progressBar.setMaxWidth(Double.MAX_VALUE); progressBar.setStyle("-fx-accent: -bs-green;"); progressBar.progressProperty().bind(item.progressProperty); progressBar.setOpacity(item.isShowBlocks() ? 1 : 0.25); - - VBox vBox = new VBox(); - vBox.setSpacing(5); - vBox.getChildren().addAll(titleLabel, progressBar, startLabelPane, endLabelPane); - vBox.setAlignment(Pos.CENTER); - getChildren().add(vBox); - item.setVBox(vBox); + progressBars.getChildren().add(progressBar); + item.setProgressBar(progressBar); }); widthProperty().addListener((observable, oldValue, newValue) -> { - adjustWidth((double) newValue); + updateWidth((double) newValue); }); - UserThread.execute(() -> adjustWidth(getWidth())); } - private void addLabels() { - Label titleLabel = new Label(Res.get("dao.proposal.active.phase")); - - Label startLabel = new Label(Res.get("dao.proposal.active.startBlock")); - AnchorPane startLabelPane = new AnchorPane(); - AnchorPane.setLeftAnchor(startLabel, 0d); - startLabelPane.getChildren().add(startLabel); - - Label endLabel = new Label(Res.get("dao.proposal.active.endBlock")); - AnchorPane endLabelPane = new AnchorPane(); - AnchorPane.setRightAnchor(endLabel, 0d); - endLabelPane.getChildren().add(endLabel); - - ProgressBar progressBar = new ProgressBar(); - progressBar.setMinHeight(9); - progressBar.setMaxHeight(9); - progressBar.setVisible(false); - - vBoxLabels = new VBox(); - vBoxLabels.setSpacing(5); - vBoxLabels.getChildren().addAll(titleLabel, progressBar, startLabelPane, endLabelPane); - vBoxLabels.setAlignment(Pos.CENTER); - vBoxLabels.setPadding(new Insets(0, 10, 0, 0)); - getChildren().add(vBoxLabels); + public void updateWidth() { + updateWidth(getWidth()); } - private void adjustWidth(double availableWidth) { - totalDuration = items.stream().mapToInt(SeparatedPhaseBarsItem::getDuration).sum(); - availableWidth -= vBoxLabels.getWidth(); - if (availableWidth > 0 && totalDuration > 0) { - final double finalAvailableWidth = availableWidth; - items.stream().forEach(item -> { - final double width = (double) item.duration / (double) totalDuration * finalAvailableWidth; - item.getVBox().setPrefWidth(width); - }); + private void updateWidth(double availableWidth) { + if (availableWidth > 0) { + totalDuration = items.stream().mapToInt(SeparatedPhaseBarsItem::getDuration).sum(); + if (totalDuration > 0) { + // We want to have a min. width for the breaks and for the phases which are important to the user but + // quite short (blind vote, vote reveal, result). If we display it correctly most of the space is + // consumed by the proposal phase. We we apply a min and max width and adjust the available width so + // we have all phases displayed so that the text is fully readable. The proposal phase is shorter as + // it would be with correct display but we take that into account to have a better overall overview. + final double finalAvailableWidth = availableWidth; + AtomicReference adjustedAvailableWidth = new AtomicReference<>(availableWidth); + items.forEach(item -> { + double calculatedWidth = (double) item.duration / (double) totalDuration * finalAvailableWidth; + double minWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : labelMinWidth; + double maxWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : calculatedWidth; + if (calculatedWidth < minWidth) { + double missing = minWidth - calculatedWidth; + adjustedAvailableWidth.set(adjustedAvailableWidth.get() - missing); + } else if (calculatedWidth > maxWidth) { + double remaining = calculatedWidth - maxWidth; + adjustedAvailableWidth.set(adjustedAvailableWidth.get() + remaining); + } + }); + + items.forEach(item -> { + double calculatedWidth = (double) item.duration / (double) totalDuration * adjustedAvailableWidth.get(); + double minWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : labelMinWidth; + double maxWidth = item.phase.name().startsWith("BREAK") ? breakMinWidth : calculatedWidth; + double width = calculatedWidth; + if (calculatedWidth < minWidth) { + width = minWidth; + } else if (calculatedWidth > maxWidth) { + width = maxWidth; + } + item.getTitleLabel().setPrefWidth(width); + item.getProgressBar().setPrefWidth(width); + }); + } } } @Getter public static class SeparatedPhaseBarsItem { - private final PeriodService.Phase phase; + private final DaoPhase.Phase phase; private final boolean showBlocks; - private final IntegerProperty startValueProperty = new SimpleIntegerProperty(); - private final IntegerProperty endValueProperty = new SimpleIntegerProperty(); + private final IntegerProperty startBlockProperty = new SimpleIntegerProperty(); + private final IntegerProperty lastBlockProperty = new SimpleIntegerProperty(); private final DoubleProperty progressProperty = new SimpleDoubleProperty(); private int duration; @Setter - private javafx.scene.layout.VBox VBox; + private javafx.scene.layout.VBox progressVBox; @Setter private Label titleLabel; + @Setter + private ProgressBar progressBar; - public SeparatedPhaseBarsItem(PeriodService.Phase phase, boolean showBlocks) { + public SeparatedPhaseBarsItem(DaoPhase.Phase phase, boolean showBlocks) { this.phase = phase; this.showBlocks = showBlocks; } @@ -163,10 +154,10 @@ public void setActive() { titleLabel.setStyle("-fx-text-fill: -fx-accent;"); } - public void setStartAndEnd(int startBlock, int endBlock) { - startValueProperty.set(startBlock); - endValueProperty.set(endBlock); - duration = endValueProperty.get() - startValueProperty.get() + 1; + public void setPeriodRange(int firstBlock, int lastBlock, int duration) { + startBlockProperty.set(firstBlock); + lastBlockProperty.set(lastBlock); + this.duration = duration; } } } diff --git a/src/main/java/bisq/desktop/components/TxConfidenceListItem.java b/src/main/java/bisq/desktop/components/TxConfidenceListItem.java new file mode 100644 index 00000000000..b47e8b67227 --- /dev/null +++ b/src/main/java/bisq/desktop/components/TxConfidenceListItem.java @@ -0,0 +1,75 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components; + +import bisq.desktop.components.indicator.TxConfidenceIndicator; +import bisq.desktop.util.GUIUtil; + +import bisq.core.btc.listeners.TxConfidenceListener; +import bisq.core.btc.wallet.BsqWalletService; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import javafx.scene.control.Tooltip; + +import lombok.Data; + + +@Data +public class TxConfidenceListItem { + protected final BsqWalletService bsqWalletService; + protected final String txId; + protected int confirmations = 0; + protected TxConfidenceIndicator txConfidenceIndicator; + protected TxConfidenceListener txConfidenceListener; + + protected TxConfidenceListItem(Transaction transaction, + BsqWalletService bsqWalletService) { + this.bsqWalletService = bsqWalletService; + + txId = transaction.getHashAsString(); + txConfidenceIndicator = new TxConfidenceIndicator(); + txConfidenceIndicator.setId("funds-confidence"); + Tooltip tooltip = new Tooltip(); + txConfidenceIndicator.setProgress(0); + txConfidenceIndicator.setPrefSize(24, 24); + txConfidenceIndicator.setTooltip(tooltip); + + txConfidenceListener = new TxConfidenceListener(txId) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + updateConfidence(confidence, tooltip); + } + }; + bsqWalletService.addTxConfidenceListener(txConfidenceListener); + updateConfidence(bsqWalletService.getConfidenceForTxId(txId), tooltip); + } + + private void updateConfidence(TransactionConfidence confidence, Tooltip tooltip) { + if (confidence != null) { + GUIUtil.updateConfidence(confidence, tooltip, txConfidenceIndicator); + confirmations = confidence.getDepthInBlocks(); + } + } + + public void cleanup() { + bsqWalletService.removeTxConfidenceListener(txConfidenceListener); + } +} + diff --git a/src/main/java/bisq/desktop/components/TxIdTextField.java b/src/main/java/bisq/desktop/components/TxIdTextField.java index b847d67fb7b..851f2cfce60 100644 --- a/src/main/java/bisq/desktop/components/TxIdTextField.java +++ b/src/main/java/bisq/desktop/components/TxIdTextField.java @@ -23,6 +23,7 @@ import bisq.core.btc.listeners.TxConfidenceListener; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.Res; +import bisq.core.user.BlockChainExplorer; import bisq.core.user.Preferences; import bisq.common.util.Utilities; @@ -37,12 +38,9 @@ import javafx.scene.control.Tooltip; import javafx.scene.layout.AnchorPane; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.Setter; public class TxIdTextField extends AnchorPane { - private static final Logger log = LoggerFactory.getLogger(TxIdTextField.class); - private static Preferences preferences; public static void setPreferences(Preferences preferences) { @@ -61,6 +59,8 @@ public static void setWalletService(BtcWalletService walletService) { private final Label copyIcon; private final Label blockExplorerIcon; private TxConfidenceListener txConfidenceListener; + @Setter + private boolean isBsq; /////////////////////////////////////////////////////////////////////////////////////////// @@ -70,12 +70,13 @@ public static void setWalletService(BtcWalletService walletService) { public TxIdTextField() { txConfidenceIndicator = new TxConfidenceIndicator(); txConfidenceIndicator.setFocusTraversable(false); - txConfidenceIndicator.setPrefSize(24, 24); + txConfidenceIndicator.setMaxSize(20, 20); txConfidenceIndicator.setId("funds-confidence"); txConfidenceIndicator.setLayoutY(1); txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setVisible(false); AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); + AnchorPane.setTopAnchor(txConfidenceIndicator, 3.0); progressIndicatorTooltip = new Tooltip("-"); txConfidenceIndicator.setTooltip(progressIndicatorTooltip); @@ -106,23 +107,23 @@ public TxIdTextField() { getChildren().addAll(textField, copyIcon, blockExplorerIcon, txConfidenceIndicator); } - public void setup(String txID) { + public void setup(String txId) { if (txConfidenceListener != null) walletService.removeTxConfidenceListener(txConfidenceListener); - txConfidenceListener = new TxConfidenceListener(txID) { + txConfidenceListener = new TxConfidenceListener(txId) { @Override public void onTransactionConfidenceChanged(TransactionConfidence confidence) { updateConfidence(confidence); } }; walletService.addTxConfidenceListener(txConfidenceListener); - updateConfidence(walletService.getConfidenceForTxId(txID)); + updateConfidence(walletService.getConfidenceForTxId(txId)); - textField.setText(txID); - textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txID)); - blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txID)); - copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txID)); + textField.setText(txId); + textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); + blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); + copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId)); } public void cleanup() { @@ -135,14 +136,17 @@ public void cleanup() { textField.setText(""); } - /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// - private void openBlockExplorer(String txID) { - if (preferences != null) - GUIUtil.openWebPage(preferences.getBlockChainExplorer().txUrl + txID); + private void openBlockExplorer(String txId) { + if (preferences != null) { + BlockChainExplorer blockChainExplorer = isBsq ? + preferences.getBsqBlockChainExplorer() : + preferences.getBlockChainExplorer(); + GUIUtil.openWebPage(blockChainExplorer.txUrl + txId); + } } private void updateConfidence(TransactionConfidence confidence) { diff --git a/src/main/java/bisq/desktop/main/MainViewModel.java b/src/main/java/bisq/desktop/main/MainViewModel.java index bb5f03032e4..f54415c432b 100644 --- a/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/src/main/java/bisq/desktop/main/MainViewModel.java @@ -36,11 +36,10 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsSetup; import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.FiatCurrency; import bisq.core.locale.Res; import bisq.core.payment.AccountAgeWitnessService; +import bisq.core.payment.AliPayAccount; import bisq.core.payment.CryptoCurrencyAccount; -import bisq.core.payment.PerfectMoneyAccount; import bisq.core.presentation.BalancePresentation; import bisq.core.presentation.DisputePresentation; import bisq.core.presentation.TradePresentation; @@ -425,20 +424,19 @@ private void showSecondPopupIfResyncSPVRequested(Popup firstPopup) { private void setupDevDummyPaymentAccounts() { if (user.getPaymentAccounts() != null && user.getPaymentAccounts().isEmpty()) { - PerfectMoneyAccount perfectMoneyAccount = new PerfectMoneyAccount(); - perfectMoneyAccount.init(); - perfectMoneyAccount.setAccountNr("dummy_" + new Random().nextInt(100)); - perfectMoneyAccount.setAccountName("PerfectMoney dummy");// Don't translate only for dev - perfectMoneyAccount.setSelectedTradeCurrency(new FiatCurrency("USD")); - user.addPaymentAccount(perfectMoneyAccount); + AliPayAccount aliPayAccount = new AliPayAccount(); + aliPayAccount.init(); + aliPayAccount.setAccountNr("dummy_" + new Random().nextInt(100)); + aliPayAccount.setAccountName("AliPayAccount dummy");// Don't translate only for dev + user.addPaymentAccount(aliPayAccount); if (p2PService.isBootstrapped()) { - accountAgeWitnessService.publishMyAccountAgeWitness(perfectMoneyAccount.getPaymentAccountPayload()); + accountAgeWitnessService.publishMyAccountAgeWitness(aliPayAccount.getPaymentAccountPayload()); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onUpdatedDataReceived() { - accountAgeWitnessService.publishMyAccountAgeWitness(perfectMoneyAccount.getPaymentAccountPayload()); + accountAgeWitnessService.publishMyAccountAgeWitness(aliPayAccount.getPaymentAccountPayload()); } }); } diff --git a/src/main/java/bisq/desktop/main/account/AccountView.java b/src/main/java/bisq/desktop/main/account/AccountView.java index 1a78e1e1ca5..3ee932e3e48 100644 --- a/src/main/java/bisq/desktop/main/account/AccountView.java +++ b/src/main/java/bisq/desktop/main/account/AccountView.java @@ -60,7 +60,7 @@ public class AccountView extends ActivatableView { private final ViewLoader viewLoader; private final Navigation navigation; private Tab selectedTab; - Tab arbitratorRegistrationTab; + private Tab arbitratorRegistrationTab; private ArbitratorRegistrationView arbitratorRegistrationView; private AccountSettingsView accountSettingsView; private Scene scene; @@ -68,7 +68,6 @@ public class AccountView extends ActivatableView { @Inject private AccountView(CachingViewLoader viewLoader, Navigation navigation) { - super(); this.viewLoader = viewLoader; this.navigation = navigation; } diff --git a/src/main/java/bisq/desktop/main/account/AccountViewModel.java b/src/main/java/bisq/desktop/main/account/AccountViewModel.java deleted file mode 100644 index 84af84cde7e..00000000000 --- a/src/main/java/bisq/desktop/main/account/AccountViewModel.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.desktop.main.account; - -import bisq.desktop.common.model.ViewModel; - -import com.google.inject.Inject; - -class AccountViewModel implements ViewModel { - - @Inject - public AccountViewModel() { - } -} diff --git a/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java b/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java index 1ffa41fddf0..035cfde20e6 100644 --- a/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java +++ b/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java @@ -136,12 +136,12 @@ void onRemoveLanguage(String code) { } boolean setPrivKeyAndCheckPubKey(String privKeyString) { - ECKey _registrationKey = arbitratorManager.getRegistrationKey(privKeyString); - if (_registrationKey != null) { - String _registrationPubKeyAsHex = Utils.HEX.encode(_registrationKey.getPubKey()); + ECKey registrationKey = arbitratorManager.getRegistrationKey(privKeyString); + if (registrationKey != null) { + String _registrationPubKeyAsHex = Utils.HEX.encode(registrationKey.getPubKey()); boolean isKeyValid = arbitratorManager.isPublicKeyInList(_registrationPubKeyAsHex); if (isKeyValid) { - registrationKey = _registrationKey; + this.registrationKey = registrationKey; registrationPubKeyAsHex.set(_registrationPubKeyAsHex); } updateDisableStates(); diff --git a/src/main/java/bisq/desktop/main/dao/DaoView.fxml b/src/main/java/bisq/desktop/main/dao/DaoView.fxml index 90bd84c653d..5c75ca6afbd 100644 --- a/src/main/java/bisq/desktop/main/dao/DaoView.fxml +++ b/src/main/java/bisq/desktop/main/dao/DaoView.fxml @@ -17,13 +17,10 @@ ~ along with Bisq. If not, see . --> - - - diff --git a/src/main/java/bisq/desktop/main/dao/DaoView.java b/src/main/java/bisq/desktop/main/dao/DaoView.java index 6ae456db495..fcc380da237 100644 --- a/src/main/java/bisq/desktop/main/dao/DaoView.java +++ b/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -25,8 +25,8 @@ import bisq.desktop.common.view.View; import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; -import bisq.desktop.main.dao.proposal.ProposalView; -import bisq.desktop.main.dao.voting.VotingView; +import bisq.desktop.main.dao.bonding.BondingView; +import bisq.desktop.main.dao.governance.GovernanceView; import bisq.desktop.main.dao.wallet.BsqWalletView; import bisq.desktop.main.dao.wallet.dashboard.BsqDashboardView; @@ -48,7 +48,12 @@ public class DaoView extends ActivatableViewAndModel { @FXML - Tab bsqWalletTab, compensationTab, votingTab; + private + Tab bsqWalletTab; + @FXML + private Tab proposalsTab; + @FXML + private Tab bondingTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; @@ -58,7 +63,6 @@ public class DaoView extends ActivatableViewAndModel { private Tab selectedTab; private BsqWalletView bsqWalletView; - @Inject private DaoView(CachingViewLoader viewLoader, Navigation navigation) { this.viewLoader = viewLoader; @@ -67,22 +71,24 @@ private DaoView(CachingViewLoader viewLoader, Navigation navigation) { @Override public void initialize() { - compensationTab = new Tab(Res.get("dao.tab.proposals")); - votingTab = new Tab(Res.get("dao.tab.voting")); - compensationTab.setClosable(false); - votingTab.setClosable(false); - root.getTabs().addAll(compensationTab, votingTab); + bsqWalletTab = new Tab(Res.get("dao.tab.bsqWallet")); + proposalsTab = new Tab(Res.get("dao.tab.proposals")); + bondingTab = new Tab(Res.get("dao.tab.bonding")); + + bsqWalletTab.setClosable(false); + proposalsTab.setClosable(false); + bondingTab.setClosable(false); + + root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab); if (!BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq() || !DevEnv.isDaoPhase2Activated()) { - votingTab.setDisable(true); - compensationTab.setDisable(true); + bondingTab.setDisable(true); + proposalsTab.setDisable(true); } - bsqWalletTab.setText(Res.get("dao.tab.bsqWallet")); - navigationListener = viewPath -> { if (viewPath.size() == 3 && viewPath.indexOf(DaoView.class) == 1) { - if (compensationTab == null && viewPath.get(2).equals(BsqWalletView.class)) + if (proposalsTab == null && viewPath.get(2).equals(BsqWalletView.class)) navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class); else loadView(viewPath.tip()); @@ -96,10 +102,10 @@ public void initialize() { navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, BsqDashboardView.class); else navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, selectedViewClass); - } else if (newValue == compensationTab) { - navigation.navigateTo(MainView.class, DaoView.class, ProposalView.class); - } else if (newValue == votingTab) { - navigation.navigateTo(MainView.class, DaoView.class, VotingView.class); + } else if (newValue == proposalsTab) { + navigation.navigateTo(MainView.class, DaoView.class, GovernanceView.class); + } else if (newValue == bondingTab) { + navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); } }; } @@ -113,10 +119,10 @@ protected void activate() { Tab selectedItem = root.getSelectionModel().getSelectedItem(); if (selectedItem == bsqWalletTab) navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class); - else if (selectedItem == compensationTab) - navigation.navigateTo(MainView.class, DaoView.class, ProposalView.class); - else if (selectedItem == votingTab) - navigation.navigateTo(MainView.class, DaoView.class, VotingView.class); + else if (selectedItem == proposalsTab) + navigation.navigateTo(MainView.class, DaoView.class, GovernanceView.class); + else if (selectedItem == bondingTab) + navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); } } @@ -131,10 +137,10 @@ private void loadView(Class viewClass) { if (view instanceof BsqWalletView) { selectedTab = bsqWalletTab; bsqWalletView = (BsqWalletView) view; - } else if (view instanceof ProposalView) { - selectedTab = compensationTab; - } else if (view instanceof VotingView) { - selectedTab = votingTab; + } else if (view instanceof GovernanceView) { + selectedTab = proposalsTab; + } else if (view instanceof BondingView) { + selectedTab = bondingTab; } selectedTab.setContent(view.getRoot()); diff --git a/src/main/java/bisq/desktop/main/dao/proposal/ProposalView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml similarity index 62% rename from src/main/java/bisq/desktop/main/dao/proposal/ProposalView.fxml rename to src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml index 35c7261a5ae..a8ac7a57741 100644 --- a/src/main/java/bisq/desktop/main/dao/proposal/ProposalView.fxml +++ b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml @@ -17,16 +17,21 @@ ~ along with Bisq. If not, see . --> - + - - - + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java new file mode 100644 index 00000000000..973464a6920 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java @@ -0,0 +1,137 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.bonding.dashboard.BondingDashboardView; +import bisq.desktop.main.dao.bonding.lockup.LockupView; +import bisq.desktop.main.dao.bonding.roles.BondedRolesView; +import bisq.desktop.main.dao.bonding.unlock.UnlockView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class BondingView extends ActivatableViewAndModel { + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem dashboard, bondedRoles, lockupBSQ, unlockBSQ; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + + @Inject + private BondingView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + listener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(bisq.desktop.main.dao.bonding.BondingView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + ToggleGroup toggleGroup = new ToggleGroup(); + final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, bisq.desktop.main.dao.bonding.BondingView.class); + dashboard = new MenuItem(navigation, toggleGroup, Res.get("shared.dashboard"), + BondingDashboardView.class, AwesomeIcon.DASHBOARD, baseNavPath); + bondedRoles = new MenuItem(navigation, toggleGroup, Res.get("dao.bonding.menuItem.bondedRoles"), + BondedRolesView.class, AwesomeIcon.SHIELD, baseNavPath); + lockupBSQ = new MenuItem(navigation, toggleGroup, Res.get("dao.bonding.menuItem.lockupBSQ"), + LockupView.class, AwesomeIcon.LOCK, baseNavPath); + unlockBSQ = new MenuItem(navigation, toggleGroup, Res.get("dao.bonding.menuItem.unlockBSQ"), + UnlockView.class, AwesomeIcon.UNLOCK, baseNavPath); + + leftVBox.getChildren().addAll(dashboard, bondedRoles, lockupBSQ, unlockBSQ); + } + + @Override + protected void activate() { + dashboard.activate(); + bondedRoles.activate(); + lockupBSQ.activate(); + unlockBSQ.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(BondingView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = BondedRolesView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(BondingView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @Override + protected void deactivate() { + navigation.removeListener(listener); + + dashboard.deactivate(); + bondedRoles.deactivate(); + lockupBSQ.deactivate(); + unlockBSQ.deactivate(); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof BondingDashboardView) dashboard.setSelected(true); + else if (view instanceof BondedRolesView) bondedRoles.setSelected(true); + else if (view instanceof LockupView) lockupBSQ.setSelected(true); + else if (view instanceof UnlockView) unlockBSQ.setSelected(true); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java b/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java new file mode 100644 index 00000000000..c25e9107bdd --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java @@ -0,0 +1,154 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding; + +import bisq.desktop.Navigation; +import bisq.desktop.main.MainView; +import bisq.desktop.main.funds.FundsView; +import bisq.desktop.main.funds.deposit.DepositView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; + +import bisq.core.btc.wallet.WalletsSetup; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.bonding.lockup.LockupType; +import bisq.core.dao.governance.role.BondedRole; +import bisq.core.dao.governance.role.BondedRoleType; +import bisq.core.dao.state.blockchain.TxOutput; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; + +import bisq.network.p2p.P2PService; + +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BondingViewUtils { + + private final P2PService p2PService; + private final WalletsSetup walletsSetup; + private final DaoFacade daoFacade; + private final Navigation navigation; + private final BsqFormatter bsqFormatter; + + @Inject + public BondingViewUtils(P2PService p2PService, WalletsSetup walletsSetup, DaoFacade daoFacade, + Navigation navigation, BsqFormatter bsqFormatter) { + this.p2PService = p2PService; + this.walletsSetup = walletsSetup; + this.daoFacade = daoFacade; + this.navigation = navigation; + this.bsqFormatter = bsqFormatter; + } + + public void lockupBondForBondedRole(BondedRole bondedRole, ResultHandler resultHandler) { + if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + BondedRoleType bondedRoleType = bondedRole.getBondedRoleType(); + Coin lockupAmount = Coin.valueOf(bondedRoleType.getRequiredBond()); + int lockupTime = bondedRoleType.getUnlockTime(); + LockupType lockupType = LockupType.BONDED_ROLE; + new Popup<>().headLine(Res.get("dao.bonding.lock.sendFunds.headline")) + .confirmation(Res.get("dao.bonding.lock.sendFunds.details", + bsqFormatter.formatCoinWithCode(lockupAmount), + lockupTime + )) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + daoFacade.publishLockupTx(lockupAmount, + lockupTime, + lockupType, + bondedRole, + () -> { + new Popup<>().feedback(Res.get("dao.tx.published.success")).show(); + }, + this::handleError + ); + if (resultHandler != null) + resultHandler.handleResult(); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); + } + } + + public void unLock(String lockupTxId) { + if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + Optional lockupTxOutput = daoFacade.getLockupTxOutput(lockupTxId); + if (!lockupTxOutput.isPresent()) { + log.warn("Lockup output not found, txId = ", lockupTxId); + return; + } + + Coin unlockAmount = Coin.valueOf(lockupTxOutput.get().getValue()); + Optional opLockTime = daoFacade.getLockTime(lockupTxId); + int lockTime = opLockTime.orElse(-1); + + try { + new Popup<>().headLine(Res.get("dao.bonding.unlock.sendTx.headline")) + .confirmation(Res.get("dao.bonding.unlock.sendTx.details", + bsqFormatter.formatCoinWithCode(unlockAmount), + lockTime + )) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + daoFacade.publishUnlockTx(lockupTxId, + () -> { + new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup<>().warning(errorMessage.toString()).show() + ); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + new Popup<>().warning(t.getMessage()).show(); + } + } else { + GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); + } + log.info("unlock tx: {}", lockupTxId); + } + + private void handleError(Throwable throwable) { + if (throwable instanceof InsufficientMoneyException) { + final Coin missingCoin = ((InsufficientMoneyException) throwable).missing; + final String missing = missingCoin != null ? missingCoin.toFriendlyString() : "null"; + new Popup<>().warning(Res.get("popup.warning.insufficientBtcFundsForBsqTx", missing)) + .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) + .show(); + } else { + log.error(throwable.toString()); + throwable.printStackTrace(); + new Popup<>().warning(throwable.toString()).show(); + } + } +} diff --git a/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.fxml new file mode 100644 index 00000000000..48d8c4cd00e --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/voting/dashboard/VotingDashboardView.java b/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.java similarity index 68% rename from src/main/java/bisq/desktop/main/dao/voting/dashboard/VotingDashboardView.java rename to src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.java index 034979eaa23..1d0ae8ea84f 100644 --- a/src/main/java/bisq/desktop/main/dao/voting/dashboard/VotingDashboardView.java +++ b/src/main/java/bisq/desktop/main/dao/bonding/dashboard/BondingDashboardView.java @@ -15,37 +15,44 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.dao.voting.dashboard; +package bisq.desktop.main.dao.bonding.dashboard; import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.dao.wallet.BsqBalanceUtil; import javax.inject.Inject; import javafx.scene.layout.GridPane; @FxmlView -public class VotingDashboardView extends ActivatableView { +public class BondingDashboardView extends ActivatableView { + private final BsqBalanceUtil bsqBalanceUtil; + private int gridRow = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject - private VotingDashboardView() { + private BondingDashboardView(BsqBalanceUtil bsqBalanceUtil) { + this.bsqBalanceUtil = bsqBalanceUtil; } - @Override public void initialize() { + gridRow = bsqBalanceUtil.addGroup(root, gridRow); + gridRow = bsqBalanceUtil.addBondBalanceGroup(root, gridRow); } @Override protected void activate() { + bsqBalanceUtil.activate(); } @Override protected void deactivate() { + bsqBalanceUtil.deactivate(); } } diff --git a/src/main/java/bisq/desktop/main/dao/proposal/votes/VotesView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.fxml similarity index 83% rename from src/main/java/bisq/desktop/main/dao/proposal/votes/VotesView.fxml rename to src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.fxml index 2adf38de747..0129e40e85d 100644 --- a/src/main/java/bisq/desktop/main/dao/proposal/votes/VotesView.fxml +++ b/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.fxml @@ -21,13 +21,14 @@ - + diff --git a/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java b/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java new file mode 100644 index 00000000000..ced735affee --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java @@ -0,0 +1,247 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.lockup; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.dao.bonding.BondingViewUtils; +import bisq.desktop.main.dao.wallet.BsqBalanceUtil; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.Restrictions; +import bisq.core.btc.wallet.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.bonding.BondingConsensus; +import bisq.core.dao.bonding.lockup.LockupType; +import bisq.core.dao.governance.role.BondedRole; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; +import bisq.core.util.validation.IntegerValidator; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.GridPane; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.Arrays; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addLabelInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@FxmlView +public class LockupView extends ActivatableView implements BsqBalanceListener { + private final BsqWalletService bsqWalletService; + private final BsqFormatter bsqFormatter; + private final BsqBalanceUtil bsqBalanceUtil; + private final BondingViewUtils bondingViewUtils; + private final BsqValidator bsqValidator; + private final DaoFacade daoFacade; + private final IntegerValidator timeInputTextFieldValidator; + + private int gridRow = 0; + private InputTextField amountInputTextField; + private InputTextField timeInputTextField; + private ComboBox lockupTypeComboBox; + private ComboBox bondedRolesComboBox; + private Button lockupButton; + private ChangeListener focusOutListener; + private ChangeListener inputTextFieldListener; + private ChangeListener bondedRolesListener; + private ChangeListener lockupTypeListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private LockupView(BsqWalletService bsqWalletService, + BsqFormatter bsqFormatter, + BsqBalanceUtil bsqBalanceUtil, + BondingViewUtils bondingViewUtils, + BsqValidator bsqValidator, + DaoFacade daoFacade) { + this.bsqWalletService = bsqWalletService; + this.bsqFormatter = bsqFormatter; + this.bsqBalanceUtil = bsqBalanceUtil; + this.bondingViewUtils = bondingViewUtils; + this.bsqValidator = bsqValidator; + this.daoFacade = daoFacade; + + timeInputTextFieldValidator = new IntegerValidator(); + timeInputTextFieldValidator.setMinValue(BondingConsensus.getMinLockTime()); + timeInputTextFieldValidator.setMaxValue(BondingConsensus.getMaxLockTime()); + } + + @Override + public void initialize() { + gridRow = bsqBalanceUtil.addGroup(root, gridRow); + + addTitledGroupBg(root, ++gridRow, 4, Res.get("dao.bonding.lock.lockBSQ"), Layout.GROUP_DISTANCE); + + amountInputTextField = addLabelInputTextField(root, gridRow, Res.get("dao.bonding.lock.amount"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + amountInputTextField.setPromptText(Res.get("dao.bonding.lock.setAmount", bsqFormatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); + amountInputTextField.setValidator(bsqValidator); + + timeInputTextField = addLabelInputTextField(root, ++gridRow, Res.get("dao.bonding.lock.time")).second; + timeInputTextField.setPromptText(Res.get("dao.bonding.lock.setTime", + String.valueOf(BondingConsensus.getMinLockTime()), String.valueOf(BondingConsensus.getMaxLockTime()))); + timeInputTextField.setValidator(timeInputTextFieldValidator); + + lockupTypeComboBox = FormBuilder.addLabelComboBox(root, ++gridRow, Res.get("dao.bonding.lock.type")).second; + lockupTypeComboBox.setPromptText(Res.get("shared.select")); + lockupTypeComboBox.setConverter(new StringConverter() { + @Override + public String toString(LockupType lockupType) { + return lockupType.getDisplayString(); + } + + @Override + public LockupType fromString(String string) { + return null; + } + }); + lockupTypeComboBox.setItems(FXCollections.observableArrayList(Arrays.asList(LockupType.values()))); + lockupTypeListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + bondedRolesComboBox.getSelectionModel().clearSelection(); + } + }; + //TODO handle trade type + lockupTypeComboBox.getSelectionModel().select(0); + + bondedRolesComboBox = FormBuilder.addLabelComboBox(root, ++gridRow, Res.get("dao.bonding.lock.bondedRoles")).second; + bondedRolesComboBox.setPromptText(Res.get("shared.select")); + bondedRolesComboBox.setConverter(new StringConverter() { + @Override + public String toString(BondedRole bondedRole) { + return bondedRole.getDisplayString(); + } + + @Override + public BondedRole fromString(String string) { + return null; + } + }); + bondedRolesListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + amountInputTextField.setText(bsqFormatter.formatCoin(Coin.valueOf(newValue.getBondedRoleType().getRequiredBond()))); + timeInputTextField.setText(String.valueOf(newValue.getBondedRoleType().getUnlockTime())); + amountInputTextField.resetValidation(); + timeInputTextField.resetValidation(); + amountInputTextField.setEditable(false); + timeInputTextField.setEditable(false); + } else { + amountInputTextField.clear(); + timeInputTextField.clear(); + amountInputTextField.resetValidation(); + timeInputTextField.resetValidation(); + amountInputTextField.setEditable(true); + timeInputTextField.setEditable(true); + } + }; + + lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bonding.lock.lockupButton")); + lockupButton.setOnAction((event) -> { + bondingViewUtils.lockupBondForBondedRole(bondedRolesComboBox.getValue(), + () -> { + bondedRolesComboBox.getSelectionModel().clearSelection(); + }); + }); + + focusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + onUpdateBalances(); + } + }; + inputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + } + + @Override + protected void activate() { + bsqBalanceUtil.activate(); + + amountInputTextField.textProperty().addListener(inputTextFieldListener); + timeInputTextField.textProperty().addListener(inputTextFieldListener); + amountInputTextField.focusedProperty().addListener(focusOutListener); + lockupTypeComboBox.getSelectionModel().selectedItemProperty().addListener(lockupTypeListener); + bondedRolesComboBox.getSelectionModel().selectedItemProperty().addListener(bondedRolesListener); + + bsqWalletService.addBsqBalanceListener(this); + + bondedRolesComboBox.setItems(FXCollections.observableArrayList(daoFacade.getBondedRoleList())); + onUpdateBalances(); + } + + @Override + protected void deactivate() { + bsqBalanceUtil.deactivate(); + + amountInputTextField.textProperty().removeListener(inputTextFieldListener); + timeInputTextField.textProperty().removeListener(inputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(focusOutListener); + lockupTypeComboBox.getSelectionModel().selectedItemProperty().removeListener(lockupTypeListener); + bondedRolesComboBox.getSelectionModel().selectedItemProperty().removeListener(bondedRolesListener); + + bsqWalletService.removeBsqBalanceListener(this); + } + + @Override + public void onUpdateBalances(Coin confirmedBalance, + Coin availableNonBsqBalance, + Coin pendingBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + bsqValidator.setAvailableBalance(confirmedBalance); + boolean isValid = bsqValidator.validate(amountInputTextField.getText()).isValid; + lockupButton.setDisable(!isValid); + } + + private void onUpdateBalances() { + onUpdateBalances(bsqWalletService.getAvailableBalance(), + bsqWalletService.getAvailableNonBsqBalance(), + bsqWalletService.getUnverifiedBalance(), + bsqWalletService.getLockedForVotingBalance(), + bsqWalletService.getLockupBondsBalance(), + bsqWalletService.getUnlockingBondsBalance()); + } + + private void updateButtonState() { + lockupButton.setDisable(!bsqValidator.validate(amountInputTextField.getText()).isValid || + !timeInputTextFieldValidator.validate(timeInputTextField.getText()).isValid || + bondedRolesComboBox.getSelectionModel().getSelectedItem() == null || + lockupTypeComboBox.getSelectionModel().getSelectedItem() == null); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRoleTypeWindow.java b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRoleTypeWindow.java new file mode 100644 index 00000000000..f768a1d709b --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRoleTypeWindow.java @@ -0,0 +1,93 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.roles; + +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.governance.role.BondedRoleType; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; + +import org.bitcoinj.core.Coin; + +import javafx.geometry.Insets; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +class BondedRoleTypeWindow extends Overlay { + private final BondedRoleType bondedRoleType; + private final BsqFormatter bsqFormatter; + + + public BondedRoleTypeWindow(BondedRoleType bondedRoleType, BsqFormatter bsqFormatter) { + this.bondedRoleType = bondedRoleType; + this.bsqFormatter = bsqFormatter; + + width = 900; + type = Type.Confirmation; + + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void show() { + headLine = Res.get("dao.bond.bondedRoleType.details.header"); + + createGridPane(); + addHeadLine(); + addSeparator(); + addContent(); + addCloseButton(); + applyStyles(); + display(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.getStyleClass().add("grid-pane"); + } + + private void addContent() { + FormBuilder.addLabelTextField(gridPane, ++rowIndex, Res.getWithCol("dao.bond.bondedRoleType.details.role"), + bondedRoleType.getDisplayString()); + + FormBuilder.addLabelTextField(gridPane, ++rowIndex, Res.getWithCol("dao.bond.bondedRoleType.details.requiredBond"), + bsqFormatter.formatCoinWithCode(Coin.valueOf(bondedRoleType.getRequiredBond()))); + + FormBuilder.addLabelTextField(gridPane, ++rowIndex, Res.getWithCol("dao.bond.bondedRoleType.details.unlockTime"), + Res.get("dao.bond.bondedRoleType.details.blocks", bondedRoleType.getUnlockTime())); + + FormBuilder.addLabelHyperlinkWithIcon(gridPane, ++rowIndex, Res.getWithCol("dao.bond.bondedRoleType.details.link"), + bondedRoleType.getLink(), bondedRoleType.getLink()); + + FormBuilder.addLabelTextField(gridPane, ++rowIndex, Res.getWithCol("dao.bond.bondedRoleType.details.isSingleton"), + bsqFormatter.booleanToYesNo(bondedRoleType.isAllowMultipleHolders())); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java new file mode 100644 index 00000000000..35d5a4f0f04 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java @@ -0,0 +1,134 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.roles; + +import bisq.desktop.components.AutoTooltipButton; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.role.BondedRole; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; + +import javafx.scene.control.Label; + +import java.util.Date; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@EqualsAndHashCode +@Data +class BondedRolesListItem implements BsqStateListener { + private final BondedRole bondedRole; + private final DaoFacade daoFacade; + private final BsqFormatter bsqFormatter; + private final AutoTooltipButton button; + private final Label label; + + BondedRolesListItem(BondedRole bondedRole, + DaoFacade daoFacade, + BsqFormatter bsqFormatter) { + this.bondedRole = bondedRole; + this.daoFacade = daoFacade; + this.bsqFormatter = bsqFormatter; + + daoFacade.addBsqStateListener(this); + + button = new AutoTooltipButton(); + button.setMinWidth(70); + + label = new Label(); + + update(); + } + + public String getStartDate() { + return bondedRole.getStartDate() > 0 ? + bsqFormatter.formatDateTime(new Date(bondedRole.getStartDate())) : + "-"; + } + + public String getRevokeDate() { + return bondedRole.getRevokeDate() > 0 ? + bsqFormatter.formatDateTime(new Date(bondedRole.getRevokeDate())) : + "-"; + } + + public void cleanup() { + daoFacade.removeBsqStateListener(this); + setOnAction(null); + } + + public void setOnAction(Runnable handler) { + button.setOnAction(e -> handler.run()); + } + + public boolean isBonded() { + return bondedRole.isLockedUp(); + } + + private void update() { + // We have following state: + // 1. Not bonded: !isLockedUp, !isUnlocked, !isUnlocking: notBonded + // 2. Locked up: isLockedUp, !isUnlocked, !isUnlocking: lockedUp + // 3. Unlocking: isLockedUp, isUnlocked, isUnlocking: unlocking + // 4. Unlocked: isLockedUp, isUnlocked, !isUnlocking: unlocked + + boolean isLockedUp = bondedRole.isLockedUp(); + boolean isUnlocked = bondedRole.isUnlocked(); + boolean isUnlocking = bondedRole.isUnlocking(daoFacade); + log.error("name={}, isLockedUp={}, isUnlocked={}, isUnlocking={}", bondedRole.getName(), isLockedUp, isUnlocked, isUnlocking); + + String text; + if (!isLockedUp) + text = Res.get("dao.bond.table.notBonded"); + else if (!isUnlocked) + text = Res.get("dao.bond.table.lockedUp"); + else if (isUnlocking) + text = Res.get("dao.bond.table.unlocking"); + else + text = Res.get("dao.bond.table.unlocked"); + + label.setText(text); + + button.setText(isLockedUp ? Res.get("dao.bond.table.button.revoke") : Res.get("dao.bond.table.button.lockup")); + button.setVisible(!isLockedUp || !isUnlocked); + button.setManaged(button.isVisible()); + + //TODO listen to unconfirmed txs and update button and label state + } + + + // BsqStateListener + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + update(); + } + + @Override + public void onParseBlockChainComplete() { + } +} diff --git a/src/main/java/bisq/desktop/main/dao/voting/vote/VoteView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.fxml similarity index 83% rename from src/main/java/bisq/desktop/main/dao/voting/vote/VoteView.fxml rename to src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.fxml index 42c0593329f..987214b4904 100644 --- a/src/main/java/bisq/desktop/main/dao/voting/vote/VoteView.fxml +++ b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.fxml @@ -21,13 +21,14 @@ - + diff --git a/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java new file mode 100644 index 00000000000..2753fa35f02 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java @@ -0,0 +1,470 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.roles; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.TableGroupHeadline; +import bisq.desktop.main.dao.bonding.BondingViewUtils; +import bisq.desktop.main.dao.wallet.BsqBalanceUtil; +import bisq.desktop.util.GUIUtil; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.role.BondedRoleType; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.BsqFormatter; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +import javafx.geometry.Insets; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.stream.Collectors; + +@FxmlView +public class BondedRolesView extends ActivatableView implements BsqStateListener { + private TableView tableView; + + private final BsqFormatter bsqFormatter; + private final BondingViewUtils bondingViewUtils; + private final DaoFacade daoFacade; + private final Preferences preferences; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private BondedRolesView(BsqFormatter bsqFormatter, + BsqBalanceUtil bsqBalanceUtil, + BondingViewUtils bondingViewUtils, + DaoFacade daoFacade, + Preferences preferences) { + this.bsqFormatter = bsqFormatter; + this.bondingViewUtils = bondingViewUtils; + this.daoFacade = daoFacade; + this.preferences = preferences; + } + + @Override + public void initialize() { + TableGroupHeadline headline = new TableGroupHeadline(Res.get("dao.bond.table.header")); + int gridRow = 0; + GridPane.setRowIndex(headline, gridRow); + GridPane.setMargin(headline, new Insets(0, -10, -10, -10)); + GridPane.setColumnSpan(headline, 2); + root.getChildren().add(headline); + + tableView = new TableView<>(); + tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noData"))); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + + createColumns(); + + GridPane.setRowIndex(tableView, gridRow); + GridPane.setMargin(tableView, new Insets(20, -10, 5, -10)); + GridPane.setColumnSpan(tableView, 2); + root.getChildren().add(tableView); + + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + tableView.setItems(sortedList); + } + + @Override + protected void activate() { + daoFacade.addBsqStateListener(this); + + updateList(); + } + + @Override + protected void deactivate() { + daoFacade.removeBsqStateListener(this); + + observableList.forEach(BondedRolesListItem::cleanup); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + updateList(); + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + + private void updateList() { + observableList.forEach(BondedRolesListItem::cleanup); + observableList.setAll(daoFacade.getBondedRoleList().stream() + .map(bondedRole -> new BondedRolesListItem(bondedRole, daoFacade, bsqFormatter)) + .collect(Collectors.toList())); + } + + private void openTxInBlockExplorer(String transactionId) { + if (transactionId != null) + GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().txUrl + transactionId); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.name")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getBondedRole().getName()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.linkToAccount")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + String link = item.getBondedRole().getLink(); + hyperlinkWithIcon = new HyperlinkWithIcon(link, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openWebPage(link)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("shared.openURL", link))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.bondedRoleType")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private Hyperlink hyperlink; + + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + BondedRoleType bondedRoleType = item.getBondedRole().getBondedRoleType(); + String type = bondedRoleType.getDisplayString(); + hyperlink = new Hyperlink(type); + hyperlink.setOnAction(event -> { + new BondedRoleTypeWindow(bondedRoleType, bsqFormatter).show(); + }); + hyperlink.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails", type))); + setGraphic(hyperlink); + } else { + setGraphic(null); + if (hyperlink != null) + hyperlink.setOnAction(null); + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.startDate")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(120); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getStartDate()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.revokeDate")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(120); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getRevokeDate()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.lockupTxId")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private HyperlinkWithIcon hyperlinkWithIcon; + private Label label; + + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + String transactionId = item.getBondedRole().getLockupTxId(); + if (transactionId != null) { + hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> openTxInBlockExplorer(transactionId)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + label = new Label("-"); + setGraphic(label); + } + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + if (label != null) + label = null; + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.unlockTxId")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private HyperlinkWithIcon hyperlinkWithIcon; + private Label label; + + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + String transactionId = item.getBondedRole().getUnlockTxId(); + if (transactionId != null) { + hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> openTxInBlockExplorer(transactionId)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + label = new Label("-"); + setGraphic(label); + } + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + if (label != null) + label = null; + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.bond.table.column.header.bondState")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(120); + column.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + Label label; + + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (label == null) { + label = item.getLabel(); + setGraphic(label); + } + } else { + setGraphic(null); + if (label != null) + label = null; + } + } + }; + } + }); + tableView.getColumns().add(column); + + column = new TableColumn<>(); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + Button button; + + @Override + public void updateItem(final BondedRolesListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = item.getButton(); + item.setOnAction(() -> { + if (item.isBonded()) + bondingViewUtils.unLock(item.getBondedRole().getLockupTxId()); + else + bondingViewUtils.lockupBondForBondedRole(item.getBondedRole(), null); + }); + setGraphic(button); + } + } else { + setGraphic(null); + if (button != null) + button = null; + } + } + }; + } + }); + tableView.getColumns().add(column); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java b/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java new file mode 100644 index 00000000000..0ce07b86698 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java @@ -0,0 +1,106 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.unlock; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.TxConfidenceListItem; +import bisq.desktop.components.indicator.TxConfidenceIndicator; + +import bisq.core.btc.listeners.TxConfidenceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.blockchain.TxOutput; +import bisq.core.dao.state.blockchain.TxType; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import java.util.Date; +import java.util.Optional; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +import static com.google.common.base.Preconditions.checkNotNull; + +@EqualsAndHashCode(callSuper = true) +@Data +class LockupTxListItem extends TxConfidenceListItem { + private final BtcWalletService btcWalletService; + private final DaoFacade daoFacade; + + private final BsqFormatter bsqFormatter; + private final Date date; + + private Coin amount = Coin.ZERO; + private int lockTime; + private AutoTooltipButton button; + + private TxConfidenceIndicator txConfidenceIndicator; + private TxConfidenceListener txConfidenceListener; + private boolean issuanceTx; + + LockupTxListItem(Transaction transaction, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + DaoFacade daoFacade, + Date date, + BsqFormatter bsqFormatter) { + super(transaction, bsqWalletService); + + this.btcWalletService = btcWalletService; + this.daoFacade = daoFacade; + this.date = date; + this.bsqFormatter = bsqFormatter; + + checkNotNull(transaction, "transaction must not be null as we only have list items from transactions " + + "which are available in the wallet"); + + daoFacade.getLockupTxOutput(transaction.getHashAsString()) + .ifPresent(out -> amount = Coin.valueOf(out.getValue())); + + Optional opLockTime = daoFacade.getLockTime(transaction.getHashAsString()); + lockTime = opLockTime.orElse(-1); + + button = new AutoTooltipButton(); + button.setMinWidth(70); + button.setText(Res.get("dao.bonding.unlock.unlock")); + button.setVisible(true); + button.setManaged(true); + } + + public boolean isLockupAndUnspent() { + return !isSpent() && getTxType() == TxType.LOCKUP; + } + + private boolean isSpent() { + Optional optionalTxOutput = daoFacade.getLockupTxOutput(txId); + return optionalTxOutput.map(txOutput -> !daoFacade.isUnspent(txOutput.getKey())) + .orElse(true); + + } + + public TxType getTxType() { + return daoFacade.getTx(txId) + .flatMap(tx -> daoFacade.getOptionalTxType(tx.getId())) + .orElse(confirmations == 0 ? TxType.UNVERIFIED : TxType.UNDEFINED_TX_TYPE); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/proposal/myvotes/MyVotesView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.fxml similarity index 83% rename from src/main/java/bisq/desktop/main/dao/proposal/myvotes/MyVotesView.fxml rename to src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.fxml index 2424e2b2021..025947f64cf 100644 --- a/src/main/java/bisq/desktop/main/dao/proposal/myvotes/MyVotesView.fxml +++ b/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.fxml @@ -21,13 +21,14 @@ - + diff --git a/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java b/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java new file mode 100644 index 00000000000..54874c4a204 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java @@ -0,0 +1,387 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.unlock; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.main.dao.bonding.BondingViewUtils; +import bisq.desktop.main.dao.wallet.BsqBalanceUtil; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.wallet.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.dao.state.blockchain.TxType; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.BsqFormatter; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Button; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; + +import javafx.util.Callback; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +@FxmlView +public class UnlockView extends ActivatableView implements BsqBalanceListener, BsqStateListener { + private TableView tableView; + + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final BsqFormatter bsqFormatter; + private final BsqBalanceUtil bsqBalanceUtil; + private final BsqValidator bsqValidator; + private final BondingViewUtils bondingViewUtils; + private final DaoFacade daoFacade; + private final Preferences preferences; + + + private int gridRow = 0; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final FilteredList lockupTxs = new FilteredList<>(observableList); + + private ListChangeListener walletBsqTransactionsListener; + private ChangeListener walletChainHeightListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private UnlockView(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + BsqFormatter bsqFormatter, + BsqBalanceUtil bsqBalanceUtil, + BsqValidator bsqValidator, + BondingViewUtils bondingViewUtils, + DaoFacade daoFacade, + Preferences preferences) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.bsqFormatter = bsqFormatter; + this.bsqBalanceUtil = bsqBalanceUtil; + this.bsqValidator = bsqValidator; + this.bondingViewUtils = bondingViewUtils; + this.daoFacade = daoFacade; + this.preferences = preferences; + } + + @Override + public void initialize() { + gridRow = bsqBalanceUtil.addGroup(root, gridRow); + + tableView = new TableView<>(); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tableView.setPrefHeight(300); + addTxIdColumn(); + addAmountColumn(); + addLockTimeColumn(); + addUnlockColumn(); + + lockupTxs.setPredicate(LockupTxListItem::isLockupAndUnspent); + walletBsqTransactionsListener = change -> updateList(); + walletChainHeightListener = (observable, oldValue, newValue) -> updateList(); + + VBox vBox = new VBox(); + vBox.setSpacing(10); + GridPane.setRowIndex(vBox, ++gridRow); + GridPane.setColumnSpan(vBox, 2); + GridPane.setMargin(vBox, new Insets(40, -10, 5, -10)); + vBox.getChildren().addAll(tableView); + root.getChildren().add(vBox); + } + + @Override + protected void activate() { + bsqBalanceUtil.activate(); + bsqWalletService.addBsqBalanceListener(this); + onUpdateBalances(bsqWalletService.getAvailableBalance(), + bsqWalletService.getAvailableNonBsqBalance(), + bsqWalletService.getUnverifiedBalance(), + bsqWalletService.getLockedForVotingBalance(), + bsqWalletService.getLockupBondsBalance(), + bsqWalletService.getUnlockingBondsBalance()); + + bsqWalletService.getWalletTransactions().addListener(walletBsqTransactionsListener); + bsqWalletService.addBsqBalanceListener(this); + btcWalletService.getChainHeightProperty().addListener(walletChainHeightListener); + + tableView.setItems(lockupTxs); + + daoFacade.addBsqStateListener(this); + + updateList(); + } + + @Override + protected void deactivate() { + bsqBalanceUtil.deactivate(); + bsqWalletService.removeBsqBalanceListener(this); + + lockupTxs.predicateProperty().unbind(); + bsqWalletService.getWalletTransactions().removeListener(walletBsqTransactionsListener); + bsqWalletService.removeBsqBalanceListener(this); + btcWalletService.getChainHeightProperty().removeListener(walletChainHeightListener); + daoFacade.removeBsqStateListener(this); + + observableList.forEach(LockupTxListItem::cleanup); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin confirmedBalance, + Coin availableNonBsqBalance, + Coin pendingBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + bsqValidator.setAvailableBalance(confirmedBalance); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + updateList(); + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void openTxInBlockExplorer(LockupTxListItem item) { + if (item.getTxId() != null) + GUIUtil.openWebPage(preferences.getBsqBlockChainExplorer().txUrl + item.getTxId()); + } + + private void updateList() { + observableList.forEach(LockupTxListItem::cleanup); + + // copy list to avoid ConcurrentModificationException + final List walletTransactions = new ArrayList<>(bsqWalletService.getWalletTransactions()); + List items = walletTransactions.stream() + .map(transaction -> { + return new LockupTxListItem(transaction, + bsqWalletService, + btcWalletService, + daoFacade, + transaction.getUpdateTime(), + bsqFormatter); + }) + .collect(Collectors.toList()); + observableList.setAll(items); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addTxIdColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.txId")); + + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final LockupTxListItem item, boolean empty) { + super.updateItem(item, empty); + + //noinspection Duplicates + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> openTxInBlockExplorer(item)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + tableView.getColumns().add(column); + } + + private void addAmountColumn() { + TableColumn column = + new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", "BSQ")); + column.setMinWidth(120); + column.setMaxWidth(column.getMinWidth()); + + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback, + TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + + @Override + public void updateItem(final LockupTxListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + TxType txType = item.getTxType(); + setText(item.getConfirmations() > 0 && txType.ordinal() > TxType.INVALID.ordinal() ? + bsqFormatter.formatCoin(item.getAmount()) : + Res.get("shared.na")); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + } + + private void addLockTimeColumn() { + TableColumn column = + new AutoTooltipTableColumn<>(Res.get("dao.bonding.unlock.time")); + column.setMinWidth(120); + column.setMaxWidth(column.getMinWidth()); + + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback, + TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + + @Override + public void updateItem(final LockupTxListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + TxType txType = item.getTxType(); + setText(item.getConfirmations() > 0 && txType.ordinal() > TxType.INVALID.ordinal() ? + Integer.toString(item.getLockTime()) : + Res.get("shared.na")); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + } + + private void addUnlockColumn() { + TableColumn unlockColumn = new TableColumn<>(); + unlockColumn.setMinWidth(130); + unlockColumn.setMaxWidth(unlockColumn.getMinWidth()); + + unlockColumn.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + + unlockColumn.setCellFactory(new Callback, + TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + Button button; + + @Override + public void updateItem(final LockupTxListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = item.getButton(); + button.setOnAction(e -> bondingViewUtils.unLock(item.getTxId())); + setGraphic(button); + } + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + unlockColumn.setComparator(Comparator.comparing(LockupTxListItem::getConfirmations)); + tableView.getColumns().add(unlockColumn); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/voting/VotingView.fxml b/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.fxml similarity index 62% rename from src/main/java/bisq/desktop/main/dao/voting/VotingView.fxml rename to src/main/java/bisq/desktop/main/dao/governance/GovernanceView.fxml index 35fed3b3c38..23580ff992f 100644 --- a/src/main/java/bisq/desktop/main/dao/voting/VotingView.fxml +++ b/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.fxml @@ -17,15 +17,20 @@ ~ along with Bisq. If not, see . --> + - - - - + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/proposal/ProposalView.java b/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java similarity index 57% rename from src/main/java/bisq/desktop/main/dao/proposal/ProposalView.java rename to src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java index 9c3a4a67a7e..c84ce9d2141 100644 --- a/src/main/java/bisq/desktop/main/dao/proposal/ProposalView.java +++ b/src/main/java/bisq/desktop/main/dao/governance/GovernanceView.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.dao.proposal; +package bisq.desktop.main.dao.governance; import bisq.desktop.Navigation; import bisq.desktop.common.view.ActivatableViewAndModel; @@ -27,13 +27,13 @@ import bisq.desktop.components.MenuItem; import bisq.desktop.main.MainView; import bisq.desktop.main.dao.DaoView; -import bisq.desktop.main.dao.proposal.active.ActiveProposalsView; -import bisq.desktop.main.dao.proposal.closed.ClosedProposalsView; -import bisq.desktop.main.dao.proposal.dashboard.ProposalDashboardView; -import bisq.desktop.main.dao.proposal.make.MakeProposalView; -import bisq.desktop.main.dao.proposal.myvotes.MyVotesView; -import bisq.desktop.main.dao.proposal.votes.VotesView; +import bisq.desktop.main.dao.governance.dashboard.ProposalDashboardView; +import bisq.desktop.main.dao.governance.make.MakeProposalView; +import bisq.desktop.main.dao.governance.proposals.ProposalsView; +import bisq.desktop.main.dao.governance.result.VoteResultView; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.period.DaoPhase; import bisq.core.locale.Res; import javax.inject.Inject; @@ -46,17 +46,20 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.VBox; +import javafx.beans.value.ChangeListener; + import java.util.Arrays; import java.util.List; @FxmlView -public class ProposalView extends ActivatableViewAndModel { +public class GovernanceView extends ActivatableViewAndModel { private final ViewLoader viewLoader; private final Navigation navigation; + private final DaoFacade daoFacade; - private MenuItem dashboard, make, active, myVotes, votes, closed; - private Navigation.Listener listener; + private MenuItem dashboard, make, open, result; + private Navigation.Listener navigationListener; @FXML private VBox leftVBox; @@ -64,74 +67,80 @@ public class ProposalView extends ActivatableViewAndModel { private AnchorPane content; private Class selectedViewClass; + private ChangeListener phaseChangeListener; @Inject - private ProposalView(CachingViewLoader viewLoader, Navigation navigation) { + private GovernanceView(CachingViewLoader viewLoader, Navigation navigation, DaoFacade daoFacade) { this.viewLoader = viewLoader; this.navigation = navigation; + this.daoFacade = daoFacade; } @Override public void initialize() { - listener = viewPath -> { - if (viewPath.size() != 4 || viewPath.indexOf(ProposalView.class) != 2) + navigationListener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(GovernanceView.class) != 2) return; selectedViewClass = viewPath.tip(); loadView(selectedViewClass); }; + phaseChangeListener = (observable, oldValue, newValue) -> { + if (newValue == DaoPhase.Phase.BLIND_VOTE) + open.setLabelText(Res.get("dao.proposal.menuItem.vote")); + else + open.setLabelText(Res.get("dao.proposal.menuItem.browse")); + }; + ToggleGroup toggleGroup = new ToggleGroup(); - final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, ProposalView.class); + final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, GovernanceView.class); dashboard = new MenuItem(navigation, toggleGroup, Res.get("shared.dashboard"), ProposalDashboardView.class, AwesomeIcon.DASHBOARD, baseNavPath); make = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.make"), MakeProposalView.class, AwesomeIcon.EDIT, baseNavPath); - active = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.active"), - ActiveProposalsView.class, AwesomeIcon.LIST_UL, baseNavPath); - myVotes = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.myVotes"), - MyVotesView.class, AwesomeIcon.THUMBS_UP, baseNavPath); - votes = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.votes"), - VotesView.class, AwesomeIcon.THUMBS_UP_ALT, baseNavPath); - closed = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.closed"), - ClosedProposalsView.class, AwesomeIcon.LIST_ALT, baseNavPath); - leftVBox.getChildren().addAll(dashboard, make, active, myVotes, votes, closed); + open = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.browse"), + ProposalsView.class, AwesomeIcon.LIST_UL, baseNavPath); + result = new MenuItem(navigation, toggleGroup, Res.get("dao.proposal.menuItem.result"), + VoteResultView.class, AwesomeIcon.LIST_ALT, baseNavPath); + leftVBox.getChildren().addAll(dashboard, make, open, result); } @Override protected void activate() { + daoFacade.phaseProperty().addListener(phaseChangeListener); + dashboard.activate(); make.activate(); - active.activate(); - myVotes.activate(); - votes.activate(); - closed.activate(); + open.activate(); + result.activate(); - navigation.addListener(listener); + navigation.addListener(navigationListener); ViewPath viewPath = navigation.getCurrentPath(); - if (viewPath.size() == 3 && viewPath.indexOf(ProposalView.class) == 2 || + if (viewPath.size() == 3 && viewPath.indexOf(GovernanceView.class) == 2 || viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { if (selectedViewClass == null) selectedViewClass = MakeProposalView.class; loadView(selectedViewClass); - } else if (viewPath.size() == 4 && viewPath.indexOf(ProposalView.class) == 2) { + } else if (viewPath.size() == 4 && viewPath.indexOf(GovernanceView.class) == 2) { selectedViewClass = viewPath.get(3); loadView(selectedViewClass); } } + @SuppressWarnings("Duplicates") @Override protected void deactivate() { - navigation.removeListener(listener); + daoFacade.phaseProperty().removeListener(phaseChangeListener); + + navigation.removeListener(navigationListener); dashboard.deactivate(); make.deactivate(); - active.deactivate(); - myVotes.deactivate(); - votes.deactivate(); - closed.deactivate(); + open.deactivate(); + result.deactivate(); } private void loadView(Class viewClass) { @@ -140,14 +149,8 @@ private void loadView(Class viewClass) { if (view instanceof ProposalDashboardView) dashboard.setSelected(true); else if (view instanceof MakeProposalView) make.setSelected(true); - else if (view instanceof ActiveProposalsView) active.setSelected(true); - else if (view instanceof MyVotesView) myVotes.setSelected(true); - else if (view instanceof VotesView) votes.setSelected(true); - else if (view instanceof ClosedProposalsView) closed.setSelected(true); - } - - public Class getSelectedViewClass() { - return selectedViewClass; + else if (view instanceof ProposalsView) open.setSelected(true); + else if (view instanceof VoteResultView) result.setSelected(true); } } diff --git a/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java b/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java new file mode 100644 index 00000000000..828a80aef6e --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/PhasesView.java @@ -0,0 +1,147 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.governance; + +import bisq.desktop.components.SeparatedPhaseBars; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.dao.state.period.DaoPhase; +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.scene.layout.GridPane; + +import javafx.geometry.Insets; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import java.util.Arrays; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@Slf4j +public class PhasesView implements BsqStateListener { + private final DaoFacade daoFacade; + private SeparatedPhaseBars separatedPhaseBars; + private List phaseBarsItems; + private Subscription phaseSubscription; + + @Inject + private PhasesView(DaoFacade daoFacade) { + this.daoFacade = daoFacade; + } + + public int addGroup(GridPane gridPane, int gridRow) { + addTitledGroupBg(gridPane, gridRow, 1, Res.get("dao.cycle.headline")); + separatedPhaseBars = createSeparatedPhaseBars(); + GridPane.setColumnSpan(separatedPhaseBars, 2); + GridPane.setColumnIndex(separatedPhaseBars, 0); + GridPane.setMargin(separatedPhaseBars, new Insets(Layout.FIRST_ROW_DISTANCE - 6, 0, 0, 0)); + GridPane.setRowIndex(separatedPhaseBars, gridRow); + gridPane.getChildren().add(separatedPhaseBars); + return gridRow; + } + + public void activate() { + daoFacade.addBsqStateListener(this); + + phaseSubscription = EasyBind.subscribe(daoFacade.phaseProperty(), phase -> { + phaseBarsItems.forEach(item -> { + if (item.getPhase() == phase) { + item.setActive(); + } else { + item.setInActive(); + } + }); + + }); + + applyData(daoFacade.getChainHeight()); + } + + public void deactivate() { + daoFacade.removeBsqStateListener(this); + + phaseSubscription.unsubscribe(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int height) { + applyData(height); + } + + @Override + public void onParseTxsComplete(Block block) { + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private SeparatedPhaseBars createSeparatedPhaseBars() { + phaseBarsItems = Arrays.asList( + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.PROPOSAL, true), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK1, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BLIND_VOTE, true), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK2, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.VOTE_REVEAL, true), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK3, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.RESULT, false), + new SeparatedPhaseBars.SeparatedPhaseBarsItem(DaoPhase.Phase.BREAK4, false)); + return new SeparatedPhaseBars(phaseBarsItems); + } + + private void applyData(int height) { + if (height > 0) { + phaseBarsItems.forEach(item -> { + int firstBlock = daoFacade.getFirstBlockOfPhase(height, item.getPhase()); + int lastBlock = daoFacade.getLastBlockOfPhase(height, item.getPhase()); + final int duration = daoFacade.getDurationForPhase(item.getPhase()); + item.setPeriodRange(firstBlock, lastBlock, duration); + double progress = 0; + if (height >= firstBlock && height <= lastBlock) { + progress = (double) (height - firstBlock + 1) / (double) duration; + } else if (height < firstBlock) { + progress = 0; + } else if (height > lastBlock) { + progress = 1; + } + item.getProgressProperty().set(progress); + }); + separatedPhaseBars.updateWidth(); + } + } +} diff --git a/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java b/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java new file mode 100644 index 00000000000..50cb4b9095c --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java @@ -0,0 +1,525 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.governance; + +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.components.TxIdTextField; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqAddressValidator; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.ballot.Ballot; +import bisq.core.dao.governance.ballot.vote.Vote; +import bisq.core.dao.governance.proposal.Proposal; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.governance.proposal.compensation.CompensationConsensus; +import bisq.core.dao.governance.proposal.compensation.CompensationProposal; +import bisq.core.dao.governance.proposal.confiscatebond.ConfiscateBondProposal; +import bisq.core.dao.governance.proposal.param.ChangeParamProposal; +import bisq.core.dao.governance.proposal.role.BondedRoleProposal; +import bisq.core.dao.governance.role.BondedRole; +import bisq.core.dao.governance.role.BondedRoleType; +import bisq.core.dao.governance.voteresult.EvaluatedProposal; +import bisq.core.dao.governance.voteresult.ProposalVoteResult; +import bisq.core.dao.state.blockchain.Tx; +import bisq.core.dao.state.governance.Param; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.IntegerValidator; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; + +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.geometry.HPos; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.*; +import static com.google.common.base.Preconditions.checkNotNull; + +@SuppressWarnings("ConstantConditions") +@Slf4j +public class ProposalDisplay { + private final GridPane gridPane; + private final BsqFormatter bsqFormatter; + private final BsqWalletService bsqWalletService; + private final DaoFacade daoFacade; + + @Nullable + private TextField uidTextField, proposalFeeTextField; + private TextField proposalTypeTextField, myVoteTextField, voteResultTextField; + private Label myVoteLabel, voteResultLabel; + public InputTextField nameTextField; + public InputTextField linkInputTextField; + @Nullable + public InputTextField requestedBsqTextField, bsqAddressTextField, paramValueTextField; + @Nullable + public ComboBox paramComboBox; + @Nullable + public ComboBox confiscateBondComboBox; + @Nullable + public ComboBox bondedRoleTypeComboBox; + + @Getter + private int gridRow; + private HyperlinkWithIcon linkHyperlinkWithIcon; + @Nullable + private TxIdTextField txIdTextField; + private int gridRowStartIndex; + private final List inputChangedListeners = new ArrayList<>(); + @Getter + private List inputControls = new ArrayList<>(); + @Getter + private List comboBoxes = new ArrayList<>(); + private final ChangeListener focusOutListener; + private final ChangeListener inputListener; + + public ProposalDisplay(GridPane gridPane, BsqFormatter bsqFormatter, BsqWalletService bsqWalletService, + DaoFacade daoFacade) { + this.gridPane = gridPane; + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.daoFacade = daoFacade; + + // focusOutListener = observable -> inputChangedListeners.forEach(Runnable::run); + + focusOutListener = (observable, oldValue, newValue) -> { + if (oldValue && !newValue) + inputChangedListeners.forEach(Runnable::run); + }; + inputListener = (observable, oldValue, newValue) -> inputChangedListeners.forEach(Runnable::run); + } + + public void addInputChangedListener(Runnable listener) { + inputChangedListeners.add(listener); + } + + public void removeInputChangedListener(Runnable listener) { + inputChangedListeners.remove(listener); + } + + public void createAllFields(String title, int gridRowStartIndex, double top, ProposalType proposalType, + boolean isMakeProposalScreen) { + removeAllFields(); + this.gridRowStartIndex = gridRowStartIndex; + this.gridRow = gridRowStartIndex; + int titledGroupBgRowSpan = 6; + + switch (proposalType) { + case COMPENSATION_REQUEST: + titledGroupBgRowSpan += 1; + break; + case BONDED_ROLE: + break; + case REMOVE_ALTCOIN: + break; + case CHANGE_PARAM: + titledGroupBgRowSpan += 1; + break; + case GENERIC: + break; + case CONFISCATE_BOND: + break; + } + // at isMakeProposalScreen we show fee but no uid and txID (+1) + // otherwise we don't show fee but show uid and txID (+2) + if (isMakeProposalScreen) + titledGroupBgRowSpan += 1; + else + titledGroupBgRowSpan += 2; + + addTitledGroupBg(gridPane, gridRow, titledGroupBgRowSpan, title, top); + double proposalTypeTop = top == Layout.GROUP_DISTANCE ? Layout.FIRST_ROW_AND_GROUP_DISTANCE : Layout.FIRST_ROW_DISTANCE; + proposalTypeTextField = addLabelTextField(gridPane, gridRow, + Res.getWithCol("dao.proposal.display.type"), proposalType.getDisplayName(), proposalTypeTop).second; + + if (!isMakeProposalScreen) + uidTextField = addLabelTextField(gridPane, ++gridRow, Res.getWithCol("shared.id")).second; + + nameTextField = addLabelInputTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.name")).second; + nameTextField.setValidator(new InputValidator()); + inputControls.add(nameTextField); + + Tuple2 tuple = addLabelInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.link")); + linkInputTextField = tuple.second; + linkInputTextField.setPromptText(Res.get("dao.proposal.display.link.prompt")); + linkInputTextField.setValidator(new InputValidator()); + inputControls.add(linkInputTextField); + + linkHyperlinkWithIcon = addLabelHyperlinkWithIcon(gridPane, gridRow, + "", "", "").second; + linkHyperlinkWithIcon.setVisible(false); + linkHyperlinkWithIcon.setManaged(false); + + switch (proposalType) { + case COMPENSATION_REQUEST: + requestedBsqTextField = addLabelInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.requestedBsq")).second; + BsqValidator bsqValidator = new BsqValidator(bsqFormatter); + bsqValidator.setMinValue(CompensationConsensus.getMinCompensationRequestAmount()); + checkNotNull(requestedBsqTextField, "requestedBsqTextField must not be null"); + requestedBsqTextField.setValidator(bsqValidator); + inputControls.add(requestedBsqTextField); + + // TODO validator, addressTF + bsqAddressTextField = addLabelInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.bsqAddress")).second; + checkNotNull(bsqAddressTextField, "bsqAddressTextField must not be null"); + bsqAddressTextField.setText("B" + bsqWalletService.getUnusedAddress().toBase58()); + bsqAddressTextField.setValidator(new BsqAddressValidator(bsqFormatter)); + inputControls.add(bsqAddressTextField); + break; + case BONDED_ROLE: + bondedRoleTypeComboBox = FormBuilder.addLabelComboBox(gridPane, ++gridRow, + Res.getWithCol("dao.proposal.display.bondedRoleComboBox.label")).second; + checkNotNull(bondedRoleTypeComboBox, "bondedRoleTypeComboBox must not be null"); + bondedRoleTypeComboBox.setPromptText(Res.get("shared.select")); + bondedRoleTypeComboBox.setItems(FXCollections.observableArrayList(BondedRoleType.values())); + bondedRoleTypeComboBox.setConverter(new StringConverter() { + @Override + public String toString(BondedRoleType bondedRoleType) { + return bondedRoleType != null ? bondedRoleType.getDisplayString() : ""; + } + + @Override + public BondedRoleType fromString(String string) { + return null; + } + }); + comboBoxes.add(bondedRoleTypeComboBox); + break; + case REMOVE_ALTCOIN: + break; + case CHANGE_PARAM: + checkNotNull(gridPane, "gridPane must not be null"); + paramComboBox = FormBuilder.addLabelComboBox(gridPane, ++gridRow, + Res.getWithCol("dao.proposal.display.paramComboBox.label")).second; + checkNotNull(paramComboBox, "paramComboBox must not be null"); + paramComboBox.setPromptText(Res.get("shared.select")); + List list = Arrays.stream(Param.values()) + .filter(e -> e != Param.UNDEFINED && e != Param.PHASE_UNDEFINED) + .collect(Collectors.toList()); + paramComboBox.setItems(FXCollections.observableArrayList(list)); + paramComboBox.setConverter(new StringConverter() { + @Override + public String toString(Param param) { + return param != null ? param.getDisplayString() : ""; + } + + @Override + public Param fromString(String string) { + return null; + } + }); + comboBoxes.add(paramComboBox); + paramValueTextField = addLabelInputTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.paramValue")).second; + //noinspection ConstantConditions + paramValueTextField.setValidator(new IntegerValidator()); + inputControls.add(paramValueTextField); + break; + case GENERIC: + break; + case CONFISCATE_BOND: + confiscateBondComboBox = FormBuilder.addLabelComboBox(gridPane, ++gridRow, + Res.getWithCol("dao.proposal.display.confiscateBondComboBox.label")).second; + checkNotNull(confiscateBondComboBox, "confiscateBondComboBox must not be null"); + confiscateBondComboBox.setPromptText(Res.get("shared.select")); + confiscateBondComboBox.setItems(FXCollections.observableArrayList(daoFacade.getValidBondedRoleList())); + confiscateBondComboBox.setConverter(new StringConverter() { + @Override + public String toString(BondedRole bondedRole) { + return bondedRole != null ? bondedRole.getDisplayString() : ""; + } + + @Override + public BondedRole fromString(String string) { + return null; + } + }); + comboBoxes.add(confiscateBondComboBox); + break; + } + + if (!isMakeProposalScreen) { + txIdTextField = addLabelTxIdTextField(gridPane, ++gridRow, + Res.get("dao.proposal.display.txId"), "").second; + txIdTextField.setBsq(true); + } + + if (isMakeProposalScreen) { + proposalFeeTextField = addLabelTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.proposalFee")).second; + //noinspection ConstantConditions + proposalFeeTextField.setText(bsqFormatter.formatCoinWithCode(daoFacade.getProposalFee(daoFacade.getChainHeight()))); + } + + Tuple2 tuple2 = addLabelTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.myVote")); + myVoteLabel = tuple2.first; + myVoteLabel.setVisible(false); + myVoteLabel.setManaged(false); + myVoteTextField = tuple2.second; + myVoteTextField.setVisible(false); + myVoteTextField.setManaged(false); + + tuple2 = addLabelTextField(gridPane, ++gridRow, Res.get("dao.proposal.display.voteResult")); + voteResultLabel = tuple2.first; + voteResultLabel.setVisible(false); + voteResultLabel.setManaged(false); + voteResultTextField = tuple2.second; + voteResultTextField.setVisible(false); + voteResultTextField.setManaged(false); + + addListeners(); + } + + public void applyBallot(@Nullable Ballot ballot) { + String myVote = Res.get("dao.proposal.display.myVote.ignored"); + boolean isNotNull = ballot != null; + Vote vote = isNotNull ? ballot.getVote() : null; + if (vote != null) { + myVote = vote.isAccepted() ? Res.get("dao.proposal.display.myVote.accepted") : + Res.get("dao.proposal.display.myVote.rejected"); + } + myVoteTextField.setText(myVote); + + myVoteLabel.setVisible(isNotNull); + myVoteLabel.setManaged(isNotNull); + myVoteTextField.setVisible(isNotNull); + myVoteTextField.setManaged(isNotNull); + } + + public void applyEvaluatedProposal(@Nullable EvaluatedProposal evaluatedProposal) { + boolean isEvaluatedProposalNotNull = evaluatedProposal != null; + if (isEvaluatedProposalNotNull) { + String result = evaluatedProposal.isAccepted() ? Res.get("dao.proposal.voteResult.success") : + Res.get("dao.proposal.voteResult.failed"); + ProposalVoteResult proposalVoteResult = evaluatedProposal.getProposalVoteResult(); + String threshold = (proposalVoteResult.getThreshold() / 100D) + "%"; + String requiredThreshold = (evaluatedProposal.getRequiredThreshold() / 100D) + "%"; + String quorum = bsqFormatter.formatCoinWithCode(Coin.valueOf(proposalVoteResult.getQuorum())); + String requiredQuorum = bsqFormatter.formatCoinWithCode(Coin.valueOf(evaluatedProposal.getRequiredQuorum())); + String summary = Res.get("dao.proposal.voteResult.summary", result, + threshold, requiredThreshold, quorum, requiredQuorum); + voteResultTextField.setText(summary); + } + voteResultLabel.setVisible(isEvaluatedProposalNotNull); + voteResultLabel.setManaged(isEvaluatedProposalNotNull); + voteResultTextField.setVisible(isEvaluatedProposalNotNull); + voteResultTextField.setManaged(isEvaluatedProposalNotNull); + } + + public void applyBallotAndVoteWeight(@Nullable Ballot ballot, long merit, long stake) { + boolean ballotIsNotNull = ballot != null; + boolean hasVoted = stake > 0; + if (hasVoted) { + String myVote = Res.get("dao.proposal.display.myVote.ignored"); + Vote vote = ballotIsNotNull ? ballot.getVote() : null; + if (vote != null) { + myVote = vote.isAccepted() ? Res.get("dao.proposal.display.myVote.accepted") : + Res.get("dao.proposal.display.myVote.rejected"); + } + + String meritString = bsqFormatter.formatCoinWithCode(Coin.valueOf(merit)); + String stakeString = bsqFormatter.formatCoinWithCode(Coin.valueOf(stake)); + String weight = bsqFormatter.formatCoinWithCode(Coin.valueOf(merit + stake)); + String myVoteSummary = Res.get("dao.proposal.myVote.summary", myVote, + weight, meritString, stakeString); + myVoteTextField.setText(myVoteSummary); + } + + boolean show = ballotIsNotNull && hasVoted; + myVoteLabel.setVisible(show); + myVoteLabel.setManaged(show); + myVoteTextField.setVisible(show); + myVoteTextField.setManaged(show); + } + + public void applyProposalPayload(Proposal proposal) { + proposalTypeTextField.setText(proposal.getType().getDisplayName()); + if (uidTextField != null) + uidTextField.setText(proposal.getTxId()); + + nameTextField.setText(proposal.getName()); + linkInputTextField.setVisible(false); + linkInputTextField.setManaged(false); + linkHyperlinkWithIcon.setVisible(true); + linkHyperlinkWithIcon.setManaged(true); + linkHyperlinkWithIcon.setText(proposal.getLink()); + linkHyperlinkWithIcon.setOnAction(e -> GUIUtil.openWebPage(proposal.getLink())); + if (proposal instanceof CompensationProposal) { + CompensationProposal compensationProposal = (CompensationProposal) proposal; + checkNotNull(requestedBsqTextField, "requestedBsqTextField must not be null"); + requestedBsqTextField.setText(bsqFormatter.formatCoinWithCode(compensationProposal.getRequestedBsq())); + if (bsqAddressTextField != null) + bsqAddressTextField.setText(compensationProposal.getBsqAddress()); + } else if (proposal instanceof ChangeParamProposal) { + ChangeParamProposal changeParamProposal = (ChangeParamProposal) proposal; + checkNotNull(paramComboBox, "paramComboBox must not be null"); + paramComboBox.getSelectionModel().select(changeParamProposal.getParam()); + checkNotNull(paramValueTextField, "paramValueTextField must not be null"); + paramValueTextField.setText(String.valueOf(changeParamProposal.getParamValue())); + } else if (proposal instanceof BondedRoleProposal) { + BondedRoleProposal bondedRoleProposal = (BondedRoleProposal) proposal; + checkNotNull(bondedRoleTypeComboBox, "bondedRoleComboBox must not be null"); + BondedRole bondedRole = bondedRoleProposal.getBondedRole(); + bondedRoleTypeComboBox.getSelectionModel().select(bondedRole.getBondedRoleType()); + + } else if (proposal instanceof ConfiscateBondProposal) { + ConfiscateBondProposal confiscateBondProposal = (ConfiscateBondProposal) proposal; + checkNotNull(confiscateBondComboBox, "confiscateBondComboBox must not be null"); + daoFacade.getBondedRoleFromHash(confiscateBondProposal.getHash()) + .ifPresent(bondedRole -> confiscateBondComboBox.getSelectionModel().select(bondedRole)); + } + int chainHeight; + if (txIdTextField != null) { + txIdTextField.setup(proposal.getTxId()); + chainHeight = daoFacade.getChainHeight(); + } else { + chainHeight = daoFacade.getTx(proposal.getTxId()).map(Tx::getBlockHeight).orElse(0); + } + if (proposalFeeTextField != null) + proposalFeeTextField.setText(bsqFormatter.formatCoinWithCode(daoFacade.getProposalFee(chainHeight))); + } + + private void addListeners() { + inputControls.stream() + .filter(Objects::nonNull).forEach(inputControl -> { + inputControl.textProperty().addListener(inputListener); + inputControl.focusedProperty().addListener(focusOutListener); + }); + comboBoxes.stream() + .filter(Objects::nonNull).forEach(comboBox -> { + //noinspection unchecked + comboBox.getSelectionModel().selectedItemProperty().addListener(inputListener); + }); + } + + public void removeListeners() { + inputControls.stream() + .filter(Objects::nonNull).forEach(inputControl -> { + inputControl.textProperty().removeListener(inputListener); + inputControl.focusedProperty().removeListener(focusOutListener); + }); + comboBoxes.stream() + .filter(Objects::nonNull).forEach(comboBox -> { + //noinspection unchecked + comboBox.getSelectionModel().selectedItemProperty().removeListener(inputListener); + }); + } + + public void clearForm() { + inputControls.stream().filter(Objects::nonNull).forEach(TextInputControl::clear); + + if (uidTextField != null) uidTextField.clear(); + if (linkHyperlinkWithIcon != null) linkHyperlinkWithIcon.clear(); + if (txIdTextField != null) txIdTextField.cleanup(); + + comboBoxes.stream() + .filter(Objects::nonNull).forEach(comboBox -> { + comboBox.getSelectionModel().clearSelection(); + }); + } + + public void setEditable(boolean isEditable) { + inputControls.stream().filter(Objects::nonNull).forEach(e -> e.setEditable(isEditable)); + comboBoxes.stream().filter(Objects::nonNull).forEach(comboBox -> comboBox.setDisable(!isEditable)); + + linkInputTextField.setVisible(true); + linkInputTextField.setManaged(true); + linkHyperlinkWithIcon.setVisible(false); + linkHyperlinkWithIcon.setManaged(false); + linkHyperlinkWithIcon.setOnAction(null); + } + + public void removeAllFields() { + if (gridRow > 0) { + clearForm(); + GUIUtil.removeChildrenFromGridPaneRows(gridPane, gridRowStartIndex, gridRow); + gridRow = gridRowStartIndex; + } + inputControls.clear(); + comboBoxes.clear(); + } + + public int incrementAndGetGridRow() { + return ++gridRow; + } + + @SuppressWarnings("Duplicates") + public ScrollPane getView() { + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + //scrollPane.setMinHeight(280); // just enough to display overview at voting without scroller + + AnchorPane anchorPane = new AnchorPane(); + scrollPane.setContent(anchorPane); + + gridPane.setHgap(5); + gridPane.setVgap(5); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHalignment(HPos.RIGHT); + columnConstraints1.setHgrow(Priority.SOMETIMES); + columnConstraints1.setMinWidth(140); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setHgrow(Priority.ALWAYS); + columnConstraints2.setMinWidth(300); + + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + AnchorPane.setBottomAnchor(gridPane, 20d); + AnchorPane.setRightAnchor(gridPane, 10d); + AnchorPane.setLeftAnchor(gridPane, 10d); + AnchorPane.setTopAnchor(gridPane, 20d); + anchorPane.getChildren().add(gridPane); + + return scrollPane; + } +} diff --git a/src/main/java/bisq/desktop/main/dao/governance/dashboard/ProposalDashboardView.fxml b/src/main/java/bisq/desktop/main/dao/governance/dashboard/ProposalDashboardView.fxml new file mode 100644 index 00000000000..55ecbbb0459 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/dashboard/ProposalDashboardView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/governance/dashboard/ProposalDashboardView.java b/src/main/java/bisq/desktop/main/dao/governance/dashboard/ProposalDashboardView.java new file mode 100644 index 00000000000..05f9012a048 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/dashboard/ProposalDashboardView.java @@ -0,0 +1,144 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.governance.dashboard; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.dao.governance.PhasesView; +import bisq.desktop.util.Layout; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.dao.state.period.DaoPhase; +import bisq.core.locale.Res; +import bisq.core.util.BSFormatter; + +import javax.inject.Inject; + +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import java.util.Date; + +import static bisq.desktop.util.FormBuilder.addLabelTextField; +import static bisq.desktop.util.FormBuilder.addMultilineLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +// We use here ChainHeightListener because we are interested in period changes not in the result of a completed +// block. The event from the ChainHeightListener is sent before parsing starts. +// The event from the ChainHeightListener would notify after parsing a new block. +@FxmlView +public class ProposalDashboardView extends ActivatableView implements BsqStateListener { + private final DaoFacade daoFacade; + private final PhasesView phasesView; + private final BSFormatter formatter; + + private int gridRow = 0; + private TextField currentPhaseTextField, currentBlockHeightTextField, proposalTextField, blindVoteTextField, voteRevealTextField, voteResultTextField; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProposalDashboardView(DaoFacade daoFacade, PhasesView phasesView, BSFormatter formatter) { + this.daoFacade = daoFacade; + this.phasesView = phasesView; + this.formatter = formatter; + } + + @Override + public void initialize() { + gridRow = phasesView.addGroup(root, gridRow); + + addTitledGroupBg(root, ++gridRow, 6, Res.get("dao.cycle.overview.headline"), Layout.GROUP_DISTANCE); + currentBlockHeightTextField = addLabelTextField(root, gridRow, Res.get("dao.cycle.currentBlockHeight"), + "", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + currentPhaseTextField = addLabelTextField(root, ++gridRow, Res.get("dao.cycle.currentPhase"), "").second; + proposalTextField = addLabelTextField(root, ++gridRow, Res.get("dao.cycle.proposal"), "").second; + blindVoteTextField = addLabelTextField(root, ++gridRow, Res.get("dao.cycle.blindVote"), "").second; + voteRevealTextField = addLabelTextField(root, ++gridRow, Res.get("dao.cycle.voteReveal"), "").second; + voteResultTextField = addLabelTextField(root, ++gridRow, Res.get("dao.cycle.voteResult"), "").second; + + addTitledGroupBg(root, ++gridRow, 1, Res.get("dao.cycle.info.headline"), Layout.GROUP_DISTANCE); + addMultilineLabel(root, gridRow, Res.get("dao.cycle.info.details"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + } + + @Override + protected void activate() { + super.activate(); + + phasesView.activate(); + + daoFacade.addBsqStateListener(this); + + applyData(daoFacade.getChainHeight()); + } + + @Override + protected void deactivate() { + super.deactivate(); + + phasesView.deactivate(); + + daoFacade.removeBsqStateListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int height) { + applyData(height); + } + + @Override + public void onParseTxsComplete(Block block) { + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyData(int height) { + currentBlockHeightTextField.setText(String.valueOf(daoFacade.getChainHeight())); + currentPhaseTextField.setText(Res.get("dao.phase." + daoFacade.phaseProperty().get().name())); + proposalTextField.setText(getPhaseDuration(height, DaoPhase.Phase.PROPOSAL)); + blindVoteTextField.setText(getPhaseDuration(height, DaoPhase.Phase.BLIND_VOTE)); + voteRevealTextField.setText(getPhaseDuration(height, DaoPhase.Phase.VOTE_REVEAL)); + voteResultTextField.setText(getPhaseDuration(height, DaoPhase.Phase.RESULT)); + } + + private String getPhaseDuration(int height, DaoPhase.Phase phase) { + final long start = daoFacade.getFirstBlockOfPhase(height, phase); + final long end = daoFacade.getLastBlockOfPhase(height, phase); + long now = new Date().getTime(); + String startDateTime = formatter.formatDateTime(new Date(now + (start - height) * 10 * 60 * 1000L)); + String endDateTime = formatter.formatDateTime(new Date(now + (end - height) * 10 * 60 * 1000L)); + return Res.get("dao.cycle.phaseDuration", start, end, startDateTime, endDateTime); + } +} diff --git a/src/main/java/bisq/desktop/main/dao/proposal/make/MakeProposalView.fxml b/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.fxml similarity index 82% rename from src/main/java/bisq/desktop/main/dao/proposal/make/MakeProposalView.fxml rename to src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.fxml index 8b573addc05..5d2df742340 100644 --- a/src/main/java/bisq/desktop/main/dao/proposal/make/MakeProposalView.fxml +++ b/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.fxml @@ -20,15 +20,13 @@ - - - + - diff --git a/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java new file mode 100644 index 00000000000..cbc7ad915b1 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -0,0 +1,364 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.governance.make; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.dao.governance.PhasesView; +import bisq.desktop.main.dao.governance.ProposalDisplay; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; + +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.InsufficientBsqException; +import bisq.core.btc.wallet.WalletsSetup; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.ValidationException; +import bisq.core.dao.governance.proposal.Proposal; +import bisq.core.dao.governance.proposal.ProposalType; +import bisq.core.dao.governance.proposal.ProposalWithTransaction; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.governance.role.BondedRole; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.dao.state.governance.Param; +import bisq.core.dao.state.period.DaoPhase; +import bisq.core.locale.Res; +import bisq.core.provider.fee.FeeService; +import bisq.core.util.BSFormatter; +import bisq.core.util.BsqFormatter; + +import bisq.network.p2p.P2PService; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.GridPane; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; + +import javafx.util.StringConverter; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static com.google.common.base.Preconditions.checkNotNull; + +@FxmlView +public class MakeProposalView extends ActivatableView implements BsqStateListener { + private final DaoFacade daoFacade; + private final BsqWalletService bsqWalletService; + private final WalletsSetup walletsSetup; + private final P2PService p2PService; + private final PhasesView phasesView; + private final BSFormatter btcFormatter; + private final BsqFormatter bsqFormatter; + + private ProposalDisplay proposalDisplay; + private Button makeProposalButton; + private ComboBox proposalTypeComboBox; + private ChangeListener proposalTypeChangeListener; + @Nullable + private ProposalType selectedProposalType; + private int gridRow; + private int alwaysVisibleGridRowIndex; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private MakeProposalView(DaoFacade daoFacade, + BsqWalletService bsqWalletService, + WalletsSetup walletsSetup, + P2PService p2PService, + FeeService feeService, + PhasesView phasesView, + BSFormatter btcFormatter, + BsqFormatter bsqFormatter) { + this.daoFacade = daoFacade; + this.bsqWalletService = bsqWalletService; + this.walletsSetup = walletsSetup; + this.p2PService = p2PService; + this.phasesView = phasesView; + this.btcFormatter = btcFormatter; + this.bsqFormatter = bsqFormatter; + } + + + @Override + public void initialize() { + gridRow = phasesView.addGroup(root, gridRow); + + addTitledGroupBg(root, ++gridRow, 1, Res.get("dao.proposal.create.selectProposalType"), Layout.GROUP_DISTANCE); + proposalTypeComboBox = FormBuilder.addLabelComboBox(root, gridRow, + Res.getWithCol("dao.proposal.create.proposalType"), Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + proposalTypeComboBox.setConverter(new StringConverter() { + @Override + public String toString(ProposalType proposalType) { + return proposalType.getDisplayName(); + } + + @Override + public ProposalType fromString(String string) { + return null; + } + }); + proposalTypeComboBox.setPromptText(Res.get("shared.select")); + proposalTypeChangeListener = (observable, oldValue, newValue) -> { + selectedProposalType = newValue; + removeProposalDisplay(); + addProposalDisplay(); + }; + alwaysVisibleGridRowIndex = gridRow + 1; + + //TODO remove filter once all are implemented + List proposalTypes = Arrays.stream(ProposalType.values()) + .filter(proposalType -> proposalType != ProposalType.GENERIC && + proposalType != ProposalType.REMOVE_ALTCOIN) + .collect(Collectors.toList()); + proposalTypeComboBox.setItems(FXCollections.observableArrayList(proposalTypes)); + } + + @Override + protected void activate() { + phasesView.activate(); + + daoFacade.addBsqStateListener(this); + + proposalTypeComboBox.getSelectionModel().selectedItemProperty().addListener(proposalTypeChangeListener); + if (makeProposalButton != null) + setMakeProposalButtonHandler(); + + onNewBlockHeight(daoFacade.getChainHeight()); + } + + @Override + protected void deactivate() { + phasesView.deactivate(); + + daoFacade.removeBsqStateListener(this); + + proposalTypeComboBox.getSelectionModel().selectedItemProperty().removeListener(proposalTypeChangeListener); + if (makeProposalButton != null) + makeProposalButton.setOnAction(null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int height) { + boolean isProposalPhase = daoFacade.isInPhaseButNotLastBlock(DaoPhase.Phase.PROPOSAL); + proposalTypeComboBox.setDisable(!isProposalPhase); + if (!isProposalPhase) + proposalTypeComboBox.getSelectionModel().clearSelection(); + } + + @Override + public void onParseTxsComplete(Block block) { + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void publishMyProposal(ProposalType type) { + try { + final ProposalWithTransaction proposalWithTransaction = getProposalWithTransaction(type); + Proposal proposal = proposalWithTransaction.getProposal(); + Transaction transaction = proposalWithTransaction.getTransaction(); + Coin miningFee = transaction.getFee(); + int txSize = transaction.bitcoinSerialize().length; + final Coin fee = daoFacade.getProposalFee(daoFacade.getChainHeight()); + GUIUtil.showBsqFeeInfoPopup(fee, miningFee, txSize, bsqFormatter, btcFormatter, + Res.get("dao.proposal"), () -> doPublishMyProposal(proposal, transaction)); + + } catch (InsufficientMoneyException e) { + BSFormatter formatter = e instanceof InsufficientBsqException ? bsqFormatter : btcFormatter; + new Popup<>().warning(Res.get("dao.proposal.create.missingFunds", + formatter.formatCoinWithCode(e.missing))).show(); + } catch (ValidationException e) { + String message; + if (e.getMinRequestAmount() != null) { + message = Res.get("validation.bsq.amountBelowMinAmount", + bsqFormatter.formatCoinWithCode(e.getMinRequestAmount())); + } else { + message = e.getMessage(); + } + new Popup<>().warning(message).show(); + } catch (TxException e) { + log.error(e.toString()); + e.printStackTrace(); + new Popup<>().warning(e.toString()).show(); + } + } + + private void doPublishMyProposal(Proposal proposal, Transaction transaction) { + daoFacade.publishMyProposal(proposal, + transaction, + () -> { + proposalDisplay.clearForm(); + proposalTypeComboBox.getSelectionModel().clearSelection(); + if (!DevEnv.isDevMode()) + new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup<>().warning(errorMessage).show()); + } + + private ProposalWithTransaction getProposalWithTransaction(ProposalType type) + throws InsufficientMoneyException, ValidationException, TxException { + + BondedRole bondedRole; + switch (type) { + case COMPENSATION_REQUEST: + checkNotNull(proposalDisplay.requestedBsqTextField, + "proposalDisplay.requestedBsqTextField must not be null"); + checkNotNull(proposalDisplay.bsqAddressTextField, + "proposalDisplay.bsqAddressTextField must not be null"); + return daoFacade.getCompensationProposalWithTransaction(proposalDisplay.nameTextField.getText(), + proposalDisplay.linkInputTextField.getText(), + bsqFormatter.parseToCoin(proposalDisplay.requestedBsqTextField.getText()), + proposalDisplay.bsqAddressTextField.getText()); + case BONDED_ROLE: + checkNotNull(proposalDisplay.bondedRoleTypeComboBox, + "proposalDisplay.bondedRoleTypeComboBox must not be null"); + bondedRole = new BondedRole(proposalDisplay.nameTextField.getText(), + proposalDisplay.linkInputTextField.getText(), + proposalDisplay.bondedRoleTypeComboBox.getSelectionModel().getSelectedItem()); + return daoFacade.getBondedRoleProposalWithTransaction(bondedRole); + case REMOVE_ALTCOIN: + //TODO + throw new RuntimeException("Not implemented yet"); + case CHANGE_PARAM: + checkNotNull(proposalDisplay.paramComboBox, + "proposalDisplay.paramComboBox must no tbe null"); + checkNotNull(proposalDisplay.paramValueTextField, + "proposalDisplay.paramValueTextField must no tbe null"); + Param selectedParam = proposalDisplay.paramComboBox.getSelectionModel().getSelectedItem(); + if (selectedParam == null) + throw new ValidationException("selectedParam is null"); + String paramValueAsString = proposalDisplay.paramValueTextField.getText(); + if (paramValueAsString == null || paramValueAsString.isEmpty()) + throw new ValidationException("paramValue is null or empty"); + long paramValue; + try { + paramValue = Long.valueOf(paramValueAsString); + } catch (Throwable t) { + throw new ValidationException("paramValue is not a long value", t); + } + //TODO add more custom param validation + return daoFacade.getParamProposalWithTransaction(proposalDisplay.nameTextField.getText(), + proposalDisplay.linkInputTextField.getText(), + selectedParam, + paramValue); + case GENERIC: + //TODO + throw new RuntimeException("Not implemented yet"); + case CONFISCATE_BOND: + checkNotNull(proposalDisplay.confiscateBondComboBox, + "proposalDisplay.confiscateBondComboBox must not be null"); + bondedRole = proposalDisplay.confiscateBondComboBox.getSelectionModel().getSelectedItem(); + return daoFacade.getConfiscateBondProposalWithTransaction(proposalDisplay.nameTextField.getText(), + proposalDisplay.linkInputTextField.getText(), + bondedRole.getHash()); + default: + final String msg = "Undefined ProposalType " + selectedProposalType; + log.error(msg); + throw new RuntimeException(msg); + } + } + + private void addProposalDisplay() { + if (selectedProposalType != null) { + proposalDisplay = new ProposalDisplay(root, bsqFormatter, bsqWalletService, daoFacade); + proposalDisplay.createAllFields(Res.get("dao.proposal.create.createNew"), alwaysVisibleGridRowIndex, Layout.GROUP_DISTANCE, + selectedProposalType, true); + + makeProposalButton = addButtonAfterGroup(root, proposalDisplay.incrementAndGetGridRow(), Res.get("dao.proposal.create.create.button")); + setMakeProposalButtonHandler(); + proposalDisplay.addInputChangedListener(this::updateButtonState); + updateButtonState(); + } + } + + private void removeProposalDisplay() { + if (proposalDisplay != null) { + proposalDisplay.removeAllFields(); + GUIUtil.removeChildrenFromGridPaneRows(root, alwaysVisibleGridRowIndex, proposalDisplay.getGridRow()); + proposalDisplay.removeInputChangedListener(this::updateButtonState); + proposalDisplay.removeListeners(); + proposalDisplay = null; + } + } + + private void setMakeProposalButtonHandler() { + makeProposalButton.setOnAction(event -> { + if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + publishMyProposal(selectedProposalType); + } else { + GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); + } + }); + } + + private void updateButtonState() { + AtomicBoolean inputsValid = new AtomicBoolean(true); + proposalDisplay.getInputControls().stream() + .filter(Objects::nonNull).forEach(e -> { + if (e instanceof InputTextField) { + InputTextField inputTextField = (InputTextField) e; + inputsValid.set(inputsValid.get() && inputTextField.getValidator().validate(e.getText()).isValid); + } + }); + proposalDisplay.getComboBoxes().stream() + .filter(Objects::nonNull).forEach(comboBox -> { + inputsValid.set(inputsValid.get() && comboBox.getSelectionModel().getSelectedItem() != null); + }); + + makeProposalButton.setDisable(!inputsValid.get()); + } +} + diff --git a/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java b/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java new file mode 100644 index 00000000000..1569d77c8dc --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsListItem.java @@ -0,0 +1,133 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.governance.proposals; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.ballot.Ballot; +import bisq.core.dao.governance.ballot.vote.Vote; +import bisq.core.dao.governance.proposal.Proposal; +import bisq.core.dao.state.period.DaoPhase; +import bisq.core.util.BsqFormatter; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Label; + +import javafx.beans.value.ChangeListener; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@ToString +@Slf4j +@EqualsAndHashCode +//TODO merge with vote result ProposalListItem +public class ProposalsListItem { + @Getter + private final Proposal proposal; + private final DaoFacade daoFacade; + private final BsqFormatter bsqFormatter; + + @Getter + @Nullable + private Ballot ballot; + + @Getter + private Label icon; + + private ChangeListener phaseChangeListener; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + ProposalsListItem(Proposal proposal, + DaoFacade daoFacade, + BsqFormatter bsqFormatter) { + this.proposal = proposal; + this.daoFacade = daoFacade; + this.bsqFormatter = bsqFormatter; + + init(); + } + + ProposalsListItem(Ballot ballot, + DaoFacade daoFacade, + BsqFormatter bsqFormatter) { + this.ballot = ballot; + this.proposal = ballot.getProposal(); + this.daoFacade = daoFacade; + this.bsqFormatter = bsqFormatter; + + init(); + } + + private void init() { + phaseChangeListener = (observable, oldValue, newValue) -> onPhaseChanged(newValue); + + daoFacade.phaseProperty().addListener(phaseChangeListener); + + onPhaseChanged(daoFacade.phaseProperty().get()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void cleanup() { + daoFacade.phaseProperty().removeListener(phaseChangeListener); + } + + public void onPhaseChanged(DaoPhase.Phase phase) { + //noinspection IfCanBeSwitch + if (phase == DaoPhase.Phase.PROPOSAL) { + icon = AwesomeDude.createIconLabel(AwesomeIcon.FILE_TEXT); + icon.getStyleClass().addAll("icon", "dao-remove-proposal-icon"); + boolean isMyProposal = daoFacade.isMyProposal(proposal); + icon.setVisible(isMyProposal); + icon.setManaged(isMyProposal); + } else if (icon != null) { + icon.setVisible(true); + icon.setManaged(true); + } + + // ballot + if (ballot != null) { + final Vote vote = ballot.getVote(); + if (vote != null) { + if ((vote).isAccepted()) { + icon = AwesomeDude.createIconLabel(AwesomeIcon.THUMBS_UP); + icon.getStyleClass().addAll("icon", "dao-accepted-icon"); + } else { + icon = AwesomeDude.createIconLabel(AwesomeIcon.THUMBS_DOWN); + icon.getStyleClass().addAll("icon", "dao-rejected-icon"); + } + } else { + icon = AwesomeDude.createIconLabel(AwesomeIcon.MINUS); + icon.getStyleClass().addAll("icon", "dao-ignored-icon"); + } + icon.layout(); + } + } +} diff --git a/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.fxml b/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.fxml new file mode 100644 index 00000000000..68e0bdfc403 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java b/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java new file mode 100644 index 00000000000..517902b0ffa --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/governance/proposals/ProposalsView.java @@ -0,0 +1,855 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.governance.proposals; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.components.TableGroupHeadline; +import bisq.desktop.components.TitledGroupBg; +import bisq.desktop.components.TxIdTextField; +import bisq.desktop.main.dao.governance.PhasesView; +import bisq.desktop.main.dao.governance.ProposalDisplay; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.ballot.Ballot; +import bisq.core.dao.governance.ballot.vote.Vote; +import bisq.core.dao.governance.myvote.MyVote; +import bisq.core.dao.governance.proposal.Proposal; +import bisq.core.dao.governance.voteresult.EvaluatedProposal; +import bisq.core.dao.governance.voteresult.VoteResultService; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.dao.state.period.DaoPhase; +import bisq.core.locale.Res; +import bisq.core.util.BSFormatter; +import bisq.core.util.BsqFormatter; + +import bisq.common.app.DevEnv; +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import javax.inject.Inject; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.geometry.Insets; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.*; + +@FxmlView +public class ProposalsView extends ActivatableView implements BsqBalanceListener, BsqStateListener { + private final DaoFacade daoFacade; + private final BsqWalletService bsqWalletService; + private final PhasesView phasesView; + private final VoteResultService voteResultService; + private final BsqFormatter bsqFormatter; + private final BSFormatter btcFormatter; + + private final ObservableList listItems = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(listItems); + private final List