From 2628eea9db34773b6338c0afdcddb63836ec35d0 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 25 Jul 2022 00:12:13 +0200 Subject: [PATCH 1/7] Add bsq bonds --- .../proofofburn/ProofOfBurnService.java | 2 +- .../src/main/java/bisq/daonode/DaoNode.java | 4 + .../daonode/DaoNodeRestApiApplication.java | 3 + .../bisq/daonode/dto/BondedReputationDto.java | 48 ++++++++ .../java/bisq/daonode/dto/ProofOfBurnDto.java | 14 +-- .../endpoints/BondedReputationApi.java | 103 ++++++++++++++++++ .../daonode/endpoints/ProofOfBurnApi.java | 28 ++--- .../proofofburn/MyProofOfBurnListItem.java | 2 +- .../proofofburn/ProofOfBurnListItem.java | 2 +- 9 files changed, 178 insertions(+), 28 deletions(-) create mode 100644 daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java create mode 100644 daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java index febcb8b449..b8f41358c7 100644 --- a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java @@ -167,7 +167,7 @@ public class ProofOfBurnService implements DaoSetupService, DaoStateListener { myProofOfBurnListService.addMyProofOfBurn(myProofOfBurn); } - public byte[] getHashFromOpReturnData(Tx tx) { + public static byte[] getHashFromOpReturnData(Tx tx) { return ProofOfBurnConsensus.getHashFromOpReturnData(tx.getLastTxOutput().getOpReturnData()); } diff --git a/daonode/src/main/java/bisq/daonode/DaoNode.java b/daonode/src/main/java/bisq/daonode/DaoNode.java index f3a4654479..884f19bce6 100644 --- a/daonode/src/main/java/bisq/daonode/DaoNode.java +++ b/daonode/src/main/java/bisq/daonode/DaoNode.java @@ -22,6 +22,7 @@ import bisq.core.app.TorSetup; import bisq.core.app.misc.AppSetupWithP2PAndDAO; import bisq.core.app.misc.ExecutableForAppWithP2p; import bisq.core.app.misc.ModuleForAppWithP2p; +import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.DaoStateSnapshotService; import bisq.core.user.Cookie; @@ -55,6 +56,8 @@ public class DaoNode extends ExecutableForAppWithP2p { private Timer checkConnectionLossTime; @Getter private DaoStateService daoStateService; + @Getter + private BondedReputationRepository bondedReputationRepository; public DaoNode() { super("Bisq Dao Node", "bisq-dao-node", "bisq_dao_node", Version.VERSION); @@ -117,6 +120,7 @@ public class DaoNode extends ExecutableForAppWithP2p { injector.getInstance(AppSetupWithP2PAndDAO.class).start(); daoStateService = injector.getInstance(DaoStateService.class); + bondedReputationRepository = injector.getInstance(BondedReputationRepository.class); injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() { @Override diff --git a/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java b/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java index e258f24a55..063e88693c 100644 --- a/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java +++ b/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java @@ -28,6 +28,7 @@ import lombok.extern.slf4j.Slf4j; +import bisq.daonode.endpoints.BondedReputationApi; import bisq.daonode.endpoints.ProofOfBurnApi; import bisq.daonode.error.CustomExceptionMapper; import bisq.daonode.error.StatusException; @@ -54,6 +55,7 @@ public class DaoNodeRestApiApplication extends ResourceConfig { .register(CustomExceptionMapper.class) .register(StatusException.StatusExceptionMapper.class) .register(ProofOfBurnApi.class) + .register(BondedReputationApi.class) .register(SwaggerResolution.class); daoNodeRestApiApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort); }); @@ -62,6 +64,7 @@ public class DaoNodeRestApiApplication extends ResourceConfig { @Getter private final DaoNode daoNode; + private HttpServer httpServer; public DaoNodeRestApiApplication() { diff --git a/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java b/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java new file mode 100644 index 0000000000..5ce8585334 --- /dev/null +++ b/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java @@ -0,0 +1,48 @@ +/* + * 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.daonode.dto; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + + + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * Minimal data required for Bisq 2 bonded reputation use case. + * Need to be in sync with the Bisq 2 BondedReputationDto class. + */ +@Getter +@Slf4j +@Schema(title = "BondedReputation") +public class BondedReputationDto { + private long amount; + private long time; + private String hash; + private int blockHeight; + private int lockTime; + + public BondedReputationDto(long amount, long time, String hash, int blockHeight, int lockTime) { + this.amount = amount; + this.time = time; + this.hash = hash; + this.blockHeight = blockHeight; + this.lockTime = lockTime; + } +} diff --git a/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java b/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java index 1ab49ee606..aa16b10396 100644 --- a/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java +++ b/daonode/src/main/java/bisq/daonode/dto/ProofOfBurnDto.java @@ -24,23 +24,21 @@ import lombok.Getter; import io.swagger.v3.oas.annotations.media.Schema; /** - * Minimal data required for Bisq 2 reputation use case. + * Minimal data required for Bisq 2 proof of burn use case. * Need to be in sync with the Bisq 2 ProofOfBurnDto class. */ @Getter @Schema(title = "ProofOfBurn") public class ProofOfBurnDto { - private final String txId; - private final long burnedAmount; - private final int blockHeight; + private final long amount; private final long time; private final String hash; + private final int blockHeight; - public ProofOfBurnDto(String txId, long burnedAmount, int blockHeight, long time, String hash) { - this.txId = txId; - this.burnedAmount = burnedAmount; - this.blockHeight = blockHeight; + public ProofOfBurnDto(long amount, long time, String hash, int blockHeight) { + this.amount = amount; this.time = time; this.hash = hash; + this.blockHeight = blockHeight; } } diff --git a/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java b/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java new file mode 100644 index 0000000000..83f9b84796 --- /dev/null +++ b/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java @@ -0,0 +1,103 @@ +/* + * 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.daonode.endpoints; + +import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; + +import bisq.common.util.Hex; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.daonode.DaoNode; +import bisq.daonode.DaoNodeRestApiApplication; +import bisq.daonode.dto.BondedReputationDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +/** + * Endpoint for getting the bonded reputation data from a given block height. + * Used for reputation system in Bisq 2. + * Request with block height 0 + */ +@Slf4j +@Path("/bonded-reputation") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "Bonded reputation API") +public class BondedReputationApi { + private static final String DESC_BLOCK_HEIGHT = "The block height from which we request the bonded reputation data"; + private final BondedReputationRepository bondedReputationRepository; + private final DaoStateService daoStateService; + + public BondedReputationApi(@Context Application application) { + DaoNode daoNode = ((DaoNodeRestApiApplication) application).getDaoNode(); + daoStateService = daoNode.getDaoStateService(); + bondedReputationRepository = daoNode.getBondedReputationRepository(); + } + + @Operation(description = "Request the bonded reputation data") + @ApiResponse(responseCode = "200", description = "The bonded reputation data", + content = {@Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(allOf = BondedReputationDto.class))} + ) + @GET + @Path("get-bonded-reputation/{block-height}") + public List getBondedReputation(@Parameter(description = DESC_BLOCK_HEIGHT) + @PathParam("block-height") + int fromBlockHeight) { + return bondedReputationRepository.getActiveBonds().stream() + .map(bondedReputation -> { + Optional optionalTx = daoStateService.getTx(bondedReputation.getLockupTxId()); + if (optionalTx.isEmpty()) { + return null; + } + Tx tx = optionalTx.get(); + int blockHeight = tx.getBlockHeight(); + if (blockHeight >= fromBlockHeight) { + return new BondedReputationDto(bondedReputation.getAmount(), + tx.getTime(), + Hex.encode(bondedReputation.getBondedAsset().getHash()), + blockHeight, + bondedReputation.getLockTime() + ); + } else { + return null; + } + }).filter(Objects::nonNull) + .collect(Collectors.toList()); + } +} diff --git a/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java b/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java index b505859322..08b3e0124e 100644 --- a/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java +++ b/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java @@ -17,11 +17,10 @@ package bisq.daonode.endpoints; -import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; import bisq.common.util.Hex; -import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -48,6 +47,11 @@ import jakarta.ws.rs.core.Application; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; +/** + * Endpoint for getting the proof of burn data from a given block height. + * Used for reputation system in Bisq 2. + * Request with block height 0 + */ @Slf4j @Path("/proof-of-burn") @Produces(MediaType.APPLICATION_JSON) @@ -69,23 +73,13 @@ public class ProofOfBurnApi { @Path("get-proof-of-burn/{block-height}") public List getProofOfBurn(@Parameter(description = DESC_BLOCK_HEIGHT) @PathParam("block-height") - int blockHeight) { + int fromBlockHeight) { return checkNotNull(daoNode.getDaoStateService()).getProofOfBurnTxs().stream() - .filter(tx -> tx.getBlockHeight() >= blockHeight) - .map(tx -> new ProofOfBurnDto(tx.getId(), - tx.getBurntBsq(), - tx.getBlockHeight(), + .filter(tx -> tx.getBlockHeight() >= fromBlockHeight) + .map(tx -> new ProofOfBurnDto(tx.getBurntBsq(), tx.getTime(), - getHash(tx))) + Hex.encode(ProofOfBurnService.getHashFromOpReturnData(tx)), + tx.getBlockHeight())) .collect(Collectors.toList()); } - - // We strip out the version bytes - private String getHash(Tx tx) { - byte[] opReturnData = tx.getLastTxOutput().getOpReturnData(); - if (opReturnData == null) { - return ""; - } - return Hex.encode(Arrays.copyOfRange(opReturnData, 2, 22)); - } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java index 0fe3afe189..d0da47fef9 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java @@ -58,7 +58,7 @@ class MyProofOfBurnListItem { amount = proofOfBurnService.getAmount(tx); amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount)); txId = tx.getId(); - hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx)); + hashAsHex = Utilities.bytesAsHexString(ProofOfBurnService.getHashFromOpReturnData(tx)); pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId)); } else { amount = 0; diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java index 5dfe0fbdf8..2303e58d8a 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java @@ -45,7 +45,7 @@ class ProofOfBurnListItem { amount = proofOfBurnService.getAmount(tx); amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount)); txId = tx.getId(); - hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx)); + hashAsHex = Utilities.bytesAsHexString(ProofOfBurnService.getHashFromOpReturnData(tx)); pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId)); date = new Date(tx.getTime()); dateAsString = DisplayUtils.formatDateTime(date); From 2b6dd187c606f2be24d91d036bc34faa33bf7dc2 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 25 Jul 2022 20:50:43 +0200 Subject: [PATCH 2/7] Add export feature for account age --- .../witness/AccountAgeWitnessService.java | 2 +- .../witness/AccountAgeWitnessUtils.java | 52 +++++++++++++++++++ .../resources/i18n/displayStrings.properties | 5 +- .../fiataccounts/FiatAccountsView.java | 46 ++++++++++++++-- .../main/java/bisq/desktop/util/GUIUtil.java | 4 +- 5 files changed, 103 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index d817d4d2e4..ea1e71f18d 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -287,7 +287,7 @@ public class AccountAgeWitnessService { } public Optional findWitness(PaymentAccountPayload paymentAccountPayload, - PubKeyRing pubKeyRing) { + PubKeyRing pubKeyRing) { if (paymentAccountPayload == null) { return Optional.empty(); } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java index f537456db6..34b9429abb 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java @@ -19,17 +19,25 @@ package bisq.core.account.witness; import bisq.core.account.sign.SignedWitness; import bisq.core.account.sign.SignedWitnessService; +import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.trade.model.bisq_v1.Trade; +import bisq.core.util.JsonUtil; import bisq.network.p2p.storage.P2PDataStorage; +import bisq.common.crypto.CryptoException; import bisq.common.crypto.Hash; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; +import bisq.common.util.Hex; import bisq.common.util.Utilities; +import java.security.KeyPair; + import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Optional; import java.util.Stack; @@ -168,4 +176,48 @@ public class AccountAgeWitnessUtils { accountAgeWitnessService.tradeAmountIsSufficient(trade.getAmount()), isSignWitnessTrade); } + + static class AccountAgeWitnessDto { + private final String profileId; + private final String hashAsHex; + private final long date; + private final String pubKeyBase64; + private final String signatureBase64; + + public AccountAgeWitnessDto(String profileId, + String hashAsHex, + long date, + String pubKeyBase64, + String signatureBase64) { + this.profileId = profileId; + this.hashAsHex = hashAsHex; + this.date = date; + this.pubKeyBase64 = pubKeyBase64; + this.signatureBase64 = signatureBase64; + } + } + + public static Optional exportAccount(AccountAgeWitnessService accountAgeWitnessService, + PaymentAccount account, + KeyRing keyRing, + String profileId) { + return accountAgeWitnessService.findWitness(account.getPaymentAccountPayload(), keyRing.getPubKeyRing()) + .map(accountAgeWitness -> { + try { + String hashAsHex = Hex.encode(accountAgeWitness.getHash()); + String message = profileId + hashAsHex + accountAgeWitness.getDate(); + KeyPair signatureKeyPair = keyRing.getSignatureKeyPair(); + String signatureBase64 = Sig.sign(signatureKeyPair.getPrivate(), message); + String pubKeyBase64 = Base64.getEncoder().encodeToString(Sig.getPublicKeyBytes(signatureKeyPair.getPublic())); + AccountAgeWitnessDto dto = new AccountAgeWitnessDto(profileId, + hashAsHex, + accountAgeWitness.getDate(), + pubKeyBase64, + signatureBase64); + return JsonUtil.objectToJson(dto); + } catch (CryptoException e) { + throw new RuntimeException(e); + } + }); + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 95d996087b..5a388a3964 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1818,7 +1818,10 @@ providing cryptographic proof to the mediator or refund agent.\n\n\ If you do not understand these requirements, do not trade L-BTC on Bisq. account.fiat.yourFiatAccounts=Your national currency accounts - +account.fiat.exportAccountAge=Export account age for Bisq 2 +account.fiat.exportAccountAge.popup=Your account age and Bisq 2 profile ID got signed and the data is copied \ + to the clipboard.\n\ + Go back to Bisq 2 and follow the instructions there. account.backup.title=Backup wallet account.backup.location=Backup location account.backup.selectLocation=Select backup location diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java index 4160d08a45..0c982d54d1 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java @@ -18,6 +18,7 @@ package bisq.desktop.main.account.content.fiataccounts; import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.components.paymentmethods.AchTransferForm; import bisq.desktop.components.paymentmethods.AdvancedCashForm; @@ -100,6 +101,7 @@ import bisq.desktop.util.validation.UpholdValidator; import bisq.desktop.util.validation.WeChatPayValidator; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.account.witness.AccountAgeWitnessUtils; import bisq.core.locale.Res; import bisq.core.offer.OfferRestrictions; import bisq.core.payment.AmazonGiftCardAccount; @@ -120,6 +122,8 @@ import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; +import bisq.common.util.Hex; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; import bisq.common.util.Utilities; @@ -135,7 +139,10 @@ import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListView; +import javafx.scene.input.Clipboard; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.collections.FXCollections; @@ -157,6 +164,7 @@ public class FiatAccountsView extends PaymentAccountsView onUpdateAccount(paymentMethodForm.getPaymentAccount())); Button deleteAccountButton = tuple.second; @@ -497,10 +508,39 @@ public class FiatAccountsView extends PaymentAccountsView onCancelSelectedAccount(paymentMethodForm.getPaymentAccount())); GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); + + Button exportAccountAgeButton = new AutoTooltipButton(Res.get("account.fiat.exportAccountAge")); + exportAccountAgeButton.setOnAction(event -> onExportAccountAge(paymentMethodForm.getPaymentAccount())); + exportAccountAgeButton.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(exportAccountAgeButton, Priority.ALWAYS); + HBox hBox = (HBox) cancelButton.getParent(); + hBox.getChildren().add(exportAccountAgeButton); + model.onSelectAccount(current); } } + private void onExportAccountAge(PaymentAccount paymentAccount) { + String clipboardText = Clipboard.getSystemClipboard().getString(); + String prefix = "BISQ2_ACCOUNT_AGE_REQUEST:"; + if (clipboardText != null && clipboardText.startsWith(prefix)) { + String profileId = clipboardText.replace(prefix, ""); + if (profileId.length() == 40) { + try { + Hex.decode(profileId); + AccountAgeWitnessUtils.exportAccount(accountAgeWitnessService, paymentAccount, keyRing, profileId) + .ifPresent(json -> { + Utilities.copyToClipboard(json); + new Popup().information(Res.get("account.fiat.exportAccountAge.popup")).show(); + }); + return; + } catch (Throwable ignore) { + } + } + } + log.warn("Clipboard text not in expected format."); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Utils diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index b80e4aa920..82a1791d0e 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -843,7 +843,9 @@ public class GUIUtil { return true; } - public static boolean canCreateOrTakeOfferOrShowPopup(User user, Navigation navigation, @Nullable TradeCurrency currency) { + public static boolean canCreateOrTakeOfferOrShowPopup(User user, + Navigation navigation, + @Nullable TradeCurrency currency) { if (currency != null && currency.getCode().equals("BSQ")) { return true; } From ef81bde7f2af837aaea7d127a902e85a20764d1e Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 31 Jul 2022 14:40:18 +0200 Subject: [PATCH 3/7] Add export feature for signed account age witness --- .../witness/AccountAgeWitnessService.java | 13 ++++ .../witness/AccountAgeWitnessUtils.java | 67 +++++++++++++++++-- .../resources/i18n/displayStrings.properties | 8 ++- .../fiataccounts/FiatAccountsView.java | 60 ++++++++++++----- 4 files changed, 127 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index ea1e71f18d..9b16329fd2 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -381,6 +381,15 @@ public class AccountAgeWitnessService { } } + public long getWitnessSignDate(AccountAgeWitness accountAgeWitness) { + List dates = signedWitnessService.getVerifiedWitnessDateList(accountAgeWitness); + if (dates.isEmpty()) { + return -1L; + } else { + return dates.get(0); + } + } + // Return -1 if not signed public long getWitnessSignAge(Offer offer, Date now) { return findWitness(offer) @@ -951,4 +960,8 @@ public class AccountAgeWitnessService { return null; } } + + public boolean isFilteredWitness(AccountAgeWitness accountAgeWitness) { + return signedWitnessService.isFilteredWitness(accountAgeWitness); + } } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java index 34b9429abb..2c40597e44 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java @@ -26,6 +26,7 @@ import bisq.core.util.JsonUtil; import bisq.network.p2p.storage.P2PDataStorage; +import bisq.common.app.DevEnv; import bisq.common.crypto.CryptoException; import bisq.common.crypto.Hash; import bisq.common.crypto.KeyRing; @@ -41,11 +42,13 @@ import java.util.Base64; import java.util.Collection; import java.util.Optional; import java.util.Stack; +import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -197,13 +200,37 @@ public class AccountAgeWitnessUtils { } } - public static Optional exportAccount(AccountAgeWitnessService accountAgeWitnessService, - PaymentAccount account, - KeyRing keyRing, - String profileId) { + static class SignedWitnessDto { + private final String profileId; + private final String hashAsHex; + private final long accountAgeWitnessDate; + private final long witnessSignDate; + private final String pubKeyBase64; + private final String signatureBase64; + + public SignedWitnessDto(String profileId, + String hashAsHex, + long accountAgeWitnessDate, + long witnessSignDate, + String pubKeyBase64, + String signatureBase64) { + this.profileId = profileId; + this.hashAsHex = hashAsHex; + this.accountAgeWitnessDate = accountAgeWitnessDate; + this.witnessSignDate = witnessSignDate; + this.pubKeyBase64 = pubKeyBase64; + this.signatureBase64 = signatureBase64; + } + } + + public static Optional signAccountAgeAndBisq2ProfileId(AccountAgeWitnessService accountAgeWitnessService, + PaymentAccount account, + KeyRing keyRing, + String profileId) { return accountAgeWitnessService.findWitness(account.getPaymentAccountPayload(), keyRing.getPubKeyRing()) .map(accountAgeWitness -> { try { + checkArgument(!accountAgeWitnessService.isFilteredWitness(accountAgeWitness), "Invalid account age witness"); String hashAsHex = Hex.encode(accountAgeWitness.getHash()); String message = profileId + hashAsHex + accountAgeWitness.getDate(); KeyPair signatureKeyPair = keyRing.getSignatureKeyPair(); @@ -220,4 +247,36 @@ public class AccountAgeWitnessUtils { } }); } + + public static Optional signSignedWitnessAndBisq2ProfileId(AccountAgeWitnessService accountAgeWitnessService, + PaymentAccount account, + KeyRing keyRing, + String profileId) { + return accountAgeWitnessService.findWitness(account.getPaymentAccountPayload(), keyRing.getPubKeyRing()) + .map(accountAgeWitness -> { + try { + checkArgument(!accountAgeWitnessService.isFilteredWitness(accountAgeWitness), "Invalid account age witness"); + long witnessSignDate = accountAgeWitnessService.getWitnessSignDate(accountAgeWitness); + long ageInDays = (System.currentTimeMillis() - witnessSignDate) / TimeUnit.DAYS.toMillis(1); + if (!DevEnv.isDevMode()) { + checkArgument(witnessSignDate > 0, "Account is not signed yet"); + checkArgument(ageInDays > 60, "Account must have been signed at least 61 days ago"); + } + String hashAsHex = Hex.encode(accountAgeWitness.getHash()); + String message = profileId + hashAsHex + accountAgeWitness.getDate() + witnessSignDate; + KeyPair signatureKeyPair = keyRing.getSignatureKeyPair(); + String signatureBase64 = Sig.sign(signatureKeyPair.getPrivate(), message); + String pubKeyBase64 = Base64.getEncoder().encodeToString(Sig.getPublicKeyBytes(signatureKeyPair.getPublic())); + SignedWitnessDto dto = new SignedWitnessDto(profileId, + hashAsHex, + accountAgeWitness.getDate(), + witnessSignDate, + pubKeyBase64, + signatureBase64); + return JsonUtil.objectToJson(dto); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 5a388a3964..7b35c16bf2 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1819,8 +1819,12 @@ If you do not understand these requirements, do not trade L-BTC on Bisq. account.fiat.yourFiatAccounts=Your national currency accounts account.fiat.exportAccountAge=Export account age for Bisq 2 -account.fiat.exportAccountAge.popup=Your account age and Bisq 2 profile ID got signed and the data is copied \ - to the clipboard.\n\ +account.fiat.exportAccountAge.popup=Your 'account age' and Bisq 2 profile ID got signed and the data is copied \ + to the clipboard.\n\n\This is the data in json format:\n\n\{0}\n\n\ + Go back to Bisq 2 and follow the instructions there. +account.fiat.signedWitness=Export signed witness for Bisq 2 +account.fiat.signedWitness.popup=Your 'signed account age witness' and Bisq 2 profile ID got signed and the data is copied \ + to the clipboard.\n\n\This is the data in json format:\n\n\{0}\n\n\ Go back to Bisq 2 and follow the instructions there. account.backup.title=Backup wallet account.backup.location=Backup location diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java index 0c982d54d1..3e07dd7416 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsView.java @@ -510,35 +510,65 @@ public class FiatAccountsView extends PaymentAccountsView onExportAccountAge(paymentMethodForm.getPaymentAccount())); + exportAccountAgeButton.setOnAction(event -> onExportAccountAgeForBisq2(paymentMethodForm.getPaymentAccount())); exportAccountAgeButton.setMaxWidth(Double.MAX_VALUE); HBox.setHgrow(exportAccountAgeButton, Priority.ALWAYS); + + Button signedWitnessButton = new AutoTooltipButton(Res.get("account.fiat.signedWitness")); + signedWitnessButton.setOnAction(event -> onExportSignedWitnessForBisq2(paymentMethodForm.getPaymentAccount())); + signedWitnessButton.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(signedWitnessButton, Priority.ALWAYS); + + HBox hBox = (HBox) cancelButton.getParent(); - hBox.getChildren().add(exportAccountAgeButton); + hBox.getChildren().addAll(exportAccountAgeButton, signedWitnessButton); model.onSelectAccount(current); } } - private void onExportAccountAge(PaymentAccount paymentAccount) { + private void onExportAccountAgeForBisq2(PaymentAccount paymentAccount) { + String prefix = "BISQ2_ACCOUNT_AGE:"; + try { + String profileId = getProfileIdFromClipBoard(prefix); + AccountAgeWitnessUtils.signAccountAgeAndBisq2ProfileId(accountAgeWitnessService, paymentAccount, keyRing, profileId) + .ifPresent(json -> { + Utilities.copyToClipboard(prefix + json); + new Popup().information(Res.get("account.fiat.exportAccountAge.popup", json)) + .width(900).show(); + }); + } catch (Exception e) { + String error = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + new Popup().warning(error).show(); + } + } + + private void onExportSignedWitnessForBisq2(PaymentAccount paymentAccount) { + String prefix = "BISQ2_SIGNED_WITNESS:"; + try { + String profileId = getProfileIdFromClipBoard(prefix); + AccountAgeWitnessUtils.signSignedWitnessAndBisq2ProfileId(accountAgeWitnessService, paymentAccount, keyRing, profileId) + .ifPresent(json -> { + Utilities.copyToClipboard(prefix + json); + new Popup().information(Res.get("account.fiat.signedWitness.popup", json)) + .width(900).show(); + }); + } catch (Exception e) { + String error = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); + new Popup().warning(error).show(); + } + } + + private String getProfileIdFromClipBoard(String prefix) { String clipboardText = Clipboard.getSystemClipboard().getString(); - String prefix = "BISQ2_ACCOUNT_AGE_REQUEST:"; if (clipboardText != null && clipboardText.startsWith(prefix)) { String profileId = clipboardText.replace(prefix, ""); if (profileId.length() == 40) { - try { - Hex.decode(profileId); - AccountAgeWitnessUtils.exportAccount(accountAgeWitnessService, paymentAccount, keyRing, profileId) - .ifPresent(json -> { - Utilities.copyToClipboard(json); - new Popup().information(Res.get("account.fiat.exportAccountAge.popup")).show(); - }); - return; - } catch (Throwable ignore) { - } + Hex.decode(profileId); + return profileId; } } - log.warn("Clipboard text not in expected format."); + throw new RuntimeException("Clipboard text not in expected format. " + clipboardText); } From aebabcb53e795053f320a69811439f20d6b92025 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Aug 2022 00:34:06 +0200 Subject: [PATCH 4/7] Add api for account age and signed account age witness --- .../witness/AccountAgeWitnessService.java | 4 +- .../daonode/DaoNodeRestApiApplication.java | 16 ++-- .../{DaoNode.java => ServiceNode.java} | 12 ++- .../bisq/daonode/endpoints/AccountAgeApi.java | 76 ++++++++++++++++++ .../endpoints/BondedReputationApi.java | 8 +- .../daonode/endpoints/ProofOfBurnApi.java | 8 +- .../daonode/endpoints/SignedWitnessApi.java | 77 +++++++++++++++++++ 7 files changed, 182 insertions(+), 19 deletions(-) rename daonode/src/main/java/bisq/daonode/{DaoNode.java => ServiceNode.java} (93%) create mode 100644 daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java create mode 100644 daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 9b16329fd2..ffb1173619 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -315,7 +315,7 @@ public class AccountAgeWitnessService { findWitness(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing()); } - private Optional getWitnessByHash(byte[] hash) { + public Optional getWitnessByHash(byte[] hash) { P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash); // First we look up in our fast lookup cache @@ -335,7 +335,7 @@ public class AccountAgeWitnessService { return Optional.empty(); } - private Optional getWitnessByHashAsHex(String hashAsHex) { + public Optional getWitnessByHashAsHex(String hashAsHex) { return getWitnessByHash(Utilities.decodeFromHex(hashAsHex)); } diff --git a/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java b/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java index 063e88693c..aa3a6aa95b 100644 --- a/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java +++ b/daonode/src/main/java/bisq/daonode/DaoNodeRestApiApplication.java @@ -28,8 +28,10 @@ import lombok.extern.slf4j.Slf4j; +import bisq.daonode.endpoints.AccountAgeApi; import bisq.daonode.endpoints.BondedReputationApi; import bisq.daonode.endpoints.ProofOfBurnApi; +import bisq.daonode.endpoints.SignedWitnessApi; import bisq.daonode.error.CustomExceptionMapper; import bisq.daonode.error.StatusException; import bisq.daonode.util.StaticFileHandler; @@ -56,6 +58,8 @@ public class DaoNodeRestApiApplication extends ResourceConfig { .register(StatusException.StatusExceptionMapper.class) .register(ProofOfBurnApi.class) .register(BondedReputationApi.class) + .register(AccountAgeApi.class) + .register(SignedWitnessApi.class) .register(SwaggerResolution.class); daoNodeRestApiApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort); }); @@ -63,18 +67,18 @@ public class DaoNodeRestApiApplication extends ResourceConfig { @Getter - private final DaoNode daoNode; + private final ServiceNode serviceNode; private HttpServer httpServer; public DaoNodeRestApiApplication() { - daoNode = new DaoNode(); + serviceNode = new ServiceNode(); } private void startDaoNode(String[] args, Consumer configConsumer) { new Thread(() -> { - daoNode.execute(args); - configConsumer.accept(daoNode.getConfig()); + serviceNode.execute(args); + configConsumer.accept(serviceNode.getConfig()); try { // Keep running Thread.currentThread().setName("daoNodeThread"); @@ -109,8 +113,8 @@ public class DaoNodeRestApiApplication extends ResourceConfig { } private void shutDown() { - if (daoNode != null) { - daoNode.gracefulShutDown(this::stopServer); + if (serviceNode != null) { + serviceNode.gracefulShutDown(this::stopServer); } else { stopServer(); } diff --git a/daonode/src/main/java/bisq/daonode/DaoNode.java b/daonode/src/main/java/bisq/daonode/ServiceNode.java similarity index 93% rename from daonode/src/main/java/bisq/daonode/DaoNode.java rename to daonode/src/main/java/bisq/daonode/ServiceNode.java index 884f19bce6..dff64d3559 100644 --- a/daonode/src/main/java/bisq/daonode/DaoNode.java +++ b/daonode/src/main/java/bisq/daonode/ServiceNode.java @@ -18,6 +18,7 @@ package bisq.daonode; +import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.app.TorSetup; import bisq.core.app.misc.AppSetupWithP2PAndDAO; import bisq.core.app.misc.ExecutableForAppWithP2p; @@ -50,7 +51,7 @@ import lombok.extern.slf4j.Slf4j; //todo not sure if the restart handling from seed nodes is required @Slf4j -public class DaoNode extends ExecutableForAppWithP2p { +public class ServiceNode extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; private Timer checkConnectionLossTime; @@ -58,8 +59,10 @@ public class DaoNode extends ExecutableForAppWithP2p { private DaoStateService daoStateService; @Getter private BondedReputationRepository bondedReputationRepository; + @Getter + private AccountAgeWitnessService accountAgeWitnessService; - public DaoNode() { + public ServiceNode() { super("Bisq Dao Node", "bisq-dao-node", "bisq_dao_node", Version.VERSION); } @@ -120,6 +123,7 @@ public class DaoNode extends ExecutableForAppWithP2p { injector.getInstance(AppSetupWithP2PAndDAO.class).start(); daoStateService = injector.getInstance(DaoStateService.class); + accountAgeWitnessService = injector.getInstance(AccountAgeWitnessService.class); bondedReputationRepository = injector.getInstance(BondedReputationRepository.class); injector.getInstance(P2PService.class).addP2PServiceListener(new P2PServiceListener() { @@ -153,9 +157,11 @@ public class DaoNode extends ExecutableForAppWithP2p { boolean preventPeriodicShutdownAtSeedNode = injector.getInstance(Key.get(boolean.class, Names.named(Config.PREVENT_PERIODIC_SHUTDOWN_AT_SEED_NODE))); if (!preventPeriodicShutdownAtSeedNode) { - startShutDownInterval(DaoNode.this); + startShutDownInterval(ServiceNode.this); } UserThread.runAfter(() -> setupConnectionLossCheck(), 60); + + accountAgeWitnessService.onAllServicesInitialized(); } @Override diff --git a/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java b/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java new file mode 100644 index 0000000000..939e23f2a2 --- /dev/null +++ b/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java @@ -0,0 +1,76 @@ +/* + * 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.daonode.endpoints; + +import bisq.core.account.witness.AccountAgeWitness; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + + + +import bisq.daonode.DaoNodeRestApiApplication; +import bisq.daonode.ServiceNode; +import bisq.daonode.dto.ProofOfBurnDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +/** + * Endpoint for getting the account age date from a given hash as hex string. + * Used for reputation system in Bisq 2. + * Request with hash + */ +@Slf4j +@Path("/account-age") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "Account age API") +public class AccountAgeApi { + private static final String DESC_HASH = "The hash of the account age witness as hex string"; + private final ServiceNode serviceNode; + + public AccountAgeApi(@Context Application application) { + serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode(); + } + + @Operation(description = "Request the account age date") + @ApiResponse(responseCode = "200", description = "The account age date", + content = {@Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(allOf = ProofOfBurnDto.class))} + ) + @GET + @Path("get-date/{hash}") + public Long getDate(@Parameter(description = DESC_HASH) + @PathParam("hash") + String hash) { + return checkNotNull(serviceNode.getAccountAgeWitnessService()).getWitnessByHashAsHex(hash) + .map(AccountAgeWitness::getDate) + .orElse(-1L); + } +} diff --git a/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java b/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java index 83f9b84796..9f3413c259 100644 --- a/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java +++ b/daonode/src/main/java/bisq/daonode/endpoints/BondedReputationApi.java @@ -32,8 +32,8 @@ import lombok.extern.slf4j.Slf4j; -import bisq.daonode.DaoNode; import bisq.daonode.DaoNodeRestApiApplication; +import bisq.daonode.ServiceNode; import bisq.daonode.dto.BondedReputationDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -64,9 +64,9 @@ public class BondedReputationApi { private final DaoStateService daoStateService; public BondedReputationApi(@Context Application application) { - DaoNode daoNode = ((DaoNodeRestApiApplication) application).getDaoNode(); - daoStateService = daoNode.getDaoStateService(); - bondedReputationRepository = daoNode.getBondedReputationRepository(); + ServiceNode serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode(); + daoStateService = serviceNode.getDaoStateService(); + bondedReputationRepository = serviceNode.getBondedReputationRepository(); } @Operation(description = "Request the bonded reputation data") diff --git a/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java b/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java index 08b3e0124e..5843e6956b 100644 --- a/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java +++ b/daonode/src/main/java/bisq/daonode/endpoints/ProofOfBurnApi.java @@ -30,8 +30,8 @@ import static com.google.common.base.Preconditions.checkNotNull; -import bisq.daonode.DaoNode; import bisq.daonode.DaoNodeRestApiApplication; +import bisq.daonode.ServiceNode; import bisq.daonode.dto.ProofOfBurnDto; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -58,10 +58,10 @@ import jakarta.ws.rs.core.MediaType; @Tag(name = "Proof of burn API") public class ProofOfBurnApi { private static final String DESC_BLOCK_HEIGHT = "The block height from which we request the proof of burn data"; - private final DaoNode daoNode; + private final ServiceNode serviceNode; public ProofOfBurnApi(@Context Application application) { - daoNode = ((DaoNodeRestApiApplication) application).getDaoNode(); + serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode(); } @Operation(description = "Request the proof of burn data") @@ -74,7 +74,7 @@ public class ProofOfBurnApi { public List getProofOfBurn(@Parameter(description = DESC_BLOCK_HEIGHT) @PathParam("block-height") int fromBlockHeight) { - return checkNotNull(daoNode.getDaoStateService()).getProofOfBurnTxs().stream() + return checkNotNull(serviceNode.getDaoStateService()).getProofOfBurnTxs().stream() .filter(tx -> tx.getBlockHeight() >= fromBlockHeight) .map(tx -> new ProofOfBurnDto(tx.getBurntBsq(), tx.getTime(), diff --git a/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java b/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java new file mode 100644 index 0000000000..1f508a3ce7 --- /dev/null +++ b/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java @@ -0,0 +1,77 @@ +/* + * 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.daonode.endpoints; + +import bisq.core.account.witness.AccountAgeWitnessService; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + + + +import bisq.daonode.DaoNodeRestApiApplication; +import bisq.daonode.ServiceNode; +import bisq.daonode.dto.ProofOfBurnDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +/** + * Endpoint for getting the signed witness date from a given hash as hex string. + * Used for reputation system in Bisq 2. + * Request with hash + */ +@Slf4j +@Path("/signed-witness") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "Signed witness API") +public class SignedWitnessApi { + private static final String DESC_HASH = "The hash of the signed account age witness as hex string"; + private final ServiceNode serviceNode; + + public SignedWitnessApi(@Context Application application) { + serviceNode = ((DaoNodeRestApiApplication) application).getServiceNode(); + } + + @Operation(description = "Request the signed witness date") + @ApiResponse(responseCode = "200", description = "The signed witness date", + content = {@Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(allOf = ProofOfBurnDto.class))} + ) + @GET + @Path("get-date/{hash}") + public Long getDate(@Parameter(description = DESC_HASH) + @PathParam("hash") + String hash) { + AccountAgeWitnessService accountAgeWitnessService = checkNotNull(serviceNode.getAccountAgeWitnessService()); + return accountAgeWitnessService.getWitnessByHashAsHex(hash) + .map(accountAgeWitnessService::getWitnessSignDate) + .orElse(-1L); + } +} From d8dc0371f9796d2c3470877dfd92cdee6eb17d73 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Aug 2022 16:57:21 +0200 Subject: [PATCH 5/7] Make fields final --- .../java/bisq/daonode/dto/BondedReputationDto.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java b/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java index 5ce8585334..0cc3d737f4 100644 --- a/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java +++ b/daonode/src/main/java/bisq/daonode/dto/BondedReputationDto.java @@ -32,11 +32,11 @@ import io.swagger.v3.oas.annotations.media.Schema; @Slf4j @Schema(title = "BondedReputation") public class BondedReputationDto { - private long amount; - private long time; - private String hash; - private int blockHeight; - private int lockTime; + private final long amount; + private final long time; + private final String hash; + private final int blockHeight; + private final int lockTime; public BondedReputationDto(long amount, long time, String hash, int blockHeight, int lockTime) { this.amount = amount; From 6aa91a4ee4f6ec034afd63b3deb9913b8d77dc7d Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 1 Aug 2022 17:07:17 +0200 Subject: [PATCH 6/7] Add check for date --- .../account/witness/AccountAgeWitnessUtils.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java index 2c40597e44..6674a29559 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java @@ -232,13 +232,15 @@ public class AccountAgeWitnessUtils { try { checkArgument(!accountAgeWitnessService.isFilteredWitness(accountAgeWitness), "Invalid account age witness"); String hashAsHex = Hex.encode(accountAgeWitness.getHash()); - String message = profileId + hashAsHex + accountAgeWitness.getDate(); + long date = accountAgeWitness.getDate(); + checkArgument(date > 0, "Date must be > 0"); + String message = profileId + hashAsHex + date; KeyPair signatureKeyPair = keyRing.getSignatureKeyPair(); String signatureBase64 = Sig.sign(signatureKeyPair.getPrivate(), message); String pubKeyBase64 = Base64.getEncoder().encodeToString(Sig.getPublicKeyBytes(signatureKeyPair.getPublic())); AccountAgeWitnessDto dto = new AccountAgeWitnessDto(profileId, hashAsHex, - accountAgeWitness.getDate(), + date, pubKeyBase64, signatureBase64); return JsonUtil.objectToJson(dto); @@ -262,14 +264,17 @@ public class AccountAgeWitnessUtils { checkArgument(witnessSignDate > 0, "Account is not signed yet"); checkArgument(ageInDays > 60, "Account must have been signed at least 61 days ago"); } + String hashAsHex = Hex.encode(accountAgeWitness.getHash()); - String message = profileId + hashAsHex + accountAgeWitness.getDate() + witnessSignDate; + long date = accountAgeWitness.getDate(); + checkArgument(date > 0, "AccountAgeWitness date must be > 0"); + String message = profileId + hashAsHex + date + witnessSignDate; KeyPair signatureKeyPair = keyRing.getSignatureKeyPair(); String signatureBase64 = Sig.sign(signatureKeyPair.getPrivate(), message); String pubKeyBase64 = Base64.getEncoder().encodeToString(Sig.getPublicKeyBytes(signatureKeyPair.getPublic())); SignedWitnessDto dto = new SignedWitnessDto(profileId, hashAsHex, - accountAgeWitness.getDate(), + date, witnessSignDate, pubKeyBase64, signatureBase64); From f6b4e2e5b55564641129201175770892c0f32e55 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sun, 7 Aug 2022 12:35:58 +0200 Subject: [PATCH 7/7] Use TEXT_PLAIN instead of APPLICATION_JSON for date in response --- .../src/main/java/bisq/daonode/endpoints/AccountAgeApi.java | 4 ++-- .../main/java/bisq/daonode/endpoints/SignedWitnessApi.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java b/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java index 939e23f2a2..38d679e54b 100644 --- a/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java +++ b/daonode/src/main/java/bisq/daonode/endpoints/AccountAgeApi.java @@ -49,7 +49,7 @@ import jakarta.ws.rs.core.MediaType; */ @Slf4j @Path("/account-age") -@Produces(MediaType.APPLICATION_JSON) +@Produces(MediaType.TEXT_PLAIN) @Tag(name = "Account age API") public class AccountAgeApi { private static final String DESC_HASH = "The hash of the account age witness as hex string"; @@ -61,7 +61,7 @@ public class AccountAgeApi { @Operation(description = "Request the account age date") @ApiResponse(responseCode = "200", description = "The account age date", - content = {@Content(mediaType = MediaType.APPLICATION_JSON, + content = {@Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(allOf = ProofOfBurnDto.class))} ) @GET diff --git a/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java b/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java index 1f508a3ce7..7823af758d 100644 --- a/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java +++ b/daonode/src/main/java/bisq/daonode/endpoints/SignedWitnessApi.java @@ -49,7 +49,7 @@ import jakarta.ws.rs.core.MediaType; */ @Slf4j @Path("/signed-witness") -@Produces(MediaType.APPLICATION_JSON) +@Produces(MediaType.TEXT_PLAIN) @Tag(name = "Signed witness API") public class SignedWitnessApi { private static final String DESC_HASH = "The hash of the signed account age witness as hex string"; @@ -61,7 +61,7 @@ public class SignedWitnessApi { @Operation(description = "Request the signed witness date") @ApiResponse(responseCode = "200", description = "The signed witness date", - content = {@Content(mediaType = MediaType.APPLICATION_JSON, + content = {@Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(allOf = ProofOfBurnDto.class))} ) @GET