diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index a1e57b10742..b8332104908 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 2acbbd5c0f5..72ebc9b7949 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -17,11 +17,14 @@ package bisq.core.app.misc; +import bisq.core.dao.DaoOptionKeys; 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; import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.proposal.MyProposalListService; import bisq.core.dao.governance.role.BondedRolesService; import bisq.core.filter.FilterManager; import bisq.core.payment.AccountAgeWitnessService; @@ -33,6 +36,7 @@ import bisq.common.crypto.KeyRing; import javax.inject.Inject; +import javax.inject.Named; import lombok.extern.slf4j.Slf4j; @@ -51,8 +55,11 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, MyVoteListService myVoteListService, BallotListService ballotListService, MyBlindVoteListService myBlindVoteListService, + MyProposalListService myProposalListService, BondedRolesService bondedRolesService, - AssetService assetService) { + BondedReputationService bondedReputationService, + AssetService assetService, + @Named(DaoOptionKeys.DAO_ACTIVATED) boolean daoActivated) { super(encryptionService, keyRing, p2PService, @@ -62,11 +69,16 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, this.daoSetup = daoSetup; - persistedDataHosts.add(myVoteListService); - persistedDataHosts.add(ballotListService); - persistedDataHosts.add(myBlindVoteListService); - persistedDataHosts.add(bondedRolesService); - persistedDataHosts.add(assetService); + // TODO Should be refactored/removed. In the meantime keep in sync with CorePersistedDataHost + if (daoActivated) { + persistedDataHosts.add(myVoteListService); + persistedDataHosts.add(ballotListService); + persistedDataHosts.add(myBlindVoteListService); + persistedDataHosts.add(myProposalListService); + persistedDataHosts.add(bondedRolesService); + persistedDataHosts.add(bondedReputationService); + persistedDataHosts.add(assetService); + } } @Override diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 1a6f4aacffa..98ae2d52056 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 @@ 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 DaoFacade(MyProposalListService myProposalListService, GenericProposalService genericProposalService, RemoveAssetProposalService removeAssetProposalService, BondedRolesService bondedRolesService, + BondedReputationService bondedReputationService, LockupService lockupService, UnlockService unlockService, DaoStateStorageService daoStateStorageService) { @@ -153,6 +158,7 @@ public DaoFacade(MyProposalListService myProposalListService, 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 List getBondedRoleList() { 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 int getChainHeight() { // 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 List getValidBondedRoleList() { return bondedRolesService.getValidBondedRoleList(); } + /*public List getValidBondedReputationList() { + return bondedReputationService.getValidBondedReputationList(); + }*/ + /////////////////////////////////////////////////////////////////////////////////////////// // Use case: Present transaction related state @@ -549,6 +563,10 @@ public Optional getLockupTxOutput(String txId) { return daoStateService.getLockupTxOutput(txId); } + public Optional getLockupOpReturnTxOutput(String txId) { + return daoStateService.getLockupOpReturnTxOutput(txId); + } + public long getTotalBurntFee() { return daoStateService.getTotalBurntFee(); } @@ -605,8 +623,12 @@ public Optional getBondedRoleFromHash(byte[] hash) { 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 fd0b29b724f..597b791f7aa 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 @@ protected void configure() { 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 eb847069738..51fafdadb0a 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 static byte[] getHashFromOpReturnData(byte[] opReturnData) { 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 00000000000..3419e7dc7e2 --- /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 00000000000..3ffc41cb949 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputation.java @@ -0,0 +1,151 @@ +/* + * 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.DaoStateService; +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 com.google.common.base.Charsets; + +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; + + @Nullable + @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()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String getUnlockTxId() { + return unlockTxId; + } + + @Override + public byte[] getHash() { + // We use the salt as input for the hash + byte[] bytes = salt.getBytes(Charsets.UTF_8); + 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(DaoStateService daoStateService) { + return daoStateService.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 00000000000..65f78621e5e --- /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 00000000000..6f3383e5490 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/bonding/bond/BondedReputationService.java @@ -0,0 +1,199 @@ +/* + * 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.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +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, DaoStateListener { + + public interface BondedReputationListChangeListener { + void onListChanged(List list); + } + + private final DaoStateService daoStateService; + private final Storage storage; + private final BondedReputationList bondedReputationList = new BondedReputationList(); + + @Getter + private final List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public BondedReputationService(Storage storage, DaoStateService daoStateService) { + this.storage = storage; + this.daoStateService = daoStateService; + + daoStateService.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())); + } + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + bondedReputationList.getList().forEach(bondedReputation -> { + daoStateService.getLockupTxOutputs().forEach(lockupTxOutput -> { + String lockupTxId = lockupTxOutput.getTxId(); + daoStateService.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 (!daoStateService.isUnspent(lockupTxOutput.getKey())) { + daoStateService.getSpentInfo(lockupTxOutput) + .map(SpentInfo::getTxId) + .map(daoStateService::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() { + //TODO validation ??? + return bondedReputationList.getList(); + }*/ + + public Optional getBondedReputationFromHash(byte[] hash) { + return bondedReputationList.getList().stream() + .filter(bondedReputation -> { + byte[] candidateHash = bondedReputation.getHash(); + /* log.error("getBondedReputationFromHash: equals?={}, hash={}, candidateHash={}\bondedReputation={}", + 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 fdc93760c72..d8e5cbb543e 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,6 +26,7 @@ import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletsManager; 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.role.BondedRolesService; @@ -65,13 +66,13 @@ public LockupService(WalletsManager walletsManager, 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); @@ -79,6 +80,14 @@ public void publishLockupTx(Coin lockupAmount, int lockTime, LockupType lockupTy walletsManager.publishAndCommitBsqTx(lockupTx, new TxBroadcaster.Callback() { @Override public void onSuccess(Transaction transaction) { + + // TODO we should not support repeated locks + if (bondWithHash instanceof BondedRole) { + BondedRole bondedRole = (BondedRole) bondWithHash; + bondedRole.setLockupTxId(transaction.getHashAsString()); + bondedRole.setUnlockTxId(null); + } + resultHandler.handleResult(); } 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 997a6a75bf9..64ffeede2eb 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 @@ -18,6 +18,7 @@ package bisq.core.dao.governance.role; import bisq.core.dao.DaoFacade; +import bisq.core.dao.bonding.bond.BondWithHash; import bisq.core.dao.state.DaoStateService; import bisq.core.locale.Res; @@ -42,7 +43,7 @@ @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; @@ -133,9 +134,15 @@ public static BondedRole fromProto(PB.BondedRole proto) { /////////////////////////////////////////////////////////////////////////////////////////// - // Utils + // BondWithHash implementation /////////////////////////////////////////////////////////////////////////////////////////// + @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(); @@ -145,6 +152,11 @@ public byte[] getHash() { return hash; } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + public String getDisplayString() { return name + " / " + Res.get("dao.bond.bondedRoleType." + bondedRoleType.name()); } 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 a2de1debb7f..e08885b4bbf 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,8 +68,8 @@ public static BondedRoleList fromProto(PB.BondedRoleList proto) { @Override public String toString() { - return "List of UID's in BondedRoleList: " + getList().stream() - .map(BondedRole::getUid) + return "List of lockupTxIds in BondedRoleList: " + getList().stream() + .map(BondedRole::getLockupTxId) .collect(Collectors.toList()); } } diff --git a/core/src/main/java/bisq/core/dao/governance/role/BondedRoleType.java b/core/src/main/java/bisq/core/dao/governance/role/BondedRoleType.java index b912952f705..9e49280e0d9 100644 --- a/core/src/main/java/bisq/core/dao/governance/role/BondedRoleType.java +++ b/core/src/main/java/bisq/core/dao/governance/role/BondedRoleType.java @@ -23,6 +23,8 @@ // Data here must not be changed as it would break backward compatibility! In case we need to change we need to add a new // entry and maintain the old one. Once all the role holders of an old deprecated role have revoked the role might get removed. + +// Add entry to translation file "dao.bond.bondedRoleType...." public enum BondedRoleType { // admins GITHUB_ADMIN(50_000, 60, "https://github.com/bisq-network/roles/issues/16", true), @@ -70,7 +72,8 @@ public enum BondedRoleType { */ BondedRoleType(long requiredBondInBsq, int unlockTimeInDays, String link, boolean allowMultipleHolders) { this.requiredBond = requiredBondInBsq * 100; - this.unlockTimeInBlocks = unlockTimeInDays * 144; + this.unlockTimeInBlocks = 5; // TODO for dev testing + //this.unlockTimeInBlocks = unlockTimeInDays * 144; this.link = link; this.allowMultipleHolders = allowMultipleHolders; } diff --git a/core/src/main/java/bisq/core/dao/governance/role/BondedRolesService.java b/core/src/main/java/bisq/core/dao/governance/role/BondedRolesService.java index 023b48adc21..d860dce654f 100644 --- a/core/src/main/java/bisq/core/dao/governance/role/BondedRolesService.java +++ b/core/src/main/java/bisq/core/dao/governance/role/BondedRolesService.java @@ -21,6 +21,7 @@ import bisq.core.dao.bonding.BondingConsensus; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.blockchain.BaseTxOutput; import bisq.core.dao.state.blockchain.Block; import bisq.core.dao.state.blockchain.SpentInfo; import bisq.core.dao.state.blockchain.TxType; @@ -179,25 +180,46 @@ public Optional getBondedRoleFromHash(byte[] hash) { .findAny(); } + public Optional getBondedRoleFromLockupTxId(String lockupTxId) { + return bondedRoleList.getList().stream() + .filter(bondedRole -> lockupTxId.equals(bondedRole.getLockupTxId())) + .findAny(); + } + + + public Optional getBondedRoleType(String lockUpTxId) { + Optional bondedRoleType = getBondedRoleFromLockupTxId(lockUpTxId).map(BondedRole::getBondedRoleType); + return bondedRoleType; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// + private Optional getOpReturnData(String lockUpTxId) { + return daoStateService.getLockupOpReturnTxOutput(lockUpTxId).map(BaseTxOutput::getOpReturnData); + } + private void persist() { storage.queueUpForSave(20); } + /* private Optional getOptionalLockupType(String lockUpTxId) { + return getOpReturnData(lockUpTxId) + .flatMap(BondingConsensus::getLockupType); + }*/ /*public static Optional getBondedRoleByLockupTxId(String lockupTxId) { return bondedRoles.stream() .filter(bondedRole -> bondedRole.getLockupTxId().equals(lockupTxId)). findAny(); }*/ - +/* public static Optional getBondedRoleByHashOfBondId(byte[] hash) { return Optional.empty(); - /* bondedRoles.stream() + *//* bondedRoles.stream() .filter(bondedRole -> Arrays.equals(bondedRole.getHash(), hash)) - .findAny();*/ - } + .findAny();*//* + }*/ } 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 aba22f10a4a..b6dd75b202e 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -19,7 +19,7 @@ import bisq.core.dao.DaoSetupService; 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.voteresult.DecryptedBallotsWithMerits; import bisq.core.dao.governance.voteresult.EvaluatedProposal; import bisq.core.dao.state.blockchain.Block; @@ -646,6 +646,10 @@ public Optional getLockupTxOutput(String txId) { .findFirst()); } + public Optional getLockupOpReturnTxOutput(String txId) { + return getTx(txId).map(Tx::getLastTxOutput); + } + // Returns amount of all LOCKUP txOutputs (they might have been unlocking or unlocked in the meantime) public long getTotalAmountOfLockupTxOutputs() { return getLockupTxOutputs().stream() @@ -752,8 +756,8 @@ public void applyConfiscateBond(TxOutput txOutput) { // 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/dao/state/governance/Param.java b/core/src/main/java/bisq/core/dao/state/governance/Param.java index 0421db4cdd3..91017a4645a 100644 --- a/core/src/main/java/bisq/core/dao/state/governance/Param.java +++ b/core/src/main/java/bisq/core/dao/state/governance/Param.java @@ -110,7 +110,7 @@ public enum Param { // TODO for dev testing we use short periods... // Period phase ("11 blocks atm) PHASE_UNDEFINED("0", ParamType.BLOCK), - PHASE_PROPOSAL("2", ParamType.BLOCK, 3, 3), + PHASE_PROPOSAL("4", ParamType.BLOCK, 3, 3), PHASE_BREAK1("1", ParamType.BLOCK, 3, 3), PHASE_BLIND_VOTE("2", ParamType.BLOCK, 3, 3), PHASE_BREAK2("1", ParamType.BLOCK, 3, 23), diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index e663342cb78..521c9eb9e94 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 @@ 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 static List getPersistedDataHosts(Injector injector) { 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 39054835532..f1e345f16d3 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1323,6 +1323,8 @@ dao.bonding.lock.sendFunds.headline=Confirm lockup transaction dao.bonding.lock.sendFunds.details=Lockup amount: {0}\nLockup time: {1} block(s)\n\nAre you sure you want to proceed? dao.bonding.unlock.time=Lock time dao.bonding.unlock.unlock=Unlock +dao.bonding.unlock.type=Type +dao.bonding.unlock.reputation=Reputation dao.bonding.unlock.sendTx.headline=Confirm unlock transaction dao.bonding.unlock.sendTx.details=Unlock amount: {0}\nLockup time: {1} block(s)\n\nAre you sure you want to proceed? dao.bonding.dashboard.bondsHeadline=Bonded BSQ @@ -1333,12 +1335,41 @@ dao.bonding.dashboard.unlockingAmount=Unlocking funds (wait until lock time is o dao.bond.lockupType.BONDED_ROLE=Bonded role # suppress inspection "UnusedProperty" dao.bond.lockupType.REPUTATION=Bonded reputation + # suppress inspection "UnusedProperty" -dao.bond.bondedRoleType.ARBITRATOR=Arbitrator +dao.bond.bondedRoleType.GITHUB_ADMIN=Github admin # suppress inspection "UnusedProperty" -dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Domain name holder +dao.bond.bondedRoleType.FORUM_ADMIN=Forum admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.TWITTER_ADMIN=Twitter admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.SLACK_ADMIN=Slack admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.YOUTUBE_ADMIN=Youtube admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BISQ_MAINTAINER=Bisq maintainer +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.WEBSITE_OPERATOR=Website operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.FORUM_OPERATOR=Forum operator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.SEED_NODE_OPERATOR=Seed node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.PRICE_NODE_OPERATOR=Price node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Btc node operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MARKETS_OPERATOR=Markets operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=BSQ explorer operator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DOMAIN_NAME_HOLDER=Domain name holder +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.DNS_ADMIN=DNS admin +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.MEDIATOR=Mediator +# suppress inspection "UnusedProperty" +dao.bond.bondedRoleType.ARBITRATOR=Arbitrator dao.bond.bondedRoleType.details.header=Role details dao.bond.bondedRoleType.details.role=Role @@ -1348,6 +1379,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 b0c90a3fcd6..bf78c2ecde4 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.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; @@ -35,6 +37,7 @@ import bisq.network.p2p.P2PService; +import bisq.common.app.DevEnv; import bisq.common.handlers.ResultHandler; import org.bitcoinj.core.Coin; @@ -65,39 +68,55 @@ public BondingViewUtils(P2PService p2PService, WalletsSetup walletsSetup, DaoFac 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.getUnlockTimeInBlocks(); - 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(); + if (!DevEnv.isDevMode()) { + 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(() -> publishLockupTx(bondWithHash, lockupAmount, lockupTime, lockupType, resultHandler)) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + publishLockupTx(bondWithHash, lockupAmount, lockupTime, lockupType, resultHandler); + } } else { GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } } - public void unLock(String lockupTxId) { + private void publishLockupTx(BondWithHash bondWithHash, Coin lockupAmount, int lockupTime, LockupType lockupType, ResultHandler resultHandler) { + daoFacade.publishLockupTx(lockupAmount, + lockupTime, + lockupType, + bondWithHash, + () -> { + if (!DevEnv.isDevMode()) + new Popup<>().feedback(Res.get("dao.tx.published.success")).show(); + }, + this::handleError + ); + if (resultHandler != null) + resultHandler.handleResult(); + } + + public void lockupBondForBondedRole(BondedRole bondedRole, ResultHandler resultHandler) { + BondedRoleType bondedRoleType = bondedRole.getBondedRoleType(); + Coin lockupAmount = Coin.valueOf(bondedRoleType.getRequiredBond()); + int lockupTime = bondedRoleType.getUnlockTimeInBlocks(); + 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, ResultHandler resultHandler) { if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { Optional lockupTxOutput = daoFacade.getLockupTxOutput(lockupTxId); if (!lockupTxOutput.isPresent()) { @@ -110,22 +129,19 @@ public void unLock(String 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(); + if (!DevEnv.isDevMode()) { + 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(() -> publishUnlockTx(lockupTxId, resultHandler)) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + publishUnlockTx(lockupTxId, resultHandler); + } } catch (Throwable t) { log.error(t.toString()); t.printStackTrace(); @@ -137,6 +153,18 @@ public void unLock(String lockupTxId) { log.info("unlock tx: {}", lockupTxId); } + private void publishUnlockTx(String lockupTxId, ResultHandler resultHandler) { + daoFacade.publishUnlockTx(lockupTxId, + () -> { + if (!DevEnv.isDevMode()) + new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + + resultHandler.handleResult(); + }, + errorMessage -> new Popup<>().warning(errorMessage.toString()).show() + ); + } + private void handleError(Throwable throwable) { if (throwable instanceof InsufficientMoneyException) { final Coin missingCoin = ((InsufficientMoneyException) throwable).missing; 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 373583d1da3..924493cbb08 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 @@ 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; @@ -28,7 +29,6 @@ import bisq.core.btc.listeners.BsqBalanceListener; import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.btc.wallet.Restrictions; import bisq.core.dao.DaoFacade; import bisq.core.dao.bonding.BondingConsensus; import bisq.core.dao.bonding.lockup.LockupType; @@ -73,10 +73,11 @@ public class LockupView extends ActivatableView implements BsqBa private ComboBox lockupTypeComboBox; private ComboBox bondedRolesComboBox; private Button lockupButton; - private ChangeListener focusOutListener; - private ChangeListener inputTextFieldListener; + private ChangeListener amountFocusOutListener, timeFocusOutListener; + private ChangeListener amountInputTextFieldListener, timeInputTextFieldListener; private ChangeListener bondedRolesListener; private ChangeListener lockupTypeListener; + private TitledGroupBg titledGroupBg; /////////////////////////////////////////////////////////////////////////////////////////// @@ -106,19 +107,27 @@ private LockupView(BsqWalletService bsqWalletService, public void initialize() { gridRow = bsqBalanceUtil.addGroup(root, gridRow); - addTitledGroupBg(root, ++gridRow, 4, Res.get("dao.bonding.lock.lockBSQ"), Layout.GROUP_DISTANCE); + int columnSpan = 3; + titledGroupBg = addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.bonding.lock.lockBSQ"), + Layout.GROUP_DISTANCE); + GridPane.setColumnSpan(titledGroupBg, columnSpan); amountInputTextField = addInputTextField(root, gridRow, Res.get("dao.bonding.lock.amount"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); - amountInputTextField.setPromptText(Res.get("dao.bonding.lock.setAmount", bsqFormatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); + //amountInputTextField.setPromptText(Res.get("dao.bonding.lock.setAmount", bsqFormatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); amountInputTextField.setValidator(bsqValidator); + GridPane.setColumnSpan(amountInputTextField, columnSpan); timeInputTextField = FormBuilder.addInputTextField(root, ++gridRow, Res.get("dao.bonding.lock.time")); - timeInputTextField.setPromptText(Res.get("dao.bonding.lock.setTime", - String.valueOf(BondingConsensus.getMinLockTime()), String.valueOf(BondingConsensus.getMaxLockTime()))); + + /* timeInputTextField.setPromptText(Res.get("dao.bonding.lock.setTime", + String.valueOf(BondingConsensus.getMinLockTime()), String.valueOf(BondingConsensus.getMaxLockTime())));*/ + timeInputTextField.setValidator(timeInputTextFieldValidator); + GridPane.setColumnSpan(timeInputTextField, columnSpan); lockupTypeComboBox = FormBuilder.addComboBox(root, ++gridRow, Res.get("dao.bonding.lock.type")); + GridPane.setColumnSpan(lockupTypeComboBox, columnSpan); lockupTypeComboBox.setConverter(new StringConverter<>() { @Override public String toString(LockupType lockupType) { @@ -135,11 +144,24 @@ public LockupType fromString(String string) { if (newValue != null) { bondedRolesComboBox.getSelectionModel().clearSelection(); } + int lockupRows = 3; + if (newValue == LockupType.BONDED_ROLE) { + bondedRolesComboBox.setVisible(true); + lockupRows++; + + bondedRolesComboBox.setItems(FXCollections.observableArrayList(daoFacade.getBondedRoleList())); + } else { + bondedRolesComboBox.setVisible(false); + bondedRolesComboBox.getItems().clear(); + } + 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")); + GridPane.setColumnSpan(bondedRolesComboBox, columnSpan); + bondedRolesComboBox.setVisible(false); bondedRolesComboBox.setConverter(new StringConverter<>() { @Override public String toString(BondedRole bondedRole) { @@ -169,36 +191,60 @@ public BondedRole fromString(String string) { } }; - lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bonding.lock.lockupButton")); + 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) -> { + amountFocusOutListener = (observable, oldValue, newValue) -> { if (!newValue) { updateButtonState(); onUpdateBalances(); } }; - inputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + timeFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + onUpdateBalances(); + } + }; + amountInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + timeInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); } @Override protected void activate() { bsqBalanceUtil.activate(); - amountInputTextField.textProperty().addListener(inputTextFieldListener); - timeInputTextField.textProperty().addListener(inputTextFieldListener); - amountInputTextField.focusedProperty().addListener(focusOutListener); + amountInputTextField.textProperty().addListener(amountInputTextFieldListener); + timeInputTextField.textProperty().addListener(timeInputTextFieldListener); + amountInputTextField.focusedProperty().addListener(amountFocusOutListener); + timeInputTextField.focusedProperty().addListener(timeFocusOutListener); lockupTypeComboBox.getSelectionModel().selectedItemProperty().addListener(lockupTypeListener); bondedRolesComboBox.getSelectionModel().selectedItemProperty().addListener(bondedRolesListener); bsqWalletService.addBsqBalanceListener(this); - bondedRolesComboBox.setItems(FXCollections.observableArrayList(daoFacade.getBondedRoleList())); + lockupTypeComboBox.getSelectionModel().clearSelection(); + bondedRolesComboBox.getSelectionModel().clearSelection(); + onUpdateBalances(); } @@ -206,9 +252,10 @@ protected void activate() { protected void deactivate() { bsqBalanceUtil.deactivate(); - amountInputTextField.textProperty().removeListener(inputTextFieldListener); - timeInputTextField.textProperty().removeListener(inputTextFieldListener); - amountInputTextField.focusedProperty().removeListener(focusOutListener); + amountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + timeInputTextField.textProperty().removeListener(timeInputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + timeInputTextField.focusedProperty().removeListener(timeFocusOutListener); lockupTypeComboBox.getSelectionModel().selectedItemProperty().removeListener(lockupTypeListener); bondedRolesComboBox.getSelectionModel().selectedItemProperty().removeListener(bondedRolesListener); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java index d4bc46dff64..9a2916294fb 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesListItem.java @@ -96,7 +96,6 @@ private void update() { 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) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java index cdbb158247b..3a75156ae02 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/roles/BondedRolesView.java @@ -433,12 +433,11 @@ public void updateItem(final BondedRolesListItem item, boolean empty) { column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setMinWidth(80); column.setCellFactory( - new Callback, TableCell>() { + new Callback<>() { @Override public TableCell call(TableColumn column) { - return new TableCell() { + return new TableCell<>() { Button button; @Override @@ -450,7 +449,11 @@ public void updateItem(final BondedRolesListItem item, boolean empty) { button = item.getButton(); item.setOnAction(() -> { if (item.isBonded()) - bondingViewUtils.unLock(item.getBondedRole().getLockupTxId()); + bondingViewUtils.unLock(item.getBondedRole().getLockupTxId(), + () -> { + // TODO + button.setDisable(true); + }); else bondingViewUtils.lockupBondForBondedRole(item.getBondedRole(), null); }); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java index 2c80174829c..0a55270db7a 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/LockupTxListItem.java @@ -25,6 +25,12 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; 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.dao.governance.role.BondedRoleType; +import bisq.core.dao.governance.role.BondedRolesService; +import bisq.core.dao.state.blockchain.BaseTxOutput; import bisq.core.dao.state.blockchain.TxOutput; import bisq.core.dao.state.blockchain.TxType; import bisq.core.locale.Res; @@ -38,16 +44,19 @@ import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @EqualsAndHashCode(callSuper = true) @Data +@Slf4j class LockupTxListItem extends TxConfidenceListItem { private final BtcWalletService btcWalletService; private final DaoFacade daoFacade; private final BsqFormatter bsqFormatter; + private final BondedRolesService bondedRolesService; private final Date date; private Coin amount = Coin.ZERO; @@ -62,12 +71,14 @@ class LockupTxListItem extends TxConfidenceListItem { BsqWalletService bsqWalletService, BtcWalletService btcWalletService, DaoFacade daoFacade, + BondedRolesService bondedRolesService, Date date, BsqFormatter bsqFormatter) { super(transaction, bsqWalletService); this.btcWalletService = btcWalletService; this.daoFacade = daoFacade; + this.bondedRolesService = bondedRolesService; this.date = date; this.bsqFormatter = bsqFormatter; @@ -88,7 +99,18 @@ class LockupTxListItem extends TxConfidenceListItem { } public boolean isLockupAndUnspent() { - return !isSpent() && getTxType() == TxType.LOCKUP; + boolean isLocked; + Optional optionalBondedRole = bondedRolesService.getBondedRoleFromLockupTxId(txId); + if (optionalBondedRole.isPresent()) { + BondedRole bondedRole = optionalBondedRole.get(); + //TODO + //isLocked = bondedRole.getLockupTxId() != null && bondedRole.getUnlockTxId() == null; + //log.error("isLocked {}, tx={}",isLocked,bondedRole.getLockupTxId()); + } else { + //TODO get reputation + isLocked = true; + } + return /*isLocked && */!isSpent() && getTxType() == TxType.LOCKUP; } private boolean isSpent() { @@ -103,4 +125,28 @@ public TxType getTxType() { .flatMap(tx -> daoFacade.getOptionalTxType(tx.getId())) .orElse(confirmations == 0 ? TxType.UNVERIFIED : TxType.UNDEFINED_TX_TYPE); } + + private Optional getOptionalLockupType() { + return getOpReturnData() + .flatMap(BondingConsensus::getLockupType); + } + + private Optional getOpReturnData() { + return daoFacade.getLockupOpReturnTxOutput(txId).map(BaseTxOutput::getOpReturnData); + } + + public String getInfo() { + Optional optionalRoleType = bondedRolesService.getBondedRoleType(txId); + if (optionalRoleType.isPresent()) { + return optionalRoleType.get().getDisplayString(); + } else { + Optional optionalLockupType = getOptionalLockupType(); + if (optionalLockupType.isPresent()) { + LockupType lockupType = optionalLockupType.get(); + if (lockupType == LockupType.REPUTATION) + return Res.get("dao.bonding.unlock.reputation"); + } + } + return Res.get("shared.na"); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java index a37f0c3375a..f343e4bd59c 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/unlock/UnlockView.java @@ -30,6 +30,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.role.BondedRolesService; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.blockchain.Block; import bisq.core.dao.state.blockchain.TxType; @@ -79,6 +80,7 @@ public class UnlockView extends ActivatableView implements BsqBa private final BsqBalanceUtil bsqBalanceUtil; private final BsqValidator bsqValidator; private final BondingViewUtils bondingViewUtils; + private final BondedRolesService bondedRolesService; private final DaoFacade daoFacade; private final Preferences preferences; @@ -103,6 +105,7 @@ private UnlockView(BsqWalletService bsqWalletService, BsqBalanceUtil bsqBalanceUtil, BsqValidator bsqValidator, BondingViewUtils bondingViewUtils, + BondedRolesService bondedRolesService, DaoFacade daoFacade, Preferences preferences) { this.bsqWalletService = bsqWalletService; @@ -111,6 +114,7 @@ private UnlockView(BsqWalletService bsqWalletService, this.bsqBalanceUtil = bsqBalanceUtil; this.bsqValidator = bsqValidator; this.bondingViewUtils = bondingViewUtils; + this.bondedRolesService = bondedRolesService; this.daoFacade = daoFacade; this.preferences = preferences; } @@ -123,6 +127,7 @@ public void initialize() { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPrefHeight(300); addTxIdColumn(); + addInfoColumn(); addAmountColumn(); addLockTimeColumn(); addUnlockColumn(); @@ -230,6 +235,7 @@ private void updateList() { bsqWalletService, btcWalletService, daoFacade, + bondedRolesService, transaction.getUpdateTime(), bsqFormatter); }) @@ -280,6 +286,34 @@ public void updateItem(final LockupTxListItem item, boolean empty) { tableView.getColumns().add(column); } + private void addInfoColumn() { + TableColumn column = + new AutoTooltipTableColumn<>(Res.get("dao.bonding.unlock.type")); + column.setMinWidth(160); + column.setMaxWidth(column.getMinWidth()); + + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + + @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) { + setText(item.getInfo()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + } + private void addAmountColumn() { TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.amountWithCur", "BSQ")); @@ -348,16 +382,12 @@ 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>() { - + unlockColumn.setCellFactory(new Callback<>() { @Override public TableCell call(TableColumn column) { - return new TableCell() { + return new TableCell<>() { Button button; @Override @@ -367,7 +397,10 @@ public void updateItem(final LockupTxListItem item, boolean empty) { if (item != null && !empty) { if (button == null) { button = item.getButton(); - button.setOnAction(e -> bondingViewUtils.unLock(item.getTxId())); + button.setOnAction(e -> bondingViewUtils.unLock(item.getTxId(), () -> { + //TODO + button.setDisable(true); + })); setGraphic(button); } } else {