diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index a1e57b1074..b833210490 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -943,7 +943,8 @@ message PersistableEnvelope { MeritList merit_list = 23; BondedRoleList bonded_role_list = 24; RemovedAssetList removed_asset_list = 25; - DaoStateStore dao_state_store = 28; + DaoStateStore dao_state_store = 26; + BondedReputationList bonded_reputation_list = 27; } } @@ -1554,6 +1555,16 @@ message BondedRoleList { repeated BondedRole bonded_role = 1; } +message BondedReputation { + string salt = 1; + string lockup_tx_id = 2; + string unlock_tx_id = 3; +} + +message BondedReputationList { + repeated BondedReputation bonded_reputation = 1; +} + message TempProposalPayload { Proposal proposal = 1; bytes owner_pub_key_encoded = 2; diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java index 2acbbd5c0f..da1cab05a7 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -18,6 +18,7 @@ package bisq.core.app.misc; import bisq.core.dao.DaoSetup; +import bisq.core.dao.bonding.bond.BondedReputationService; import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.MyBlindVoteListService; @@ -52,6 +53,7 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P { BallotListService ballotListService, MyBlindVoteListService myBlindVoteListService, BondedRolesService bondedRolesService, + BondedReputationService bondedReputationService, AssetService assetService) { super(encryptionService, keyRing, @@ -66,6 +68,7 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P { persistedDataHosts.add(ballotListService); persistedDataHosts.add(myBlindVoteListService); persistedDataHosts.add(bondedRolesService); + persistedDataHosts.add(bondedReputationService); persistedDataHosts.add(assetService); } diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 1a6f4aacff..ecfd8d41ec 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -19,6 +19,9 @@ package bisq.core.dao; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.WalletException; +import bisq.core.dao.bonding.bond.BondWithHash; +import bisq.core.dao.bonding.bond.BondedReputation; +import bisq.core.dao.bonding.bond.BondedReputationService; import bisq.core.dao.bonding.lockup.LockupService; import bisq.core.dao.bonding.lockup.LockupType; import bisq.core.dao.bonding.unlock.UnlockService; @@ -111,6 +114,7 @@ public class DaoFacade implements DaoSetupService { private final GenericProposalService genericProposalService; private final RemoveAssetProposalService removeAssetProposalService; private final BondedRolesService bondedRolesService; + private final BondedReputationService bondedReputationService; private final LockupService lockupService; private final UnlockService unlockService; private final DaoStateStorageService daoStateStorageService; @@ -134,6 +138,7 @@ public class DaoFacade implements DaoSetupService { GenericProposalService genericProposalService, RemoveAssetProposalService removeAssetProposalService, BondedRolesService bondedRolesService, + BondedReputationService bondedReputationService, LockupService lockupService, UnlockService unlockService, DaoStateStorageService daoStateStorageService) { @@ -153,6 +158,7 @@ public class DaoFacade implements DaoSetupService { this.genericProposalService = genericProposalService; this.removeAssetProposalService = removeAssetProposalService; this.bondedRolesService = bondedRolesService; + this.bondedReputationService = bondedReputationService; this.lockupService = lockupService; this.unlockService = unlockService; this.daoStateStorageService = daoStateStorageService; @@ -277,6 +283,10 @@ public class DaoFacade implements DaoSetupService { return bondedRolesService.getBondedRoleList(); } + public List getBondedReputationList() { + return bondedReputationService.getBondedReputationList(); + } + // Show fee public Coin getProposalFee(int chainHeight) { return ProposalConsensus.getFee(daoStateService, chainHeight); @@ -474,9 +484,9 @@ public class DaoFacade implements DaoSetupService { // Use case: Bonding /////////////////////////////////////////////////////////////////////////////////////////// - public void publishLockupTx(Coin lockupAmount, int lockTime, LockupType lockupType, BondedRole bondedRole, + public void publishLockupTx(Coin lockupAmount, int lockTime, LockupType lockupType, BondWithHash bondWithHash, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { - lockupService.publishLockupTx(lockupAmount, lockTime, lockupType, bondedRole, resultHandler, exceptionHandler); + lockupService.publishLockupTx(lockupAmount, lockTime, lockupType, bondWithHash, resultHandler, exceptionHandler); } public void publishUnlockTx(String lockupTxId, ResultHandler resultHandler, @@ -504,6 +514,10 @@ public class DaoFacade implements DaoSetupService { return bondedRolesService.getValidBondedRoleList(); } + public List getValidBondedReputationList() { + return bondedReputationService.getValidBondedReputationList(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Use case: Present transaction related state @@ -605,8 +619,12 @@ public class DaoFacade implements DaoSetupService { return bondedRolesService.getBondedRoleFromHash(hash); } - public boolean isUnlocking(BondedRole bondedRole) { - return daoStateService.isUnlocking(bondedRole); + public Optional getBondedReputationFromHash(byte[] hash) { + return bondedReputationService.getBondedReputationFromHash(hash); + } + + public boolean isUnlocking(BondWithHash bondWithHash) { + return daoStateService.isUnlocking(bondWithHash); } public Coin getMinCompensationRequestAmount() { diff --git a/core/src/main/java/bisq/core/dao/DaoModule.java b/core/src/main/java/bisq/core/dao/DaoModule.java index fd0b29b724..597b791f7a 100644 --- a/core/src/main/java/bisq/core/dao/DaoModule.java +++ b/core/src/main/java/bisq/core/dao/DaoModule.java @@ -17,6 +17,7 @@ package bisq.core.dao; +import bisq.core.dao.bonding.bond.BondedReputationService; import bisq.core.dao.bonding.lockup.LockupService; import bisq.core.dao.bonding.unlock.UnlockService; import bisq.core.dao.governance.asset.AssetService; @@ -186,6 +187,7 @@ public class DaoModule extends AppModule { bind(LockupService.class).in(Singleton.class); bind(UnlockService.class).in(Singleton.class); bind(BondedRolesService.class).in(Singleton.class); + bind(BondedReputationService.class).in(Singleton.class); // Asset bind(AssetService.class).in(Singleton.class); diff --git a/core/src/main/java/bisq/core/dao/bonding/BondingConsensus.java b/core/src/main/java/bisq/core/dao/bonding/BondingConsensus.java index eb84706973..51fafdadb0 100644 --- a/core/src/main/java/bisq/core/dao/bonding/BondingConsensus.java +++ b/core/src/main/java/bisq/core/dao/bonding/BondingConsensus.java @@ -17,8 +17,8 @@ package bisq.core.dao.bonding; +import bisq.core.dao.bonding.bond.BondWithHash; import bisq.core.dao.bonding.lockup.LockupType; -import bisq.core.dao.governance.role.BondedRole; import bisq.core.dao.state.blockchain.OpReturnType; import bisq.common.app.Version; @@ -89,11 +89,7 @@ public class BondingConsensus { return Arrays.copyOfRange(opReturnData, 5, 25); } - public static byte[] getHash(LockupType lockupType, BondedRole bondedRole) { - if (lockupType == LockupType.BONDED_ROLE) { - return bondedRole.getHash(); - } else { - throw new RuntimeException("Trade bonds not implemented yet"); - } + public static byte[] getHash(BondWithHash bondWithHash) { + return bondWithHash.getHash(); } } diff --git a/core/src/main/java/bisq/core/dao/bonding/bond/BondWithHash.java b/core/src/main/java/bisq/core/dao/bonding/bond/BondWithHash.java new file mode 100644 index 0000000000..3419e7dc7e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/bonding/bond/BondWithHash.java @@ -0,0 +1,24 @@ +/* + * 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.core.dao.bonding.bond; + +public interface BondWithHash { + + String getUnlockTxId(); + byte[] getHash(); +} diff --git a/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputation.java b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputation.java new file mode 100644 index 0000000000..7a0d052d2b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputation.java @@ -0,0 +1,149 @@ +/* + * 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.core.dao.bonding.bond; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.BsqStateService; +import bisq.core.locale.Res; + +import bisq.common.crypto.Hash; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; + +import io.bisq.generated.protobuffer.PB; + +import java.math.BigInteger; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; + +@Getter +public final class BondedReputation implements PersistablePayload, NetworkPayload, BondWithHash { + private final String salt; + + @Setter + private String lockupTxId; + + @Nullable + @Setter + private String unlockTxId; + + public BondedReputation(@Nullable String salt) { + this(salt, + null, + null + ); + } + + public static BondedReputation createBondedReputation() { + return new BondedReputation(UUID.randomUUID().toString()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + public BondedReputation(String salt, + @Nullable String lockupTxId, + @Nullable String unlockTxId) { + this.salt = salt; + this.lockupTxId = lockupTxId; + this.unlockTxId = unlockTxId; + } + + @Override + public PB.BondedReputation toProtoMessage() { + PB.BondedReputation.Builder builder = PB.BondedReputation.newBuilder() + .setSalt(salt); + Optional.ofNullable(lockupTxId).ifPresent(builder::setLockupTxId); + Optional.ofNullable(unlockTxId).ifPresent(builder::setUnlockTxId); + return builder.build(); + } + + public static BondedReputation fromProto(PB.BondedReputation proto) { + return new BondedReputation(proto.getSalt(), + proto.getLockupTxId().isEmpty() ? null : proto.getLockupTxId(), + proto.getUnlockTxId().isEmpty() ? null : proto.getUnlockTxId()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + @Override + public String getUnlockTxId() { + return unlockTxId; + } + + @Override + public byte[] getHash() { + // We use the salt as input for the hash + byte[] bytes = BigInteger.valueOf(hashCode()).toByteArray(); + byte[] hash = Hash.getSha256Ripemd160hash(bytes); + return hash; + } + + public String getDisplayString() { + return Res.get("dao.bond.bondedReputation"); + } + + public boolean isLockedUp() { + return lockupTxId != null; + } + + public boolean isUnlocked() { + return unlockTxId != null; + } + + public boolean isUnlocking(DaoFacade daoFacade) { + return daoFacade.isUnlocking(this); + } + + public boolean isUnlocking(BsqStateService bsqStateService) { + return bsqStateService.isUnlocking(this); + } + + // We use only the immutable data + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BondedReputation that = (BondedReputation) o; + return Objects.equals(salt, that.salt); + } + + @Override + public int hashCode() { + return Objects.hash(salt); + } + + @Override + public String toString() { + return "BondedReputation{" + + "\n salt='" + salt + '\'' + + ",\n lockupTxId='" + lockupTxId + '\'' + + ",\n unlockTxId='" + unlockTxId + '\'' + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationList.java b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationList.java new file mode 100644 index 0000000000..65f78621e5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationList.java @@ -0,0 +1,74 @@ +/* + * 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.core.dao.bonding.bond; + +import bisq.common.proto.persistable.PersistableList; + +import io.bisq.generated.protobuffer.PB; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * PersistableEnvelope wrapper for list of BondedReputations. + */ +@EqualsAndHashCode(callSuper = true) +public class BondedReputationList extends PersistableList { + + public BondedReputationList(List list) { + super(list); + } + + public BondedReputationList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PB.PersistableEnvelope toProtoMessage() { + return PB.PersistableEnvelope.newBuilder().setBondedReputationList(getBuilder()).build(); + } + + public PB.BondedReputationList.Builder getBuilder() { + return PB.BondedReputationList.newBuilder() + .addAllBondedReputation(getList().stream() + .map(BondedReputation::toProtoMessage) + .collect(Collectors.toList())); + } + + public static BondedReputationList fromProto(PB.BondedReputationList proto) { + return new BondedReputationList(new ArrayList<>(proto.getBondedReputationList().stream() + .map(BondedReputation::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "List of salts in BondedReputationList: " + getList().stream() + .map(BondedReputation::getSalt) + .collect(Collectors.toList()); + } +} + diff --git a/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationService.java b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationService.java new file mode 100644 index 0000000000..b1f8fef003 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationService.java @@ -0,0 +1,201 @@ +/* + * 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.core.dao.bonding.bond; + +import bisq.core.app.BisqEnvironment; +import bisq.core.dao.bonding.BondingConsensus; +import bisq.core.dao.state.BsqStateListener; +import bisq.core.dao.state.BsqStateService; +import bisq.core.dao.state.blockchain.Block; +import bisq.core.dao.state.blockchain.SpentInfo; +import bisq.core.dao.state.blockchain.TxType; + +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.storage.Storage; + +import javax.inject.Inject; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BondedReputationService implements PersistedDataHost, BsqStateListener { + + public interface BondedReputationListChangeListener { + void onListChanged(List list); + } + + private final BsqStateService bsqStateService; + private final Storage storage; + private final BondedReputationList BondedReputationList = new BondedReputationList(); + + @Getter + private final List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BondedReputationService(Storage storage, BsqStateService bsqStateService) { + this.storage = storage; + this.bsqStateService = bsqStateService; + + bsqStateService.addBsqStateListener(this); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted() { + if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { + BondedReputationList persisted = storage.initAndGetPersisted(BondedReputationList, 100); + if (persisted != null) { + BondedReputationList.clear(); + BondedReputationList.addAll(persisted.getList()); + listeners.forEach(l -> l.onListChanged(BondedReputationList.getList())); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + BondedReputationList.getList().forEach(BondedReputation -> { + + bsqStateService.getLockupTxOutputs().forEach(lockupTxOutput -> { + String lockupTxId = lockupTxOutput.getTxId(); + // log.error("lockupTxId " + lockupTxId); + + bsqStateService.getTx(lockupTxId) + .ifPresent(lockupTx -> { + byte[] opReturnData = lockupTx.getLastTxOutput().getOpReturnData(); + byte[] hash = BondingConsensus.getHashFromOpReturnData(opReturnData); + Optional candidate = getBondedReputationFromHash(hash); + if (candidate.isPresent() && BondedReputation.equals(candidate.get())) { + if (BondedReputation.getLockupTxId() == null) { + BondedReputation.setLockupTxId(lockupTxId); + persist(); + } + + if (!bsqStateService.isUnspent(lockupTxOutput.getKey())) { + bsqStateService.getSpentInfo(lockupTxOutput) + .map(SpentInfo::getTxId) + .map(bsqStateService::getTx) + .map(Optional::get) + // TODO(sq): What if the tx is burnt and not unlocked, need to check on that + .filter(unlockTx -> unlockTx.getTxType() == TxType.UNLOCK) + .ifPresent(unlockTx -> { + if (BondedReputation.getUnlockTxId() == null) { + BondedReputation.setUnlockTxId(unlockTx.getId()); + persist(); + } + + // TODO check lock time + }); + } + } + }); + }); + }); + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void start() { + } + + public void addListener(BondedReputationListChangeListener listener) { + listeners.add(listener); + } + +// public void addAcceptedBondedReputation(BondedReputation BondedReputation) { +// if (BondedReputationList.getList().stream().noneMatch(role -> role.equals(BondedReputation))) { +// BondedReputationList.add(BondedReputation); +// persist(); +// listeners.forEach(l -> l.onListChanged(BondedReputationList.getList())); +// } +// } + + public List getBondedReputationList() { + return BondedReputationList.getList(); + } + + public List getValidBondedReputationList() { + return BondedReputationList.getList(); + } + + public Optional getBondedReputationFromHash(byte[] hash) { + return BondedReputationList.getList().stream() + .filter(BondedReputation -> { + byte[] candidateHash = BondedReputation.getHash(); + /* log.error("getBondedReputationFromHash: equals?={}, hash={}, candidateHash={}\nBondedReputation={}", + Arrays.equals(candidateHash, hash), + Utilities.bytesAsHexString(hash), + Utilities.bytesAsHexString(candidateHash), + BondedReputation.toString());*/ + return Arrays.equals(candidateHash, hash); + }) + .findAny(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void persist() { + storage.queueUpForSave(20); + } + + + /*public static Optional getBondedReputationByLockupTxId(String lockupTxId) { + return BondedReputations.stream() + .filter(BondedReputation -> BondedReputation.getLockupTxId().equals(lockupTxId)). + findAny(); + }*/ + + public static Optional getBondedReputationByHashOfBondId(byte[] hash) { + return Optional.empty(); + /* BondedReputations.stream() + .filter(BondedReputation -> Arrays.equals(BondedReputation.getHash(), hash)) + .findAny();*/ + } +} diff --git a/core/src/main/java/bisq/core/dao/bonding/lockup/LockupService.java b/core/src/main/java/bisq/core/dao/bonding/lockup/LockupService.java index fdc93760c7..08479553a1 100644 --- a/core/src/main/java/bisq/core/dao/bonding/lockup/LockupService.java +++ b/core/src/main/java/bisq/core/dao/bonding/lockup/LockupService.java @@ -26,7 +26,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletsManager; import bisq.core.dao.bonding.BondingConsensus; -import bisq.core.dao.governance.role.BondedRole; +import bisq.core.dao.bonding.bond.BondWithHash; import bisq.core.dao.governance.role.BondedRolesService; import bisq.common.handlers.ExceptionHandler; @@ -65,13 +65,13 @@ public class LockupService { this.btcWalletService = btcWalletService; } - public void publishLockupTx(Coin lockupAmount, int lockTime, LockupType lockupType, BondedRole bondedRole, + public void publishLockupTx(Coin lockupAmount, int lockTime, LockupType lockupType, BondWithHash bondWithHash, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { checkArgument(lockTime <= BondingConsensus.getMaxLockTime() && lockTime >= BondingConsensus.getMinLockTime(), "lockTime not in rage"); try { - byte[] hash = BondingConsensus.getHash(lockupType, bondedRole); + byte[] hash = BondingConsensus.getHash(bondWithHash); byte[] opReturnData = BondingConsensus.getLockupOpReturnData(lockTime, lockupType, hash); final Transaction lockupTx = getLockupTx(lockupAmount, opReturnData); diff --git a/core/src/main/java/bisq/core/dao/governance/role/BondedRole.java b/core/src/main/java/bisq/core/dao/governance/role/BondedRole.java index 997a6a75bf..a936c287eb 100644 --- a/core/src/main/java/bisq/core/dao/governance/role/BondedRole.java +++ b/core/src/main/java/bisq/core/dao/governance/role/BondedRole.java @@ -19,6 +19,7 @@ package bisq.core.dao.governance.role; import bisq.core.dao.DaoFacade; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.bonding.bond.BondWithHash; import bisq.core.locale.Res; import bisq.common.crypto.Hash; @@ -42,7 +43,7 @@ import javax.annotation.Nullable; @Slf4j @Getter -public final class BondedRole implements PersistablePayload, NetworkPayload { +public final class BondedRole implements PersistablePayload, NetworkPayload, BondWithHash { private final String uid; private final String name; private final String link; @@ -136,6 +137,12 @@ public final class BondedRole implements PersistablePayload, NetworkPayload { // Utils /////////////////////////////////////////////////////////////////////////////////////////// + @Override + public String getUnlockTxId() { + return unlockTxId; + } + + @Override public byte[] getHash() { // We use only the immutable data as input for hash byte[] bytes = BigInteger.valueOf(hashCode()).toByteArray(); diff --git a/core/src/main/java/bisq/core/dao/governance/role/BondedRoleList.java b/core/src/main/java/bisq/core/dao/governance/role/BondedRoleList.java index a2de1debb7..71f1a15b93 100644 --- a/core/src/main/java/bisq/core/dao/governance/role/BondedRoleList.java +++ b/core/src/main/java/bisq/core/dao/governance/role/BondedRoleList.java @@ -68,7 +68,7 @@ public class BondedRoleList extends PersistableList implements Conse @Override public String toString() { - return "List of UID's in BondedRoleList: " + getList().stream() + return "List of UIDs in BondedRoleList: " + getList().stream() .map(BondedRole::getUid) .collect(Collectors.toList()); } diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index aba22f10a4..5b4a65943c 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -19,6 +19,7 @@ package bisq.core.dao.state; import bisq.core.dao.DaoSetupService; import bisq.core.dao.bonding.BondingConsensus; +import bisq.core.dao.bonding.bond.BondWithHash; import bisq.core.dao.governance.role.BondedRole; import bisq.core.dao.governance.voteresult.DecryptedBallotsWithMerits; import bisq.core.dao.governance.voteresult.EvaluatedProposal; @@ -752,8 +753,8 @@ public class DaoStateService implements DaoSetupService { // txOutput.setTxOutputType(TxOutputType.BTC_OUTPUT); } - public boolean isUnlocking(BondedRole bondedRole) { - Optional optionalTx = getTx(bondedRole.getUnlockTxId()); + public boolean isUnlocking(BondWithHash bondWithHash) { + Optional optionalTx = getTx(bondWithHash.getUnlockTxId()); return optionalTx.isPresent() && isUnlockingOutput(optionalTx.get().getTxOutputs().get(0)); } diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index e663342cb7..521c9eb9e9 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -20,6 +20,7 @@ package bisq.core.setup; import bisq.core.arbitration.DisputeManager; import bisq.core.btc.model.AddressEntryList; import bisq.core.dao.DaoOptionKeys; +import bisq.core.dao.bonding.bond.BondedReputationService; import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.MyBlindVoteListService; @@ -68,6 +69,7 @@ public class CorePersistedDataHost { persistedDataHosts.add(injector.getInstance(MyVoteListService.class)); persistedDataHosts.add(injector.getInstance(MyProposalListService.class)); persistedDataHosts.add(injector.getInstance(BondedRolesService.class)); + persistedDataHosts.add(injector.getInstance(BondedReputationService.class)); persistedDataHosts.add(injector.getInstance(AssetService.class)); } return persistedDataHosts; diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 3905483553..b859859b6b 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1348,6 +1348,8 @@ dao.bond.bondedRoleType.details.link=Link to role description dao.bond.bondedRoleType.details.isSingleton=Can be taken by multiple role holders dao.bond.bondedRoleType.details.blocks={0} blocks +dao.bond.bondedReputation=Bonded Reputation + dao.bond.table.header=Bonded roles dao.bond.table.column.header.name=Name dao.bond.table.column.header.linkToAccount=Account diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java index 12d3feb416..ce5772dfe2 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java @@ -26,6 +26,8 @@ import bisq.desktop.util.GUIUtil; import bisq.core.btc.setup.WalletsSetup; import bisq.core.dao.DaoFacade; +import bisq.core.dao.bonding.bond.BondWithHash; +import bisq.core.dao.bonding.bond.BondedReputation; import bisq.core.dao.bonding.lockup.LockupType; import bisq.core.dao.governance.role.BondedRole; import bisq.core.dao.governance.role.BondedRoleType; @@ -65,12 +67,9 @@ public class BondingViewUtils { this.bsqFormatter = bsqFormatter; } - public void lockupBondForBondedRole(BondedRole bondedRole, ResultHandler resultHandler) { + private void lockupBond(BondWithHash bondWithHash, Coin lockupAmount, int lockupTime, LockupType lockupType, + 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), @@ -81,7 +80,7 @@ public class BondingViewUtils { daoFacade.publishLockupTx(lockupAmount, lockupTime, lockupType, - bondedRole, + bondWithHash, () -> { new Popup<>().feedback(Res.get("dao.tx.published.success")).show(); }, @@ -97,6 +96,18 @@ public class BondingViewUtils { } } + public void lockupBondForBondedRole(BondedRole bondedRole, ResultHandler resultHandler) { + BondedRoleType bondedRoleType = bondedRole.getBondedRoleType(); + Coin lockupAmount = Coin.valueOf(bondedRoleType.getRequiredBond()); + int lockupTime = bondedRoleType.getUnlockTime(); + lockupBond(bondedRole, lockupAmount, lockupTime, LockupType.BONDED_ROLE, resultHandler); + } + + public void lockupBondForReputation(Coin lockupAmount, int lockupTime, ResultHandler resultHandler) { + BondedReputation bondedReputation = BondedReputation.createBondedReputation(); + lockupBond(bondedReputation, lockupAmount, lockupTime, LockupType.REPUTATION, resultHandler); + } + public void unLock(String lockupTxId) { if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { Optional lockupTxOutput = daoFacade.getLockupTxOutput(lockupTxId); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java index e84404d331..187e6f214d 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/lockup/LockupView.java @@ -20,6 +20,7 @@ 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.components.TitledGroupBg; import bisq.desktop.main.dao.bonding.BondingViewUtils; import bisq.desktop.main.dao.wallet.BsqBalanceUtil; import bisq.desktop.util.FormBuilder; @@ -37,12 +38,15 @@ import bisq.core.locale.Res; import bisq.core.util.BsqFormatter; import bisq.core.util.validation.IntegerValidator; +import bisq.common.util.Tuple2; + import org.bitcoinj.core.Coin; import javax.inject.Inject; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.scene.layout.GridPane; import javafx.beans.value.ChangeListener; @@ -71,6 +75,7 @@ public class LockupView extends ActivatableView implements BsqBa private InputTextField amountInputTextField; private InputTextField timeInputTextField; private ComboBox lockupTypeComboBox; + private Label bondedRolesLabel; private ComboBox bondedRolesComboBox; private Button lockupButton; private ChangeListener focusOutListener; @@ -106,7 +111,8 @@ public class LockupView extends ActivatableView implements BsqBa public void initialize() { gridRow = bsqBalanceUtil.addGroup(root, gridRow); - addTitledGroupBg(root, ++gridRow, 4, Res.get("dao.bonding.lock.lockBSQ"), Layout.GROUP_DISTANCE); + TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 4, Res.get("dao.bonding.lock.lockBSQ"), + Layout.GROUP_DISTANCE); amountInputTextField = addInputTextField(root, gridRow, Res.get("dao.bonding.lock.amount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); @@ -119,6 +125,7 @@ public class LockupView extends ActivatableView implements BsqBa timeInputTextField.setValidator(timeInputTextFieldValidator); lockupTypeComboBox = FormBuilder.addComboBox(root, ++gridRow, Res.get("dao.bonding.lock.type")); + lockupTypeComboBox.setPromptText(Res.get("shared.select")); lockupTypeComboBox.setConverter(new StringConverter<>() { @Override public String toString(LockupType lockupType) { @@ -135,11 +142,23 @@ public class LockupView extends ActivatableView implements BsqBa if (newValue != null) { bondedRolesComboBox.getSelectionModel().clearSelection(); } + int lockupRows = 3; + if (newValue == LockupType.BONDED_ROLE) { + bondedRolesComboBox.setVisible(true); + bondedRolesLabel.setVisible(true); + lockupRows++; + } else { + bondedRolesComboBox.setVisible(false); + bondedRolesLabel.setVisible(false); + } + GridPane.setRowSpan(titledGroupBg, lockupRows); + GridPane.setRowIndex(lockupButton, GridPane.getRowIndex(amountInputTextField) + lockupRows); }; //TODO handle trade type lockupTypeComboBox.getSelectionModel().select(0); - bondedRolesComboBox = FormBuilder.addComboBox(root, ++gridRow, Res.get("dao.bonding.lock.bondedRoles")); + bondedRolesComboBox = FormBuilder.addComboBox(root, ++gridRow, Res.get("dao.bonding.lock.bondedRoles")); + bondedRolesComboBox.setPromptText(Res.get("shared.select")); bondedRolesComboBox.setConverter(new StringConverter<>() { @Override public String toString(BondedRole bondedRole) { @@ -171,10 +190,24 @@ public class LockupView extends ActivatableView implements BsqBa lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bonding.lock.lockupButton")); lockupButton.setOnAction((event) -> { - bondingViewUtils.lockupBondForBondedRole(bondedRolesComboBox.getValue(), - () -> { - bondedRolesComboBox.getSelectionModel().clearSelection(); - }); + switch (lockupTypeComboBox.getValue()) { + case BONDED_ROLE: + if (bondedRolesComboBox.getValue() != null) { + bondingViewUtils.lockupBondForBondedRole(bondedRolesComboBox.getValue(), + () -> bondedRolesComboBox.getSelectionModel().clearSelection()); + } + break; + case REPUTATION: + bondingViewUtils.lockupBondForReputation(bsqFormatter.parseToCoin(amountInputTextField.getText()), + Integer.parseInt(timeInputTextField.getText()), + () -> { + amountInputTextField.setText(""); + timeInputTextField.setText(""); + }); + break; + default: + log.error("Unknown lockup option=" + lockupTypeComboBox.getValue()); + } }); focusOutListener = (observable, oldValue, newValue) -> {