mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-03 18:56:59 +01:00
Merge pull request #6423 from HenrikJannsen/add-burningman-accounting
Add burningman accounting
This commit is contained in:
commit
71d6e126dd
120 changed files with 9021 additions and 441 deletions
|
@ -129,6 +129,9 @@ public class Config {
|
||||||
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
|
||||||
public static final String DAO_NODE_API_URL = "daoNodeApiUrl";
|
public static final String DAO_NODE_API_URL = "daoNodeApiUrl";
|
||||||
public static final String DAO_NODE_API_PORT = "daoNodeApiPort";
|
public static final String DAO_NODE_API_PORT = "daoNodeApiPort";
|
||||||
|
public static final String IS_BM_FULL_NODE = "isBmFullNode";
|
||||||
|
public static final String BM_ORACLE_NODE_PUB_KEY = "bmOracleNodePubKey";
|
||||||
|
public static final String BM_ORACLE_NODE_PRIV_KEY = "bmOracleNodePrivKey";
|
||||||
public static final String SEED_NODE_REPORTING_SERVER_URL = "seedNodeReportingServerUrl";
|
public static final String SEED_NODE_REPORTING_SERVER_URL = "seedNodeReportingServerUrl";
|
||||||
|
|
||||||
// Default values for certain options
|
// Default values for certain options
|
||||||
|
@ -221,6 +224,9 @@ public class Config {
|
||||||
public final boolean bypassMempoolValidation;
|
public final boolean bypassMempoolValidation;
|
||||||
public final String daoNodeApiUrl;
|
public final String daoNodeApiUrl;
|
||||||
public final int daoNodeApiPort;
|
public final int daoNodeApiPort;
|
||||||
|
public final boolean isBmFullNode;
|
||||||
|
public final String bmOracleNodePubKey;
|
||||||
|
public final String bmOracleNodePrivKey;
|
||||||
public final String seedNodeReportingServerUrl;
|
public final String seedNodeReportingServerUrl;
|
||||||
|
|
||||||
// Properties derived from options but not exposed as options themselves
|
// Properties derived from options but not exposed as options themselves
|
||||||
|
@ -681,6 +687,23 @@ public class Config {
|
||||||
.withRequiredArg()
|
.withRequiredArg()
|
||||||
.ofType(Integer.class)
|
.ofType(Integer.class)
|
||||||
.defaultsTo(8082);
|
.defaultsTo(8082);
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<Boolean> isBmFullNode =
|
||||||
|
parser.accepts(IS_BM_FULL_NODE, "Run as Burningman full node")
|
||||||
|
.withRequiredArg()
|
||||||
|
.ofType(boolean.class)
|
||||||
|
.defaultsTo(false);
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<String> bmOracleNodePubKey =
|
||||||
|
parser.accepts(BM_ORACLE_NODE_PUB_KEY, "Burningman oracle node public key")
|
||||||
|
.withRequiredArg()
|
||||||
|
.defaultsTo("");
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<String> bmOracleNodePrivKey =
|
||||||
|
parser.accepts(BM_ORACLE_NODE_PRIV_KEY, "Burningman oracle node private key")
|
||||||
|
.withRequiredArg()
|
||||||
|
.defaultsTo("");
|
||||||
|
|
||||||
ArgumentAcceptingOptionSpec<String> seedNodeReportingServerUrlOpt =
|
ArgumentAcceptingOptionSpec<String> seedNodeReportingServerUrlOpt =
|
||||||
parser.accepts(SEED_NODE_REPORTING_SERVER_URL, "URL of seed node reporting server")
|
parser.accepts(SEED_NODE_REPORTING_SERVER_URL, "URL of seed node reporting server")
|
||||||
.withRequiredArg()
|
.withRequiredArg()
|
||||||
|
@ -806,6 +829,9 @@ public class Config {
|
||||||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||||
this.daoNodeApiUrl = options.valueOf(daoNodeApiUrlOpt);
|
this.daoNodeApiUrl = options.valueOf(daoNodeApiUrlOpt);
|
||||||
this.daoNodeApiPort = options.valueOf(daoNodeApiPortOpt);
|
this.daoNodeApiPort = options.valueOf(daoNodeApiPortOpt);
|
||||||
|
this.isBmFullNode = options.valueOf(isBmFullNode);
|
||||||
|
this.bmOracleNodePubKey = options.valueOf(bmOracleNodePubKey);
|
||||||
|
this.bmOracleNodePrivKey = options.valueOf(bmOracleNodePrivKey);
|
||||||
this.seedNodeReportingServerUrl = options.valueOf(seedNodeReportingServerUrlOpt);
|
this.seedNodeReportingServerUrl = options.valueOf(seedNodeReportingServerUrlOpt);
|
||||||
} catch (OptionException ex) {
|
} catch (OptionException ex) {
|
||||||
throw new ConfigException("problem parsing option '%s': %s",
|
throw new ConfigException("problem parsing option '%s': %s",
|
||||||
|
|
60
common/src/main/java/bisq/common/util/DateUtil.java
Normal file
60
common/src/main/java/bisq/common/util/DateUtil.java
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.common.util;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
|
public class DateUtil {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param date The date which should be reset to first day of month
|
||||||
|
* @return First day in given date with time set to zero.
|
||||||
|
*/
|
||||||
|
public static Date getStartOfMonth(Date date) {
|
||||||
|
Calendar calendar = new GregorianCalendar();
|
||||||
|
calendar.setTime(date);
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
calendar.set(Calendar.MINUTE, 0);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
calendar.set(Calendar.MILLISECOND, 0);
|
||||||
|
return calendar.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param year The year
|
||||||
|
* @param month The month starts with 0 for January
|
||||||
|
* @return First day in given month with time set to zero.
|
||||||
|
*/
|
||||||
|
public static Date getStartOfMonth(int year, int month) {
|
||||||
|
Calendar calendar = new GregorianCalendar();
|
||||||
|
calendar.setTime(new Date());
|
||||||
|
calendar.set(Calendar.YEAR, year);
|
||||||
|
calendar.set(Calendar.MONTH, month);
|
||||||
|
calendar.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
calendar.set(Calendar.MINUTE, 0);
|
||||||
|
calendar.set(Calendar.SECOND, 0);
|
||||||
|
calendar.set(Calendar.MILLISECOND, 0);
|
||||||
|
Date time = calendar.getTime();
|
||||||
|
return time;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,4 +28,8 @@ public class Hex {
|
||||||
public static String encode(byte[] bytes) {
|
public static String encode(byte[] bytes) {
|
||||||
return BaseEncoding.base16().lowerCase().encode(bytes);
|
return BaseEncoding.base16().lowerCase().encode(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] decodeLast4Bytes(String hex) {
|
||||||
|
return decode(hex.substring(hex.length() - 8));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -591,7 +591,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
|
||||||
coinSelector.setUtxoCandidates(null); // We reuse the selectors. Reset the transactionOutputCandidates field
|
coinSelector.setUtxoCandidates(null); // We reuse the selectors. Reset the transactionOutputCandidates field
|
||||||
return tx;
|
return tx;
|
||||||
} catch (InsufficientMoneyException e) {
|
} catch (InsufficientMoneyException e) {
|
||||||
log.error("getPreparedSendTx: tx={}", tx.toString());
|
log.error("getPreparedSendTx: tx={}", tx);
|
||||||
log.error(e.toString());
|
log.error(e.toString());
|
||||||
throw new InsufficientBsqException(e.missing);
|
throw new InsufficientBsqException(e.missing);
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
public class TradeWalletService {
|
public class TradeWalletService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
|
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
|
||||||
private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
|
public static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
|
||||||
|
|
||||||
private final WalletsSetup walletsSetup;
|
private final WalletsSetup walletsSetup;
|
||||||
private final Preferences preferences;
|
private final Preferences preferences;
|
||||||
|
@ -696,16 +696,31 @@ public class TradeWalletService {
|
||||||
// Delayed payout tx
|
// Delayed payout tx
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx,
|
||||||
|
List<Tuple2<Long, String>> receivers,
|
||||||
|
long lockTime)
|
||||||
|
throws AddressFormatException, TransactionVerificationException {
|
||||||
|
TransactionOutput depositTxOutput = depositTx.getOutput(0);
|
||||||
|
Transaction delayedPayoutTx = new Transaction(params);
|
||||||
|
delayedPayoutTx.addInput(depositTxOutput);
|
||||||
|
applyLockTime(lockTime, delayedPayoutTx);
|
||||||
|
checkArgument(!receivers.isEmpty(), "receivers must not be empty");
|
||||||
|
receivers.forEach(receiver -> delayedPayoutTx.addOutput(Coin.valueOf(receiver.first), Address.fromString(params, receiver.second)));
|
||||||
|
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
|
||||||
|
WalletService.verifyTransaction(delayedPayoutTx);
|
||||||
|
return delayedPayoutTx;
|
||||||
|
}
|
||||||
|
|
||||||
public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx,
|
public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx,
|
||||||
String donationAddressString,
|
String donationAddressString,
|
||||||
Coin minerFee,
|
Coin minerFee,
|
||||||
long lockTime)
|
long lockTime)
|
||||||
throws AddressFormatException, TransactionVerificationException {
|
throws AddressFormatException, TransactionVerificationException {
|
||||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
TransactionOutput depositTxOutput = depositTx.getOutput(0);
|
||||||
Transaction delayedPayoutTx = new Transaction(params);
|
Transaction delayedPayoutTx = new Transaction(params);
|
||||||
delayedPayoutTx.addInput(hashedMultiSigOutput);
|
delayedPayoutTx.addInput(depositTxOutput);
|
||||||
applyLockTime(lockTime, delayedPayoutTx);
|
applyLockTime(lockTime, delayedPayoutTx);
|
||||||
Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee);
|
Coin outputAmount = depositTxOutput.getValue().subtract(minerFee);
|
||||||
delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString));
|
delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString));
|
||||||
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
|
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
|
||||||
WalletService.verifyTransaction(delayedPayoutTx);
|
WalletService.verifyTransaction(delayedPayoutTx);
|
||||||
|
|
127
core/src/main/java/bisq/core/dao/CyclesInDaoStateService.java
Normal file
127
core/src/main/java/bisq/core/dao/CyclesInDaoStateService.java
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao;
|
||||||
|
|
||||||
|
import bisq.core.dao.governance.period.CycleService;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
import bisq.core.dao.state.model.governance.Cycle;
|
||||||
|
import bisq.core.dao.state.model.governance.DaoPhase;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for Cycle related methods.
|
||||||
|
* As they might be called often we use caching.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class CyclesInDaoStateService {
|
||||||
|
private final DaoStateService daoStateService;
|
||||||
|
private final CycleService cycleService;
|
||||||
|
|
||||||
|
// Cached results
|
||||||
|
private final Map<Integer, Cycle> cyclesByHeight = new HashMap<>();
|
||||||
|
private final Map<Cycle, Integer> indexByCycle = new HashMap<>();
|
||||||
|
private final Map<Integer, Cycle> cyclesByIndex = new HashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CyclesInDaoStateService(DaoStateService daoStateService, CycleService cycleService) {
|
||||||
|
this.daoStateService = daoStateService;
|
||||||
|
this.cycleService = cycleService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCycleIndexAtChainHeight(int chainHeight) {
|
||||||
|
return findCycleAtHeight(chainHeight)
|
||||||
|
.map(cycleService::getCycleIndex)
|
||||||
|
.orElse(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeightOfFirstBlockOfResultPhaseOfPastCycle(int chainHeight, int numPastCycles) {
|
||||||
|
return findCycleAtHeight(chainHeight)
|
||||||
|
.map(cycle -> {
|
||||||
|
int cycleIndex = getIndexForCycle(cycle);
|
||||||
|
int targetIndex = Math.max(0, (cycleIndex - numPastCycles));
|
||||||
|
return getCycleAtIndex(targetIndex);
|
||||||
|
})
|
||||||
|
.map(cycle -> cycle.getFirstBlockOfPhase(DaoPhase.Phase.RESULT))
|
||||||
|
.orElse(daoStateService.getGenesisBlockHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param chainHeight Chain height from where we start
|
||||||
|
* @param numPastCycles Number of past cycles
|
||||||
|
* @return The height at the same offset from the first block of the cycle as in the current cycle minus the past cycles.
|
||||||
|
*/
|
||||||
|
public int getChainHeightOfPastCycle(int chainHeight, int numPastCycles) {
|
||||||
|
int firstBlockOfPastCycle = getHeightOfFirstBlockOfPastCycle(chainHeight, numPastCycles);
|
||||||
|
if (firstBlockOfPastCycle == daoStateService.getGenesisBlockHeight()) {
|
||||||
|
return firstBlockOfPastCycle;
|
||||||
|
}
|
||||||
|
return firstBlockOfPastCycle + getOffsetFromFirstBlockInCycle(chainHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getOffsetFromFirstBlockInCycle(int chainHeight) {
|
||||||
|
return daoStateService.getCycle(chainHeight)
|
||||||
|
.map(c -> chainHeight - c.getHeightOfFirstBlock())
|
||||||
|
.orElse(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeightOfFirstBlockOfPastCycle(int chainHeight, int numPastCycles) {
|
||||||
|
return findCycleAtHeight(chainHeight)
|
||||||
|
.map(cycle -> getIndexForCycle(cycle) - numPastCycles)
|
||||||
|
.filter(targetIndex -> targetIndex > 0)
|
||||||
|
.map(targetIndex -> getCycleAtIndex(targetIndex).getHeightOfFirstBlock())
|
||||||
|
.orElse(daoStateService.getGenesisBlockHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cycle getCycleAtIndex(int index) {
|
||||||
|
int cycleIndex = Math.max(0, index);
|
||||||
|
return Optional.ofNullable(cyclesByIndex.get(cycleIndex))
|
||||||
|
.orElseGet(() -> {
|
||||||
|
Cycle cycle = daoStateService.getCycleAtIndex(cycleIndex);
|
||||||
|
cyclesByIndex.put(cycleIndex, cycle);
|
||||||
|
return cycle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndexForCycle(Cycle cycle) {
|
||||||
|
return Optional.ofNullable(indexByCycle.get(cycle))
|
||||||
|
.orElseGet(() -> {
|
||||||
|
int index = cycleService.getCycleIndex(cycle);
|
||||||
|
indexByCycle.put(cycle, index);
|
||||||
|
return index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Cycle> findCycleAtHeight(int chainHeight) {
|
||||||
|
return Optional.ofNullable(cyclesByHeight.get(chainHeight))
|
||||||
|
.or(() -> {
|
||||||
|
Optional<Cycle> optionalCycle = daoStateService.getCycle(chainHeight);
|
||||||
|
optionalCycle.ifPresent(cycle -> cyclesByHeight.put(chainHeight, cycle));
|
||||||
|
return optionalCycle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -252,11 +252,13 @@ public class DaoFacade implements DaoSetupService {
|
||||||
// Creation of Proposal and proposalTransaction
|
// Creation of Proposal and proposalTransaction
|
||||||
public ProposalWithTransaction getCompensationProposalWithTransaction(String name,
|
public ProposalWithTransaction getCompensationProposalWithTransaction(String name,
|
||||||
String link,
|
String link,
|
||||||
Coin requestedBsq)
|
Coin requestedBsq,
|
||||||
|
Optional<String> burningManReceiverAddress)
|
||||||
throws ProposalValidationException, InsufficientMoneyException, TxException {
|
throws ProposalValidationException, InsufficientMoneyException, TxException {
|
||||||
return compensationProposalFactory.createProposalWithTransaction(name,
|
return compensationProposalFactory.createProposalWithTransaction(name,
|
||||||
link,
|
link,
|
||||||
requestedBsq);
|
requestedBsq,
|
||||||
|
burningManReceiverAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProposalWithTransaction getReimbursementProposalWithTransaction(String name,
|
public ProposalWithTransaction getReimbursementProposalWithTransaction(String name,
|
||||||
|
|
|
@ -222,6 +222,9 @@ public class DaoModule extends AppModule {
|
||||||
bindConstant().annotatedWith(named(Config.RPC_BLOCK_NOTIFICATION_HOST)).to(config.rpcBlockNotificationHost);
|
bindConstant().annotatedWith(named(Config.RPC_BLOCK_NOTIFICATION_HOST)).to(config.rpcBlockNotificationHost);
|
||||||
bindConstant().annotatedWith(named(Config.DUMP_BLOCKCHAIN_DATA)).to(config.dumpBlockchainData);
|
bindConstant().annotatedWith(named(Config.DUMP_BLOCKCHAIN_DATA)).to(config.dumpBlockchainData);
|
||||||
bindConstant().annotatedWith(named(Config.FULL_DAO_NODE)).to(config.fullDaoNode);
|
bindConstant().annotatedWith(named(Config.FULL_DAO_NODE)).to(config.fullDaoNode);
|
||||||
|
bindConstant().annotatedWith(named(Config.IS_BM_FULL_NODE)).to(config.isBmFullNode);
|
||||||
|
bindConstant().annotatedWith(named(Config.BM_ORACLE_NODE_PUB_KEY)).to(config.bmOracleNodePubKey);
|
||||||
|
bindConstant().annotatedWith(named(Config.BM_ORACLE_NODE_PRIV_KEY)).to(config.bmOracleNodePrivKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
|
|
||||||
package bisq.core.dao;
|
package bisq.core.dao;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.AccountingNode;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.AccountingNodeProvider;
|
||||||
import bisq.core.dao.governance.asset.AssetService;
|
import bisq.core.dao.governance.asset.AssetService;
|
||||||
import bisq.core.dao.governance.ballot.BallotListService;
|
import bisq.core.dao.governance.ballot.BallotListService;
|
||||||
import bisq.core.dao.governance.blindvote.BlindVoteListService;
|
import bisq.core.dao.governance.blindvote.BlindVoteListService;
|
||||||
|
@ -54,9 +57,11 @@ import java.util.function.Consumer;
|
||||||
public class DaoSetup {
|
public class DaoSetup {
|
||||||
private final BsqNode bsqNode;
|
private final BsqNode bsqNode;
|
||||||
private final List<DaoSetupService> daoSetupServices = new ArrayList<>();
|
private final List<DaoSetupService> daoSetupServices = new ArrayList<>();
|
||||||
|
private final AccountingNode accountingNode;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public DaoSetup(BsqNodeProvider bsqNodeProvider,
|
public DaoSetup(BsqNodeProvider bsqNodeProvider,
|
||||||
|
AccountingNodeProvider accountingNodeProvider,
|
||||||
DaoStateService daoStateService,
|
DaoStateService daoStateService,
|
||||||
CycleService cycleService,
|
CycleService cycleService,
|
||||||
BallotListService ballotListService,
|
BallotListService ballotListService,
|
||||||
|
@ -79,9 +84,11 @@ public class DaoSetup {
|
||||||
DaoStateMonitoringService daoStateMonitoringService,
|
DaoStateMonitoringService daoStateMonitoringService,
|
||||||
ProposalStateMonitoringService proposalStateMonitoringService,
|
ProposalStateMonitoringService proposalStateMonitoringService,
|
||||||
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
|
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
|
||||||
DaoStateSnapshotService daoStateSnapshotService) {
|
DaoStateSnapshotService daoStateSnapshotService,
|
||||||
|
BurningManAccountingService burningManAccountingService) {
|
||||||
|
|
||||||
bsqNode = bsqNodeProvider.getBsqNode();
|
bsqNode = bsqNodeProvider.getBsqNode();
|
||||||
|
accountingNode = accountingNodeProvider.getAccountingNode();
|
||||||
|
|
||||||
// We need to take care of order of execution.
|
// We need to take care of order of execution.
|
||||||
daoSetupServices.add(daoStateService);
|
daoSetupServices.add(daoStateService);
|
||||||
|
@ -107,8 +114,10 @@ public class DaoSetup {
|
||||||
daoSetupServices.add(proposalStateMonitoringService);
|
daoSetupServices.add(proposalStateMonitoringService);
|
||||||
daoSetupServices.add(blindVoteStateMonitoringService);
|
daoSetupServices.add(blindVoteStateMonitoringService);
|
||||||
daoSetupServices.add(daoStateSnapshotService);
|
daoSetupServices.add(daoStateSnapshotService);
|
||||||
|
daoSetupServices.add(burningManAccountingService);
|
||||||
|
|
||||||
daoSetupServices.add(bsqNodeProvider.getBsqNode());
|
daoSetupServices.add(bsqNode);
|
||||||
|
daoSetupServices.add(accountingNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAllServicesInitialized(Consumer<String> errorMessageHandler,
|
public void onAllServicesInitialized(Consumer<String> errorMessageHandler,
|
||||||
|
@ -116,6 +125,9 @@ public class DaoSetup {
|
||||||
bsqNode.setErrorMessageHandler(errorMessageHandler);
|
bsqNode.setErrorMessageHandler(errorMessageHandler);
|
||||||
bsqNode.setWarnMessageHandler(warnMessageHandler);
|
bsqNode.setWarnMessageHandler(warnMessageHandler);
|
||||||
|
|
||||||
|
accountingNode.setErrorMessageHandler(errorMessageHandler);
|
||||||
|
accountingNode.setWarnMessageHandler(warnMessageHandler);
|
||||||
|
|
||||||
// We add first all listeners at all services and then call the start methods.
|
// We add first all listeners at all services and then call the start methods.
|
||||||
// Some services are listening on others so we need to make sure that the
|
// Some services are listening on others so we need to make sure that the
|
||||||
// listeners are set before we call start as that might trigger state change
|
// listeners are set before we call start as that might trigger state change
|
||||||
|
@ -126,5 +138,6 @@ public class DaoSetup {
|
||||||
|
|
||||||
public void shutDown() {
|
public void shutDown() {
|
||||||
bsqNode.shutDown();
|
bsqNode.shutDown();
|
||||||
|
accountingNode.shutDown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.model.BurningManCandidate;
|
||||||
|
import bisq.core.dao.state.DaoStateListener;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
import bisq.core.dao.state.model.blockchain.Block;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class BtcFeeReceiverService implements DaoStateListener {
|
||||||
|
private final BurningManService burningManService;
|
||||||
|
|
||||||
|
private int currentChainHeight;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BtcFeeReceiverService(DaoStateService daoStateService, BurningManService burningManService) {
|
||||||
|
this.burningManService = burningManService;
|
||||||
|
|
||||||
|
daoStateService.addDaoStateListener(this);
|
||||||
|
daoStateService.getLastBlock().ifPresent(this::applyBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DaoStateListener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
|
||||||
|
applyBlock(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBlock(Block block) {
|
||||||
|
currentChainHeight = block.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
if (!BurningManService.isActivated()) {
|
||||||
|
// Before activation, we fall back to the current fee receiver address
|
||||||
|
return BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_ADDRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, BurningManCandidate> burningManCandidatesByName = burningManService.getBurningManCandidatesByName(currentChainHeight);
|
||||||
|
if (burningManCandidatesByName.isEmpty()) {
|
||||||
|
// If there are no compensation requests (e.g. at dev testing) we fall back to the default address
|
||||||
|
return burningManService.getLegacyBurningManAddress(currentChainHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// It might be that we do not reach 100% if some entries had a cappedBurnAmountShare.
|
||||||
|
// In that case we fill up the gap to 100% with the legacy BM.
|
||||||
|
// cappedBurnAmountShare is a % value represented as double. Smallest supported value is 0.01% -> 0.0001.
|
||||||
|
// By multiplying it with 10000 and using Math.floor we limit the candidate to 0.01%.
|
||||||
|
// Entries with 0 will be ignored in the selection method, so we do not need to filter them out.
|
||||||
|
List<BurningManCandidate> burningManCandidates = new ArrayList<>(burningManCandidatesByName.values());
|
||||||
|
int ceiling = 10000;
|
||||||
|
List<Long> amountList = burningManCandidates.stream()
|
||||||
|
.map(BurningManCandidate::getCappedBurnAmountShare)
|
||||||
|
.map(cappedBurnAmountShare -> (long) Math.floor(cappedBurnAmountShare * ceiling))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
long sum = amountList.stream().mapToLong(e -> e).sum();
|
||||||
|
// If we have not reached the 100% we fill the missing gap with the legacy BM
|
||||||
|
if (sum < ceiling) {
|
||||||
|
amountList.add(ceiling - sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
int winnerIndex = getRandomIndex(amountList, new Random());
|
||||||
|
if (winnerIndex == burningManCandidates.size()) {
|
||||||
|
// If we have filled up the missing gap to 100% with the legacy BM we would get an index out of bounds of
|
||||||
|
// the burningManCandidates as we added for the legacy BM an entry at the end.
|
||||||
|
return burningManService.getLegacyBurningManAddress(currentChainHeight);
|
||||||
|
}
|
||||||
|
return burningManCandidates.get(winnerIndex).getMostRecentAddress()
|
||||||
|
.orElse(burningManService.getLegacyBurningManAddress(currentChainHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static int getRandomIndex(List<Long> weights, Random random) {
|
||||||
|
long sum = weights.stream().mapToLong(n -> n).sum();
|
||||||
|
if (sum == 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
long target = random.longs(0, sum).findFirst().orElseThrow() + 1;
|
||||||
|
return findIndex(weights, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static int findIndex(List<Long> weights, long target) {
|
||||||
|
int currentRange = 0;
|
||||||
|
for (int i = 0; i < weights.size(); i++) {
|
||||||
|
currentRange += weights.get(i);
|
||||||
|
if (currentRange >= target) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,252 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
import bisq.core.dao.CyclesInDaoStateService;
|
||||||
|
import bisq.core.dao.burningman.model.BurnOutputModel;
|
||||||
|
import bisq.core.dao.burningman.model.BurningManCandidate;
|
||||||
|
import bisq.core.dao.burningman.model.ReimbursementModel;
|
||||||
|
import bisq.core.dao.governance.param.Param;
|
||||||
|
import bisq.core.dao.governance.proposal.ProposalService;
|
||||||
|
import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
import bisq.core.dao.state.model.blockchain.Tx;
|
||||||
|
import bisq.core.dao.state.model.governance.Cycle;
|
||||||
|
import bisq.core.dao.state.model.governance.Issuance;
|
||||||
|
import bisq.core.dao.state.model.governance.IssuanceType;
|
||||||
|
import bisq.core.dao.state.model.governance.ReimbursementProposal;
|
||||||
|
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Burn target related API. Not touching trade protocol aspects and parameters can be changed here without risking to
|
||||||
|
* break trade protocol validations.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
class BurnTargetService {
|
||||||
|
// Number of cycles for accumulating reimbursement amounts. Used for the burn target.
|
||||||
|
private static final int NUM_CYCLES_BURN_TARGET = 12;
|
||||||
|
private static final int NUM_CYCLES_AVERAGE_DISTRIBUTION = 3;
|
||||||
|
|
||||||
|
// Default value for the estimated BTC trade fees per month as BSQ sat value (100 sat = 1 BSQ).
|
||||||
|
// Default is roughly average of last 12 months at Nov 2022.
|
||||||
|
// Can be changed with DAO parameter voting.
|
||||||
|
private static final long DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE = Config.baseCurrencyNetwork().isRegtest() ? 1000000 : 6200000;
|
||||||
|
|
||||||
|
private final DaoStateService daoStateService;
|
||||||
|
private final CyclesInDaoStateService cyclesInDaoStateService;
|
||||||
|
private final ProposalService proposalService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BurnTargetService(DaoStateService daoStateService,
|
||||||
|
CyclesInDaoStateService cyclesInDaoStateService,
|
||||||
|
ProposalService proposalService) {
|
||||||
|
this.daoStateService = daoStateService;
|
||||||
|
this.cyclesInDaoStateService = cyclesInDaoStateService;
|
||||||
|
this.proposalService = proposalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
Set<ReimbursementModel> getReimbursements(int chainHeight) {
|
||||||
|
Set<ReimbursementModel> reimbursements = new HashSet<>();
|
||||||
|
daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT).stream()
|
||||||
|
.filter(issuance -> issuance.getChainHeight() <= chainHeight)
|
||||||
|
.forEach(issuance -> getReimbursementProposalsForIssuance(issuance)
|
||||||
|
.forEach(reimbursementProposal -> {
|
||||||
|
int issuanceHeight = issuance.getChainHeight();
|
||||||
|
long issuanceAmount = issuance.getAmount();
|
||||||
|
long issuanceDate = daoStateService.getBlockTime(issuanceHeight);
|
||||||
|
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(issuanceHeight);
|
||||||
|
reimbursements.add(new ReimbursementModel(
|
||||||
|
issuanceAmount,
|
||||||
|
issuanceHeight,
|
||||||
|
issuanceDate,
|
||||||
|
cycleIndex,
|
||||||
|
reimbursementProposal.getTxId()));
|
||||||
|
}));
|
||||||
|
return reimbursements;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getBurnTarget(int chainHeight, Collection<BurningManCandidate> burningManCandidates) {
|
||||||
|
// Reimbursements are taken into account at result vote block
|
||||||
|
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET);
|
||||||
|
long accumulatedReimbursements = getAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle);
|
||||||
|
|
||||||
|
// Param changes are taken into account at first block at next cycle after voting
|
||||||
|
int heightOfFirstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET - 1);
|
||||||
|
long accumulatedEstimatedBtcTradeFees = getAccumulatedEstimatedBtcTradeFees(chainHeight, heightOfFirstBlockOfPastCycle);
|
||||||
|
|
||||||
|
// Legacy BurningMan
|
||||||
|
Set<Tx> proofOfBurnTxs = getProofOfBurnTxs(chainHeight, chainHeightOfPastCycle);
|
||||||
|
long burnedAmountFromLegacyBurningManDPT = getBurnedAmountFromLegacyBurningManDPT(proofOfBurnTxs, chainHeight, chainHeightOfPastCycle);
|
||||||
|
long burnedAmountFromLegacyBurningMansBtcFees = getBurnedAmountFromLegacyBurningMansBtcFees(proofOfBurnTxs, chainHeight, chainHeightOfPastCycle);
|
||||||
|
|
||||||
|
// Distributed BurningMen
|
||||||
|
long burnedAmountFromBurningMen = getBurnedAmountFromBurningMen(burningManCandidates, chainHeight, chainHeightOfPastCycle);
|
||||||
|
|
||||||
|
long burnTarget = accumulatedReimbursements
|
||||||
|
+ accumulatedEstimatedBtcTradeFees
|
||||||
|
- burnedAmountFromLegacyBurningManDPT
|
||||||
|
- burnedAmountFromLegacyBurningMansBtcFees
|
||||||
|
- burnedAmountFromBurningMen;
|
||||||
|
|
||||||
|
log.info("accumulatedReimbursements: {}\n" +
|
||||||
|
"+ accumulatedEstimatedBtcTradeFees: {}\n" +
|
||||||
|
"- burnedAmountFromLegacyBurningManDPT: {}\n" +
|
||||||
|
"- burnedAmountFromLegacyBurningMansBtcFees: {}\n" +
|
||||||
|
"- burnedAmountFromBurningMen: {}\n" +
|
||||||
|
"= burnTarget: {}\n",
|
||||||
|
accumulatedReimbursements,
|
||||||
|
accumulatedEstimatedBtcTradeFees,
|
||||||
|
burnedAmountFromLegacyBurningManDPT,
|
||||||
|
burnedAmountFromLegacyBurningMansBtcFees,
|
||||||
|
burnedAmountFromBurningMen,
|
||||||
|
burnTarget);
|
||||||
|
return burnTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
long getAverageDistributionPerCycle(int chainHeight) {
|
||||||
|
// Reimbursements are taken into account at result vote block
|
||||||
|
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION);
|
||||||
|
long reimbursements = getAccumulatedReimbursements(chainHeight, chainHeightOfPastCycle);
|
||||||
|
|
||||||
|
// Param changes are taken into account at first block at next cycle after voting
|
||||||
|
int firstBlockOfPastCycle = cyclesInDaoStateService.getHeightOfFirstBlockOfPastCycle(chainHeight, NUM_CYCLES_AVERAGE_DISTRIBUTION - 1);
|
||||||
|
long btcTradeFees = getAccumulatedEstimatedBtcTradeFees(chainHeight, firstBlockOfPastCycle);
|
||||||
|
|
||||||
|
return Math.round((reimbursements + btcTradeFees) / (double) NUM_CYCLES_AVERAGE_DISTRIBUTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private Stream<ReimbursementProposal> getReimbursementProposalsForIssuance(Issuance issuance) {
|
||||||
|
return proposalService.getProposalPayloads().stream()
|
||||||
|
.map(ProposalPayload::getProposal)
|
||||||
|
.filter(proposal -> issuance.getTxId().equals(proposal.getTxId()))
|
||||||
|
.filter(proposal -> proposal instanceof ReimbursementProposal)
|
||||||
|
.map(proposal -> (ReimbursementProposal) proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getAccumulatedReimbursements(int chainHeight, int fromBlock) {
|
||||||
|
return getReimbursements(chainHeight).stream()
|
||||||
|
.filter(reimbursementModel -> reimbursementModel.getHeight() > fromBlock)
|
||||||
|
.filter(reimbursementModel -> reimbursementModel.getHeight() <= chainHeight)
|
||||||
|
.mapToLong(ReimbursementModel::getAmount)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The BTC fees are set by parameter and becomes active at first block of the next cycle after voting.
|
||||||
|
private long getAccumulatedEstimatedBtcTradeFees(int chainHeight, int fromBlock) {
|
||||||
|
return daoStateService.getCycles().stream()
|
||||||
|
.filter(cycle -> cycle.getHeightOfFirstBlock() >= fromBlock)
|
||||||
|
.filter(cycle -> cycle.getHeightOfFirstBlock() <= chainHeight)
|
||||||
|
.mapToLong(this::getBtcTradeFeeFromParam)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getBtcTradeFeeFromParam(Cycle cycle) {
|
||||||
|
int value = daoStateService.getParamValueAsBlock(Param.LOCK_TIME_TRADE_PAYOUT, cycle.getHeightOfFirstBlock());
|
||||||
|
// Ignore default value (4320)
|
||||||
|
return value != 4320 ? value : DEFAULT_ESTIMATED_BTC_TRADE_FEE_REVENUE_PER_CYCLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Tx> getProofOfBurnTxs(int chainHeight, int fromBlock) {
|
||||||
|
return daoStateService.getProofOfBurnTxs().stream()
|
||||||
|
.filter(tx -> tx.getBlockHeight() > fromBlock)
|
||||||
|
.filter(tx -> tx.getBlockHeight() <= chainHeight)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private long getBurnedAmountFromLegacyBurningManDPT(Set<Tx> proofOfBurnTxs, int chainHeight, int fromBlock) {
|
||||||
|
// Legacy burningman use those opReturn data to mark their burn transactions from delayed payout transaction cases.
|
||||||
|
// opReturn data from delayed payout txs when BM traded with the refund agent: 1701e47e5d8030f444c182b5e243871ebbaeadb5e82f
|
||||||
|
// opReturn data from delayed payout txs when BM traded with traders who got reimbursed by the DAO: 1701293c488822f98e70e047012f46f5f1647f37deb7
|
||||||
|
return proofOfBurnTxs.stream()
|
||||||
|
.filter(tx -> tx.getBlockHeight() > fromBlock)
|
||||||
|
.filter(tx -> tx.getBlockHeight() <= chainHeight)
|
||||||
|
.filter(tx -> {
|
||||||
|
String hash = Hex.encode(tx.getLastTxOutput().getOpReturnData());
|
||||||
|
return "1701e47e5d8030f444c182b5e243871ebbaeadb5e82f".equals(hash) ||
|
||||||
|
"1701293c488822f98e70e047012f46f5f1647f37deb7".equals(hash);
|
||||||
|
})
|
||||||
|
.mapToLong(Tx::getBurntBsq)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getBurnedAmountFromLegacyBurningMansBtcFees(Set<Tx> proofOfBurnTxs, int chainHeight, int fromBlock) {
|
||||||
|
// Legacy burningman use the below opReturn data to mark their burn transactions from Btc trade fees.
|
||||||
|
return proofOfBurnTxs.stream()
|
||||||
|
.filter(tx -> tx.getBlockHeight() > fromBlock)
|
||||||
|
.filter(tx -> tx.getBlockHeight() <= chainHeight)
|
||||||
|
.filter(tx -> "1701721206fe6b40777763de1c741f4fd2706d94775d".equals(Hex.encode(tx.getLastTxOutput().getOpReturnData())))
|
||||||
|
.mapToLong(Tx::getBurntBsq)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getBurnedAmountFromBurningMen(Collection<BurningManCandidate> burningManCandidates,
|
||||||
|
int chainHeight,
|
||||||
|
int fromBlock) {
|
||||||
|
return burningManCandidates.stream()
|
||||||
|
.map(burningManCandidate -> burningManCandidate.getBurnOutputModels().stream()
|
||||||
|
.filter(burnOutputModel -> burnOutputModel.getHeight() > fromBlock)
|
||||||
|
.filter(burnOutputModel -> burnOutputModel.getHeight() <= chainHeight)
|
||||||
|
.mapToLong(BurnOutputModel::getAmount)
|
||||||
|
.sum())
|
||||||
|
.mapToLong(e -> e)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
long getAccumulatedDecayedBurnedAmount(Collection<BurningManCandidate> burningManCandidates, int chainHeight) {
|
||||||
|
int fromBlock = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_TARGET);
|
||||||
|
return getAccumulatedDecayedBurnedAmount(burningManCandidates, chainHeight, fromBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getAccumulatedDecayedBurnedAmount(Collection<BurningManCandidate> burningManCandidates,
|
||||||
|
int chainHeight,
|
||||||
|
int fromBlock) {
|
||||||
|
return burningManCandidates.stream()
|
||||||
|
.map(burningManCandidate -> burningManCandidate.getBurnOutputModels().stream()
|
||||||
|
.filter(burnOutputModel -> burnOutputModel.getHeight() > fromBlock)
|
||||||
|
.filter(burnOutputModel -> burnOutputModel.getHeight() <= chainHeight)
|
||||||
|
.mapToLong(BurnOutputModel::getDecayedAmount)
|
||||||
|
.sum())
|
||||||
|
.mapToLong(e -> e)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,366 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
import bisq.core.btc.wallet.BsqWalletService;
|
||||||
|
import bisq.core.dao.CyclesInDaoStateService;
|
||||||
|
import bisq.core.dao.burningman.model.BurnOutputModel;
|
||||||
|
import bisq.core.dao.burningman.model.BurningManCandidate;
|
||||||
|
import bisq.core.dao.burningman.model.LegacyBurningMan;
|
||||||
|
import bisq.core.dao.burningman.model.ReimbursementModel;
|
||||||
|
import bisq.core.dao.governance.proposal.MyProposalListService;
|
||||||
|
import bisq.core.dao.state.DaoStateListener;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
import bisq.core.dao.state.model.blockchain.BaseTx;
|
||||||
|
import bisq.core.dao.state.model.blockchain.Block;
|
||||||
|
import bisq.core.dao.state.model.blockchain.Tx;
|
||||||
|
import bisq.core.dao.state.model.blockchain.TxOutput;
|
||||||
|
import bisq.core.dao.state.model.governance.CompensationProposal;
|
||||||
|
import bisq.core.dao.state.model.governance.Proposal;
|
||||||
|
|
||||||
|
import bisq.network.p2p.storage.P2PDataStorage;
|
||||||
|
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides APIs for burningman data representation in the UI.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class BurningManPresentationService implements DaoStateListener {
|
||||||
|
// Burn target gets increased by that amount to give more flexibility.
|
||||||
|
// Burn target is calculated from reimbursements + estimated BTC fees - burned amounts.
|
||||||
|
private static final long BURN_TARGET_BOOST_AMOUNT = Config.baseCurrencyNetwork().isRegtest() ? 1000000 : 10000000;
|
||||||
|
// To avoid that the BM get locked in small total burn amounts we allow to burn up to 1000 BSQ more than the
|
||||||
|
// calculation to not exceed the cap would suggest.
|
||||||
|
private static final long MAX_BURN_TARGET_LOWER_FLOOR = 100000;
|
||||||
|
public static final String LEGACY_BURNING_MAN_DPT_NAME = "Legacy Burningman (DPT)";
|
||||||
|
public static final String LEGACY_BURNING_MAN_BTC_FEES_NAME = "Legacy Burningman (BTC fees)";
|
||||||
|
static final String LEGACY_BURNING_MAN_BTC_FEES_ADDRESS = "38bZBj5peYS3Husdz7AH3gEUiUbYRD951t";
|
||||||
|
// Those are the opReturn data used by legacy BM for burning BTC received from DPT.
|
||||||
|
// For regtest testing burn bsq and use the pre-image `dpt` which has the hash 14af04ea7e34bd7378b034ddf90da53b7c27a277.
|
||||||
|
// The opReturn data gets additionally prefixed with 1701
|
||||||
|
private static final Set<String> OP_RETURN_DATA_LEGACY_BM_DPT = Config.baseCurrencyNetwork().isRegtest() ?
|
||||||
|
Set.of("170114af04ea7e34bd7378b034ddf90da53b7c27a277") :
|
||||||
|
Set.of("1701e47e5d8030f444c182b5e243871ebbaeadb5e82f",
|
||||||
|
"1701293c488822f98e70e047012f46f5f1647f37deb7");
|
||||||
|
// The opReturn data used by legacy BM for burning BTC received from BTC trade fees.
|
||||||
|
// For regtest testing burn bsq and use the pre-image `fee` which has the hash b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6.
|
||||||
|
// The opReturn data gets additionally prefixed with 1701
|
||||||
|
private static final Set<String> OP_RETURN_DATA_LEGACY_BM_FEES = Config.baseCurrencyNetwork().isRegtest() ?
|
||||||
|
Set.of("1701b3253b7b92bb7f0916b05f10d4fa92be8e48f5e6") :
|
||||||
|
Set.of("1701721206fe6b40777763de1c741f4fd2706d94775d");
|
||||||
|
|
||||||
|
private final DaoStateService daoStateService;
|
||||||
|
private final CyclesInDaoStateService cyclesInDaoStateService;
|
||||||
|
private final MyProposalListService myProposalListService;
|
||||||
|
private final BsqWalletService bsqWalletService;
|
||||||
|
private final BurningManService burningManService;
|
||||||
|
private final BurnTargetService burnTargetService;
|
||||||
|
|
||||||
|
private int currentChainHeight;
|
||||||
|
private Optional<Long> burnTarget = Optional.empty();
|
||||||
|
private final Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>();
|
||||||
|
private final Set<ReimbursementModel> reimbursements = new HashSet<>();
|
||||||
|
private Optional<Long> averageDistributionPerCycle = Optional.empty();
|
||||||
|
private Set<String> myCompensationRequestNames = null;
|
||||||
|
@SuppressWarnings("OptionalAssignedToNull")
|
||||||
|
private Optional<Set<String>> myGenesisOutputNames = null;
|
||||||
|
private Optional<LegacyBurningMan> legacyBurningManDPT = Optional.empty();
|
||||||
|
private Optional<LegacyBurningMan> legacyBurningManBtcFees = Optional.empty();
|
||||||
|
private final Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash = new HashMap<>();
|
||||||
|
private final Map<String, String> burningManNameByAddress = new HashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BurningManPresentationService(DaoStateService daoStateService,
|
||||||
|
CyclesInDaoStateService cyclesInDaoStateService,
|
||||||
|
MyProposalListService myProposalListService,
|
||||||
|
BsqWalletService bsqWalletService,
|
||||||
|
BurningManService burningManService,
|
||||||
|
BurnTargetService burnTargetService) {
|
||||||
|
this.daoStateService = daoStateService;
|
||||||
|
this.cyclesInDaoStateService = cyclesInDaoStateService;
|
||||||
|
this.myProposalListService = myProposalListService;
|
||||||
|
this.bsqWalletService = bsqWalletService;
|
||||||
|
this.burningManService = burningManService;
|
||||||
|
this.burnTargetService = burnTargetService;
|
||||||
|
|
||||||
|
daoStateService.addDaoStateListener(this);
|
||||||
|
daoStateService.getLastBlock().ifPresent(this::applyBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DaoStateListener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
|
||||||
|
applyBlock(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBlock(Block block) {
|
||||||
|
currentChainHeight = block.getHeight();
|
||||||
|
burningManCandidatesByName.clear();
|
||||||
|
reimbursements.clear();
|
||||||
|
burnTarget = Optional.empty();
|
||||||
|
myCompensationRequestNames = null;
|
||||||
|
averageDistributionPerCycle = Optional.empty();
|
||||||
|
legacyBurningManDPT = Optional.empty();
|
||||||
|
legacyBurningManBtcFees = Optional.empty();
|
||||||
|
proofOfBurnOpReturnTxOutputByHash.clear();
|
||||||
|
burningManNameByAddress.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public long getBurnTarget() {
|
||||||
|
if (burnTarget.isPresent()) {
|
||||||
|
return burnTarget.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
burnTarget = Optional.of(burnTargetService.getBurnTarget(currentChainHeight, getBurningManCandidatesByName().values()));
|
||||||
|
return burnTarget.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getBoostedBurnTarget() {
|
||||||
|
return getBurnTarget() + BURN_TARGET_BOOST_AMOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getAverageDistributionPerCycle() {
|
||||||
|
if (averageDistributionPerCycle.isPresent()) {
|
||||||
|
return averageDistributionPerCycle.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
averageDistributionPerCycle = Optional.of(burnTargetService.getAverageDistributionPerCycle(currentChainHeight));
|
||||||
|
return averageDistributionPerCycle.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpectedRevenue(BurningManCandidate burningManCandidate) {
|
||||||
|
return Math.round(burningManCandidate.getCappedBurnAmountShare() * getAverageDistributionPerCycle());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tuple2<Long, Long> getCandidateBurnTarget(BurningManCandidate burningManCandidate) {
|
||||||
|
long burnTarget = getBurnTarget();
|
||||||
|
double compensationShare = burningManCandidate.getCompensationShare();
|
||||||
|
if (burnTarget == 0 || compensationShare == 0) {
|
||||||
|
return new Tuple2<>(0L, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
double maxCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare);
|
||||||
|
long lowerBaseTarget = Math.round(burnTarget * maxCompensationShare);
|
||||||
|
long boostedBurnAmount = burnTarget + BURN_TARGET_BOOST_AMOUNT;
|
||||||
|
double maxBoostedCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR);
|
||||||
|
long upperBaseTarget = Math.round(boostedBurnAmount * maxBoostedCompensationShare);
|
||||||
|
long totalBurnedAmount = burnTargetService.getAccumulatedDecayedBurnedAmount(getBurningManCandidatesByName().values(), currentChainHeight);
|
||||||
|
|
||||||
|
if (totalBurnedAmount == 0) {
|
||||||
|
return new Tuple2<>(lowerBaseTarget, upperBaseTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
double burnAmountShare = burningManCandidate.getBurnAmountShare();
|
||||||
|
long candidatesBurnAmount = burningManCandidate.getAccumulatedDecayedBurnAmount();
|
||||||
|
if (burnAmountShare < maxBoostedCompensationShare) {
|
||||||
|
long myBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxCompensationShare);
|
||||||
|
long myMaxBurnAmount = getMissingAmountToReachTargetShare(totalBurnedAmount, candidatesBurnAmount, maxBoostedCompensationShare);
|
||||||
|
|
||||||
|
// We limit to base targets
|
||||||
|
myBurnAmount = Math.min(myBurnAmount, lowerBaseTarget);
|
||||||
|
myMaxBurnAmount = Math.min(myMaxBurnAmount, upperBaseTarget);
|
||||||
|
|
||||||
|
// We allow at least MAX_BURN_TARGET_LOWER_FLOOR (1000 BSQ) to burn, even if that means to hit the cap to give more flexibility
|
||||||
|
// when low amounts are burned and the 11% cap would lock in BM to small increments per burn iteration.
|
||||||
|
myMaxBurnAmount = Math.max(myMaxBurnAmount, MAX_BURN_TARGET_LOWER_FLOOR);
|
||||||
|
|
||||||
|
// If below dust we set value to 0
|
||||||
|
myBurnAmount = myBurnAmount < 546 ? 0 : myBurnAmount;
|
||||||
|
return new Tuple2<>(myBurnAmount, myMaxBurnAmount);
|
||||||
|
} else {
|
||||||
|
// We have reached our cap.
|
||||||
|
return new Tuple2<>(0L, MAX_BURN_TARGET_LOWER_FLOOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static long getMissingAmountToReachTargetShare(long total, long myAmount, double myTargetShare) {
|
||||||
|
long others = total - myAmount;
|
||||||
|
double shareTargetOthers = 1 - myTargetShare;
|
||||||
|
double targetAmount = shareTargetOthers > 0 ? myTargetShare / shareTargetOthers * others : 0;
|
||||||
|
return Math.round(targetAmount) - myAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ReimbursementModel> getReimbursements() {
|
||||||
|
if (!reimbursements.isEmpty()) {
|
||||||
|
return reimbursements;
|
||||||
|
}
|
||||||
|
|
||||||
|
reimbursements.addAll(burnTargetService.getReimbursements(currentChainHeight));
|
||||||
|
return reimbursements;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Set<String>> findMyGenesisOutputNames() {
|
||||||
|
// Optional.empty is valid case, so we use null to detect if it was set.
|
||||||
|
// As it does not change at new blocks its only set once.
|
||||||
|
//noinspection OptionalAssignedToNull
|
||||||
|
if (myGenesisOutputNames != null) {
|
||||||
|
return myGenesisOutputNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
myGenesisOutputNames = daoStateService.getGenesisTx()
|
||||||
|
.flatMap(tx -> Optional.ofNullable(bsqWalletService.getTransaction(tx.getId()))
|
||||||
|
.map(genesisTransaction -> genesisTransaction.getOutputs().stream()
|
||||||
|
.filter(transactionOutput -> transactionOutput.isMine(bsqWalletService.getWallet()))
|
||||||
|
.map(transactionOutput -> BurningManService.GENESIS_OUTPUT_PREFIX + transactionOutput.getIndex())
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return myGenesisOutputNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getMyCompensationRequestNames() {
|
||||||
|
// Can be empty, so we compare with null and reset to null at new block
|
||||||
|
if (myCompensationRequestNames != null) {
|
||||||
|
return myCompensationRequestNames;
|
||||||
|
}
|
||||||
|
myCompensationRequestNames = myProposalListService.getList().stream()
|
||||||
|
.filter(proposal -> proposal instanceof CompensationProposal)
|
||||||
|
.map(Proposal::getName)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
return myCompensationRequestNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, BurningManCandidate> getBurningManCandidatesByName() {
|
||||||
|
// Cached value is only used for currentChainHeight
|
||||||
|
if (!burningManCandidatesByName.isEmpty()) {
|
||||||
|
return burningManCandidatesByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
burningManCandidatesByName.putAll(burningManService.getBurningManCandidatesByName(currentChainHeight));
|
||||||
|
return burningManCandidatesByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegacyBurningMan getLegacyBurningManForDPT() {
|
||||||
|
if (legacyBurningManDPT.isPresent()) {
|
||||||
|
return legacyBurningManDPT.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not add the legacy burningman to the list but keep it as class field only to avoid that it
|
||||||
|
// interferes with usage of the burningManCandidatesByName map.
|
||||||
|
LegacyBurningMan legacyBurningManDPT = getLegacyBurningMan(burningManService.getLegacyBurningManAddress(currentChainHeight), OP_RETURN_DATA_LEGACY_BM_DPT);
|
||||||
|
this.legacyBurningManDPT = Optional.of(legacyBurningManDPT);
|
||||||
|
return legacyBurningManDPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LegacyBurningMan getLegacyBurningManForBtcFees() {
|
||||||
|
if (legacyBurningManBtcFees.isPresent()) {
|
||||||
|
return legacyBurningManBtcFees.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not add the legacy burningman to the list but keep it as class field only to avoid that it
|
||||||
|
// interferes with usage of the burningManCandidatesByName map.
|
||||||
|
LegacyBurningMan legacyBurningManBtcFees = getLegacyBurningMan(LEGACY_BURNING_MAN_BTC_FEES_ADDRESS, OP_RETURN_DATA_LEGACY_BM_FEES);
|
||||||
|
|
||||||
|
this.legacyBurningManBtcFees = Optional.of(legacyBurningManBtcFees);
|
||||||
|
return legacyBurningManBtcFees;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LegacyBurningMan getLegacyBurningMan(String address, Set<String> opReturnData) {
|
||||||
|
LegacyBurningMan legacyBurningMan = new LegacyBurningMan(address);
|
||||||
|
// The opReturnData used by legacy BM at burning BSQ.
|
||||||
|
getProofOfBurnOpReturnTxOutputByHash().values().stream()
|
||||||
|
.flatMap(txOutputs -> txOutputs.stream()
|
||||||
|
.filter(txOutput -> {
|
||||||
|
String opReturnAsHex = Hex.encode(txOutput.getOpReturnData());
|
||||||
|
return opReturnData.stream().anyMatch(e -> e.equals(opReturnAsHex));
|
||||||
|
}))
|
||||||
|
.forEach(burnOutput -> {
|
||||||
|
int burnOutputHeight = burnOutput.getBlockHeight();
|
||||||
|
Optional<Tx> optionalTx = daoStateService.getTx(burnOutput.getTxId());
|
||||||
|
long burnOutputAmount = optionalTx.map(Tx::getBurntBsq).orElse(0L);
|
||||||
|
long date = optionalTx.map(BaseTx::getTime).orElse(0L);
|
||||||
|
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(burnOutputHeight);
|
||||||
|
legacyBurningMan.addBurnOutputModel(new BurnOutputModel(burnOutputAmount,
|
||||||
|
burnOutputAmount,
|
||||||
|
burnOutputHeight,
|
||||||
|
burnOutput.getTxId(),
|
||||||
|
date,
|
||||||
|
cycleIndex));
|
||||||
|
});
|
||||||
|
// Set remaining share if the sum of all capped shares does not reach 100%.
|
||||||
|
double burnAmountShareOfOthers = getBurningManCandidatesByName().values().stream()
|
||||||
|
.mapToDouble(BurningManCandidate::getCappedBurnAmountShare)
|
||||||
|
.sum();
|
||||||
|
legacyBurningMan.applyBurnAmountShare(1 - burnAmountShareOfOthers);
|
||||||
|
return legacyBurningMan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getBurningManNameByAddress() {
|
||||||
|
if (!burningManNameByAddress.isEmpty()) {
|
||||||
|
return burningManNameByAddress;
|
||||||
|
}
|
||||||
|
// clone to not alter source map. We do not store legacy BM in the source map.
|
||||||
|
Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>(getBurningManCandidatesByName());
|
||||||
|
burningManCandidatesByName.put(LEGACY_BURNING_MAN_DPT_NAME, getLegacyBurningManForDPT());
|
||||||
|
burningManCandidatesByName.put(LEGACY_BURNING_MAN_BTC_FEES_NAME, getLegacyBurningManForBtcFees());
|
||||||
|
|
||||||
|
Map<String, Set<String>> receiverAddressesByBurningManName = new HashMap<>();
|
||||||
|
burningManCandidatesByName.forEach((name, burningManCandidate) -> {
|
||||||
|
receiverAddressesByBurningManName.putIfAbsent(name, new HashSet<>());
|
||||||
|
receiverAddressesByBurningManName.get(name).addAll(burningManCandidate.getAllAddresses());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
receiverAddressesByBurningManName
|
||||||
|
.forEach((name, addresses) -> addresses
|
||||||
|
.forEach(address -> map.putIfAbsent(address, name)));
|
||||||
|
burningManNameByAddress.putAll(map);
|
||||||
|
return burningManNameByAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGenesisTxId() {
|
||||||
|
return daoStateService.getGenesisTxId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<P2PDataStorage.ByteArray, Set<TxOutput>> getProofOfBurnOpReturnTxOutputByHash() {
|
||||||
|
if (!proofOfBurnOpReturnTxOutputByHash.isEmpty()) {
|
||||||
|
return proofOfBurnOpReturnTxOutputByHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
proofOfBurnOpReturnTxOutputByHash.putAll(burningManService.getProofOfBurnOpReturnTxOutputByHash(currentChainHeight));
|
||||||
|
return proofOfBurnOpReturnTxOutputByHash;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
import bisq.core.dao.CyclesInDaoStateService;
|
||||||
|
import bisq.core.dao.burningman.model.BurnOutputModel;
|
||||||
|
import bisq.core.dao.burningman.model.BurningManCandidate;
|
||||||
|
import bisq.core.dao.burningman.model.CompensationModel;
|
||||||
|
import bisq.core.dao.governance.param.Param;
|
||||||
|
import bisq.core.dao.governance.proofofburn.ProofOfBurnConsensus;
|
||||||
|
import bisq.core.dao.governance.proposal.ProposalService;
|
||||||
|
import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
import bisq.core.dao.state.model.blockchain.BaseTx;
|
||||||
|
import bisq.core.dao.state.model.blockchain.Tx;
|
||||||
|
import bisq.core.dao.state.model.blockchain.TxOutput;
|
||||||
|
import bisq.core.dao.state.model.governance.CompensationProposal;
|
||||||
|
import bisq.core.dao.state.model.governance.Issuance;
|
||||||
|
import bisq.core.dao.state.model.governance.IssuanceType;
|
||||||
|
|
||||||
|
import bisq.network.p2p.storage.P2PDataStorage;
|
||||||
|
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.util.Utilities;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods are used by the DelayedPayoutTxReceiverService, which is used in the trade protocol for creating and
|
||||||
|
* verifying the delayed payout transaction. As verification is done by trade peer it requires data to be deterministic.
|
||||||
|
* Parameters listed here must not be changed as they could break verification of the peers
|
||||||
|
* delayed payout transaction in case not both traders are using the same version.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class BurningManService {
|
||||||
|
private static final Date ACTIVATION_DATE = Utilities.getUTCDate(2023, GregorianCalendar.JANUARY, 1);
|
||||||
|
|
||||||
|
public static boolean isActivated() {
|
||||||
|
return Config.baseCurrencyNetwork().isRegtest() || new Date().after(ACTIVATION_DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
// Cannot be changed after release as it would break trade protocol verification of DPT receivers.
|
||||||
|
|
||||||
|
// Prefix for generic names for the genesis outputs. Appended with output index.
|
||||||
|
// Used as pre-image for burning.
|
||||||
|
static final String GENESIS_OUTPUT_PREFIX = "Bisq co-founder ";
|
||||||
|
|
||||||
|
// Factor for weighting the genesis output amounts.
|
||||||
|
private static final double GENESIS_OUTPUT_AMOUNT_FACTOR = 0.05;
|
||||||
|
|
||||||
|
// The number of cycles we go back for the decay function used for compensation request amounts.
|
||||||
|
private static final int NUM_CYCLES_COMP_REQUEST_DECAY = 24;
|
||||||
|
|
||||||
|
// The number of cycles we go back for the decay function used for burned amounts.
|
||||||
|
private static final int NUM_CYCLES_BURN_AMOUNT_DECAY = 12;
|
||||||
|
|
||||||
|
// Factor for boosting the issuance share (issuance is compensation requests + genesis output).
|
||||||
|
// This will be used for increasing the allowed burn amount. The factor gives more flexibility
|
||||||
|
// and compensates for those who do not burn. The burn share is capped by that factor as well.
|
||||||
|
// E.g. a contributor with 10% issuance share will be able to receive max 20% of the BTC fees or DPT output
|
||||||
|
// even if they had burned more and had a higher burn share than 20%.
|
||||||
|
public static final double ISSUANCE_BOOST_FACTOR = 3;
|
||||||
|
|
||||||
|
// The max amount the burn share can reach. This value is derived from the min. security deposit in a trade and
|
||||||
|
// ensures that an attack where a BM would take all sell offers cannot be economically profitable as they would
|
||||||
|
// lose their deposit and cannot gain more than 11% of the DPT payout. As the total amount in a trade is 2 times
|
||||||
|
// that deposit plus the trade amount the limiting factor here is 11% (0.15 / 1.3).
|
||||||
|
public static final double MAX_BURN_SHARE = 0.11;
|
||||||
|
|
||||||
|
|
||||||
|
private final DaoStateService daoStateService;
|
||||||
|
private final CyclesInDaoStateService cyclesInDaoStateService;
|
||||||
|
private final ProposalService proposalService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BurningManService(DaoStateService daoStateService,
|
||||||
|
CyclesInDaoStateService cyclesInDaoStateService,
|
||||||
|
ProposalService proposalService) {
|
||||||
|
this.daoStateService = daoStateService;
|
||||||
|
this.cyclesInDaoStateService = cyclesInDaoStateService;
|
||||||
|
this.proposalService = proposalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Package scope API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
Map<String, BurningManCandidate> getBurningManCandidatesByName(int chainHeight) {
|
||||||
|
Map<String, BurningManCandidate> burningManCandidatesByName = new HashMap<>();
|
||||||
|
Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash = getProofOfBurnOpReturnTxOutputByHash(chainHeight);
|
||||||
|
|
||||||
|
// Add contributors who made a compensation request
|
||||||
|
daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream()
|
||||||
|
.filter(issuance -> issuance.getChainHeight() <= chainHeight)
|
||||||
|
.forEach(issuance -> {
|
||||||
|
getCompensationProposalsForIssuance(issuance).forEach(compensationProposal -> {
|
||||||
|
String name = compensationProposal.getName();
|
||||||
|
burningManCandidatesByName.putIfAbsent(name, new BurningManCandidate());
|
||||||
|
BurningManCandidate candidate = burningManCandidatesByName.get(name);
|
||||||
|
|
||||||
|
// Issuance
|
||||||
|
compensationProposal.getBurningManReceiverAddress()
|
||||||
|
.or(() -> daoStateService.getTx(compensationProposal.getTxId())
|
||||||
|
.map(this::getAddressFromCompensationRequest))
|
||||||
|
.ifPresent(address -> {
|
||||||
|
int issuanceHeight = issuance.getChainHeight();
|
||||||
|
long issuanceAmount = getIssuanceAmountForCompensationRequest(issuance);
|
||||||
|
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(issuanceHeight);
|
||||||
|
if (isValidCompensationRequest(name, cycleIndex, issuanceAmount)) {
|
||||||
|
long decayedIssuanceAmount = getDecayedCompensationAmount(issuanceAmount, issuanceHeight, chainHeight);
|
||||||
|
long issuanceDate = daoStateService.getBlockTime(issuanceHeight);
|
||||||
|
candidate.addCompensationModel(CompensationModel.fromCompensationRequest(address,
|
||||||
|
issuanceAmount,
|
||||||
|
decayedIssuanceAmount,
|
||||||
|
issuanceHeight,
|
||||||
|
issuance.getTxId(),
|
||||||
|
issuanceDate,
|
||||||
|
cycleIndex));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addBurnOutputModel(chainHeight, proofOfBurnOpReturnTxOutputByHash, name, candidate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add output receivers of genesis transaction
|
||||||
|
daoStateService.getGenesisTx()
|
||||||
|
.ifPresent(tx -> tx.getTxOutputs().forEach(txOutput -> {
|
||||||
|
String name = GENESIS_OUTPUT_PREFIX + txOutput.getIndex();
|
||||||
|
burningManCandidatesByName.putIfAbsent(name, new BurningManCandidate());
|
||||||
|
BurningManCandidate candidate = burningManCandidatesByName.get(name);
|
||||||
|
|
||||||
|
// Issuance
|
||||||
|
int issuanceHeight = txOutput.getBlockHeight();
|
||||||
|
long issuanceAmount = txOutput.getValue();
|
||||||
|
long decayedAmount = getDecayedGenesisOutputAmount(issuanceAmount);
|
||||||
|
long issuanceDate = daoStateService.getBlockTime(issuanceHeight);
|
||||||
|
candidate.addCompensationModel(CompensationModel.fromGenesisOutput(txOutput.getAddress(),
|
||||||
|
issuanceAmount,
|
||||||
|
decayedAmount,
|
||||||
|
issuanceHeight,
|
||||||
|
txOutput.getTxId(),
|
||||||
|
txOutput.getIndex(),
|
||||||
|
issuanceDate));
|
||||||
|
addBurnOutputModel(chainHeight, proofOfBurnOpReturnTxOutputByHash, name, candidate);
|
||||||
|
}));
|
||||||
|
|
||||||
|
Collection<BurningManCandidate> burningManCandidates = burningManCandidatesByName.values();
|
||||||
|
double totalDecayedCompensationAmounts = burningManCandidates.stream()
|
||||||
|
.mapToDouble(BurningManCandidate::getAccumulatedDecayedCompensationAmount)
|
||||||
|
.sum();
|
||||||
|
double totalDecayedBurnAmounts = burningManCandidates.stream()
|
||||||
|
.mapToDouble(BurningManCandidate::getAccumulatedDecayedBurnAmount)
|
||||||
|
.sum();
|
||||||
|
burningManCandidates.forEach(candidate -> candidate.calculateShares(totalDecayedCompensationAmounts, totalDecayedBurnAmounts));
|
||||||
|
return burningManCandidatesByName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getLegacyBurningManAddress(int chainHeight) {
|
||||||
|
return daoStateService.getParamValue(Param.RECIPIENT_BTC_ADDRESS, chainHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
Map<P2PDataStorage.ByteArray, Set<TxOutput>> getProofOfBurnOpReturnTxOutputByHash(int chainHeight) {
|
||||||
|
Map<P2PDataStorage.ByteArray, Set<TxOutput>> map = new HashMap<>();
|
||||||
|
daoStateService.getProofOfBurnOpReturnTxOutputs().stream()
|
||||||
|
.filter(txOutput -> txOutput.getBlockHeight() <= chainHeight)
|
||||||
|
.forEach(txOutput -> {
|
||||||
|
P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(ProofOfBurnConsensus.getHashFromOpReturnData(txOutput.getOpReturnData()));
|
||||||
|
map.putIfAbsent(key, new HashSet<>());
|
||||||
|
map.get(key).add(txOutput);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<CompensationProposal> getCompensationProposalsForIssuance(Issuance issuance) {
|
||||||
|
return proposalService.getProposalPayloads().stream()
|
||||||
|
.map(ProposalPayload::getProposal)
|
||||||
|
.filter(proposal -> issuance.getTxId().equals(proposal.getTxId()))
|
||||||
|
.filter(proposal -> proposal instanceof CompensationProposal)
|
||||||
|
.map(proposal -> (CompensationProposal) proposal);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private String getAddressFromCompensationRequest(Tx tx) {
|
||||||
|
ImmutableList<TxOutput> txOutputs = tx.getTxOutputs();
|
||||||
|
// The compensation request tx has usually 4 outputs. If there is no BTC change its 3 outputs.
|
||||||
|
// BTC change output is at index 2 if present otherwise
|
||||||
|
// we use the BSQ address of the compensation candidate output at index 1.
|
||||||
|
// See https://docs.bisq.network/dao-technical-overview.html#compensation-request-txreimbursement-request-tx
|
||||||
|
if (txOutputs.size() == 4) {
|
||||||
|
return txOutputs.get(2).getAddress();
|
||||||
|
} else {
|
||||||
|
return txOutputs.get(1).getAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getIssuanceAmountForCompensationRequest(Issuance issuance) {
|
||||||
|
// There was a reimbursement for a conference sponsorship with 44776 BSQ. We remove that as well.
|
||||||
|
// See https://github.com/bisq-network/compensation/issues/498
|
||||||
|
if (issuance.getTxId().equals("01455fc4c88fca0665a5f56a90ff03fb9e3e88c3430ffc5217246e32d180aa64")) {
|
||||||
|
return 119400; // That was the compensation part
|
||||||
|
} else {
|
||||||
|
return issuance.getAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidCompensationRequest(String name, int cycleIndex, long issuanceAmount) {
|
||||||
|
// Up to cycle 15 the RefundAgent made reimbursement requests as compensation requests. We filter out those entries.
|
||||||
|
// As it is mixed with RefundAgents real compensation requests we take out all above 3500 BSQ.
|
||||||
|
boolean isReimbursementOfRefundAgent = name.equals("RefundAgent") && cycleIndex <= 15 && issuanceAmount > 350000;
|
||||||
|
return !isReimbursementOfRefundAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getDecayedCompensationAmount(long amount, int issuanceHeight, int chainHeight) {
|
||||||
|
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_COMP_REQUEST_DECAY);
|
||||||
|
return getDecayedAmount(amount, issuanceHeight, chainHeight, chainHeightOfPastCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear decay between currentBlockHeight (100% of amount) and issuanceHeight
|
||||||
|
// chainHeightOfPastCycle is currentBlockHeight - numCycles*cycleDuration. It changes with each block and
|
||||||
|
// distance to currentBlockHeight is the same if cycle durations have not changed (possible via DAo voting but never done).
|
||||||
|
@VisibleForTesting
|
||||||
|
static long getDecayedAmount(long amount,
|
||||||
|
int issuanceHeight,
|
||||||
|
int currentBlockHeight,
|
||||||
|
int chainHeightOfPastCycle) {
|
||||||
|
if (issuanceHeight > currentBlockHeight)
|
||||||
|
throw new IllegalArgumentException("issuanceHeight must not be larger than currentBlockHeight. issuanceHeight=" + issuanceHeight + "; currentBlockHeight=" + currentBlockHeight);
|
||||||
|
if (currentBlockHeight < 0)
|
||||||
|
throw new IllegalArgumentException("currentBlockHeight must not be negative. currentBlockHeight=" + currentBlockHeight);
|
||||||
|
if (amount < 0)
|
||||||
|
throw new IllegalArgumentException("amount must not be negative. amount" + amount);
|
||||||
|
if (issuanceHeight < 0)
|
||||||
|
throw new IllegalArgumentException("issuanceHeight must not be negative. issuanceHeight=" + issuanceHeight);
|
||||||
|
|
||||||
|
if (currentBlockHeight <= chainHeightOfPastCycle) {
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
double factor = Math.max(0, (issuanceHeight - chainHeightOfPastCycle) / (double) (currentBlockHeight - chainHeightOfPastCycle));
|
||||||
|
long weighted = Math.round(amount * factor);
|
||||||
|
return Math.max(0, weighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBurnOutputModel(int chainHeight,
|
||||||
|
Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash,
|
||||||
|
String name,
|
||||||
|
BurningManCandidate candidate) {
|
||||||
|
getProofOfBurnOpReturnTxOutputSetForName(proofOfBurnOpReturnTxOutputByHash, name)
|
||||||
|
.forEach(burnOutput -> {
|
||||||
|
int burnOutputHeight = burnOutput.getBlockHeight();
|
||||||
|
Optional<Tx> optionalTx = daoStateService.getTx(burnOutput.getTxId());
|
||||||
|
long burnOutputAmount = optionalTx.map(Tx::getBurntBsq).orElse(0L);
|
||||||
|
long decayedBurnOutputAmount = getDecayedBurnedAmount(burnOutputAmount, burnOutputHeight, chainHeight);
|
||||||
|
long date = optionalTx.map(BaseTx::getTime).orElse(0L);
|
||||||
|
int cycleIndex = cyclesInDaoStateService.getCycleIndexAtChainHeight(burnOutputHeight);
|
||||||
|
candidate.addBurnOutputModel(new BurnOutputModel(burnOutputAmount,
|
||||||
|
decayedBurnOutputAmount,
|
||||||
|
burnOutputHeight,
|
||||||
|
burnOutput.getTxId(),
|
||||||
|
date,
|
||||||
|
cycleIndex));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<TxOutput> getProofOfBurnOpReturnTxOutputSetForName(Map<P2PDataStorage.ByteArray, Set<TxOutput>> proofOfBurnOpReturnTxOutputByHash,
|
||||||
|
String name) {
|
||||||
|
byte[] preImage = name.getBytes(Charsets.UTF_8);
|
||||||
|
byte[] hash = ProofOfBurnConsensus.getHash(preImage);
|
||||||
|
P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(hash);
|
||||||
|
if (proofOfBurnOpReturnTxOutputByHash.containsKey(key)) {
|
||||||
|
return proofOfBurnOpReturnTxOutputByHash.get(key);
|
||||||
|
} else {
|
||||||
|
return new HashSet<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getDecayedBurnedAmount(long amount, int issuanceHeight, int chainHeight) {
|
||||||
|
int chainHeightOfPastCycle = cyclesInDaoStateService.getChainHeightOfPastCycle(chainHeight, NUM_CYCLES_BURN_AMOUNT_DECAY);
|
||||||
|
return getDecayedAmount(amount,
|
||||||
|
issuanceHeight,
|
||||||
|
chainHeight,
|
||||||
|
chainHeightOfPastCycle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getDecayedGenesisOutputAmount(long amount) {
|
||||||
|
return Math.round(amount * GENESIS_OUTPUT_AMOUNT_FACTOR);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,188 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
|
import bisq.core.dao.burningman.model.BurningManCandidate;
|
||||||
|
import bisq.core.dao.state.DaoStateListener;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
import bisq.core.dao.state.model.blockchain.Block;
|
||||||
|
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in the trade protocol for creating and verifying the delayed payout transaction.
|
||||||
|
* Requires to be deterministic.
|
||||||
|
* Changes in the parameters related to the receivers list could break verification of the peers
|
||||||
|
* delayed payout transaction in case not both are using the same version.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class DelayedPayoutTxReceiverService implements DaoStateListener {
|
||||||
|
// One part of the limit for the min. amount to be included in the DPT outputs.
|
||||||
|
// The miner fee rate multiplied by 2 times the output size is the other factor.
|
||||||
|
// The higher one of both is used. 1000 sat is about 2 USD @ 20k price.
|
||||||
|
private static final long DPT_MIN_OUTPUT_AMOUNT = 1000;
|
||||||
|
|
||||||
|
// If at DPT there is some leftover amount due to capping of some receivers (burn share is
|
||||||
|
// max. ISSUANCE_BOOST_FACTOR times the issuance share) we send it to legacy BM if it is larger
|
||||||
|
// than DPT_MIN_REMAINDER_TO_LEGACY_BM, otherwise we spend it as miner fee.
|
||||||
|
// 50000 sat is about 10 USD @ 20k price. We use a rather high value as we want to avoid that the legacy BM
|
||||||
|
// gets still payouts.
|
||||||
|
private static final long DPT_MIN_REMAINDER_TO_LEGACY_BM = 50000;
|
||||||
|
|
||||||
|
// Min. fee rate for DPT. If fee rate used at take offer time was higher we use that.
|
||||||
|
// We prefer a rather high fee rate to not risk that the DPT gets stuck if required fee rate would
|
||||||
|
// spike when opening arbitration.
|
||||||
|
private static final long DPT_MIN_TX_FEE_RATE = 10;
|
||||||
|
|
||||||
|
|
||||||
|
private final DaoStateService daoStateService;
|
||||||
|
private final BurningManService burningManService;
|
||||||
|
private int currentChainHeight;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public DelayedPayoutTxReceiverService(DaoStateService daoStateService, BurningManService burningManService) {
|
||||||
|
this.daoStateService = daoStateService;
|
||||||
|
this.burningManService = burningManService;
|
||||||
|
|
||||||
|
daoStateService.addDaoStateListener(this);
|
||||||
|
daoStateService.getLastBlock().ifPresent(this::applyBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DaoStateListener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onParseBlockCompleteAfterBatchProcessing(Block block) {
|
||||||
|
applyBlock(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyBlock(Block block) {
|
||||||
|
currentChainHeight = block.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// We use a snapshot blockHeight to avoid failed trades in case maker and taker have different block heights.
|
||||||
|
// The selection is deterministic based on DAO data.
|
||||||
|
// The block height is the last mod(10) height from the range of the last 10-20 blocks (139 -> 120; 140 -> 130, 141 -> 130).
|
||||||
|
// We do not have the latest dao state by that but can ensure maker and taker have the same block.
|
||||||
|
public int getBurningManSelectionHeight() {
|
||||||
|
return getSnapshotHeight(daoStateService.getGenesisBlockHeight(), currentChainHeight, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Tuple2<Long, String>> getReceivers(int burningManSelectionHeight,
|
||||||
|
long inputAmount,
|
||||||
|
long tradeTxFee) {
|
||||||
|
Collection<BurningManCandidate> burningManCandidates = burningManService.getBurningManCandidatesByName(burningManSelectionHeight).values();
|
||||||
|
if (burningManCandidates.isEmpty()) {
|
||||||
|
// If there are no compensation requests (e.g. at dev testing) we fall back to the legacy BM
|
||||||
|
return List.of(new Tuple2<>(inputAmount, burningManService.getLegacyBurningManAddress(burningManSelectionHeight)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to use the same txFeePerVbyte value for both traders.
|
||||||
|
// We use the tradeTxFee value which is calculated from the average of taker fee tx size and deposit tx size.
|
||||||
|
// Otherwise, we would need to sync the fee rate of both traders.
|
||||||
|
// In case of very large taker fee tx we would get a too high fee, but as fee rate is anyway rather
|
||||||
|
// arbitrary and volatile we are on the safer side. The delayed payout tx is published long after the
|
||||||
|
// take offer event and the recommended fee at that moment might be very different to actual
|
||||||
|
// recommended fee. To avoid that the delayed payout tx would get stuck due too low fees we use a
|
||||||
|
// min. fee rate of 10 sat/vByte.
|
||||||
|
|
||||||
|
// Deposit tx has a clearly defined structure, so we know the size. It is only one optional output if range amount offer was taken.
|
||||||
|
// Smallest tx size is 246. With additional change output we add 32. To be safe we use the largest expected size.
|
||||||
|
double txSize = 278;
|
||||||
|
long txFeePerVbyte = Math.max(DPT_MIN_TX_FEE_RATE, Math.round(tradeTxFee / txSize));
|
||||||
|
long spendableAmount = getSpendableAmount(burningManCandidates.size(), inputAmount, txFeePerVbyte);
|
||||||
|
// We only use outputs > 1000 sat or at least 2 times the cost for the output (32 bytes).
|
||||||
|
// If we remove outputs it will be spent as miner fee.
|
||||||
|
long minOutputAmount = Math.max(DPT_MIN_OUTPUT_AMOUNT, txFeePerVbyte * 32 * 2);
|
||||||
|
// We accumulate small amounts which gets filtered out and subtract it from 1 to get an adjustment factor
|
||||||
|
// used later to be applied to the remaining burningmen share.
|
||||||
|
double adjustment = 1 - burningManCandidates.stream()
|
||||||
|
.filter(candidate -> candidate.getMostRecentAddress().isPresent())
|
||||||
|
.mapToDouble(candidate -> {
|
||||||
|
double cappedBurnAmountShare = candidate.getCappedBurnAmountShare();
|
||||||
|
long amount = Math.round(cappedBurnAmountShare * spendableAmount);
|
||||||
|
return amount < minOutputAmount ? cappedBurnAmountShare : 0d;
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
List<Tuple2<Long, String>> receivers = burningManCandidates.stream()
|
||||||
|
.filter(candidate -> candidate.getMostRecentAddress().isPresent())
|
||||||
|
.map(candidate -> {
|
||||||
|
double cappedBurnAmountShare = candidate.getCappedBurnAmountShare() / adjustment;
|
||||||
|
return new Tuple2<>(Math.round(cappedBurnAmountShare * spendableAmount),
|
||||||
|
candidate.getMostRecentAddress().get());
|
||||||
|
})
|
||||||
|
.filter(tuple -> tuple.first >= minOutputAmount)
|
||||||
|
.sorted(Comparator.<Tuple2<Long, String>, Long>comparing(tuple -> tuple.first)
|
||||||
|
.thenComparing(tuple -> tuple.second))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
long totalOutputValue = receivers.stream().mapToLong(e -> e.first).sum();
|
||||||
|
if (totalOutputValue < spendableAmount) {
|
||||||
|
long available = spendableAmount - totalOutputValue;
|
||||||
|
// If the available is larger than DPT_MIN_REMAINDER_TO_LEGACY_BM we send it to legacy BM
|
||||||
|
// Otherwise we use it as miner fee
|
||||||
|
if (available > DPT_MIN_REMAINDER_TO_LEGACY_BM) {
|
||||||
|
receivers.add(new Tuple2<>(available, burningManService.getLegacyBurningManAddress(burningManSelectionHeight)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return receivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long getSpendableAmount(int numOutputs, long inputAmount, long txFeePerVbyte) {
|
||||||
|
// Output size: 32 bytes
|
||||||
|
// Tx size without outputs: 51 bytes
|
||||||
|
int txSize = 51 + numOutputs * 32; // min value: txSize=83
|
||||||
|
long minerFee = txFeePerVbyte * txSize; // min value: minerFee=830
|
||||||
|
// We need to make sure we have at least 1000 sat as defined in TradeWalletService
|
||||||
|
minerFee = Math.max(TradeWalletService.MIN_DELAYED_PAYOUT_TX_FEE.value, minerFee);
|
||||||
|
return inputAmount - minerFee;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borrowed from DaoStateSnapshotService. We prefer to not reuse to avoid dependency to an unrelated domain.
|
||||||
|
@VisibleForTesting
|
||||||
|
static int getSnapshotHeight(int genesisHeight, int height, int grid) {
|
||||||
|
int minSnapshotHeight = genesisHeight + 3 * grid;
|
||||||
|
if (height > minSnapshotHeight) {
|
||||||
|
int ratio = (int) Math.round(height / (double) grid);
|
||||||
|
return ratio * grid - grid;
|
||||||
|
} else {
|
||||||
|
return genesisHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting;
|
||||||
|
|
||||||
|
import bisq.core.dao.DaoSetupService;
|
||||||
|
import bisq.core.dao.burningman.BurningManPresentationService;
|
||||||
|
import bisq.core.dao.burningman.accounting.balance.BalanceEntry;
|
||||||
|
import bisq.core.dao.burningman.accounting.balance.BalanceModel;
|
||||||
|
import bisq.core.dao.burningman.accounting.balance.ReceivedBtcBalanceEntry;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingTx;
|
||||||
|
import bisq.core.dao.burningman.accounting.exceptions.BlockHashNotConnectingException;
|
||||||
|
import bisq.core.dao.burningman.accounting.exceptions.BlockHeightNotConnectingException;
|
||||||
|
import bisq.core.dao.burningman.accounting.storage.BurningManAccountingStoreService;
|
||||||
|
import bisq.core.monetary.Price;
|
||||||
|
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||||
|
import bisq.core.user.Preferences;
|
||||||
|
import bisq.core.util.AveragePriceUtil;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.util.DateUtil;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides APIs for the accounting related aspects of burningmen.
|
||||||
|
* Combines the received funds from BTC trade fees and DPT payouts and the burned BSQ.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class BurningManAccountingService implements DaoSetupService {
|
||||||
|
// now 763195 -> 107159 blocks takes about 14h
|
||||||
|
// First tx at BM address 656036 Sun Nov 08 19:02:18 EST 2020
|
||||||
|
// 2 months ago 754555
|
||||||
|
public static final int EARLIEST_BLOCK_HEIGHT = Config.baseCurrencyNetwork().isRegtest() ? 111 : 656035;
|
||||||
|
public static final int EARLIEST_DATE_YEAR = 2020;
|
||||||
|
public static final int EARLIEST_DATE_MONTH = 10;
|
||||||
|
|
||||||
|
private final BurningManAccountingStoreService burningManAccountingStoreService;
|
||||||
|
private final BurningManPresentationService burningManPresentationService;
|
||||||
|
private final TradeStatisticsManager tradeStatisticsManager;
|
||||||
|
private final Preferences preferences;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Map<Date, Price> averageBsqPriceByMonth = new HashMap<>(getHistoricalAverageBsqPriceByMonth());
|
||||||
|
@Getter
|
||||||
|
private final Map<String, BalanceModel> balanceModelByBurningManName = new HashMap<>();
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BurningManAccountingService(BurningManAccountingStoreService burningManAccountingStoreService,
|
||||||
|
BurningManPresentationService burningManPresentationService,
|
||||||
|
TradeStatisticsManager tradeStatisticsManager,
|
||||||
|
Preferences preferences) {
|
||||||
|
this.burningManAccountingStoreService = burningManAccountingStoreService;
|
||||||
|
this.burningManPresentationService = burningManPresentationService;
|
||||||
|
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||||
|
this.preferences = preferences;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DaoSetupService
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addListeners() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
// Create the map from now back to the last entry of the historical data (April 2019-Nov. 2022).
|
||||||
|
averageBsqPriceByMonth.putAll(getAverageBsqPriceByMonth(new Date(), 2022, 10));
|
||||||
|
|
||||||
|
updateBalanceModelByAddress();
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
Map<String, BalanceModel> map = new HashMap<>();
|
||||||
|
// addAccountingBlockToBalanceModel takes about 500ms for 100k items, so we run it in a non UI thread.
|
||||||
|
getBlocks().forEach(block -> addAccountingBlockToBalanceModel(map, block));
|
||||||
|
UserThread.execute(() -> balanceModelByBurningManName.putAll(map));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void onInitialBlockRequestsComplete() {
|
||||||
|
updateBalanceModelByAddress();
|
||||||
|
getBlocks().forEach(this::addAccountingBlockToBalanceModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onNewBlockReceived(AccountingBlock accountingBlock) {
|
||||||
|
updateBalanceModelByAddress();
|
||||||
|
addAccountingBlockToBalanceModel(accountingBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBlock(AccountingBlock block) throws BlockHashNotConnectingException, BlockHeightNotConnectingException {
|
||||||
|
if (!getBlocks().contains(block)) {
|
||||||
|
Optional<AccountingBlock> optionalLastBlock = getLastBlock();
|
||||||
|
if (optionalLastBlock.isPresent()) {
|
||||||
|
AccountingBlock lastBlock = optionalLastBlock.get();
|
||||||
|
if (block.getHeight() != lastBlock.getHeight() + 1) {
|
||||||
|
throw new BlockHeightNotConnectingException();
|
||||||
|
}
|
||||||
|
if (!Arrays.equals(block.getTruncatedPreviousBlockHash(), lastBlock.getTruncatedHash())) {
|
||||||
|
throw new BlockHashNotConnectingException();
|
||||||
|
}
|
||||||
|
} else if (block.getHeight() != EARLIEST_BLOCK_HEIGHT) {
|
||||||
|
throw new BlockHeightNotConnectingException();
|
||||||
|
}
|
||||||
|
log.info("Add new accountingBlock at height {} at {} with {} txs", block.getHeight(),
|
||||||
|
new Date(block.getDate()), block.getTxs().size());
|
||||||
|
burningManAccountingStoreService.addBlock(block);
|
||||||
|
} else {
|
||||||
|
log.info("We have that block already. Height: {}", block.getHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBlockHeightOfLastBlock() {
|
||||||
|
return getLastBlock().map(AccountingBlock::getHeight).orElse(BurningManAccountingService.EARLIEST_BLOCK_HEIGHT - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AccountingBlock> getLastBlock() {
|
||||||
|
return getBlocks().stream().max(Comparator.comparing(AccountingBlock::getHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<AccountingBlock> getBlockAtHeight(int height) {
|
||||||
|
return getBlocks().stream().filter(block -> block.getHeight() == height).findAny();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Delegates
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public List<AccountingBlock> getBlocks() {
|
||||||
|
return burningManAccountingStoreService.getBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getBurningManNameByAddress() {
|
||||||
|
return burningManPresentationService.getBurningManNameByAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGenesisTxId() {
|
||||||
|
return burningManPresentationService.getGenesisTxId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void purgeLastTenBlocks() {
|
||||||
|
burningManAccountingStoreService.purgeLastTenBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void updateBalanceModelByAddress() {
|
||||||
|
burningManPresentationService.getBurningManCandidatesByName().keySet()
|
||||||
|
.forEach(key -> balanceModelByBurningManName.putIfAbsent(key, new BalanceModel()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAccountingBlockToBalanceModel(AccountingBlock accountingBlock) {
|
||||||
|
addAccountingBlockToBalanceModel(balanceModelByBurningManName, accountingBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAccountingBlockToBalanceModel(Map<String, BalanceModel> balanceModelByBurningManName,
|
||||||
|
AccountingBlock accountingBlock) {
|
||||||
|
accountingBlock.getTxs().forEach(tx -> {
|
||||||
|
tx.getOutputs().forEach(txOutput -> {
|
||||||
|
String name = txOutput.getName();
|
||||||
|
balanceModelByBurningManName.putIfAbsent(name, new BalanceModel());
|
||||||
|
balanceModelByBurningManName.get(name).addReceivedBtcBalanceEntry(new ReceivedBtcBalanceEntry(tx.getTruncatedTxId(),
|
||||||
|
txOutput.getValue(),
|
||||||
|
new Date(accountingBlock.getDate()),
|
||||||
|
toBalanceEntryType(tx.getType())));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Date, Price> getAverageBsqPriceByMonth(Date from, int toYear, int toMonth) {
|
||||||
|
Map<Date, Price> averageBsqPriceByMonth = new HashMap<>();
|
||||||
|
Calendar calendar = new GregorianCalendar();
|
||||||
|
calendar.setTime(from);
|
||||||
|
int year = calendar.get(Calendar.YEAR);
|
||||||
|
int month = calendar.get(Calendar.MONTH);
|
||||||
|
do {
|
||||||
|
for (; month >= 0; month--) {
|
||||||
|
if (year == toYear && month == toMonth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Date date = DateUtil.getStartOfMonth(year, month);
|
||||||
|
Price averageBsqPrice = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, 30, date).second;
|
||||||
|
averageBsqPriceByMonth.put(date, averageBsqPrice);
|
||||||
|
}
|
||||||
|
year--;
|
||||||
|
month = 11;
|
||||||
|
} while (year >= toYear);
|
||||||
|
return averageBsqPriceByMonth;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BalanceEntry.Type toBalanceEntryType(AccountingTx.Type type) {
|
||||||
|
return type == AccountingTx.Type.BTC_TRADE_FEE_TX ?
|
||||||
|
BalanceEntry.Type.BTC_TRADE_FEE_TX :
|
||||||
|
BalanceEntry.Type.DPT_TX;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("CommentedOutCode")
|
||||||
|
private static Map<Date, Price> getHistoricalAverageBsqPriceByMonth() {
|
||||||
|
// We use the average 30 day BSQ price from the first day of a month back 30 days. So for 1.Nov 2022 we take the average during October 2022.
|
||||||
|
// Filling the map takes a bit of computation time (about 5 sec), so we use for historical data a pre-calculated list.
|
||||||
|
// Average price from 1. May 2019 (April average) - 1. Nov 2022 (Oct average)
|
||||||
|
String historical = "1648789200000=2735, 1630472400000=3376, 1612155600000=6235, 1559365200000=13139, 1659330000000=3609, 1633064400000=3196, 1583038800000=7578, 1622523600000=3918, 1625115600000=3791, 1667278800000=3794, 1561957200000=10882, 1593579600000=6153, 1577854800000=9034, 1596258000000=6514, 1604206800000=5642, 1643691600000=3021, 1606798800000=4946, 1569906000000=10445, 1567314000000=9885, 1614574800000=5052, 1656651600000=3311, 1638334800000=3015, 1564635600000=8788, 1635742800000=3065, 1654059600000=3207, 1646110800000=2824, 1609477200000=4199, 1664600400000=3820, 1662008400000=3756, 1556686800000=24094, 1588309200000=7986, 1585717200000=7994, 1627794000000=3465, 1580533200000=5094, 1590987600000=7411, 1619845200000=3956, 1617253200000=4024, 1575176400000=9571, 1572584400000=9058, 1641013200000=3052, 1601528400000=5648, 1651381200000=2908, 1598936400000=6032";
|
||||||
|
|
||||||
|
// Create historical data as string
|
||||||
|
/* log.info("averageBsqPriceByMonth=" + getAverageBsqPriceByMonth(new Date(), 2019, 3).entrySet().stream()
|
||||||
|
.map(e -> e.getKey().getTime() + "=" + e.getValue().getValue())
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
*/
|
||||||
|
return Arrays.stream(historical.split(", "))
|
||||||
|
.map(chunk -> chunk.split("="))
|
||||||
|
.collect(Collectors.toMap(tuple -> new Date(Long.parseLong(tuple[0])),
|
||||||
|
tuple -> Price.valueOf("BSQ", Long.parseLong(tuple[1]))));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public interface BalanceEntry {
|
||||||
|
Date getDate();
|
||||||
|
|
||||||
|
Date getMonth();
|
||||||
|
|
||||||
|
enum Type {
|
||||||
|
BTC_TRADE_FEE_TX,
|
||||||
|
DPT_TX,
|
||||||
|
BURN_TX
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.model.BurnOutputModel;
|
||||||
|
import bisq.core.dao.burningman.model.BurningManCandidate;
|
||||||
|
|
||||||
|
import bisq.common.util.DateUtil;
|
||||||
|
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static bisq.core.dao.burningman.accounting.BurningManAccountingService.EARLIEST_DATE_MONTH;
|
||||||
|
import static bisq.core.dao.burningman.accounting.BurningManAccountingService.EARLIEST_DATE_YEAR;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class BalanceModel {
|
||||||
|
private final Set<ReceivedBtcBalanceEntry> receivedBtcBalanceEntries = new HashSet<>();
|
||||||
|
private final Map<Date, Set<ReceivedBtcBalanceEntry>> receivedBtcBalanceEntriesByMonth = new HashMap<>();
|
||||||
|
|
||||||
|
public BalanceModel() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addReceivedBtcBalanceEntry(ReceivedBtcBalanceEntry balanceEntry) {
|
||||||
|
receivedBtcBalanceEntries.add(balanceEntry);
|
||||||
|
|
||||||
|
Date month = balanceEntry.getMonth();
|
||||||
|
receivedBtcBalanceEntriesByMonth.putIfAbsent(month, new HashSet<>());
|
||||||
|
receivedBtcBalanceEntriesByMonth.get(month).add(balanceEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ReceivedBtcBalanceEntry> getReceivedBtcBalanceEntries() {
|
||||||
|
return receivedBtcBalanceEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<BurnedBsqBalanceEntry> getBurnedBsqBalanceEntries(Set<BurnOutputModel> burnOutputModels) {
|
||||||
|
return burnOutputModels.stream()
|
||||||
|
.map(burnOutputModel -> new BurnedBsqBalanceEntry(burnOutputModel.getTxId(),
|
||||||
|
burnOutputModel.getAmount(),
|
||||||
|
new Date(burnOutputModel.getDate())));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MonthlyBalanceEntry> getMonthlyBalanceEntries(BurningManCandidate burningManCandidate,
|
||||||
|
Predicate<BaseBalanceEntry> predicate) {
|
||||||
|
Map<Date, Set<BurnOutputModel>> burnOutputModelsByMonth = burningManCandidate.getBurnOutputModelsByMonth();
|
||||||
|
Set<Date> months = getMonths(new Date(), EARLIEST_DATE_YEAR, EARLIEST_DATE_MONTH);
|
||||||
|
return months.stream()
|
||||||
|
.map(date -> {
|
||||||
|
long sumBurnedBsq = 0;
|
||||||
|
Set<BalanceEntry.Type> types = new HashSet<>();
|
||||||
|
if (burnOutputModelsByMonth.containsKey(date)) {
|
||||||
|
Set<BurnOutputModel> burnOutputModels = burnOutputModelsByMonth.get(date);
|
||||||
|
Set<MonthlyBurnedBsqBalanceEntry> monthlyBurnedBsqBalanceEntries = burnOutputModels.stream()
|
||||||
|
.map(burnOutputModel -> new MonthlyBurnedBsqBalanceEntry(burnOutputModel.getTxId(),
|
||||||
|
burnOutputModel.getAmount(),
|
||||||
|
date))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
sumBurnedBsq = monthlyBurnedBsqBalanceEntries.stream()
|
||||||
|
.filter(predicate)
|
||||||
|
.peek(e -> types.add(e.getType()))
|
||||||
|
.mapToLong(MonthlyBurnedBsqBalanceEntry::getAmount)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
long sumReceivedBtc = 0;
|
||||||
|
if (receivedBtcBalanceEntriesByMonth.containsKey(date)) {
|
||||||
|
sumReceivedBtc = receivedBtcBalanceEntriesByMonth.get(date).stream()
|
||||||
|
.filter(predicate)
|
||||||
|
.peek(e -> types.add(e.getType()))
|
||||||
|
.mapToLong(BaseBalanceEntry::getAmount)
|
||||||
|
.sum();
|
||||||
|
}
|
||||||
|
return new MonthlyBalanceEntry(sumReceivedBtc, sumBurnedBsq, date, types);
|
||||||
|
})
|
||||||
|
.filter(balanceEntry -> balanceEntry.getBurnedBsq() > 0 || balanceEntry.getReceivedBtc() > 0)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Date> getMonths(Date from, int toYear, int toMonth) {
|
||||||
|
Set<Date> map = new HashSet<>();
|
||||||
|
Calendar calendar = new GregorianCalendar();
|
||||||
|
calendar.setTime(from);
|
||||||
|
int year = calendar.get(Calendar.YEAR);
|
||||||
|
int month = calendar.get(Calendar.MONTH);
|
||||||
|
do {
|
||||||
|
for (; month >= 0; month--) {
|
||||||
|
if (year == toYear && month == toMonth) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
map.add(DateUtil.getStartOfMonth(year, month));
|
||||||
|
}
|
||||||
|
year--;
|
||||||
|
month = 11;
|
||||||
|
} while (year >= toYear);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import bisq.common.util.DateUtil;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public abstract class BaseBalanceEntry implements BalanceEntry {
|
||||||
|
private final String txId;
|
||||||
|
private final long amount;
|
||||||
|
private final Date date;
|
||||||
|
private final Date month;
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
protected BaseBalanceEntry(String txId, long amount, Date date, Type type) {
|
||||||
|
this.txId = txId;
|
||||||
|
this.amount = amount;
|
||||||
|
this.date = date;
|
||||||
|
month = DateUtil.getStartOfMonth(date);
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BaseBalanceEntry{" +
|
||||||
|
"\r\n txId=" + txId +
|
||||||
|
"\r\n amount=" + amount +
|
||||||
|
",\r\n date=" + date +
|
||||||
|
",\r\n month=" + month +
|
||||||
|
",\r\n type=" + type +
|
||||||
|
"\r\n}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Getter
|
||||||
|
public class BurnedBsqBalanceEntry extends BaseBalanceEntry {
|
||||||
|
public BurnedBsqBalanceEntry(String txId, long amount, Date date) {
|
||||||
|
super(txId, amount, date, Type.BURN_TX);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import bisq.common.util.DateUtil;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public class MonthlyBalanceEntry implements BalanceEntry {
|
||||||
|
private final long receivedBtc;
|
||||||
|
private final long burnedBsq;
|
||||||
|
private final Date date;
|
||||||
|
private final Date month;
|
||||||
|
private final Set<Type> types;
|
||||||
|
|
||||||
|
public MonthlyBalanceEntry(long receivedBtc, long burnedBsq, Date date, Set<Type> types) {
|
||||||
|
this.receivedBtc = receivedBtc;
|
||||||
|
this.burnedBsq = burnedBsq;
|
||||||
|
this.date = date;
|
||||||
|
month = DateUtil.getStartOfMonth(date);
|
||||||
|
this.types = types;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Getter
|
||||||
|
public class MonthlyBurnedBsqBalanceEntry extends BurnedBsqBalanceEntry {
|
||||||
|
public MonthlyBurnedBsqBalanceEntry(String txId, long amount, Date date) {
|
||||||
|
super(txId, amount, date);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.balance;
|
||||||
|
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Getter
|
||||||
|
public class ReceivedBtcBalanceEntry extends BaseBalanceEntry {
|
||||||
|
// We store only last 4 bytes in the AccountTx which is used to create a ReceivedBtcBalanceEntry instance.
|
||||||
|
public ReceivedBtcBalanceEntry(byte[] truncatedTxId, long amount, Date date, BalanceEntry.Type type) {
|
||||||
|
super(Hex.encode(truncatedTxId), amount, date, type);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.blockchain;
|
||||||
|
|
||||||
|
import bisq.common.proto.network.NetworkPayload;
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
// Block data is aggressively optimized for minimal size.
|
||||||
|
// Block has 21 bytes base cost
|
||||||
|
// Tx has 2 byte base cost.
|
||||||
|
// TxOutput has variable byte size depending on name length, usually about 10-20 bytes.
|
||||||
|
// Some extra overhead of a few bytes is present depending on lists filled or not.
|
||||||
|
// Example fee tx (1 output) has about 40 bytes
|
||||||
|
// Example DPT tx with 2 outputs has about 60 bytes, typical DPT with 15-20 outputs might have 500 bytes.
|
||||||
|
// 2 year legacy BM had about 100k fee txs and 1000 DPTs. Would be about 4MB for fee txs and 500kB for DPT.
|
||||||
|
// As most blocks have at least 1 tx we might not have empty blocks.
|
||||||
|
// With above estimates we can expect about 2 MB growth per year.
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
|
||||||
|
public final class AccountingBlock implements NetworkPayload {
|
||||||
|
@Getter
|
||||||
|
private final int height;
|
||||||
|
private final int timeInSec;
|
||||||
|
// We use only last 4 bytes of 32 byte hash to save space.
|
||||||
|
// We use a byte array for flexibility if we would need to change the length of the hash later.
|
||||||
|
@Getter
|
||||||
|
private final byte[] truncatedHash;
|
||||||
|
@Getter
|
||||||
|
private final byte[] truncatedPreviousBlockHash;
|
||||||
|
@Getter
|
||||||
|
private final List<AccountingTx> txs;
|
||||||
|
|
||||||
|
public AccountingBlock(int height,
|
||||||
|
int timeInSec,
|
||||||
|
byte[] truncatedHash,
|
||||||
|
byte[] truncatedPreviousBlockHash,
|
||||||
|
List<AccountingTx> txs) {
|
||||||
|
this.height = height;
|
||||||
|
this.timeInSec = timeInSec;
|
||||||
|
this.truncatedHash = truncatedHash;
|
||||||
|
this.truncatedPreviousBlockHash = truncatedPreviousBlockHash;
|
||||||
|
this.txs = txs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public protobuf.AccountingBlock toProtoMessage() {
|
||||||
|
return protobuf.AccountingBlock.newBuilder()
|
||||||
|
.setHeight(height)
|
||||||
|
.setTimeInSec(timeInSec)
|
||||||
|
.setTruncatedHash(ByteString.copyFrom(truncatedHash))
|
||||||
|
.setTruncatedPreviousBlockHash(ByteString.copyFrom(truncatedPreviousBlockHash))
|
||||||
|
.addAllTxs(txs.stream().map(AccountingTx::toProtoMessage).collect(Collectors.toList()))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountingBlock fromProto(protobuf.AccountingBlock proto) {
|
||||||
|
List<AccountingTx> txs = proto.getTxsList().stream()
|
||||||
|
.map(AccountingTx::fromProto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
// log.error("AccountingBlock.getSerializedSize {}, txs.size={}", proto.getSerializedSize(), txs.size());
|
||||||
|
return new AccountingBlock(proto.getHeight(),
|
||||||
|
proto.getTimeInSec(),
|
||||||
|
proto.getTruncatedHash().toByteArray(),
|
||||||
|
proto.getTruncatedPreviousBlockHash().toByteArray(),
|
||||||
|
txs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDate() {
|
||||||
|
return timeInSec * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AccountingBlock{" +
|
||||||
|
"\r\n height=" + height +
|
||||||
|
",\r\n timeInSec=" + timeInSec +
|
||||||
|
",\r\n truncatedHash=" + Hex.encode(truncatedHash) +
|
||||||
|
",\r\n truncatedPreviousBlockHash=" + Hex.encode(truncatedPreviousBlockHash) +
|
||||||
|
",\r\n txs=" + txs +
|
||||||
|
"\r\n}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.blockchain;
|
||||||
|
|
||||||
|
import bisq.common.proto.network.NetworkPayload;
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public final class AccountingTx implements NetworkPayload {
|
||||||
|
public enum Type {
|
||||||
|
BTC_TRADE_FEE_TX,
|
||||||
|
DPT_TX
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Type type;
|
||||||
|
private final List<AccountingTxOutput> outputs;
|
||||||
|
// We store only last 4 bytes to have a unique ID. Chance for collusion is very low, and we take that risk that
|
||||||
|
// one object might get overridden in a hashset by the colluding truncatedTxId and all other data being the same as well.
|
||||||
|
private final byte[] truncatedTxId;
|
||||||
|
|
||||||
|
public AccountingTx(Type type, List<AccountingTxOutput> outputs, String txId) {
|
||||||
|
this(type, outputs, Hex.decodeLast4Bytes(txId));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private AccountingTx(Type type, List<AccountingTxOutput> outputs, byte[] truncatedTxId) {
|
||||||
|
this.type = type;
|
||||||
|
this.outputs = outputs;
|
||||||
|
this.truncatedTxId = truncatedTxId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public protobuf.AccountingTx toProtoMessage() {
|
||||||
|
return protobuf.AccountingTx.newBuilder()
|
||||||
|
.setType(type.ordinal())
|
||||||
|
.addAllOutputs(outputs.stream().map(AccountingTxOutput::toProtoMessage).collect(Collectors.toList()))
|
||||||
|
.setTruncatedTxId(ByteString.copyFrom(truncatedTxId)).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountingTx fromProto(protobuf.AccountingTx proto) {
|
||||||
|
List<AccountingTxOutput> outputs = proto.getOutputsList().stream()
|
||||||
|
.map(AccountingTxOutput::fromProto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return new AccountingTx(Type.values()[proto.getType()],
|
||||||
|
outputs,
|
||||||
|
proto.getTruncatedTxId().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AccountingTx{" +
|
||||||
|
",\n type='" + type + '\'' +
|
||||||
|
",\n outputs=" + outputs +
|
||||||
|
",\n truncatedTxId=" + Hex.encode(truncatedTxId) +
|
||||||
|
"\n }";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.blockchain;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.BurningManPresentationService;
|
||||||
|
|
||||||
|
import bisq.common.proto.network.NetworkPayload;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
// Outputs get pruned to required number of outputs depending on tx type.
|
||||||
|
// We store value as integer in protobuf as we do not support larger amounts than 21.47483647 BTC for our tx types.
|
||||||
|
// Name is burningman candidate name. For legacy Burningman we shorten to safe space.
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public final class AccountingTxOutput implements NetworkPayload {
|
||||||
|
private static final String LEGACY_BM_FEES_SHORT = "LBMF";
|
||||||
|
private static final String LEGACY_BM_DPT_SHORT = "LBMD";
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final long value;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public AccountingTxOutput(long value, String name) {
|
||||||
|
this.value = value;
|
||||||
|
this.name = maybeShortenLBM(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maybeShortenLBM(String name) {
|
||||||
|
return name.equals(BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_NAME) ?
|
||||||
|
LEGACY_BM_FEES_SHORT :
|
||||||
|
name.equals(BurningManPresentationService.LEGACY_BURNING_MAN_DPT_NAME) ?
|
||||||
|
LEGACY_BM_DPT_SHORT :
|
||||||
|
name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name.equals(LEGACY_BM_FEES_SHORT) ?
|
||||||
|
BurningManPresentationService.LEGACY_BURNING_MAN_BTC_FEES_NAME :
|
||||||
|
name.equals(LEGACY_BM_DPT_SHORT) ?
|
||||||
|
BurningManPresentationService.LEGACY_BURNING_MAN_DPT_NAME :
|
||||||
|
name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public protobuf.AccountingTxOutput toProtoMessage() {
|
||||||
|
checkArgument(value < Coin.valueOf(2147483647).getValue(),
|
||||||
|
"We only support integer value in protobuf storage for the amount and it need to be below 21.47483647 BTC");
|
||||||
|
return protobuf.AccountingTxOutput.newBuilder()
|
||||||
|
.setValue((int) value)
|
||||||
|
.setName(name).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountingTxOutput fromProto(protobuf.AccountingTxOutput proto) {
|
||||||
|
int intValue = proto.getValue();
|
||||||
|
checkArgument(intValue >= 0, "Value must not be negative");
|
||||||
|
return new AccountingTxOutput(intValue, proto.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "AccountingTxOutput{" +
|
||||||
|
",\r\n value=" + value +
|
||||||
|
",\r\n name='" + name + '\'' +
|
||||||
|
"\r\n}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.blockchain.temp;
|
||||||
|
|
||||||
|
import bisq.core.dao.node.full.rpc.dto.DtoPubKeyScript;
|
||||||
|
import bisq.core.dao.node.full.rpc.dto.RawDtoTransaction;
|
||||||
|
import bisq.core.dao.state.model.blockchain.ScriptType;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public final class TempAccountingTx {
|
||||||
|
private final String txId;
|
||||||
|
private final boolean isValidDptLockTime;
|
||||||
|
private final List<TempAccountingTxInput> inputs;
|
||||||
|
private final List<TempAccountingTxOutput> outputs;
|
||||||
|
|
||||||
|
public TempAccountingTx(RawDtoTransaction tx) {
|
||||||
|
txId = tx.getTxId();
|
||||||
|
|
||||||
|
// If lockTime is < 500000000 it is interpreted as block height, otherwise as unix time. We use block height.
|
||||||
|
// We only handle blocks from EARLIEST_BLOCK_HEIGHT on
|
||||||
|
//todo for dev testing
|
||||||
|
isValidDptLockTime = /*tx.getLockTime() >= BurningManAccountingService.EARLIEST_BLOCK_HEIGHT &&*/ tx.getLockTime() < 500000000;
|
||||||
|
|
||||||
|
inputs = tx.getVIn().stream()
|
||||||
|
.map(input -> {
|
||||||
|
List<String> txInWitness = input.getTxInWitness() != null ? input.getTxInWitness() : new ArrayList<>();
|
||||||
|
return new TempAccountingTxInput(input.getSequence(), txInWitness);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
outputs = tx.getVOut().stream()
|
||||||
|
.map(output -> {
|
||||||
|
long value = BigDecimal.valueOf(output.getValue()).movePointRight(8).longValueExact();
|
||||||
|
// We use a non-null field for address as in the final object we require that the address is available
|
||||||
|
String address = "";
|
||||||
|
DtoPubKeyScript scriptPubKey = output.getScriptPubKey();
|
||||||
|
if (scriptPubKey != null) {
|
||||||
|
List<String> addresses = scriptPubKey.getAddresses();
|
||||||
|
if (addresses != null && addresses.size() == 1) {
|
||||||
|
address = addresses.get(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScriptType scriptType = output.getScriptPubKey() != null ? output.getScriptPubKey().getType() : null;
|
||||||
|
return new TempAccountingTxOutput(value, address, scriptType);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.blockchain.temp;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public final class TempAccountingTxInput {
|
||||||
|
private final long sequence;
|
||||||
|
private final List<String> txInWitness;
|
||||||
|
|
||||||
|
public TempAccountingTxInput(long sequence, List<String> txInWitness) {
|
||||||
|
this.sequence = sequence;
|
||||||
|
this.txInWitness = txInWitness;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.blockchain.temp;
|
||||||
|
|
||||||
|
import bisq.core.dao.state.model.blockchain.ScriptType;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public final class TempAccountingTxOutput {
|
||||||
|
private final long value;
|
||||||
|
private final String address;
|
||||||
|
private final ScriptType scriptType;
|
||||||
|
|
||||||
|
public TempAccountingTxOutput(long value, String address, ScriptType scriptType) {
|
||||||
|
this.value = value;
|
||||||
|
this.address = address;
|
||||||
|
this.scriptType = scriptType;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.exceptions;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class BlockHashNotConnectingException extends Exception {
|
||||||
|
public BlockHashNotConnectingException() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.exceptions;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class BlockHeightNotConnectingException extends Exception {
|
||||||
|
public BlockHeightNotConnectingException() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node;
|
||||||
|
|
||||||
|
import bisq.core.dao.DaoSetupService;
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.full.AccountingBlockParser;
|
||||||
|
import bisq.core.dao.state.DaoStateListener;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
|
||||||
|
import bisq.network.p2p.BootstrapListener;
|
||||||
|
import bisq.network.p2p.P2PService;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.app.DevEnv;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
import org.bitcoinj.core.Sha256Hash;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static org.bitcoinj.core.Utils.HEX;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public abstract class AccountingNode implements DaoSetupService, DaoStateListener {
|
||||||
|
public static final Set<String> PERMITTED_PUB_KEYS = Set.of("034527b1c2b644283c19c180efbcc9ba51258fbe5c5ce0c95b522f1c9b07896e48",
|
||||||
|
"02a06121632518eef4400419a3aaf944534e8cf357138dd82bba3ad78ce5902f27",
|
||||||
|
"029ff0da89aa03507dfe0529eb74a53bc65fbee7663ceba04f014b0ee4520973b5",
|
||||||
|
"0205c992604969bc70914fc89a6daa43cd5157ac886224a8c0901dd1dc6dd1df45",
|
||||||
|
"023e699281b3ee41f35991f064a5c12cb1b61286dda22c8220b3f707aa21235efb");
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Oracle verification
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public static Sha256Hash getSha256Hash(AccountingBlock block) {
|
||||||
|
return Sha256Hash.of(block.toProtoMessage().toByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Sha256Hash getSha256Hash(Collection<AccountingBlock> blocks) {
|
||||||
|
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
|
for (AccountingBlock accountingBlock : blocks) {
|
||||||
|
outputStream.write(accountingBlock.toProtoMessage().toByteArray());
|
||||||
|
}
|
||||||
|
return Sha256Hash.of(outputStream.toByteArray());
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getSignature(Sha256Hash sha256Hash, ECKey privKey) {
|
||||||
|
ECKey.ECDSASignature ecdsaSignature = privKey.sign(sha256Hash);
|
||||||
|
return ecdsaSignature.encodeToDER();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isValidPubKeyAndSignature(Sha256Hash sha256Hash,
|
||||||
|
String pubKey,
|
||||||
|
byte[] signature,
|
||||||
|
boolean useDevPrivilegeKeys) {
|
||||||
|
if (!getPermittedPubKeys(useDevPrivilegeKeys).contains(pubKey)) {
|
||||||
|
log.warn("PubKey is not in supported key set. pubKey={}", pubKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ECKey.ECDSASignature ecdsaSignature = ECKey.ECDSASignature.decodeFromDER(signature);
|
||||||
|
ECKey ecPubKey = ECKey.fromPublicOnly(HEX.decode(pubKey));
|
||||||
|
return ecPubKey.verify(sha256Hash, ecdsaSignature);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
log.warn("Signature verification failed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPermittedPubKey(boolean useDevPrivilegeKeys, String pubKey) {
|
||||||
|
return getPermittedPubKeys(useDevPrivilegeKeys).contains(pubKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> getPermittedPubKeys(boolean useDevPrivilegeKeys) {
|
||||||
|
return useDevPrivilegeKeys ? Set.of(DevEnv.DEV_PRIVILEGE_PUB_KEY) : PERMITTED_PUB_KEYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Class fields
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected final P2PService p2PService;
|
||||||
|
private final DaoStateService daoStateService;
|
||||||
|
protected final BurningManAccountingService burningManAccountingService;
|
||||||
|
protected final AccountingBlockParser accountingBlockParser;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected Consumer<String> errorMessageHandler;
|
||||||
|
@Nullable
|
||||||
|
protected Consumer<String> warnMessageHandler;
|
||||||
|
protected BootstrapListener bootstrapListener;
|
||||||
|
protected int tryReorgCounter;
|
||||||
|
protected boolean p2pNetworkReady;
|
||||||
|
protected boolean initialBlockRequestsComplete;
|
||||||
|
|
||||||
|
public AccountingNode(P2PService p2PService,
|
||||||
|
DaoStateService daoStateService,
|
||||||
|
BurningManAccountingService burningManAccountingService,
|
||||||
|
AccountingBlockParser accountingBlockParser) {
|
||||||
|
this.p2PService = p2PService;
|
||||||
|
this.daoStateService = daoStateService;
|
||||||
|
this.burningManAccountingService = burningManAccountingService;
|
||||||
|
this.accountingBlockParser = accountingBlockParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DaoStateListener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onParseBlockChainComplete() {
|
||||||
|
onInitialDaoBlockParsingComplete();
|
||||||
|
|
||||||
|
// We get called onParseBlockChainComplete at each new block arriving but we want to react only after initial
|
||||||
|
// parsing is done, so we remove after getting called ourself as listener.
|
||||||
|
daoStateService.removeDaoStateListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// DaoSetupService
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addListeners() {
|
||||||
|
if (daoStateService.isParseBlockChainComplete()) {
|
||||||
|
log.info("daoStateService.isParseBlockChainComplete is already true, " +
|
||||||
|
"we call onInitialDaoBlockParsingComplete directly");
|
||||||
|
onInitialDaoBlockParsingComplete();
|
||||||
|
} else {
|
||||||
|
daoStateService.addDaoStateListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrapListener = new BootstrapListener() {
|
||||||
|
@Override
|
||||||
|
public void onNoSeedNodeAvailable() {
|
||||||
|
onP2PNetworkReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdatedDataReceived() {
|
||||||
|
onP2PNetworkReady();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void start();
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void setErrorMessageHandler(@SuppressWarnings("NullableProblems") Consumer<String> errorMessageHandler) {
|
||||||
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWarnMessageHandler(@SuppressWarnings("NullableProblems") Consumer<String> warnMessageHandler) {
|
||||||
|
this.warnMessageHandler = warnMessageHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void shutDown();
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protected
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected abstract void onInitialDaoBlockParsingComplete();
|
||||||
|
|
||||||
|
protected void onInitialized() {
|
||||||
|
if (p2PService.isBootstrapped()) {
|
||||||
|
log.info("p2PService.isBootstrapped is already true, we call onP2PNetworkReady directly.");
|
||||||
|
onP2PNetworkReady();
|
||||||
|
} else {
|
||||||
|
p2PService.addP2PServiceListener(bootstrapListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onP2PNetworkReady() {
|
||||||
|
p2pNetworkReady = true;
|
||||||
|
p2PService.removeP2PServiceListener(bootstrapListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void startRequestBlocks();
|
||||||
|
|
||||||
|
protected void onInitialBlockRequestsComplete() {
|
||||||
|
initialBlockRequestsComplete = true;
|
||||||
|
burningManAccountingService.onInitialBlockRequestsComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void applyReOrg() {
|
||||||
|
log.warn("applyReOrg called");
|
||||||
|
tryReorgCounter++;
|
||||||
|
if (tryReorgCounter < 5) {
|
||||||
|
burningManAccountingService.purgeLastTenBlocks();
|
||||||
|
// Increase delay at each retry
|
||||||
|
UserThread.runAfter(this::startRequestBlocks, (long) tryReorgCounter * tryReorgCounter);
|
||||||
|
} else {
|
||||||
|
log.warn("We tried {} times to request blocks again after a reorg signal but it is still failing.", tryReorgCounter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.full.AccountingFullNode;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.lite.AccountingLiteNode;
|
||||||
|
import bisq.core.user.Preferences;
|
||||||
|
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class AccountingNodeProvider {
|
||||||
|
@Getter
|
||||||
|
private final AccountingNode accountingNode;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AccountingNodeProvider(AccountingLiteNode liteNode,
|
||||||
|
AccountingFullNode fullNode,
|
||||||
|
InActiveAccountingNode inActiveAccountingNode,
|
||||||
|
@Named(Config.IS_BM_FULL_NODE) boolean isBmFullNode,
|
||||||
|
Preferences preferences) {
|
||||||
|
|
||||||
|
boolean rpcDataSet = preferences.getRpcUser() != null &&
|
||||||
|
!preferences.getRpcUser().isEmpty()
|
||||||
|
&& preferences.getRpcPw() != null &&
|
||||||
|
!preferences.getRpcPw().isEmpty() &&
|
||||||
|
preferences.getBlockNotifyPort() > 0;
|
||||||
|
if (BurningManService.isActivated()) {
|
||||||
|
accountingNode = isBmFullNode && rpcDataSet ? fullNode : liteNode;
|
||||||
|
} else {
|
||||||
|
accountingNode = inActiveAccountingNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.full.AccountingBlockParser;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
|
||||||
|
import bisq.network.p2p.P2PService;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
// Dummy implementation for a do-nothing AccountingNode. Used for the time before the burningman domain gets activated.
|
||||||
|
@Singleton
|
||||||
|
class InActiveAccountingNode extends AccountingNode {
|
||||||
|
@Inject
|
||||||
|
public InActiveAccountingNode(P2PService p2PService,
|
||||||
|
DaoStateService daoStateService,
|
||||||
|
BurningManAccountingService burningManAccountingService,
|
||||||
|
AccountingBlockParser accountingBlockParser) {
|
||||||
|
super(p2PService, daoStateService, burningManAccountingService, accountingBlockParser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addListeners() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutDown() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onInitialDaoBlockParsingComplete() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startRequestBlocks() {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.full;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingTx;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingTxOutput;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.temp.TempAccountingTx;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.temp.TempAccountingTxInput;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.temp.TempAccountingTxOutput;
|
||||||
|
import bisq.core.dao.node.full.rpc.dto.RawDtoBlock;
|
||||||
|
import bisq.core.dao.state.model.blockchain.ScriptType;
|
||||||
|
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.TransactionInput;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class AccountingBlockParser {
|
||||||
|
private final BurningManAccountingService burningManAccountingService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AccountingBlockParser(BurningManAccountingService burningManAccountingService) {
|
||||||
|
this.burningManAccountingService = burningManAccountingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountingBlock parse(RawDtoBlock rawDtoBlock) {
|
||||||
|
Map<String, String> burningManNameByAddress = burningManAccountingService.getBurningManNameByAddress();
|
||||||
|
String genesisTxId = burningManAccountingService.getGenesisTxId();
|
||||||
|
|
||||||
|
// We filter early for first output address match. DPT txs have multiple outputs which need to match and will be checked later.
|
||||||
|
Set<String> receiverAddresses = burningManNameByAddress.keySet();
|
||||||
|
List<AccountingTx> txs = rawDtoBlock.getTx().stream()
|
||||||
|
.map(TempAccountingTx::new)
|
||||||
|
.filter(tempAccountingTx -> receiverAddresses.contains(tempAccountingTx.getOutputs().get(0).getAddress()))
|
||||||
|
.map(tempAccountingTx -> toAccountingTx(tempAccountingTx, burningManNameByAddress, genesisTxId))
|
||||||
|
.filter(Optional::isPresent)
|
||||||
|
.map(Optional::get)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
// Time in rawDtoBlock is in seconds
|
||||||
|
int timeInSec = rawDtoBlock.getTime().intValue();
|
||||||
|
byte[] truncatedHash = Hex.decodeLast4Bytes(rawDtoBlock.getHash());
|
||||||
|
byte[] truncatedPreviousBlockHash = Hex.decodeLast4Bytes(rawDtoBlock.getPreviousBlockHash());
|
||||||
|
return new AccountingBlock(rawDtoBlock.getHeight(),
|
||||||
|
timeInSec,
|
||||||
|
truncatedHash,
|
||||||
|
truncatedPreviousBlockHash,
|
||||||
|
txs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot know for sure if it's a DPT or BTC fee tx as we do not have the spending tx output with more
|
||||||
|
// data for verification as we do not keep the full blockchain data for lookup unspent tx outputs.
|
||||||
|
// The DPT can be very narrowly detected. The BTC fee txs might have false positives.
|
||||||
|
private Optional<AccountingTx> toAccountingTx(TempAccountingTx tempAccountingTx,
|
||||||
|
Map<String, String> burningManNameByAddress,
|
||||||
|
String genesisTxId) {
|
||||||
|
if (genesisTxId.equals(tempAccountingTx.getTxId())) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> receiverAddresses = burningManNameByAddress.keySet();
|
||||||
|
// DPT has 1 input from P2WSH with lock time and sequence number set.
|
||||||
|
// We only use native segwit P2SH, so we expect a txInWitness
|
||||||
|
List<TempAccountingTxInput> inputs = tempAccountingTx.getInputs();
|
||||||
|
List<TempAccountingTxOutput> outputs = tempAccountingTx.getOutputs();
|
||||||
|
// Max DPT output amount is currently 4 BTC. Max. security deposit is 50% of trade amount.
|
||||||
|
// We give some extra headroom to cover potential future changes.
|
||||||
|
// We store value as integer in protobuf to safe space, so max. possible value is 21.47483647 BTC.
|
||||||
|
long maxDptValue = 6 * Coin.COIN.getValue();
|
||||||
|
if (inputs.size() == 1 &&
|
||||||
|
isValidTimeLock(tempAccountingTx, inputs.get(0)) &&
|
||||||
|
doAllOutputsMatchReceivers(outputs, receiverAddresses) &&
|
||||||
|
isWitnessDataWP2SH(inputs.get(0).getTxInWitness()) &&
|
||||||
|
outputs.stream().allMatch(txOutput -> isExpectedScriptType(txOutput, tempAccountingTx)) &&
|
||||||
|
outputs.stream().allMatch(txOutput -> txOutput.getValue() <= maxDptValue)) {
|
||||||
|
|
||||||
|
List<AccountingTxOutput> accountingTxOutputs = outputs.stream()
|
||||||
|
.map(output -> new AccountingTxOutput(output.getValue(), burningManNameByAddress.get(output.getAddress())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return Optional.of(new AccountingTx(AccountingTx.Type.DPT_TX, accountingTxOutputs, tempAccountingTx.getTxId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// BTC trade fee tx has 2 or 3 outputs.
|
||||||
|
// First output to receiver, second reserved for trade and an optional 3rd for change.
|
||||||
|
// Address check is done in parse method above already.
|
||||||
|
// Amounts are in a certain range but as fee can change with DAO param changes we cannot set hard limits.
|
||||||
|
// The min. trade fee in Nov. 2022 is 5000 sat. We use 2500 as lower bound.
|
||||||
|
// The largest taker fee for a 2 BTC trade is about 0.0059976 BTC (599_760 sat). We use 1_000_000 sat as upper bound.
|
||||||
|
// Inputs are not constrained.
|
||||||
|
// Max fees amount is currently 0.0176 BTC.
|
||||||
|
// We give some extra headroom to cover potential future fee changes.
|
||||||
|
// We store value as integer in protobuf to safe space, so max. possible value is 21.47483647 BTC.
|
||||||
|
long maxTradeFeeValue = Coin.parseCoin("0.1").getValue();
|
||||||
|
TempAccountingTxOutput firstOutput = outputs.get(0);
|
||||||
|
if (outputs.size() >= 2 &&
|
||||||
|
outputs.size() <= 3 &&
|
||||||
|
firstOutput.getValue() > 2500 &&
|
||||||
|
firstOutput.getValue() < 1_000_000 &&
|
||||||
|
isExpectedScriptType(firstOutput, tempAccountingTx) &&
|
||||||
|
firstOutput.getValue() <= maxTradeFeeValue) {
|
||||||
|
// We only keep first output.
|
||||||
|
String name = burningManNameByAddress.get(firstOutput.getAddress());
|
||||||
|
return Optional.of(new AccountingTx(AccountingTx.Type.BTC_TRADE_FEE_TX,
|
||||||
|
List.of(new AccountingTxOutput(firstOutput.getValue(), name)),
|
||||||
|
tempAccountingTx.getTxId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO not sure if other ScriptType are to be expected
|
||||||
|
private boolean isExpectedScriptType(TempAccountingTxOutput txOutput, TempAccountingTx accountingTx) {
|
||||||
|
boolean result = txOutput.getScriptType() != null &&
|
||||||
|
(txOutput.getScriptType() == ScriptType.PUB_KEY_HASH ||
|
||||||
|
txOutput.getScriptType() == ScriptType.SCRIPT_HASH ||
|
||||||
|
txOutput.getScriptType() == ScriptType.WITNESS_V0_KEYHASH);
|
||||||
|
if (!result) {
|
||||||
|
log.error("isExpectedScriptType txOutput.getScriptType()={}, txIf={}", txOutput.getScriptType(), accountingTx.getTxId());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All outputs need to be to receiver addresses (incl. legacy BM)
|
||||||
|
private boolean doAllOutputsMatchReceivers(List<TempAccountingTxOutput> outputs, Set<String> receiverAddresses) {
|
||||||
|
return outputs.stream().allMatch(output -> receiverAddresses.contains(output.getAddress()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidTimeLock(TempAccountingTx accountingTx, TempAccountingTxInput firstInput) {
|
||||||
|
// Need to be 0xfffffffe
|
||||||
|
return accountingTx.isValidDptLockTime() && firstInput.getSequence() == TransactionInput.NO_SEQUENCE - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Example txInWitness: [, 304502210098fcec3ac1c1383d40159587b42e8b79cb3e793004d6ccb080bfb93f02c15f93022039f014eb933c59f988d68a61aa7a1c787f08d94bd0b222104718792798e43e3c01, 304402201f8b37f3b8b5b9944ca88f18f6bb888c5a48dc5183edf204ae6d3781032122e102204dc6397538055d94de1ab683315aac7d87289be0c014569f7b3fa465bf70b6d401, 5221027f6da96ede171617ce79ec305a76871ecce21ad737517b667fc9735f2dc342342102d8d93e02fb0833201274b47a302b47ff81c0b3510508eb0444cb1674d0d8a6d052ae]
|
||||||
|
*/
|
||||||
|
private boolean isWitnessDataWP2SH(List<String> txInWitness) {
|
||||||
|
// txInWitness from the 2of2 multiSig has 4 chunks.
|
||||||
|
// 0 byte, sig1, sig2, redeemScript
|
||||||
|
if (txInWitness.size() != 4) {
|
||||||
|
log.error("txInWitness chunks size not 4 .txInWitness={}", txInWitness);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// First chunk is o byte (empty string)
|
||||||
|
if (!txInWitness.get(0).isEmpty()) {
|
||||||
|
log.error("txInWitness.get(0) not empty .txInWitness={}", txInWitness);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 2 signatures are 70 - 73 bytes
|
||||||
|
int minSigLength = 140;
|
||||||
|
int maxSigLength = 146;
|
||||||
|
int fistSigLength = txInWitness.get(1).length();
|
||||||
|
if (fistSigLength < minSigLength || fistSigLength > maxSigLength) {
|
||||||
|
log.error("fistSigLength wrong .txInWitness={}", txInWitness);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int secondSigLength = txInWitness.get(2).length();
|
||||||
|
if (secondSigLength < minSigLength || secondSigLength > maxSigLength) {
|
||||||
|
log.error("secondSigLength wrong .txInWitness={}", txInWitness);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String redeemScript = txInWitness.get(3);
|
||||||
|
if (redeemScript.length() != 142) {
|
||||||
|
log.error("redeemScript not valid length .txInWitness={}", txInWitness);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OP_2 pub1 pub2 OP_2 OP_CHECKMULTISIG
|
||||||
|
// In hex: "5221" + PUB_KEY_1 + "21" + PUB_KEY_2 + "52ae";
|
||||||
|
// PubKeys are 33 bytes -> length 66 in hex
|
||||||
|
String separator = redeemScript.substring(70, 72);
|
||||||
|
boolean result = redeemScript.startsWith("5221") &&
|
||||||
|
redeemScript.endsWith("52ae") &&
|
||||||
|
separator.equals("21");
|
||||||
|
if (!result) {
|
||||||
|
log.error("redeemScript not valid .txInWitness={}", txInWitness);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,323 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.full;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.exceptions.BlockHashNotConnectingException;
|
||||||
|
import bisq.core.dao.burningman.accounting.exceptions.BlockHeightNotConnectingException;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.AccountingNode;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.full.network.AccountingFullNodeNetworkService;
|
||||||
|
import bisq.core.dao.node.full.RpcException;
|
||||||
|
import bisq.core.dao.node.full.RpcService;
|
||||||
|
import bisq.core.dao.node.full.rpc.NotificationHandlerException;
|
||||||
|
import bisq.core.dao.node.full.rpc.dto.RawDtoBlock;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
|
||||||
|
import bisq.network.p2p.P2PService;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.net.ConnectException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class AccountingFullNode extends AccountingNode {
|
||||||
|
private final RpcService rpcService;
|
||||||
|
private final AccountingFullNodeNetworkService accountingFullNodeNetworkService;
|
||||||
|
|
||||||
|
private boolean addBlockHandlerAdded;
|
||||||
|
private int batchedBlocks;
|
||||||
|
private long batchStartTime;
|
||||||
|
private final List<RawDtoBlock> pendingRawDtoBlocks = new ArrayList<>();
|
||||||
|
private int requestBlocksUpToHeadHeightCounter;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AccountingFullNode(P2PService p2PService,
|
||||||
|
DaoStateService daoStateService,
|
||||||
|
BurningManAccountingService burningManAccountingService,
|
||||||
|
AccountingBlockParser accountingBlockParser,
|
||||||
|
AccountingFullNodeNetworkService accountingFullNodeNetworkService,
|
||||||
|
RpcService rpcService) {
|
||||||
|
super(p2PService, daoStateService, burningManAccountingService, accountingBlockParser);
|
||||||
|
|
||||||
|
this.rpcService = rpcService;
|
||||||
|
this.accountingFullNodeNetworkService = accountingFullNodeNetworkService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public methods
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
// We do not start yes but wait until DAO block parsing is complete to not interfere with
|
||||||
|
// that higher priority activity.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutDown() {
|
||||||
|
accountingFullNodeNetworkService.shutDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protected
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// We start after initial DAO parsing is complete
|
||||||
|
@Override
|
||||||
|
protected void onInitialDaoBlockParsingComplete() {
|
||||||
|
log.info("onInitialDaoBlockParsingComplete");
|
||||||
|
rpcService.setup(() -> {
|
||||||
|
accountingFullNodeNetworkService.addListeners();
|
||||||
|
super.onInitialized();
|
||||||
|
startRequestBlocks();
|
||||||
|
},
|
||||||
|
this::handleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onP2PNetworkReady() {
|
||||||
|
super.onP2PNetworkReady();
|
||||||
|
|
||||||
|
if (initialBlockRequestsComplete) {
|
||||||
|
addBlockHandler();
|
||||||
|
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
|
||||||
|
log.info("onP2PNetworkReady: We run requestBlocksIfNewBlockAvailable with latest block height {}.", heightOfLastBlock);
|
||||||
|
requestBlocksIfNewBlockAvailable(heightOfLastBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startRequestBlocks() {
|
||||||
|
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
|
||||||
|
log.info("startRequestBlocks: heightOfLastBlock={}", heightOfLastBlock);
|
||||||
|
rpcService.requestChainHeadHeight(headHeight -> {
|
||||||
|
// If our persisted block is equal to the chain height we have heightOfLastBlock 1 block higher,
|
||||||
|
// so we do not call parseBlocksOnHeadHeight
|
||||||
|
log.info("rpcService.requestChainHeadHeight: headHeight={}", headHeight);
|
||||||
|
requestBlocksUpToHeadHeight(heightOfLastBlock, headHeight);
|
||||||
|
},
|
||||||
|
this::handleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onInitialBlockRequestsComplete() {
|
||||||
|
super.onInitialBlockRequestsComplete();
|
||||||
|
|
||||||
|
if (p2pNetworkReady) {
|
||||||
|
addBlockHandler();
|
||||||
|
} else {
|
||||||
|
log.info("onParseBlockChainComplete but P2P network is not ready yet.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void addBlockHandler() {
|
||||||
|
if (!addBlockHandlerAdded) {
|
||||||
|
addBlockHandlerAdded = true;
|
||||||
|
rpcService.addNewRawDtoBlockHandler(rawDtoBlock -> {
|
||||||
|
parseBlock(rawDtoBlock).ifPresent(accountingBlock -> {
|
||||||
|
maybePublishAccountingBlock(accountingBlock);
|
||||||
|
burningManAccountingService.onNewBlockReceived(accountingBlock);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
this::handleError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void maybePublishAccountingBlock(AccountingBlock accountingBlock) {
|
||||||
|
if (p2pNetworkReady && initialBlockRequestsComplete) {
|
||||||
|
accountingFullNodeNetworkService.publishAccountingBlock(accountingBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestBlocksIfNewBlockAvailable(int heightOfLastBlock) {
|
||||||
|
rpcService.requestChainHeadHeight(headHeight -> {
|
||||||
|
if (headHeight > heightOfLastBlock) {
|
||||||
|
log.info("During requests new blocks have arrived. We request again to get the missing blocks." +
|
||||||
|
"heightOfLastBlock={}, headHeight={}", heightOfLastBlock, headHeight);
|
||||||
|
requestBlocksUpToHeadHeight(heightOfLastBlock, headHeight);
|
||||||
|
} else {
|
||||||
|
log.info("requestChainHeadHeight did not result in a new block, so we complete.");
|
||||||
|
log.info("Requesting {} blocks took {} seconds", batchedBlocks, (System.currentTimeMillis() - batchStartTime) / 1000d);
|
||||||
|
if (!initialBlockRequestsComplete) {
|
||||||
|
onInitialBlockRequestsComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this::handleError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestBlocksUpToHeadHeight(int heightOfLastBlock, int headHeight) {
|
||||||
|
if (heightOfLastBlock == headHeight) {
|
||||||
|
log.info("Out heightOfLastBlock is same as headHeight of Bitcoin Core.");
|
||||||
|
if (!initialBlockRequestsComplete) {
|
||||||
|
onInitialBlockRequestsComplete();
|
||||||
|
}
|
||||||
|
} else if (heightOfLastBlock < headHeight) {
|
||||||
|
batchedBlocks = headHeight - heightOfLastBlock;
|
||||||
|
batchStartTime = System.currentTimeMillis();
|
||||||
|
log.info("Request {} blocks from {} to {}", batchedBlocks, heightOfLastBlock, headHeight);
|
||||||
|
requestBlockRecursively(heightOfLastBlock,
|
||||||
|
headHeight,
|
||||||
|
() -> {
|
||||||
|
// We are done, but it might be that new blocks have arrived in the meantime,
|
||||||
|
// so we try again with heightOfLastBlock set to current headHeight
|
||||||
|
requestBlocksIfNewBlockAvailable(headHeight);
|
||||||
|
}, this::handleError);
|
||||||
|
} else {
|
||||||
|
requestBlocksUpToHeadHeightCounter++;
|
||||||
|
log.warn("We are trying to start with a block which is above the chain height of Bitcoin Core. " +
|
||||||
|
"We need probably wait longer until Bitcoin Core has fully synced. " +
|
||||||
|
"We try again after a delay of {} min.", requestBlocksUpToHeadHeightCounter * requestBlocksUpToHeadHeightCounter);
|
||||||
|
if (requestBlocksUpToHeadHeightCounter <= 5) {
|
||||||
|
UserThread.runAfter(() -> rpcService.requestChainHeadHeight(height ->
|
||||||
|
requestBlocksUpToHeadHeight(heightOfLastBlock, height),
|
||||||
|
this::handleError), requestBlocksUpToHeadHeightCounter * requestBlocksUpToHeadHeightCounter * 60L);
|
||||||
|
} else {
|
||||||
|
log.warn("We tried {} times to start with startBlockHeight {} which is above the chain height {} of Bitcoin Core. " +
|
||||||
|
"It might be that Bitcoin Core has not fully synced. We give up now.",
|
||||||
|
requestBlocksUpToHeadHeightCounter, heightOfLastBlock, headHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestBlockRecursively(int heightOfLastBlock,
|
||||||
|
int headHeight,
|
||||||
|
ResultHandler completeHandler,
|
||||||
|
Consumer<Throwable> errorHandler) {
|
||||||
|
int requestHeight = heightOfLastBlock + 1;
|
||||||
|
rpcService.requestRawDtoBlock(requestHeight,
|
||||||
|
rawDtoBlock -> {
|
||||||
|
parseBlock(rawDtoBlock).ifPresent(this::maybePublishAccountingBlock);
|
||||||
|
|
||||||
|
// Increment heightOfLastBlock and recursively call requestBlockRecursively until we reach headHeight
|
||||||
|
int newHeightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
|
||||||
|
if (requestHeight < headHeight) {
|
||||||
|
requestBlockRecursively(newHeightOfLastBlock, headHeight, completeHandler, errorHandler);
|
||||||
|
} else {
|
||||||
|
// We are done
|
||||||
|
completeHandler.handleResult();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<AccountingBlock> parseBlock(RawDtoBlock rawDtoBlock) {
|
||||||
|
// We check if we have a block with that height. If so we return. We do not use the chainHeight as with the earliest
|
||||||
|
// height we have no block but chainHeight is initially set to the earliest height (bad design ;-( but a bit tricky
|
||||||
|
// to change now as it used in many areas.)
|
||||||
|
if (burningManAccountingService.getBlockAtHeight(rawDtoBlock.getHeight()).isPresent()) {
|
||||||
|
log.info("We have already a block with the height of the new block. Height of new block={}", rawDtoBlock.getHeight());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRawDtoBlocks.remove(rawDtoBlock);
|
||||||
|
|
||||||
|
try {
|
||||||
|
AccountingBlock accountingBlock = accountingBlockParser.parse(rawDtoBlock);
|
||||||
|
burningManAccountingService.addBlock(accountingBlock);
|
||||||
|
|
||||||
|
// After parsing we check if we have pending future blocks.
|
||||||
|
// As we successfully added a new block a pending block might fit as next block.
|
||||||
|
if (!pendingRawDtoBlocks.isEmpty()) {
|
||||||
|
// We take only first element after sorting (so it is the accountingBlock with the next height) to avoid that
|
||||||
|
// we would repeat calls in recursions in case we would iterate the list.
|
||||||
|
pendingRawDtoBlocks.sort(Comparator.comparing(RawDtoBlock::getHeight));
|
||||||
|
RawDtoBlock nextPending = pendingRawDtoBlocks.get(0);
|
||||||
|
if (nextPending.getHeight() == burningManAccountingService.getBlockHeightOfLastBlock() + 1) {
|
||||||
|
parseBlock(nextPending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.of(accountingBlock);
|
||||||
|
} catch (BlockHeightNotConnectingException e) {
|
||||||
|
// If height of rawDtoBlock is not at expected heightForNextBlock but further in the future we add it to pendingRawDtoBlocks
|
||||||
|
int heightForNextBlock = burningManAccountingService.getBlockHeightOfLastBlock() + 1;
|
||||||
|
if (rawDtoBlock.getHeight() > heightForNextBlock && !pendingRawDtoBlocks.contains(rawDtoBlock)) {
|
||||||
|
pendingRawDtoBlocks.add(rawDtoBlock);
|
||||||
|
log.info("We received a block with a future block height. We store it as pending and try to apply it at the next block. " +
|
||||||
|
"heightForNextBlock={}, rawDtoBlock: height/hash={}/{}", heightForNextBlock, rawDtoBlock.getHeight(), rawDtoBlock.getHash());
|
||||||
|
}
|
||||||
|
} catch (BlockHashNotConnectingException throwable) {
|
||||||
|
Optional<AccountingBlock> lastBlock = burningManAccountingService.getLastBlock();
|
||||||
|
log.warn("Block not connecting:\n" +
|
||||||
|
"New block height={}; hash={}, previousBlockHash={}, latest block height={}; hash={}",
|
||||||
|
rawDtoBlock.getHeight(),
|
||||||
|
rawDtoBlock.getHash(),
|
||||||
|
rawDtoBlock.getPreviousBlockHash(),
|
||||||
|
lastBlock.isPresent() ? lastBlock.get().getHeight() : "lastBlock not present",
|
||||||
|
lastBlock.isPresent() ? lastBlock.get().getTruncatedHash() : "lastBlock not present");
|
||||||
|
|
||||||
|
pendingRawDtoBlocks.clear();
|
||||||
|
applyReOrg();
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleError(Throwable throwable) {
|
||||||
|
if (throwable instanceof BlockHashNotConnectingException || throwable instanceof BlockHeightNotConnectingException) {
|
||||||
|
// We do not escalate that exception as it is handled with the snapshot manager to recover its state.
|
||||||
|
log.warn(throwable.toString());
|
||||||
|
} else {
|
||||||
|
String errorMessage = "An error occurred: Error=" + throwable.toString();
|
||||||
|
log.error(errorMessage);
|
||||||
|
throwable.printStackTrace();
|
||||||
|
|
||||||
|
if (throwable instanceof RpcException) {
|
||||||
|
Throwable cause = throwable.getCause();
|
||||||
|
if (cause != null) {
|
||||||
|
if (cause instanceof ConnectException) {
|
||||||
|
if (warnMessageHandler != null)
|
||||||
|
warnMessageHandler.accept("You have configured Bisq to run as BM full node but there is no " +
|
||||||
|
"localhost Bitcoin Core node detected. You need to have Bitcoin Core started and synced before " +
|
||||||
|
"starting Bisq. Please restart Bisq with proper BM full node setup or switch to lite node mode.");
|
||||||
|
return;
|
||||||
|
} else if (cause instanceof NotificationHandlerException) {
|
||||||
|
log.error("Error from within block notification daemon: {}", cause.getCause().toString());
|
||||||
|
applyReOrg();
|
||||||
|
return;
|
||||||
|
} else if (cause instanceof Error) {
|
||||||
|
throw (Error) cause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessageHandler != null)
|
||||||
|
errorMessageHandler.accept(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,230 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.full.network;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.AccountingNode;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
|
||||||
|
|
||||||
|
import bisq.network.p2p.network.Connection;
|
||||||
|
import bisq.network.p2p.network.MessageListener;
|
||||||
|
import bisq.network.p2p.network.NetworkNode;
|
||||||
|
import bisq.network.p2p.peers.Broadcaster;
|
||||||
|
import bisq.network.p2p.peers.PeerManager;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.app.DevEnv;
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static org.bitcoinj.core.Utils.HEX;
|
||||||
|
|
||||||
|
// Taken from FullNodeNetworkService
|
||||||
|
@Singleton
|
||||||
|
@Slf4j
|
||||||
|
public class AccountingFullNodeNetworkService implements MessageListener, PeerManager.Listener {
|
||||||
|
private static final long CLEANUP_TIMER = 120;
|
||||||
|
|
||||||
|
private final NetworkNode networkNode;
|
||||||
|
private final PeerManager peerManager;
|
||||||
|
private final Broadcaster broadcaster;
|
||||||
|
private final BurningManAccountingService burningManAccountingService;
|
||||||
|
private final boolean useDevPrivilegeKeys;
|
||||||
|
@Nullable
|
||||||
|
private final String bmOracleNodePubKey;
|
||||||
|
@Nullable
|
||||||
|
private final ECKey bmOracleNodePrivKey;
|
||||||
|
|
||||||
|
// Key is connection UID
|
||||||
|
private final Map<String, GetAccountingBlocksRequestHandler> getBlocksRequestHandlers = new HashMap<>();
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AccountingFullNodeNetworkService(NetworkNode networkNode,
|
||||||
|
PeerManager peerManager,
|
||||||
|
Broadcaster broadcaster,
|
||||||
|
BurningManAccountingService burningManAccountingService,
|
||||||
|
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
|
||||||
|
@Named(Config.BM_ORACLE_NODE_PUB_KEY) String bmOracleNodePubKey,
|
||||||
|
@Named(Config.BM_ORACLE_NODE_PRIV_KEY) String bmOracleNodePrivKey) {
|
||||||
|
this.networkNode = networkNode;
|
||||||
|
this.peerManager = peerManager;
|
||||||
|
this.broadcaster = broadcaster;
|
||||||
|
this.burningManAccountingService = burningManAccountingService;
|
||||||
|
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
|
||||||
|
|
||||||
|
if (useDevPrivilegeKeys) {
|
||||||
|
bmOracleNodePubKey = DevEnv.DEV_PRIVILEGE_PUB_KEY;
|
||||||
|
bmOracleNodePrivKey = DevEnv.DEV_PRIVILEGE_PRIV_KEY;
|
||||||
|
}
|
||||||
|
this.bmOracleNodePubKey = bmOracleNodePubKey.isEmpty() ? null : bmOracleNodePubKey;
|
||||||
|
this.bmOracleNodePrivKey = bmOracleNodePrivKey.isEmpty() ? null : toEcKey(bmOracleNodePrivKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void addListeners() {
|
||||||
|
networkNode.addMessageListener(this);
|
||||||
|
peerManager.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutDown() {
|
||||||
|
stopped = true;
|
||||||
|
networkNode.removeMessageListener(this);
|
||||||
|
peerManager.removeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void publishAccountingBlock(AccountingBlock block) {
|
||||||
|
if (bmOracleNodePubKey == null || bmOracleNodePrivKey == null) {
|
||||||
|
log.warn("Ignore publishNewBlock call. bmOracleNodePubKey or bmOracleNodePrivKey are not set up");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkArgument(AccountingNode.isPermittedPubKey(useDevPrivilegeKeys, bmOracleNodePubKey),
|
||||||
|
"The bmOracleNodePubKey must be included in the hard coded list of supported pub keys");
|
||||||
|
|
||||||
|
log.info("Publish new block at height={}", block.getHeight());
|
||||||
|
byte[] signature = AccountingNode.getSignature(AccountingNode.getSha256Hash(block), bmOracleNodePrivKey);
|
||||||
|
NewAccountingBlockBroadcastMessage message = new NewAccountingBlockBroadcastMessage(block, bmOracleNodePubKey, signature);
|
||||||
|
broadcaster.broadcast(message, networkNode.getNodeAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PeerManager.Listener implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAllConnectionsLost() {
|
||||||
|
stopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewConnectionAfterAllConnectionsLost() {
|
||||||
|
stopped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAwakeFromStandby() {
|
||||||
|
stopped = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// MessageListener implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||||
|
if (networkEnvelope instanceof GetAccountingBlocksRequest) {
|
||||||
|
handleGetBlocksRequest((GetAccountingBlocksRequest) networkEnvelope, connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleGetBlocksRequest(GetAccountingBlocksRequest getBlocksRequest, Connection connection) {
|
||||||
|
if (bmOracleNodePubKey == null || bmOracleNodePrivKey == null) {
|
||||||
|
log.warn("Ignore handleGetBlocksRequest call. bmOracleNodePubKey or bmOracleNodePrivKey are not set up");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkArgument(AccountingNode.isPermittedPubKey(useDevPrivilegeKeys, bmOracleNodePubKey),
|
||||||
|
"The bmOracleNodePubKey must be included in the hard coded list of supported pub keys");
|
||||||
|
|
||||||
|
if (stopped) {
|
||||||
|
log.warn("We have stopped already. We ignore that onMessage call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String uid = connection.getUid();
|
||||||
|
if (getBlocksRequestHandlers.containsKey(uid)) {
|
||||||
|
log.warn("We have already a GetDataRequestHandler for that connection started. " +
|
||||||
|
"We start a cleanup timer if the handler has not closed by itself in between 2 minutes.");
|
||||||
|
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
if (getBlocksRequestHandlers.containsKey(uid)) {
|
||||||
|
GetAccountingBlocksRequestHandler handler = getBlocksRequestHandlers.get(uid);
|
||||||
|
handler.stop();
|
||||||
|
getBlocksRequestHandlers.remove(uid);
|
||||||
|
}
|
||||||
|
}, CLEANUP_TIMER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetAccountingBlocksRequestHandler requestHandler = new GetAccountingBlocksRequestHandler(networkNode,
|
||||||
|
burningManAccountingService,
|
||||||
|
bmOracleNodePrivKey,
|
||||||
|
bmOracleNodePubKey,
|
||||||
|
new GetAccountingBlocksRequestHandler.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
getBlocksRequestHandlers.remove(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage, @Nullable Connection connection) {
|
||||||
|
getBlocksRequestHandlers.remove(uid);
|
||||||
|
if (!stopped) {
|
||||||
|
log.trace("GetDataRequestHandler failed.\n\tConnection={}\n\t" +
|
||||||
|
"ErrorMessage={}", connection, errorMessage);
|
||||||
|
if (connection != null) {
|
||||||
|
peerManager.handleConnectionFault(connection);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getBlocksRequestHandlers.put(uid, requestHandler);
|
||||||
|
requestHandler.onGetBlocksRequest(getBlocksRequest, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ECKey toEcKey(String bmOracleNodePrivKey) {
|
||||||
|
try {
|
||||||
|
return ECKey.fromPrivate(new BigInteger(1, HEX.decode(bmOracleNodePrivKey)));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
log.error("Error at creating EC key out of bmOracleNodePrivKey", t);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.full.network;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.AccountingNode;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
|
||||||
|
|
||||||
|
import bisq.network.p2p.network.CloseConnectionReason;
|
||||||
|
import bisq.network.p2p.network.Connection;
|
||||||
|
import bisq.network.p2p.network.NetworkNode;
|
||||||
|
|
||||||
|
import bisq.common.Timer;
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
// Taken from GetBlocksRequestHandler
|
||||||
|
@Slf4j
|
||||||
|
class GetAccountingBlocksRequestHandler {
|
||||||
|
private static final long TIMEOUT_MIN = 3;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Listener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onComplete();
|
||||||
|
|
||||||
|
void onFault(String errorMessage, Connection connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final NetworkNode networkNode;
|
||||||
|
private final BurningManAccountingService burningManAccountingService;
|
||||||
|
private final ECKey bmOracleNodePrivKey;
|
||||||
|
private final String bmOracleNodePubKey;
|
||||||
|
private final Listener listener;
|
||||||
|
private Timer timeoutTimer;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
GetAccountingBlocksRequestHandler(NetworkNode networkNode,
|
||||||
|
BurningManAccountingService burningManAccountingService,
|
||||||
|
ECKey bmOracleNodePrivKey,
|
||||||
|
String bmOracleNodePubKey,
|
||||||
|
Listener listener) {
|
||||||
|
this.networkNode = networkNode;
|
||||||
|
this.burningManAccountingService = burningManAccountingService;
|
||||||
|
this.bmOracleNodePrivKey = bmOracleNodePrivKey;
|
||||||
|
this.bmOracleNodePubKey = bmOracleNodePubKey;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void onGetBlocksRequest(GetAccountingBlocksRequest request, Connection connection) {
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
List<AccountingBlock> blocks = burningManAccountingService.getBlocks().stream()
|
||||||
|
.filter(block -> block.getHeight() >= request.getFromBlockHeight())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
byte[] signature = AccountingNode.getSignature(AccountingNode.getSha256Hash(blocks), bmOracleNodePrivKey);
|
||||||
|
GetAccountingBlocksResponse getBlocksResponse = new GetAccountingBlocksResponse(blocks, request.getNonce(), bmOracleNodePubKey, signature);
|
||||||
|
log.info("Received GetAccountingBlocksRequest from {} for blocks from height {}. " +
|
||||||
|
"Building GetAccountingBlocksResponse with {} blocks took {} ms.",
|
||||||
|
connection.getPeersNodeAddressOptional(), request.getFromBlockHeight(),
|
||||||
|
blocks.size(), System.currentTimeMillis() - ts);
|
||||||
|
|
||||||
|
if (timeoutTimer != null) {
|
||||||
|
timeoutTimer.stop();
|
||||||
|
log.warn("Timeout was already running. We stopped it.");
|
||||||
|
}
|
||||||
|
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
|
||||||
|
String errorMessage = "A timeout occurred for getBlocksResponse.requestNonce:" +
|
||||||
|
getBlocksResponse.getRequestNonce() +
|
||||||
|
" on connection: " + connection;
|
||||||
|
handleFault(errorMessage, CloseConnectionReason.SEND_MSG_TIMEOUT, connection);
|
||||||
|
},
|
||||||
|
TIMEOUT_MIN, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
SettableFuture<Connection> future = networkNode.sendMessage(connection, getBlocksResponse);
|
||||||
|
Futures.addCallback(future, new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Connection connection) {
|
||||||
|
if (!stopped) {
|
||||||
|
log.info("Send DataResponse to {} succeeded. getBlocksResponse.getBlocks().size()={}",
|
||||||
|
connection.getPeersNodeAddressOptional(), getBlocksResponse.getBlocks().size());
|
||||||
|
cleanup();
|
||||||
|
listener.onComplete();
|
||||||
|
} else {
|
||||||
|
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull Throwable throwable) {
|
||||||
|
if (!stopped) {
|
||||||
|
String errorMessage = "Sending getBlocksResponse to " + connection +
|
||||||
|
" failed. That is expected if the peer is offline. getBlocksResponse=" + getBlocksResponse + "." +
|
||||||
|
"Exception: " + throwable.getMessage();
|
||||||
|
handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, connection);
|
||||||
|
} else {
|
||||||
|
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, Connection connection) {
|
||||||
|
if (!stopped) {
|
||||||
|
log.warn("{}, closeConnectionReason={}", errorMessage, closeConnectionReason);
|
||||||
|
cleanup();
|
||||||
|
listener.onFault(errorMessage, connection);
|
||||||
|
} else {
|
||||||
|
log.warn("We have already stopped (handleFault)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
stopped = true;
|
||||||
|
if (timeoutTimer != null) {
|
||||||
|
timeoutTimer.stop();
|
||||||
|
timeoutTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.lite;
|
||||||
|
|
||||||
|
import bisq.core.btc.setup.WalletsSetup;
|
||||||
|
import bisq.core.btc.wallet.BsqWalletService;
|
||||||
|
import bisq.core.dao.burningman.accounting.BurningManAccountingService;
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
import bisq.core.dao.burningman.accounting.exceptions.BlockHashNotConnectingException;
|
||||||
|
import bisq.core.dao.burningman.accounting.exceptions.BlockHeightNotConnectingException;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.AccountingNode;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.full.AccountingBlockParser;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.lite.network.AccountingLiteNodeNetworkService;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
|
||||||
|
import bisq.core.dao.state.DaoStateService;
|
||||||
|
|
||||||
|
import bisq.network.p2p.P2PService;
|
||||||
|
import bisq.network.p2p.network.ConnectionState;
|
||||||
|
|
||||||
|
import bisq.common.Timer;
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.util.Hex;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class AccountingLiteNode extends AccountingNode implements AccountingLiteNodeNetworkService.Listener {
|
||||||
|
private static final int CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC = 10;
|
||||||
|
|
||||||
|
private final WalletsSetup walletsSetup;
|
||||||
|
private final BsqWalletService bsqWalletService;
|
||||||
|
private final AccountingLiteNodeNetworkService accountingLiteNodeNetworkService;
|
||||||
|
private final boolean useDevPrivilegeKeys;
|
||||||
|
|
||||||
|
private final List<AccountingBlock> pendingAccountingBlocks = new ArrayList<>();
|
||||||
|
private final ChangeListener<Number> blockDownloadListener;
|
||||||
|
private Timer checkForBlockReceivedTimer;
|
||||||
|
private int requestBlocksCounter;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AccountingLiteNode(P2PService p2PService,
|
||||||
|
DaoStateService daoStateService,
|
||||||
|
BurningManAccountingService burningManAccountingService,
|
||||||
|
AccountingBlockParser accountingBlockParser,
|
||||||
|
WalletsSetup walletsSetup,
|
||||||
|
BsqWalletService bsqWalletService,
|
||||||
|
AccountingLiteNodeNetworkService accountingLiteNodeNetworkService,
|
||||||
|
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||||
|
super(p2PService, daoStateService, burningManAccountingService, accountingBlockParser);
|
||||||
|
|
||||||
|
this.walletsSetup = walletsSetup;
|
||||||
|
this.bsqWalletService = bsqWalletService;
|
||||||
|
this.accountingLiteNodeNetworkService = accountingLiteNodeNetworkService;
|
||||||
|
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
|
||||||
|
|
||||||
|
blockDownloadListener = (observable, oldValue, newValue) -> {
|
||||||
|
if ((double) newValue == 1) {
|
||||||
|
setupWalletBestBlockListener();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public methods
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() {
|
||||||
|
// We do not start yes but wait until DAO block parsing is complete to not interfere with
|
||||||
|
// that higher priority activity.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutDown() {
|
||||||
|
accountingLiteNodeNetworkService.shutDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// AccountingLiteNodeNetworkService.Listener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestedBlocksReceived(GetAccountingBlocksResponse getBlocksResponse) {
|
||||||
|
List<AccountingBlock> blocks = getBlocksResponse.getBlocks();
|
||||||
|
if (!blocks.isEmpty() && isValidPubKeyAndSignature(AccountingNode.getSha256Hash(blocks),
|
||||||
|
getBlocksResponse.getPubKey(),
|
||||||
|
getBlocksResponse.getSignature(),
|
||||||
|
useDevPrivilegeKeys)) {
|
||||||
|
processAccountingBlocks(blocks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewBlockReceived(NewAccountingBlockBroadcastMessage message) {
|
||||||
|
AccountingBlock accountingBlock = message.getBlock();
|
||||||
|
if (isValidPubKeyAndSignature(AccountingNode.getSha256Hash(accountingBlock),
|
||||||
|
message.getPubKey(),
|
||||||
|
message.getSignature(),
|
||||||
|
useDevPrivilegeKeys)) {
|
||||||
|
processNewAccountingBlock(accountingBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protected methods
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// We start after initial DAO parsing is complete
|
||||||
|
@Override
|
||||||
|
protected void onInitialDaoBlockParsingComplete() {
|
||||||
|
accountingLiteNodeNetworkService.addListeners();
|
||||||
|
|
||||||
|
// We wait until the wallet is synced before using it for triggering requests
|
||||||
|
if (walletsSetup.isDownloadComplete()) {
|
||||||
|
setupWalletBestBlockListener();
|
||||||
|
} else {
|
||||||
|
walletsSetup.downloadPercentageProperty().addListener(blockDownloadListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onP2PNetworkReady() {
|
||||||
|
super.onP2PNetworkReady();
|
||||||
|
|
||||||
|
accountingLiteNodeNetworkService.addListener(this);
|
||||||
|
|
||||||
|
if (!initialBlockRequestsComplete) {
|
||||||
|
startRequestBlocks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startRequestBlocks() {
|
||||||
|
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
|
||||||
|
if (walletsSetup.isDownloadComplete() && heightOfLastBlock == bsqWalletService.getBestChainHeight()) {
|
||||||
|
log.info("No block request needed as we have already the most recent block. " +
|
||||||
|
"heightOfLastBlock={}, bsqWalletService.getBestChainHeight()={}",
|
||||||
|
heightOfLastBlock, bsqWalletService.getBestChainHeight());
|
||||||
|
onInitialBlockRequestsComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionState.incrementExpectedInitialDataResponses();
|
||||||
|
accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void applyReOrg() {
|
||||||
|
pendingAccountingBlocks.clear();
|
||||||
|
accountingLiteNodeNetworkService.reset();
|
||||||
|
super.applyReOrg();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void processAccountingBlocks(List<AccountingBlock> blocks) {
|
||||||
|
log.info("We received blocks from height {} to {}",
|
||||||
|
blocks.get(0).getHeight(),
|
||||||
|
blocks.get(blocks.size() - 1).getHeight());
|
||||||
|
|
||||||
|
boolean requiresReOrg = false;
|
||||||
|
for (AccountingBlock block : blocks) {
|
||||||
|
try {
|
||||||
|
burningManAccountingService.addBlock(block);
|
||||||
|
} catch (BlockHeightNotConnectingException e) {
|
||||||
|
log.info("Height not connecting. This could happen if we received multiple responses and had already applied a previous one. {}", e.toString());
|
||||||
|
} catch (BlockHashNotConnectingException e) {
|
||||||
|
log.warn("Interrupt loop because a reorg is required. {}", e.toString());
|
||||||
|
requiresReOrg = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiresReOrg) {
|
||||||
|
applyReOrg();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
|
||||||
|
if (walletsSetup.isDownloadComplete() && heightOfLastBlock < bsqWalletService.getBestChainHeight()) {
|
||||||
|
accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1);
|
||||||
|
} else {
|
||||||
|
if (!initialBlockRequestsComplete) {
|
||||||
|
onInitialBlockRequestsComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processNewAccountingBlock(AccountingBlock accountingBlock) {
|
||||||
|
int blockHeight = accountingBlock.getHeight();
|
||||||
|
log.info("onNewBlockReceived: accountingBlock at height {}", blockHeight);
|
||||||
|
|
||||||
|
pendingAccountingBlocks.remove(accountingBlock);
|
||||||
|
try {
|
||||||
|
burningManAccountingService.addBlock(accountingBlock);
|
||||||
|
burningManAccountingService.onNewBlockReceived(accountingBlock);
|
||||||
|
|
||||||
|
// After parsing we check if we have pending blocks we might have received earlier but which have been
|
||||||
|
// not connecting from the latest height we had. The list is sorted by height
|
||||||
|
if (!pendingAccountingBlocks.isEmpty()) {
|
||||||
|
// We take only first element after sorting (so it is the accountingBlock with the next height) to avoid that
|
||||||
|
// we would repeat calls in recursions in case we would iterate the list.
|
||||||
|
pendingAccountingBlocks.sort(Comparator.comparing(AccountingBlock::getHeight));
|
||||||
|
AccountingBlock nextPending = pendingAccountingBlocks.get(0);
|
||||||
|
if (nextPending.getHeight() == burningManAccountingService.getBlockHeightOfLastBlock() + 1) {
|
||||||
|
processNewAccountingBlock(nextPending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BlockHeightNotConnectingException e) {
|
||||||
|
// If height of rawDtoBlock is not at expected heightForNextBlock but further in the future we add it to pendingRawDtoBlocks
|
||||||
|
int heightForNextBlock = burningManAccountingService.getBlockHeightOfLastBlock() + 1;
|
||||||
|
if (accountingBlock.getHeight() > heightForNextBlock && !pendingAccountingBlocks.contains(accountingBlock)) {
|
||||||
|
pendingAccountingBlocks.add(accountingBlock);
|
||||||
|
log.info("We received a accountingBlock with a future accountingBlock height. We store it as pending and try to apply it at the next accountingBlock. " +
|
||||||
|
"heightForNextBlock={}, accountingBlock: height/truncatedHash={}/{}", heightForNextBlock, accountingBlock.getHeight(), accountingBlock.getTruncatedHash());
|
||||||
|
|
||||||
|
requestBlocksCounter++;
|
||||||
|
log.warn("We are trying to call requestBlocks with heightForNextBlock {} after a delay of {} min.",
|
||||||
|
heightForNextBlock, requestBlocksCounter * requestBlocksCounter);
|
||||||
|
if (requestBlocksCounter <= 5) {
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
pendingAccountingBlocks.clear();
|
||||||
|
accountingLiteNodeNetworkService.requestBlocks(heightForNextBlock);
|
||||||
|
},
|
||||||
|
requestBlocksCounter * requestBlocksCounter * 60L);
|
||||||
|
} else {
|
||||||
|
log.warn("We tried {} times to call requestBlocks with heightForNextBlock {}.",
|
||||||
|
requestBlocksCounter, heightForNextBlock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (BlockHashNotConnectingException e) {
|
||||||
|
Optional<AccountingBlock> lastBlock = burningManAccountingService.getLastBlock();
|
||||||
|
log.warn("Block not connecting:\n" +
|
||||||
|
"New block height/hash/previousBlockHash={}/{}/{}, latest block height/hash={}/{}",
|
||||||
|
accountingBlock.getHeight(),
|
||||||
|
Hex.encode(accountingBlock.getTruncatedHash()),
|
||||||
|
Hex.encode(accountingBlock.getTruncatedPreviousBlockHash()),
|
||||||
|
lastBlock.isPresent() ? lastBlock.get().getHeight() : "lastBlock not present",
|
||||||
|
lastBlock.isPresent() ? Hex.encode(lastBlock.get().getTruncatedHash()) : "lastBlock not present");
|
||||||
|
|
||||||
|
applyReOrg();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupWalletBestBlockListener() {
|
||||||
|
walletsSetup.downloadPercentageProperty().removeListener(blockDownloadListener);
|
||||||
|
|
||||||
|
bsqWalletService.addNewBestBlockListener(blockFromWallet -> {
|
||||||
|
// If we are not completed with initial block requests we return
|
||||||
|
if (!initialBlockRequestsComplete)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (checkForBlockReceivedTimer != null) {
|
||||||
|
// In case we received a new block before out timer gets called we stop the old timer
|
||||||
|
checkForBlockReceivedTimer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
int walletBlockHeight = blockFromWallet.getHeight();
|
||||||
|
log.info("New block at height {} from bsqWalletService", walletBlockHeight);
|
||||||
|
|
||||||
|
// We expect to receive the new BSQ block from the network shortly after BitcoinJ has been aware of it.
|
||||||
|
// If we don't receive it we request it manually from seed nodes
|
||||||
|
checkForBlockReceivedTimer = UserThread.runAfter(() -> {
|
||||||
|
int heightOfLastBlock = burningManAccountingService.getBlockHeightOfLastBlock();
|
||||||
|
if (heightOfLastBlock < walletBlockHeight) {
|
||||||
|
log.warn("We did not receive a block from the network {} seconds after we saw the new block in BitcoinJ. " +
|
||||||
|
"We request from our seed nodes missing blocks from block height {}.",
|
||||||
|
CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC, heightOfLastBlock + 1);
|
||||||
|
accountingLiteNodeNetworkService.requestBlocks(heightOfLastBlock + 1);
|
||||||
|
}
|
||||||
|
}, CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,392 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.lite.network;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
|
||||||
|
|
||||||
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import bisq.network.p2p.network.CloseConnectionReason;
|
||||||
|
import bisq.network.p2p.network.Connection;
|
||||||
|
import bisq.network.p2p.network.ConnectionListener;
|
||||||
|
import bisq.network.p2p.network.MessageListener;
|
||||||
|
import bisq.network.p2p.network.NetworkNode;
|
||||||
|
import bisq.network.p2p.peers.Broadcaster;
|
||||||
|
import bisq.network.p2p.peers.PeerManager;
|
||||||
|
import bisq.network.p2p.seed.SeedNodeRepository;
|
||||||
|
import bisq.network.p2p.storage.P2PDataStorage;
|
||||||
|
|
||||||
|
import bisq.common.Timer;
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.app.DevEnv;
|
||||||
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
// Taken from LiteNodeNetworkService
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class AccountingLiteNodeNetworkService implements MessageListener, ConnectionListener, PeerManager.Listener {
|
||||||
|
private static final long RETRY_DELAY_SEC = 10;
|
||||||
|
private static final long CLEANUP_TIMER = 120;
|
||||||
|
private static final int MAX_RETRY = 12;
|
||||||
|
|
||||||
|
private int retryCounter = 0;
|
||||||
|
private int lastRequestedBlockHeight;
|
||||||
|
private int lastReceivedBlockHeight;
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Listener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onRequestedBlocksReceived(GetAccountingBlocksResponse getBlocksResponse);
|
||||||
|
|
||||||
|
void onNewBlockReceived(NewAccountingBlockBroadcastMessage newBlockBroadcastMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final NetworkNode networkNode;
|
||||||
|
private final PeerManager peerManager;
|
||||||
|
private final Broadcaster broadcaster;
|
||||||
|
private final Collection<NodeAddress> seedNodeAddresses;
|
||||||
|
|
||||||
|
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
// Key is tuple of seedNode address and requested blockHeight
|
||||||
|
private final Map<Tuple2<NodeAddress, Integer>, RequestAccountingBlocksHandler> requestBlocksHandlerMap = new HashMap<>();
|
||||||
|
private Timer retryTimer;
|
||||||
|
private boolean stopped;
|
||||||
|
private final Set<P2PDataStorage.ByteArray> receivedBlockMessages = new HashSet<>();
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public AccountingLiteNodeNetworkService(NetworkNode networkNode,
|
||||||
|
PeerManager peerManager,
|
||||||
|
Broadcaster broadcaster,
|
||||||
|
SeedNodeRepository seedNodesRepository) {
|
||||||
|
this.networkNode = networkNode;
|
||||||
|
this.peerManager = peerManager;
|
||||||
|
this.broadcaster = broadcaster;
|
||||||
|
// seedNodeAddresses can be empty (in case there is only 1 seed node, the seed node starting up has no other seed nodes)
|
||||||
|
this.seedNodeAddresses = new HashSet<>(seedNodesRepository.getSeedNodeAddresses());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void addListeners() {
|
||||||
|
networkNode.addMessageListener(this);
|
||||||
|
networkNode.addConnectionListener(this);
|
||||||
|
peerManager.addListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutDown() {
|
||||||
|
stopped = true;
|
||||||
|
stopRetryTimer();
|
||||||
|
networkNode.removeMessageListener(this);
|
||||||
|
networkNode.removeConnectionListener(this);
|
||||||
|
peerManager.removeListener(this);
|
||||||
|
closeAllHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addListener(Listener listener) {
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void requestBlocks(int startBlockHeight) {
|
||||||
|
lastRequestedBlockHeight = startBlockHeight;
|
||||||
|
Optional<Connection> connectionToSeedNodeOptional = networkNode.getConfirmedConnections().stream()
|
||||||
|
.filter(peerManager::isSeedNode)
|
||||||
|
.findAny();
|
||||||
|
|
||||||
|
connectionToSeedNodeOptional.flatMap(Connection::getPeersNodeAddressOptional)
|
||||||
|
.ifPresentOrElse(candidate -> {
|
||||||
|
seedNodeAddresses.remove(candidate);
|
||||||
|
requestBlocks(candidate, startBlockHeight);
|
||||||
|
}, () -> {
|
||||||
|
tryWithNewSeedNode(startBlockHeight);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
lastRequestedBlockHeight = 0;
|
||||||
|
lastReceivedBlockHeight = 0;
|
||||||
|
retryCounter = 0;
|
||||||
|
requestBlocksHandlerMap.values().forEach(RequestAccountingBlocksHandler::terminate);
|
||||||
|
requestBlocksHandlerMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// ConnectionListener implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnection(Connection connection) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) {
|
||||||
|
closeHandler(connection);
|
||||||
|
|
||||||
|
if (peerManager.isPeerBanned(closeConnectionReason, connection)) {
|
||||||
|
connection.getPeersNodeAddressOptional().ifPresent(nodeAddress -> {
|
||||||
|
seedNodeAddresses.remove(nodeAddress);
|
||||||
|
removeFromRequestBlocksHandlerMap(nodeAddress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PeerManager.Listener implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAllConnectionsLost() {
|
||||||
|
log.info("onAllConnectionsLost");
|
||||||
|
closeAllHandlers();
|
||||||
|
stopRetryTimer();
|
||||||
|
stopped = true;
|
||||||
|
tryWithNewSeedNode(lastRequestedBlockHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewConnectionAfterAllConnectionsLost() {
|
||||||
|
log.info("onNewConnectionAfterAllConnectionsLost");
|
||||||
|
closeAllHandlers();
|
||||||
|
stopped = false;
|
||||||
|
tryWithNewSeedNode(lastRequestedBlockHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAwakeFromStandby() {
|
||||||
|
log.info("onAwakeFromStandby");
|
||||||
|
closeAllHandlers();
|
||||||
|
stopped = false;
|
||||||
|
tryWithNewSeedNode(lastRequestedBlockHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// MessageListener implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||||
|
if (networkEnvelope instanceof NewAccountingBlockBroadcastMessage) {
|
||||||
|
NewAccountingBlockBroadcastMessage newBlockBroadcastMessage = (NewAccountingBlockBroadcastMessage) networkEnvelope;
|
||||||
|
P2PDataStorage.ByteArray truncatedHash = new P2PDataStorage.ByteArray(newBlockBroadcastMessage.getBlock().getTruncatedHash());
|
||||||
|
if (receivedBlockMessages.contains(truncatedHash)) {
|
||||||
|
log.debug("We had that message already and do not further broadcast it. hash={}", truncatedHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("We received a NewAccountingBlockBroadcastMessage from peer {} and broadcast it to our peers. height={} truncatedHash={}",
|
||||||
|
connection.getPeersNodeAddressOptional().orElse(null), newBlockBroadcastMessage.getBlock().getHeight(), truncatedHash);
|
||||||
|
receivedBlockMessages.add(truncatedHash);
|
||||||
|
broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null));
|
||||||
|
listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// RequestData
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void requestBlocks(NodeAddress peersNodeAddress, int startBlockHeight) {
|
||||||
|
if (stopped) {
|
||||||
|
log.warn("We have stopped already. We ignore that requestData call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tuple2<NodeAddress, Integer> key = new Tuple2<>(peersNodeAddress, startBlockHeight);
|
||||||
|
if (requestBlocksHandlerMap.containsKey(key)) {
|
||||||
|
log.warn("We have started already a requestDataHandshake for startBlockHeight {} to peer. nodeAddress={}\n" +
|
||||||
|
"We start a cleanup timer if the handler has not closed by itself in between 2 minutes.",
|
||||||
|
peersNodeAddress, startBlockHeight);
|
||||||
|
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
if (requestBlocksHandlerMap.containsKey(key)) {
|
||||||
|
RequestAccountingBlocksHandler handler = requestBlocksHandlerMap.get(key);
|
||||||
|
handler.terminate();
|
||||||
|
requestBlocksHandlerMap.remove(key);
|
||||||
|
}
|
||||||
|
}, CLEANUP_TIMER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startBlockHeight < lastReceivedBlockHeight) {
|
||||||
|
log.warn("startBlockHeight must not be smaller than lastReceivedBlockHeight. That should never happen." +
|
||||||
|
"startBlockHeight={},lastReceivedBlockHeight={}", startBlockHeight, lastReceivedBlockHeight);
|
||||||
|
DevEnv.logErrorAndThrowIfDevMode("startBlockHeight must be larger than lastReceivedBlockHeight. startBlockHeight=" +
|
||||||
|
startBlockHeight + " / lastReceivedBlockHeight=" + lastReceivedBlockHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestAccountingBlocksHandler requestAccountingBlocksHandler = new RequestAccountingBlocksHandler(networkNode,
|
||||||
|
peerManager,
|
||||||
|
peersNodeAddress,
|
||||||
|
startBlockHeight,
|
||||||
|
new RequestAccountingBlocksHandler.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onComplete(GetAccountingBlocksResponse getBlocksResponse) {
|
||||||
|
log.info("requestBlocksHandler to {} completed", peersNodeAddress);
|
||||||
|
stopRetryTimer();
|
||||||
|
|
||||||
|
// need to remove before listeners are notified as they cause the update call
|
||||||
|
requestBlocksHandlerMap.remove(key);
|
||||||
|
// we only notify if our request was latest
|
||||||
|
if (startBlockHeight > lastReceivedBlockHeight) {
|
||||||
|
lastReceivedBlockHeight = startBlockHeight;
|
||||||
|
|
||||||
|
listeners.forEach(listener -> listener.onRequestedBlocksReceived(getBlocksResponse));
|
||||||
|
} else {
|
||||||
|
log.warn("We got a response which is already obsolete because we received a " +
|
||||||
|
"response from a request with same or higher block height.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage, @Nullable Connection connection) {
|
||||||
|
log.warn("requestBlocksHandler with outbound connection failed.\n\tnodeAddress={}\n\t" +
|
||||||
|
"ErrorMessage={}", peersNodeAddress, errorMessage);
|
||||||
|
|
||||||
|
peerManager.handleConnectionFault(peersNodeAddress);
|
||||||
|
requestBlocksHandlerMap.remove(key);
|
||||||
|
|
||||||
|
tryWithNewSeedNode(startBlockHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
requestBlocksHandlerMap.put(key, requestAccountingBlocksHandler);
|
||||||
|
requestAccountingBlocksHandler.requestBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private void tryWithNewSeedNode(int startBlockHeight) {
|
||||||
|
if (networkNode.getAllConnections().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastRequestedBlockHeight == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryTimer != null) {
|
||||||
|
log.warn("We have a retry timer already running.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCounter++;
|
||||||
|
|
||||||
|
if (retryCounter > MAX_RETRY) {
|
||||||
|
log.warn("We tried {} times but could not connect to a seed node.", retryCounter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
retryTimer = UserThread.runAfter(() -> {
|
||||||
|
stopped = false;
|
||||||
|
|
||||||
|
stopRetryTimer();
|
||||||
|
|
||||||
|
List<NodeAddress> list = seedNodeAddresses.stream()
|
||||||
|
.filter(e -> peerManager.isSeedNode(e) && !peerManager.isSelf(e))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Collections.shuffle(list);
|
||||||
|
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
NodeAddress nextCandidate = list.get(0);
|
||||||
|
seedNodeAddresses.remove(nextCandidate);
|
||||||
|
log.info("We try requestBlocks from {} with startBlockHeight={}", nextCandidate, startBlockHeight);
|
||||||
|
requestBlocks(nextCandidate, startBlockHeight);
|
||||||
|
} else {
|
||||||
|
log.warn("No more seed nodes available we could try.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RETRY_DELAY_SEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopRetryTimer() {
|
||||||
|
if (retryTimer != null) {
|
||||||
|
retryTimer.stop();
|
||||||
|
retryTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeHandler(Connection connection) {
|
||||||
|
Optional<NodeAddress> peersNodeAddressOptional = connection.getPeersNodeAddressOptional();
|
||||||
|
if (peersNodeAddressOptional.isPresent()) {
|
||||||
|
NodeAddress nodeAddress = peersNodeAddressOptional.get();
|
||||||
|
removeFromRequestBlocksHandlerMap(nodeAddress);
|
||||||
|
} else {
|
||||||
|
log.trace("closeHandler: nodeAddress not set in connection {}", connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeFromRequestBlocksHandlerMap(NodeAddress nodeAddress) {
|
||||||
|
requestBlocksHandlerMap.entrySet().stream()
|
||||||
|
.filter(e -> e.getKey().first.equals(nodeAddress))
|
||||||
|
.findAny()
|
||||||
|
.ifPresent(e -> {
|
||||||
|
e.getValue().terminate();
|
||||||
|
requestBlocksHandlerMap.remove(e.getKey());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeAllHandlers() {
|
||||||
|
requestBlocksHandlerMap.values().forEach(RequestAccountingBlocksHandler::terminate);
|
||||||
|
requestBlocksHandlerMap.clear();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,219 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.lite.network;
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
|
||||||
|
|
||||||
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import bisq.network.p2p.network.CloseConnectionReason;
|
||||||
|
import bisq.network.p2p.network.Connection;
|
||||||
|
import bisq.network.p2p.network.MessageListener;
|
||||||
|
import bisq.network.p2p.network.NetworkNode;
|
||||||
|
import bisq.network.p2p.peers.PeerManager;
|
||||||
|
|
||||||
|
import bisq.common.Timer;
|
||||||
|
import bisq.common.UserThread;
|
||||||
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
// Taken from RequestBlocksHandler
|
||||||
|
@Slf4j
|
||||||
|
public class RequestAccountingBlocksHandler implements MessageListener {
|
||||||
|
private static final long TIMEOUT_MIN = 3;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Listener
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onComplete(GetAccountingBlocksResponse getBlocksResponse);
|
||||||
|
|
||||||
|
@SuppressWarnings("UnusedParameters")
|
||||||
|
void onFault(String errorMessage, @SuppressWarnings("SameParameterValue") @Nullable Connection connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final NetworkNode networkNode;
|
||||||
|
private final PeerManager peerManager;
|
||||||
|
@Getter
|
||||||
|
private final NodeAddress nodeAddress;
|
||||||
|
@Getter
|
||||||
|
private final int startBlockHeight;
|
||||||
|
private final Listener listener;
|
||||||
|
private Timer timeoutTimer;
|
||||||
|
private final int nonce = new Random().nextInt();
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Constructor
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public RequestAccountingBlocksHandler(NetworkNode networkNode,
|
||||||
|
PeerManager peerManager,
|
||||||
|
NodeAddress nodeAddress,
|
||||||
|
int startBlockHeight,
|
||||||
|
Listener listener) {
|
||||||
|
this.networkNode = networkNode;
|
||||||
|
this.peerManager = peerManager;
|
||||||
|
this.nodeAddress = nodeAddress;
|
||||||
|
this.startBlockHeight = startBlockHeight;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void requestBlocks() {
|
||||||
|
if (stopped) {
|
||||||
|
log.warn("We have stopped already. We ignore that requestData call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetAccountingBlocksRequest getBlocksRequest = new GetAccountingBlocksRequest(startBlockHeight, nonce, networkNode.getNodeAddress());
|
||||||
|
|
||||||
|
if (timeoutTimer != null) {
|
||||||
|
log.warn("We had a timer already running and stop it.");
|
||||||
|
timeoutTimer.stop();
|
||||||
|
}
|
||||||
|
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
|
||||||
|
if (!stopped) {
|
||||||
|
String errorMessage = "A timeout occurred when sending getBlocksRequest:" + getBlocksRequest +
|
||||||
|
" on peersNodeAddress:" + nodeAddress;
|
||||||
|
log.debug("{} / RequestDataHandler={}", errorMessage, RequestAccountingBlocksHandler.this);
|
||||||
|
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
|
||||||
|
} else {
|
||||||
|
log.warn("We have stopped already. We ignore that timeoutTimer.run call. " +
|
||||||
|
"Might be caused by a previous networkNode.sendMessage.onFailure.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TIMEOUT_MIN, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
log.info("We request blocks from peer {} from block height {}.", nodeAddress, getBlocksRequest.getFromBlockHeight());
|
||||||
|
|
||||||
|
networkNode.addMessageListener(this);
|
||||||
|
|
||||||
|
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getBlocksRequest);
|
||||||
|
Futures.addCallback(future, new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Connection connection) {
|
||||||
|
log.info("Sending of GetAccountingBlocksRequest message to peer {} succeeded.", nodeAddress.getFullAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull Throwable throwable) {
|
||||||
|
if (!stopped) {
|
||||||
|
String errorMessage = "Sending getBlocksRequest to " + nodeAddress +
|
||||||
|
" failed. That is expected if the peer is offline.\n\t" +
|
||||||
|
"getBlocksRequest=" + getBlocksRequest + "." +
|
||||||
|
"\n\tException=" + throwable.getMessage();
|
||||||
|
log.error(errorMessage);
|
||||||
|
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE);
|
||||||
|
} else {
|
||||||
|
log.warn("We have stopped already. We ignore that networkNode.sendMessage.onFailure call.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// MessageListener implementation
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||||
|
if (networkEnvelope instanceof GetAccountingBlocksResponse) {
|
||||||
|
if (stopped) {
|
||||||
|
log.warn("We have stopped already. We ignore that onDataRequest call.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<NodeAddress> optionalNodeAddress = connection.getPeersNodeAddressOptional();
|
||||||
|
if (optionalNodeAddress.isEmpty()) {
|
||||||
|
log.warn("Peers node address is not present, that is not expected.");
|
||||||
|
// We do not return here as in case the connection has been created from the peers side we might not
|
||||||
|
// have the address set. As we check the nonce later we do not care that much for the check if the
|
||||||
|
// connection address is the same as the one we used.
|
||||||
|
} else if (!optionalNodeAddress.get().equals(nodeAddress)) {
|
||||||
|
log.warn("Peers node address is not the same we used for the request. This is not expected. We ignore that message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetAccountingBlocksResponse getBlocksResponse = (GetAccountingBlocksResponse) networkEnvelope;
|
||||||
|
if (getBlocksResponse.getRequestNonce() != nonce) {
|
||||||
|
log.warn("Nonce not matching. That can happen rarely if we get a response after a canceled " +
|
||||||
|
"handshake (timeout causes connection close but peer might have sent a msg before " +
|
||||||
|
"connection was closed).\n\t" +
|
||||||
|
"We drop that message. nonce={} / requestNonce={}",
|
||||||
|
nonce, getBlocksResponse.getRequestNonce());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate();
|
||||||
|
log.info("We received from peer {} a BlocksResponse with {} blocks",
|
||||||
|
nodeAddress.getFullAddress(), getBlocksResponse.getBlocks().size());
|
||||||
|
listener.onComplete(getBlocksResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void terminate() {
|
||||||
|
stopped = true;
|
||||||
|
networkNode.removeMessageListener(this);
|
||||||
|
stopTimeoutTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@SuppressWarnings("UnusedParameters")
|
||||||
|
private void handleFault(String errorMessage,
|
||||||
|
NodeAddress nodeAddress,
|
||||||
|
CloseConnectionReason closeConnectionReason) {
|
||||||
|
terminate();
|
||||||
|
peerManager.handleConnectionFault(nodeAddress);
|
||||||
|
listener.onFault(errorMessage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopTimeoutTimer() {
|
||||||
|
if (timeoutTimer != null) {
|
||||||
|
timeoutTimer.stop();
|
||||||
|
timeoutTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.messages;
|
||||||
|
|
||||||
|
import bisq.network.p2p.DirectMessage;
|
||||||
|
import bisq.network.p2p.InitialDataRequest;
|
||||||
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import bisq.network.p2p.SendersNodeAddressMessage;
|
||||||
|
import bisq.network.p2p.SupportedCapabilitiesMessage;
|
||||||
|
|
||||||
|
import bisq.common.app.Capabilities;
|
||||||
|
import bisq.common.app.Version;
|
||||||
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
// Taken from GetBlocksRequest
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Getter
|
||||||
|
@Slf4j
|
||||||
|
public final class GetAccountingBlocksRequest extends NetworkEnvelope implements DirectMessage, SendersNodeAddressMessage,
|
||||||
|
SupportedCapabilitiesMessage, InitialDataRequest {
|
||||||
|
private final int fromBlockHeight;
|
||||||
|
private final int nonce;
|
||||||
|
|
||||||
|
private final NodeAddress senderNodeAddress;
|
||||||
|
private final Capabilities supportedCapabilities;
|
||||||
|
|
||||||
|
public GetAccountingBlocksRequest(int fromBlockHeight,
|
||||||
|
int nonce,
|
||||||
|
NodeAddress senderNodeAddress) {
|
||||||
|
this(fromBlockHeight,
|
||||||
|
nonce,
|
||||||
|
senderNodeAddress,
|
||||||
|
Capabilities.app,
|
||||||
|
Version.getP2PMessageVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private GetAccountingBlocksRequest(int fromBlockHeight,
|
||||||
|
int nonce,
|
||||||
|
NodeAddress senderNodeAddress,
|
||||||
|
Capabilities supportedCapabilities,
|
||||||
|
int messageVersion) {
|
||||||
|
super(messageVersion);
|
||||||
|
this.fromBlockHeight = fromBlockHeight;
|
||||||
|
this.nonce = nonce;
|
||||||
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
|
this.supportedCapabilities = supportedCapabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
//todo
|
||||||
|
@Override
|
||||||
|
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||||
|
protobuf.GetAccountingBlocksRequest.Builder builder = protobuf.GetAccountingBlocksRequest.newBuilder()
|
||||||
|
.setFromBlockHeight(fromBlockHeight)
|
||||||
|
.setNonce(nonce);
|
||||||
|
Optional.ofNullable(senderNodeAddress).ifPresent(e -> builder.setSenderNodeAddress(e.toProtoMessage()));
|
||||||
|
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
||||||
|
return getNetworkEnvelopeBuilder().setGetAccountingBlocksRequest(builder).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NetworkEnvelope fromProto(protobuf.GetAccountingBlocksRequest proto, int messageVersion) {
|
||||||
|
protobuf.NodeAddress protoNodeAddress = proto.getSenderNodeAddress();
|
||||||
|
NodeAddress senderNodeAddress = protoNodeAddress.getHostName().isEmpty() ?
|
||||||
|
null :
|
||||||
|
NodeAddress.fromProto(protoNodeAddress);
|
||||||
|
Capabilities supportedCapabilities = proto.getSupportedCapabilitiesList().isEmpty() ?
|
||||||
|
null :
|
||||||
|
Capabilities.fromIntList(proto.getSupportedCapabilitiesList());
|
||||||
|
return new GetAccountingBlocksRequest(proto.getFromBlockHeight(),
|
||||||
|
proto.getNonce(),
|
||||||
|
senderNodeAddress,
|
||||||
|
supportedCapabilities,
|
||||||
|
messageVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GetAccountingBlocksRequest{" +
|
||||||
|
"\n fromBlockHeight=" + fromBlockHeight +
|
||||||
|
",\n nonce=" + nonce +
|
||||||
|
",\n senderNodeAddress=" + senderNodeAddress +
|
||||||
|
",\n supportedCapabilities=" + supportedCapabilities +
|
||||||
|
"\n} " + super.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.messages;
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
|
||||||
|
import bisq.network.p2p.DirectMessage;
|
||||||
|
import bisq.network.p2p.ExtendedDataSizePermission;
|
||||||
|
import bisq.network.p2p.InitialDataRequest;
|
||||||
|
import bisq.network.p2p.InitialDataResponse;
|
||||||
|
|
||||||
|
import bisq.common.app.Version;
|
||||||
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
// Taken from GetBlocksResponse
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Getter
|
||||||
|
@Slf4j
|
||||||
|
public final class GetAccountingBlocksResponse extends NetworkEnvelope implements DirectMessage,
|
||||||
|
ExtendedDataSizePermission, InitialDataResponse {
|
||||||
|
private final List<AccountingBlock> blocks;
|
||||||
|
private final int requestNonce;
|
||||||
|
private final String pubKey;
|
||||||
|
private final byte[] signature;
|
||||||
|
|
||||||
|
public GetAccountingBlocksResponse(List<AccountingBlock> blocks,
|
||||||
|
int requestNonce,
|
||||||
|
String pubKey,
|
||||||
|
byte[] signature) {
|
||||||
|
this(blocks, requestNonce, pubKey, signature, Version.getP2PMessageVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private GetAccountingBlocksResponse(List<AccountingBlock> blocks,
|
||||||
|
int requestNonce,
|
||||||
|
String pubKey,
|
||||||
|
byte[] signature,
|
||||||
|
int messageVersion) {
|
||||||
|
super(messageVersion);
|
||||||
|
|
||||||
|
this.blocks = blocks;
|
||||||
|
this.requestNonce = requestNonce;
|
||||||
|
this.pubKey = pubKey;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||||
|
protobuf.NetworkEnvelope proto = getNetworkEnvelopeBuilder()
|
||||||
|
.setGetAccountingBlocksResponse(protobuf.GetAccountingBlocksResponse.newBuilder()
|
||||||
|
.addAllBlocks(blocks.stream()
|
||||||
|
.map(AccountingBlock::toProtoMessage)
|
||||||
|
.collect(Collectors.toList()))
|
||||||
|
.setRequestNonce(requestNonce)
|
||||||
|
.setPubKey(pubKey)
|
||||||
|
.setSignature(ByteString.copyFrom(signature)))
|
||||||
|
.build();
|
||||||
|
log.info("Sending a GetBlocksResponse with {} kB", proto.getSerializedSize() / 1000d);
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NetworkEnvelope fromProto(protobuf.GetAccountingBlocksResponse proto, int messageVersion) {
|
||||||
|
List<AccountingBlock> list = proto.getBlocksList().stream()
|
||||||
|
.map(AccountingBlock::fromProto)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
log.info("Received a GetBlocksResponse with {} blocks and {} kB size", list.size(), proto.getSerializedSize() / 1000d);
|
||||||
|
return new GetAccountingBlocksResponse(proto.getBlocksList().isEmpty() ?
|
||||||
|
new ArrayList<>() :
|
||||||
|
list,
|
||||||
|
proto.getRequestNonce(),
|
||||||
|
proto.getPubKey(),
|
||||||
|
proto.getSignature().toByteArray(),
|
||||||
|
messageVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<? extends InitialDataRequest> associatedRequest() {
|
||||||
|
return GetAccountingBlocksRequest.class;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.node.messages;
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
|
||||||
|
import bisq.network.p2p.storage.messages.BroadcastMessage;
|
||||||
|
|
||||||
|
import bisq.common.app.Version;
|
||||||
|
import bisq.common.proto.network.NetworkEnvelope;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Getter
|
||||||
|
public final class NewAccountingBlockBroadcastMessage extends BroadcastMessage {
|
||||||
|
private final AccountingBlock block;
|
||||||
|
private final String pubKey;
|
||||||
|
private final byte[] signature;
|
||||||
|
|
||||||
|
public NewAccountingBlockBroadcastMessage(AccountingBlock block, String pubKey, byte[] signature) {
|
||||||
|
this(block, pubKey, signature, Version.getP2PMessageVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private NewAccountingBlockBroadcastMessage(AccountingBlock block,
|
||||||
|
String pubKey,
|
||||||
|
byte[] signature,
|
||||||
|
int messageVersion) {
|
||||||
|
super(messageVersion);
|
||||||
|
this.block = block;
|
||||||
|
this.pubKey = pubKey;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||||
|
return getNetworkEnvelopeBuilder()
|
||||||
|
.setNewAccountingBlockBroadcastMessage(protobuf.NewAccountingBlockBroadcastMessage.newBuilder()
|
||||||
|
.setBlock(block.toProtoMessage())
|
||||||
|
.setPubKey(pubKey)
|
||||||
|
.setSignature(ByteString.copyFrom(signature))
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NetworkEnvelope fromProto(protobuf.NewAccountingBlockBroadcastMessage proto, int messageVersion) {
|
||||||
|
return new NewAccountingBlockBroadcastMessage(AccountingBlock.fromProto(proto.getBlock()),
|
||||||
|
proto.getPubKey(),
|
||||||
|
proto.getSignature().toByteArray(),
|
||||||
|
messageVersion);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.storage;
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
|
||||||
|
import bisq.common.proto.persistable.PersistableEnvelope;
|
||||||
|
|
||||||
|
import com.google.protobuf.Message;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Getter
|
||||||
|
public class BurningManAccountingStore implements PersistableEnvelope {
|
||||||
|
private final LinkedList<AccountingBlock> blocks = new LinkedList<>();
|
||||||
|
|
||||||
|
public BurningManAccountingStore(List<AccountingBlock> blocks) {
|
||||||
|
this.blocks.addAll(blocks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message toProtoMessage() {
|
||||||
|
return protobuf.PersistableEnvelope.newBuilder()
|
||||||
|
.setBurningManAccountingStore(protobuf.BurningManAccountingStore.newBuilder()
|
||||||
|
.addAllBlocks(blocks.stream()
|
||||||
|
.map(AccountingBlock::toProtoMessage)
|
||||||
|
.collect(Collectors.toList())))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BurningManAccountingStore fromProto(protobuf.BurningManAccountingStore proto) {
|
||||||
|
return new BurningManAccountingStore(proto.getBlocksList().stream()
|
||||||
|
.map(AccountingBlock::fromProto)
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.accounting.storage;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.accounting.blockchain.AccountingBlock;
|
||||||
|
|
||||||
|
import bisq.network.p2p.storage.persistence.ResourceDataStoreService;
|
||||||
|
import bisq.network.p2p.storage.persistence.StoreService;
|
||||||
|
|
||||||
|
import bisq.common.config.Config;
|
||||||
|
import bisq.common.persistence.PersistenceManager;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Singleton
|
||||||
|
public class BurningManAccountingStoreService extends StoreService<BurningManAccountingStore> {
|
||||||
|
private static final String FILE_NAME = "BurningManAccountingStore";
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BurningManAccountingStoreService(ResourceDataStoreService resourceDataStoreService,
|
||||||
|
@Named(Config.STORAGE_DIR) File storageDir,
|
||||||
|
PersistenceManager<BurningManAccountingStore> persistenceManager) {
|
||||||
|
super(storageDir, persistenceManager);
|
||||||
|
|
||||||
|
resourceDataStoreService.addService(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// API
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public void requestPersistence() {
|
||||||
|
persistenceManager.requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AccountingBlock> getBlocks() {
|
||||||
|
return Collections.unmodifiableList(store.getBlocks());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBlock(AccountingBlock block) {
|
||||||
|
store.getBlocks().add(block);
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void purgeLastTenBlocks() {
|
||||||
|
List<AccountingBlock> blocks = store.getBlocks();
|
||||||
|
if (blocks.size() <= 10) {
|
||||||
|
blocks.clear();
|
||||||
|
requestPersistence();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AccountingBlock> purged = new ArrayList<>(blocks.subList(0, blocks.size() - 10));
|
||||||
|
blocks.clear();
|
||||||
|
blocks.addAll(purged);
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Protected
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BurningManAccountingStore createStore() {
|
||||||
|
return new BurningManAccountingStore(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializePersistenceManager() {
|
||||||
|
persistenceManager.initialize(store, PersistenceManager.Source.NETWORK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFileName() {
|
||||||
|
return FILE_NAME;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.model;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public final class BurnOutputModel {
|
||||||
|
private final long amount;
|
||||||
|
private final long decayedAmount;
|
||||||
|
private final int height;
|
||||||
|
private final String txId;
|
||||||
|
private final long date;
|
||||||
|
private final int cycleIndex;
|
||||||
|
|
||||||
|
public BurnOutputModel(long amount,
|
||||||
|
long decayedAmount,
|
||||||
|
int height,
|
||||||
|
String txId,
|
||||||
|
long date,
|
||||||
|
int cycleIndex) {
|
||||||
|
|
||||||
|
this.amount = amount;
|
||||||
|
this.decayedAmount = decayedAmount;
|
||||||
|
this.height = height;
|
||||||
|
this.txId = txId;
|
||||||
|
this.date = date;
|
||||||
|
this.cycleIndex = cycleIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "\n BurnOutputModel{" +
|
||||||
|
"\r\n amount=" + amount +
|
||||||
|
",\r\n decayedAmount=" + decayedAmount +
|
||||||
|
",\r\n height=" + height +
|
||||||
|
",\r\n txId='" + txId + '\'' +
|
||||||
|
",\r\n date=" + new Date(date) +
|
||||||
|
",\r\n cycleIndex=" + cycleIndex +
|
||||||
|
"\r\n }";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.model;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
|
|
||||||
|
import bisq.common.util.DateUtil;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all relevant data for a burningman candidate (any contributor who has made a compensation request or was
|
||||||
|
* a receiver of a genesis output).
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class BurningManCandidate {
|
||||||
|
private final Set<CompensationModel> compensationModels = new HashSet<>();
|
||||||
|
private long accumulatedCompensationAmount;
|
||||||
|
private long accumulatedDecayedCompensationAmount;
|
||||||
|
private double compensationShare; // Share of accumulated decayed compensation amounts in relation to total issued amounts
|
||||||
|
protected Optional<String> mostRecentAddress = Optional.empty();
|
||||||
|
|
||||||
|
private final Set<BurnOutputModel> burnOutputModels = new HashSet<>();
|
||||||
|
private final Map<Date, Set<BurnOutputModel>> burnOutputModelsByMonth = new HashMap<>();
|
||||||
|
private long accumulatedBurnAmount;
|
||||||
|
private long accumulatedDecayedBurnAmount;
|
||||||
|
protected double burnAmountShare; // Share of accumulated decayed burn amounts in relation to total burned amounts
|
||||||
|
protected double cappedBurnAmountShare; // Capped burnAmountShare. Cannot be larger than boostedCompensationShare
|
||||||
|
|
||||||
|
public BurningManCandidate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addBurnOutputModel(BurnOutputModel burnOutputModel) {
|
||||||
|
if (burnOutputModels.contains(burnOutputModel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
burnOutputModels.add(burnOutputModel);
|
||||||
|
|
||||||
|
Date month = DateUtil.getStartOfMonth(new Date(burnOutputModel.getDate()));
|
||||||
|
burnOutputModelsByMonth.putIfAbsent(month, new HashSet<>());
|
||||||
|
burnOutputModelsByMonth.get(month).add(burnOutputModel);
|
||||||
|
|
||||||
|
accumulatedDecayedBurnAmount += burnOutputModel.getDecayedAmount();
|
||||||
|
accumulatedBurnAmount += burnOutputModel.getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCompensationModel(CompensationModel compensationModel) {
|
||||||
|
if (compensationModels.contains(compensationModel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
compensationModels.add(compensationModel);
|
||||||
|
|
||||||
|
accumulatedDecayedCompensationAmount += compensationModel.getDecayedAmount();
|
||||||
|
accumulatedCompensationAmount += compensationModel.getAmount();
|
||||||
|
|
||||||
|
mostRecentAddress = compensationModels.stream()
|
||||||
|
.max(Comparator.comparing(CompensationModel::getHeight))
|
||||||
|
.map(CompensationModel::getAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getAllAddresses() {
|
||||||
|
return compensationModels.stream().map(CompensationModel::getAddress).collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void calculateShares(double totalDecayedCompensationAmounts, double totalDecayedBurnAmounts) {
|
||||||
|
compensationShare = totalDecayedCompensationAmounts > 0 ? accumulatedDecayedCompensationAmount / totalDecayedCompensationAmounts : 0;
|
||||||
|
double maxBoostedCompensationShare = Math.min(BurningManService.MAX_BURN_SHARE, compensationShare * BurningManService.ISSUANCE_BOOST_FACTOR);
|
||||||
|
|
||||||
|
burnAmountShare = totalDecayedBurnAmounts > 0 ? accumulatedDecayedBurnAmount / totalDecayedBurnAmounts : 0;
|
||||||
|
cappedBurnAmountShare = Math.min(maxBoostedCompensationShare, burnAmountShare);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BurningManCandidate{" +
|
||||||
|
"\r\n compensationModels=" + compensationModels +
|
||||||
|
",\r\n accumulatedCompensationAmount=" + accumulatedCompensationAmount +
|
||||||
|
",\r\n accumulatedDecayedCompensationAmount=" + accumulatedDecayedCompensationAmount +
|
||||||
|
",\r\n compensationShare=" + compensationShare +
|
||||||
|
",\r\n mostRecentAddress=" + mostRecentAddress +
|
||||||
|
",\r\n burnOutputModels=" + burnOutputModels +
|
||||||
|
",\r\n accumulatedBurnAmount=" + accumulatedBurnAmount +
|
||||||
|
",\r\n accumulatedDecayedBurnAmount=" + accumulatedDecayedBurnAmount +
|
||||||
|
",\r\n burnAmountShare=" + burnAmountShare +
|
||||||
|
",\r\n cappedBurnAmountShare=" + cappedBurnAmountShare +
|
||||||
|
"\r\n}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.model;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public final class CompensationModel {
|
||||||
|
public static CompensationModel fromCompensationRequest(String address,
|
||||||
|
long amount,
|
||||||
|
long decayedAmount,
|
||||||
|
int height,
|
||||||
|
String txId,
|
||||||
|
long date,
|
||||||
|
int cycleIndex) {
|
||||||
|
return new CompensationModel(address,
|
||||||
|
amount,
|
||||||
|
decayedAmount,
|
||||||
|
height,
|
||||||
|
txId,
|
||||||
|
Optional.empty(),
|
||||||
|
date,
|
||||||
|
cycleIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompensationModel fromGenesisOutput(String address,
|
||||||
|
long amount,
|
||||||
|
long decayedAmount,
|
||||||
|
int height,
|
||||||
|
String txId,
|
||||||
|
int outputIndex,
|
||||||
|
long date) {
|
||||||
|
return new CompensationModel(address,
|
||||||
|
amount,
|
||||||
|
decayedAmount,
|
||||||
|
height,
|
||||||
|
txId,
|
||||||
|
Optional.of(outputIndex),
|
||||||
|
date,
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final String address;
|
||||||
|
private final long amount;
|
||||||
|
private final long decayedAmount;
|
||||||
|
private final int height;
|
||||||
|
private final String txId;
|
||||||
|
private final Optional<Integer> outputIndex; // Only set for genesis tx outputs as needed for sorting
|
||||||
|
private final long date;
|
||||||
|
private final int cycleIndex;
|
||||||
|
|
||||||
|
private CompensationModel(String address,
|
||||||
|
long amount,
|
||||||
|
long decayedAmount,
|
||||||
|
int height,
|
||||||
|
String txId,
|
||||||
|
Optional<Integer> outputIndex,
|
||||||
|
long date,
|
||||||
|
int cycleIndex) {
|
||||||
|
this.address = address;
|
||||||
|
this.amount = amount;
|
||||||
|
this.decayedAmount = decayedAmount;
|
||||||
|
this.height = height;
|
||||||
|
this.txId = txId;
|
||||||
|
this.outputIndex = outputIndex;
|
||||||
|
this.date = date;
|
||||||
|
this.cycleIndex = cycleIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "\n CompensationModel{" +
|
||||||
|
"\r\n address='" + address + '\'' +
|
||||||
|
",\r\n amount=" + amount +
|
||||||
|
",\r\n decayedAmount=" + decayedAmount +
|
||||||
|
",\r\n height=" + height +
|
||||||
|
",\r\n txId='" + txId + '\'' +
|
||||||
|
",\r\n outputIndex=" + outputIndex +
|
||||||
|
",\r\n date=" + new Date(date) +
|
||||||
|
",\r\n cycleIndex=" + cycleIndex +
|
||||||
|
"\r\n }";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.model;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public final class LegacyBurningMan extends BurningManCandidate {
|
||||||
|
public LegacyBurningMan(String address) {
|
||||||
|
mostRecentAddress = Optional.of(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyBurnAmountShare(double burnAmountShare) {
|
||||||
|
this.burnAmountShare = burnAmountShare;
|
||||||
|
this.cappedBurnAmountShare = burnAmountShare;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void calculateShares(double totalDecayedCompensationAmounts, double totalDecayedBurnAmounts) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<String> getAllAddresses() {
|
||||||
|
return mostRecentAddress.map(Set::of).orElse(new HashSet<>());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman.model;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public final class ReimbursementModel {
|
||||||
|
private final long amount;
|
||||||
|
private final int height;
|
||||||
|
private final long date;
|
||||||
|
private final int cycleIndex;
|
||||||
|
private final String txId;
|
||||||
|
|
||||||
|
public ReimbursementModel(long amount,
|
||||||
|
int height,
|
||||||
|
long date,
|
||||||
|
int cycleIndex,
|
||||||
|
String txId) {
|
||||||
|
this.amount = amount;
|
||||||
|
this.height = height;
|
||||||
|
this.date = date;
|
||||||
|
this.cycleIndex = cycleIndex;
|
||||||
|
this.txId = txId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "\n ReimbursementModel{" +
|
||||||
|
",\r\n amount=" + amount +
|
||||||
|
",\r\n height=" + height +
|
||||||
|
",\r\n date=" + new Date(date) +
|
||||||
|
",\r\n cycleIndex=" + cycleIndex +
|
||||||
|
",\r\n txId=" + txId +
|
||||||
|
"\r\n }";
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,8 +117,14 @@ public enum Param {
|
||||||
// Min required trade volume to not get de-listed. Check starts after trial period and use trial period afterwards to look back for trade activity.
|
// Min required trade volume to not get de-listed. Check starts after trial period and use trial period afterwards to look back for trade activity.
|
||||||
ASSET_MIN_VOLUME("0.01", ParamType.BTC, 10, 10),
|
ASSET_MIN_VOLUME("0.01", ParamType.BTC, 10, 10),
|
||||||
|
|
||||||
|
// LOCK_TIME_TRADE_PAYOUT was never used.
|
||||||
|
// We re-purpose it as value for BTC fee revenue per cycle. This can be added as oracle data by DAO voting.
|
||||||
|
// We cannot change the ParamType as that would break consensus
|
||||||
LOCK_TIME_TRADE_PAYOUT("4320", ParamType.BLOCK), // 30 days, can be disabled by setting to 0
|
LOCK_TIME_TRADE_PAYOUT("4320", ParamType.BLOCK), // 30 days, can be disabled by setting to 0
|
||||||
|
|
||||||
|
// Not used
|
||||||
ARBITRATOR_FEE("0", ParamType.PERCENT), // % of trade. For new trade protocol. Arbitration will become optional and we can apply a fee to it. Initially we start with no fee.
|
ARBITRATOR_FEE("0", ParamType.PERCENT), // % of trade. For new trade protocol. Arbitration will become optional and we can apply a fee to it. Initially we start with no fee.
|
||||||
|
|
||||||
MAX_TRADE_LIMIT("2", ParamType.BTC, 2, 2), // max trade limit for lowest risk payment method. Others will get derived from that.
|
MAX_TRADE_LIMIT("2", ParamType.BTC, 2, 2), // max trade limit for lowest risk payment method. Others will get derived from that.
|
||||||
|
|
||||||
// The base factor to multiply the bonded role amount. E.g. If Twitter admin has 20 as amount and BONDED_ROLE_FACTOR is 1000 we get 20000 BSQ as required bond.
|
// The base factor to multiply the bonded role amount. E.g. If Twitter admin has 20 as amount and BONDED_ROLE_FACTOR is 1000 we get 20000 BSQ as required bond.
|
||||||
|
|
|
@ -40,6 +40,8 @@ import org.bitcoinj.core.Transaction;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -48,9 +50,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CompensationProposalFactory extends BaseProposalFactory<CompensationProposal> {
|
public class CompensationProposalFactory extends BaseProposalFactory<CompensationProposal> {
|
||||||
|
|
||||||
private Coin requestedBsq;
|
private Coin requestedBsq;
|
||||||
private String bsqAddress;
|
private String bsqAddress;
|
||||||
|
private Optional<String> burningManReceiverAddress;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CompensationProposalFactory(BsqWalletService bsqWalletService,
|
public CompensationProposalFactory(BsqWalletService bsqWalletService,
|
||||||
|
@ -65,9 +67,11 @@ public class CompensationProposalFactory extends BaseProposalFactory<Compensatio
|
||||||
|
|
||||||
public ProposalWithTransaction createProposalWithTransaction(String name,
|
public ProposalWithTransaction createProposalWithTransaction(String name,
|
||||||
String link,
|
String link,
|
||||||
Coin requestedBsq)
|
Coin requestedBsq,
|
||||||
|
Optional<String> burningManReceiverAddress)
|
||||||
throws ProposalValidationException, InsufficientMoneyException, TxException {
|
throws ProposalValidationException, InsufficientMoneyException, TxException {
|
||||||
this.requestedBsq = requestedBsq;
|
this.requestedBsq = requestedBsq;
|
||||||
|
this.burningManReceiverAddress = burningManReceiverAddress;
|
||||||
this.bsqAddress = bsqWalletService.getUnusedBsqAddressAsString();
|
this.bsqAddress = bsqWalletService.getUnusedBsqAddressAsString();
|
||||||
|
|
||||||
return super.createProposalWithTransaction(name, link);
|
return super.createProposalWithTransaction(name, link);
|
||||||
|
@ -75,12 +79,17 @@ public class CompensationProposalFactory extends BaseProposalFactory<Compensatio
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected CompensationProposal createProposalWithoutTxId() {
|
protected CompensationProposal createProposalWithoutTxId() {
|
||||||
|
Map<String, String> extraDataMap = null;
|
||||||
|
if (burningManReceiverAddress.isPresent()) {
|
||||||
|
extraDataMap = new HashMap<>();
|
||||||
|
extraDataMap.put(CompensationProposal.BURNING_MAN_RECEIVER_ADDRESS, burningManReceiverAddress.get());
|
||||||
|
}
|
||||||
return new CompensationProposal(
|
return new CompensationProposal(
|
||||||
name,
|
name,
|
||||||
link,
|
link,
|
||||||
requestedBsq,
|
requestedBsq,
|
||||||
bsqAddress,
|
bsqAddress,
|
||||||
new HashMap<>());
|
extraDataMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
package bisq.core.dao.node.full;
|
package bisq.core.dao.node.full;
|
||||||
|
|
||||||
class RpcException extends Exception {
|
public class RpcException extends Exception {
|
||||||
RpcException(String message, Throwable cause) {
|
RpcException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,8 @@ import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
@ -91,6 +93,14 @@ public class RpcService {
|
||||||
// Keep that for optimization after measuring performance differences
|
// Keep that for optimization after measuring performance differences
|
||||||
private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("RpcService");
|
private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("RpcService");
|
||||||
private volatile boolean isShutDown;
|
private volatile boolean isShutDown;
|
||||||
|
private final Set<ResultHandler> setupResultHandlers = new CopyOnWriteArraySet<>();
|
||||||
|
private final Set<Consumer<Throwable>> setupErrorHandlers = new CopyOnWriteArraySet<>();
|
||||||
|
private volatile boolean setupComplete;
|
||||||
|
private Consumer<RawDtoBlock> rawDtoBlockHandler;
|
||||||
|
private Consumer<Throwable> rawDtoBlockErrorHandler;
|
||||||
|
private Consumer<RawBlock> rawBlockHandler;
|
||||||
|
private Consumer<Throwable> rawBlockErrorHandler;
|
||||||
|
private volatile boolean isBlockHandlerSet;
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -139,7 +149,20 @@ public class RpcService {
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup(ResultHandler resultHandler, Consumer<Throwable> errorHandler) {
|
public void setup(ResultHandler resultHandler, Consumer<Throwable> errorHandler) {
|
||||||
|
if (setupComplete) {
|
||||||
|
// Setup got already called and has finished.
|
||||||
|
resultHandler.handleResult();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setupResultHandlers.add(resultHandler);
|
||||||
|
setupErrorHandlers.add(errorHandler);
|
||||||
|
if (setupResultHandlers.size() > 1) {
|
||||||
|
// Setup got already called but has not finished yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ListenableFuture<Void> future = executor.submit(() -> {
|
ListenableFuture<Void> future = executor.submit(() -> {
|
||||||
try {
|
try {
|
||||||
|
@ -159,7 +182,11 @@ public class RpcService {
|
||||||
daemon = new BitcoindDaemon(rpcBlockHost, rpcBlockPort, throwable -> {
|
daemon = new BitcoindDaemon(rpcBlockHost, rpcBlockPort, throwable -> {
|
||||||
log.error(throwable.toString());
|
log.error(throwable.toString());
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
UserThread.execute(() -> errorHandler.accept(new RpcException(throwable)));
|
UserThread.execute(() -> {
|
||||||
|
setupErrorHandlers.forEach(handler -> handler.accept(new RpcException(throwable)));
|
||||||
|
setupErrorHandlers.clear();
|
||||||
|
setupResultHandlers.clear();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
|
log.info("Setup took {} ms", System.currentTimeMillis() - startTs);
|
||||||
|
@ -173,11 +200,20 @@ public class RpcService {
|
||||||
|
|
||||||
Futures.addCallback(future, new FutureCallback<>() {
|
Futures.addCallback(future, new FutureCallback<>() {
|
||||||
public void onSuccess(Void ignore) {
|
public void onSuccess(Void ignore) {
|
||||||
UserThread.execute(resultHandler::handleResult);
|
setupComplete = true;
|
||||||
|
UserThread.execute(() -> {
|
||||||
|
setupResultHandlers.forEach(ResultHandler::handleResult);
|
||||||
|
setupResultHandlers.clear();
|
||||||
|
setupErrorHandlers.clear();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFailure(@NotNull Throwable throwable) {
|
public void onFailure(@NotNull Throwable throwable) {
|
||||||
UserThread.execute(() -> errorHandler.accept(throwable));
|
UserThread.execute(() -> {
|
||||||
|
setupErrorHandlers.forEach(handler -> handler.accept(throwable));
|
||||||
|
setupErrorHandlers.clear();
|
||||||
|
setupResultHandlers.clear();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -219,21 +255,49 @@ public class RpcService {
|
||||||
|
|
||||||
void addNewDtoBlockHandler(Consumer<RawBlock> dtoBlockHandler,
|
void addNewDtoBlockHandler(Consumer<RawBlock> dtoBlockHandler,
|
||||||
Consumer<Throwable> errorHandler) {
|
Consumer<Throwable> errorHandler) {
|
||||||
|
this.rawBlockHandler = dtoBlockHandler;
|
||||||
|
this.rawBlockErrorHandler = errorHandler;
|
||||||
|
if (!isBlockHandlerSet) {
|
||||||
|
setupBlockHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addNewRawDtoBlockHandler(Consumer<RawDtoBlock> rawDtoBlockHandler,
|
||||||
|
Consumer<Throwable> errorHandler) {
|
||||||
|
this.rawDtoBlockHandler = rawDtoBlockHandler;
|
||||||
|
this.rawDtoBlockErrorHandler = errorHandler;
|
||||||
|
if (!isBlockHandlerSet) {
|
||||||
|
setupBlockHandler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupBlockHandler() {
|
||||||
|
isBlockHandlerSet = true;
|
||||||
daemon.setBlockListener(blockHash -> {
|
daemon.setBlockListener(blockHash -> {
|
||||||
try {
|
try {
|
||||||
var rawDtoBlock = client.getBlock(blockHash, 2);
|
RawDtoBlock rawDtoBlock = client.getBlock(blockHash, 2);
|
||||||
log.info("New block received: height={}, id={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash());
|
log.info("New rawDtoBlock received: height={}, hash={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash());
|
||||||
|
|
||||||
var block = getBlockFromRawDtoBlock(rawDtoBlock);
|
if (rawBlockHandler != null) {
|
||||||
UserThread.execute(() -> dtoBlockHandler.accept(block));
|
RawBlock rawBlock = getRawBlockFromRawDtoBlock(rawDtoBlock);
|
||||||
|
UserThread.execute(() -> rawBlockHandler.accept(rawBlock));
|
||||||
|
}
|
||||||
|
if (rawDtoBlockHandler != null) {
|
||||||
|
UserThread.execute(() -> rawDtoBlockHandler.accept(rawDtoBlock));
|
||||||
|
}
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
log.error("Error at BlockHandler", throwable);
|
log.error("Error at BlockHandler", throwable);
|
||||||
errorHandler.accept(throwable);
|
if (rawBlockErrorHandler != null) {
|
||||||
|
rawBlockErrorHandler.accept(throwable);
|
||||||
|
}
|
||||||
|
if (rawDtoBlockErrorHandler != null) {
|
||||||
|
rawDtoBlockErrorHandler.accept(throwable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler) {
|
public void requestChainHeadHeight(Consumer<Integer> resultHandler, Consumer<Throwable> errorHandler) {
|
||||||
try {
|
try {
|
||||||
ListenableFuture<Integer> future = executor.submit(client::getBlockCount);
|
ListenableFuture<Integer> future = executor.submit(client::getBlockCount);
|
||||||
Futures.addCallback(future, new FutureCallback<>() {
|
Futures.addCallback(future, new FutureCallback<>() {
|
||||||
|
@ -262,7 +326,7 @@ public class RpcService {
|
||||||
long startTs = System.currentTimeMillis();
|
long startTs = System.currentTimeMillis();
|
||||||
String blockHash = client.getBlockHash(blockHeight);
|
String blockHash = client.getBlockHash(blockHeight);
|
||||||
var rawDtoBlock = client.getBlock(blockHash, 2);
|
var rawDtoBlock = client.getBlock(blockHash, 2);
|
||||||
var block = getBlockFromRawDtoBlock(rawDtoBlock);
|
var block = getRawBlockFromRawDtoBlock(rawDtoBlock);
|
||||||
log.info("requestDtoBlock from bitcoind at blockHeight {} with {} txs took {} ms",
|
log.info("requestDtoBlock from bitcoind at blockHeight {} with {} txs took {} ms",
|
||||||
blockHeight, block.getRawTxs().size(), System.currentTimeMillis() - startTs);
|
blockHeight, block.getRawTxs().size(), System.currentTimeMillis() - startTs);
|
||||||
return block;
|
return block;
|
||||||
|
@ -289,12 +353,47 @@ public class RpcService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void requestRawDtoBlock(int blockHeight,
|
||||||
|
Consumer<RawDtoBlock> rawDtoBlockHandler,
|
||||||
|
Consumer<Throwable> errorHandler) {
|
||||||
|
try {
|
||||||
|
ListenableFuture<RawDtoBlock> future = executor.submit(() -> {
|
||||||
|
long startTs = System.currentTimeMillis();
|
||||||
|
String blockHash = client.getBlockHash(blockHeight);
|
||||||
|
|
||||||
|
// getBlock with about 2000 tx takes about 500 ms on a 4GHz Intel Core i7 CPU.
|
||||||
|
var rawDtoBlock = client.getBlock(blockHash, 2);
|
||||||
|
log.info("requestRawDtoBlock from bitcoind at blockHeight {} with {} txs took {} ms",
|
||||||
|
blockHeight, rawDtoBlock.getTx().size(), System.currentTimeMillis() - startTs);
|
||||||
|
return rawDtoBlock;
|
||||||
|
});
|
||||||
|
|
||||||
|
Futures.addCallback(future, new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(RawDtoBlock rawDtoBlock) {
|
||||||
|
UserThread.execute(() -> rawDtoBlockHandler.accept(rawDtoBlock));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NotNull Throwable throwable) {
|
||||||
|
log.error("Error at requestRawDtoBlock: blockHeight={}", blockHeight);
|
||||||
|
UserThread.execute(() -> errorHandler.accept(throwable));
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (!isShutDown || !(e instanceof RejectedExecutionException)) {
|
||||||
|
log.warn(e.toString(), e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Private
|
// Private
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private static RawBlock getBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) {
|
private static RawBlock getRawBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) {
|
||||||
List<RawTx> txList = rawDtoBlock.getTx().stream()
|
List<RawTx> txList = rawDtoBlock.getTx().stream()
|
||||||
.map(e -> getTxFromRawTransaction(e, rawDtoBlock))
|
.map(e -> getTxFromRawTransaction(e, rawDtoBlock))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
|
@ -201,6 +201,32 @@ public class DaoStateService implements DaoSetupService {
|
||||||
return getCycle(blockHeight).map(cycle -> cycle.getHeightOfFirstBlock());
|
return getCycle(blockHeight).map(cycle -> cycle.getHeightOfFirstBlock());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Cycle> getNextCycle(Cycle cycle) {
|
||||||
|
return getCycle(cycle.getHeightOfLastBlock() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Cycle> getPreviousCycle(Cycle cycle) {
|
||||||
|
return getCycle(cycle.getHeightOfFirstBlock() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Cycle> getPastCycle(Cycle cycle, int numPastCycles) {
|
||||||
|
Optional<Cycle> previous = Optional.empty();
|
||||||
|
Cycle current = cycle;
|
||||||
|
for (int i = 0; i < numPastCycles; i++) {
|
||||||
|
previous = getPreviousCycle(current);
|
||||||
|
if (previous.isPresent()) {
|
||||||
|
current = previous.get();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cycle getCycleAtIndex(int index) {
|
||||||
|
return getCycles().get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Block
|
// Block
|
||||||
|
@ -440,7 +466,7 @@ public class DaoStateService implements DaoSetupService {
|
||||||
// TxOutput
|
// TxOutput
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private Stream<TxOutput> getUnorderedTxOutputStream() {
|
public Stream<TxOutput> getUnorderedTxOutputStream() {
|
||||||
return getUnorderedTxStream()
|
return getUnorderedTxStream()
|
||||||
.flatMap(tx -> tx.getTxOutputs().stream());
|
.flatMap(tx -> tx.getTxOutputs().stream());
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,11 +30,13 @@ import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.Immutable;
|
import javax.annotation.concurrent.Immutable;
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
@ -42,6 +44,9 @@ import javax.annotation.concurrent.Immutable;
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@Value
|
@Value
|
||||||
public final class CompensationProposal extends Proposal implements IssuanceProposal, ImmutableDaoStateModel {
|
public final class CompensationProposal extends Proposal implements IssuanceProposal, ImmutableDaoStateModel {
|
||||||
|
// Keys for extra map
|
||||||
|
public static final String BURNING_MAN_RECEIVER_ADDRESS = "burningManReceiverAddress";
|
||||||
|
|
||||||
private final long requestedBsq;
|
private final long requestedBsq;
|
||||||
private final String bsqAddress;
|
private final String bsqAddress;
|
||||||
|
|
||||||
|
@ -49,7 +54,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
||||||
String link,
|
String link,
|
||||||
Coin requestedBsq,
|
Coin requestedBsq,
|
||||||
String bsqAddress,
|
String bsqAddress,
|
||||||
Map<String, String> extraDataMap) {
|
@Nullable Map<String, String> extraDataMap) {
|
||||||
this(name,
|
this(name,
|
||||||
link,
|
link,
|
||||||
bsqAddress,
|
bsqAddress,
|
||||||
|
@ -72,7 +77,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
||||||
byte version,
|
byte version,
|
||||||
long creationDate,
|
long creationDate,
|
||||||
String txId,
|
String txId,
|
||||||
Map<String, String> extraDataMap) {
|
@Nullable Map<String, String> extraDataMap) {
|
||||||
super(name,
|
super(name,
|
||||||
link,
|
link,
|
||||||
version,
|
version,
|
||||||
|
@ -135,6 +140,11 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
||||||
return TxType.COMPENSATION_REQUEST;
|
return TxType.COMPENSATION_REQUEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Added at v.1.9.7
|
||||||
|
public Optional<String> getBurningManReceiverAddress() {
|
||||||
|
return Optional.ofNullable(extraDataMap).flatMap(map -> Optional.ofNullable(map.get(BURNING_MAN_RECEIVER_ADDRESS)));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Proposal cloneProposalAndAddTxId(String txId) {
|
public Proposal cloneProposalAndAddTxId(String txId) {
|
||||||
return new CompensationProposal(getName(),
|
return new CompensationProposal(getName(),
|
||||||
|
@ -152,6 +162,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
||||||
return "CompensationProposal{" +
|
return "CompensationProposal{" +
|
||||||
"\n requestedBsq=" + requestedBsq +
|
"\n requestedBsq=" + requestedBsq +
|
||||||
",\n bsqAddress='" + bsqAddress + '\'' +
|
",\n bsqAddress='" + bsqAddress + '\'' +
|
||||||
|
",\n burningManReceiverAddress='" + getBurningManReceiverAddress() + '\'' +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -145,7 +145,7 @@ public abstract class Proposal implements PersistablePayload, NetworkPayload, Co
|
||||||
"\n txId='" + txId + '\'' +
|
"\n txId='" + txId + '\'' +
|
||||||
",\n name='" + name + '\'' +
|
",\n name='" + name + '\'' +
|
||||||
",\n link='" + link + '\'' +
|
",\n link='" + link + '\'' +
|
||||||
",\n txId='" + txId + '\'' +
|
",\n extraDataMap='" + extraDataMap + '\'' +
|
||||||
",\n version=" + version +
|
",\n version=" + version +
|
||||||
",\n creationDate=" + new Date(creationDate) +
|
",\n creationDate=" + new Date(creationDate) +
|
||||||
"\n}";
|
"\n}";
|
||||||
|
|
|
@ -22,6 +22,9 @@ import bisq.core.btc.wallet.BsqWalletService;
|
||||||
import bisq.core.btc.wallet.BtcWalletService;
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.dao.DaoFacade;
|
import bisq.core.dao.DaoFacade;
|
||||||
|
import bisq.core.dao.burningman.BtcFeeReceiverService;
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
|
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||||
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
||||||
import bisq.core.filter.FilterManager;
|
import bisq.core.filter.FilterManager;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -122,6 +125,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
private final RefundAgentManager refundAgentManager;
|
private final RefundAgentManager refundAgentManager;
|
||||||
private final DaoFacade daoFacade;
|
private final DaoFacade daoFacade;
|
||||||
private final FilterManager filterManager;
|
private final FilterManager filterManager;
|
||||||
|
private final BtcFeeReceiverService btcFeeReceiverService;
|
||||||
|
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||||
private final Broadcaster broadcaster;
|
private final Broadcaster broadcaster;
|
||||||
private final PersistenceManager<TradableList<OpenOffer>> persistenceManager;
|
private final PersistenceManager<TradableList<OpenOffer>> persistenceManager;
|
||||||
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
|
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
|
||||||
|
@ -155,6 +160,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
RefundAgentManager refundAgentManager,
|
RefundAgentManager refundAgentManager,
|
||||||
DaoFacade daoFacade,
|
DaoFacade daoFacade,
|
||||||
FilterManager filterManager,
|
FilterManager filterManager,
|
||||||
|
BtcFeeReceiverService btcFeeReceiverService,
|
||||||
|
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
|
||||||
Broadcaster broadcaster,
|
Broadcaster broadcaster,
|
||||||
PersistenceManager<TradableList<OpenOffer>> persistenceManager) {
|
PersistenceManager<TradableList<OpenOffer>> persistenceManager) {
|
||||||
this.coreContext = coreContext;
|
this.coreContext = coreContext;
|
||||||
|
@ -175,6 +182,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
this.refundAgentManager = refundAgentManager;
|
this.refundAgentManager = refundAgentManager;
|
||||||
this.daoFacade = daoFacade;
|
this.daoFacade = daoFacade;
|
||||||
this.filterManager = filterManager;
|
this.filterManager = filterManager;
|
||||||
|
this.btcFeeReceiverService = btcFeeReceiverService;
|
||||||
|
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||||
this.broadcaster = broadcaster;
|
this.broadcaster = broadcaster;
|
||||||
this.persistenceManager = persistenceManager;
|
this.persistenceManager = persistenceManager;
|
||||||
|
|
||||||
|
@ -389,6 +398,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
arbitratorManager,
|
arbitratorManager,
|
||||||
tradeStatisticsManager,
|
tradeStatisticsManager,
|
||||||
daoFacade,
|
daoFacade,
|
||||||
|
btcFeeReceiverService,
|
||||||
user,
|
user,
|
||||||
filterManager);
|
filterManager);
|
||||||
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
|
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
|
||||||
|
@ -663,6 +673,22 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (BurningManService.isActivated()) {
|
||||||
|
try {
|
||||||
|
int takersBurningManSelectionHeight = request.getBurningManSelectionHeight();
|
||||||
|
checkArgument(takersBurningManSelectionHeight > 0, "takersBurningManSelectionHeight must not be 0");
|
||||||
|
|
||||||
|
int makersBurningManSelectionHeight = delayedPayoutTxReceiverService.getBurningManSelectionHeight();
|
||||||
|
checkArgument(takersBurningManSelectionHeight == makersBurningManSelectionHeight,
|
||||||
|
"takersBurningManSelectionHeight does no match makersBurningManSelectionHeight");
|
||||||
|
} catch (Throwable t) {
|
||||||
|
errorMessage = "Message validation failed. Error=" + t + ", Message=" + request;
|
||||||
|
log.warn(errorMessage);
|
||||||
|
sendAckMessage(request, peer, false, errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
|
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
|
||||||
AvailabilityResult availabilityResult;
|
AvailabilityResult availabilityResult;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package bisq.core.offer.availability;
|
package bisq.core.offer.availability;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.offer.availability.messages.OfferAvailabilityResponse;
|
import bisq.core.offer.availability.messages.OfferAvailabilityResponse;
|
||||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||||
|
@ -70,12 +71,17 @@ public class OfferAvailabilityModel implements Model {
|
||||||
@Getter
|
@Getter
|
||||||
private final boolean isTakerApiUser;
|
private final boolean isTakerApiUser;
|
||||||
|
|
||||||
|
// Added in v 1.9.7
|
||||||
|
@Getter
|
||||||
|
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||||
|
|
||||||
public OfferAvailabilityModel(Offer offer,
|
public OfferAvailabilityModel(Offer offer,
|
||||||
PubKeyRing pubKeyRing,
|
PubKeyRing pubKeyRing,
|
||||||
P2PService p2PService,
|
P2PService p2PService,
|
||||||
User user,
|
User user,
|
||||||
MediatorManager mediatorManager,
|
MediatorManager mediatorManager,
|
||||||
TradeStatisticsManager tradeStatisticsManager,
|
TradeStatisticsManager tradeStatisticsManager,
|
||||||
|
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
|
||||||
boolean isTakerApiUser) {
|
boolean isTakerApiUser) {
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.pubKeyRing = pubKeyRing;
|
this.pubKeyRing = pubKeyRing;
|
||||||
|
@ -83,6 +89,7 @@ public class OfferAvailabilityModel implements Model {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.mediatorManager = mediatorManager;
|
this.mediatorManager = mediatorManager;
|
||||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||||
|
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||||
this.isTakerApiUser = isTakerApiUser;
|
this.isTakerApiUser = isTakerApiUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,17 +44,23 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
||||||
private final Capabilities supportedCapabilities;
|
private final Capabilities supportedCapabilities;
|
||||||
private final boolean isTakerApiUser;
|
private final boolean isTakerApiUser;
|
||||||
|
|
||||||
|
// Added in v 1.9.7
|
||||||
|
private final int burningManSelectionHeight;
|
||||||
|
|
||||||
public OfferAvailabilityRequest(String offerId,
|
public OfferAvailabilityRequest(String offerId,
|
||||||
PubKeyRing pubKeyRing,
|
PubKeyRing pubKeyRing,
|
||||||
long takersTradePrice,
|
long takersTradePrice,
|
||||||
boolean isTakerApiUser) {
|
boolean isTakerApiUser,
|
||||||
|
int burningManSelectionHeight) {
|
||||||
this(offerId,
|
this(offerId,
|
||||||
pubKeyRing,
|
pubKeyRing,
|
||||||
takersTradePrice,
|
takersTradePrice,
|
||||||
isTakerApiUser,
|
isTakerApiUser,
|
||||||
|
burningManSelectionHeight,
|
||||||
Capabilities.app,
|
Capabilities.app,
|
||||||
Version.getP2PMessageVersion(),
|
Version.getP2PMessageVersion(),
|
||||||
UUID.randomUUID().toString());
|
UUID.randomUUID().toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,6 +72,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
||||||
PubKeyRing pubKeyRing,
|
PubKeyRing pubKeyRing,
|
||||||
long takersTradePrice,
|
long takersTradePrice,
|
||||||
boolean isTakerApiUser,
|
boolean isTakerApiUser,
|
||||||
|
int burningManSelectionHeight,
|
||||||
@Nullable Capabilities supportedCapabilities,
|
@Nullable Capabilities supportedCapabilities,
|
||||||
int messageVersion,
|
int messageVersion,
|
||||||
@Nullable String uid) {
|
@Nullable String uid) {
|
||||||
|
@ -73,6 +80,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
||||||
this.pubKeyRing = pubKeyRing;
|
this.pubKeyRing = pubKeyRing;
|
||||||
this.takersTradePrice = takersTradePrice;
|
this.takersTradePrice = takersTradePrice;
|
||||||
this.isTakerApiUser = isTakerApiUser;
|
this.isTakerApiUser = isTakerApiUser;
|
||||||
|
this.burningManSelectionHeight = burningManSelectionHeight;
|
||||||
this.supportedCapabilities = supportedCapabilities;
|
this.supportedCapabilities = supportedCapabilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +90,8 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
||||||
.setOfferId(offerId)
|
.setOfferId(offerId)
|
||||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||||
.setTakersTradePrice(takersTradePrice)
|
.setTakersTradePrice(takersTradePrice)
|
||||||
.setIsTakerApiUser(isTakerApiUser);
|
.setIsTakerApiUser(isTakerApiUser)
|
||||||
|
.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||||
|
|
||||||
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
||||||
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
|
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
|
||||||
|
@ -97,6 +106,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
||||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||||
proto.getTakersTradePrice(),
|
proto.getTakersTradePrice(),
|
||||||
proto.getIsTakerApiUser(),
|
proto.getIsTakerApiUser(),
|
||||||
|
proto.getBurningManSelectionHeight(),
|
||||||
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
|
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
|
||||||
messageVersion,
|
messageVersion,
|
||||||
proto.getUid().isEmpty() ? null : proto.getUid());
|
proto.getUid().isEmpty() ? null : proto.getUid());
|
||||||
|
|
|
@ -39,8 +39,12 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
|
int burningManSelectionHeight = model.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
|
||||||
OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(),
|
OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.getOffer().getId(),
|
||||||
model.getPubKeyRing(), model.getTakersTradePrice(), model.isTakerApiUser());
|
model.getPubKeyRing(),
|
||||||
|
model.getTakersTradePrice(),
|
||||||
|
model.isTakerApiUser(),
|
||||||
|
burningManSelectionHeight);
|
||||||
log.info("Send {} with offerId {} and uid {} to peer {}",
|
log.info("Send {} with offerId {} and uid {} to peer {}",
|
||||||
message.getClass().getSimpleName(), message.getOfferId(),
|
message.getClass().getSimpleName(), message.getOfferId(),
|
||||||
message.getUid(), model.getPeerNodeAddress());
|
message.getUid(), model.getPeerNodeAddress());
|
||||||
|
|
|
@ -21,6 +21,7 @@ import bisq.core.btc.wallet.BsqWalletService;
|
||||||
import bisq.core.btc.wallet.BtcWalletService;
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.dao.DaoFacade;
|
import bisq.core.dao.DaoFacade;
|
||||||
|
import bisq.core.dao.burningman.BtcFeeReceiverService;
|
||||||
import bisq.core.filter.FilterManager;
|
import bisq.core.filter.FilterManager;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.offer.OfferBookService;
|
import bisq.core.offer.OfferBookService;
|
||||||
|
@ -51,6 +52,7 @@ public class PlaceOfferModel implements Model {
|
||||||
private final ArbitratorManager arbitratorManager;
|
private final ArbitratorManager arbitratorManager;
|
||||||
private final TradeStatisticsManager tradeStatisticsManager;
|
private final TradeStatisticsManager tradeStatisticsManager;
|
||||||
private final DaoFacade daoFacade;
|
private final DaoFacade daoFacade;
|
||||||
|
private final BtcFeeReceiverService btcFeeReceiverService;
|
||||||
private final User user;
|
private final User user;
|
||||||
@Getter
|
@Getter
|
||||||
private final FilterManager filterManager;
|
private final FilterManager filterManager;
|
||||||
|
@ -71,6 +73,7 @@ public class PlaceOfferModel implements Model {
|
||||||
ArbitratorManager arbitratorManager,
|
ArbitratorManager arbitratorManager,
|
||||||
TradeStatisticsManager tradeStatisticsManager,
|
TradeStatisticsManager tradeStatisticsManager,
|
||||||
DaoFacade daoFacade,
|
DaoFacade daoFacade,
|
||||||
|
BtcFeeReceiverService btcFeeReceiverService,
|
||||||
User user,
|
User user,
|
||||||
FilterManager filterManager) {
|
FilterManager filterManager) {
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
|
@ -83,6 +86,7 @@ public class PlaceOfferModel implements Model {
|
||||||
this.arbitratorManager = arbitratorManager;
|
this.arbitratorManager = arbitratorManager;
|
||||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||||
this.daoFacade = daoFacade;
|
this.daoFacade = daoFacade;
|
||||||
|
this.btcFeeReceiverService = btcFeeReceiverService;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
this.filterManager = filterManager;
|
this.filterManager = filterManager;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,6 @@ import bisq.core.dao.exceptions.DaoDisabledException;
|
||||||
import bisq.core.dao.state.model.blockchain.TxType;
|
import bisq.core.dao.state.model.blockchain.TxType;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.offer.placeoffer.bisq_v1.PlaceOfferModel;
|
import bisq.core.offer.placeoffer.bisq_v1.PlaceOfferModel;
|
||||||
import bisq.core.util.FeeReceiverSelector;
|
|
||||||
|
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
import bisq.common.taskrunner.Task;
|
import bisq.common.taskrunner.Task;
|
||||||
|
@ -66,9 +65,8 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
|
||||||
|
|
||||||
TradeWalletService tradeWalletService = model.getTradeWalletService();
|
TradeWalletService tradeWalletService = model.getTradeWalletService();
|
||||||
|
|
||||||
String feeReceiver = FeeReceiverSelector.getAddress(model.getFilterManager());
|
|
||||||
|
|
||||||
if (offer.isCurrencyForMakerFeeBtc()) {
|
if (offer.isCurrencyForMakerFeeBtc()) {
|
||||||
|
String feeReceiver = model.getBtcFeeReceiverService().getAddress();
|
||||||
tradeWalletService.createBtcTradingFeeTx(
|
tradeWalletService.createBtcTradingFeeTx(
|
||||||
fundingAddress,
|
fundingAddress,
|
||||||
reservedForTradeAddress,
|
reservedForTradeAddress,
|
||||||
|
|
|
@ -19,6 +19,9 @@ package bisq.core.proto.network;
|
||||||
|
|
||||||
import bisq.core.alert.Alert;
|
import bisq.core.alert.Alert;
|
||||||
import bisq.core.alert.PrivateNotificationMessage;
|
import bisq.core.alert.PrivateNotificationMessage;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksRequest;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.GetAccountingBlocksResponse;
|
||||||
|
import bisq.core.dao.burningman.accounting.node.messages.NewAccountingBlockBroadcastMessage;
|
||||||
import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest;
|
import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest;
|
||||||
import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload;
|
import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload;
|
||||||
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
|
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
|
||||||
|
@ -254,6 +257,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
|
||||||
case GET_INVENTORY_RESPONSE:
|
case GET_INVENTORY_RESPONSE:
|
||||||
return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion);
|
return GetInventoryResponse.fromProto(proto.getGetInventoryResponse(), messageVersion);
|
||||||
|
|
||||||
|
case GET_ACCOUNTING_BLOCKS_REQUEST:
|
||||||
|
return GetAccountingBlocksRequest.fromProto(proto.getGetAccountingBlocksRequest(), messageVersion);
|
||||||
|
case GET_ACCOUNTING_BLOCKS_RESPONSE:
|
||||||
|
return GetAccountingBlocksResponse.fromProto(proto.getGetAccountingBlocksResponse(), messageVersion);
|
||||||
|
case NEW_ACCOUNTING_BLOCK_BROADCAST_MESSAGE:
|
||||||
|
return NewAccountingBlockBroadcastMessage.fromProto(proto.getNewAccountingBlockBroadcastMessage(), messageVersion);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
|
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
|
||||||
proto.getMessageCase() + "; proto raw data=" + proto.toString());
|
proto.getMessageCase() + "; proto raw data=" + proto.toString());
|
||||||
|
|
|
@ -21,6 +21,7 @@ import bisq.core.account.sign.SignedWitnessStore;
|
||||||
import bisq.core.account.witness.AccountAgeWitnessStore;
|
import bisq.core.account.witness.AccountAgeWitnessStore;
|
||||||
import bisq.core.btc.model.AddressEntryList;
|
import bisq.core.btc.model.AddressEntryList;
|
||||||
import bisq.core.btc.wallet.BtcWalletService;
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
|
import bisq.core.dao.burningman.accounting.storage.BurningManAccountingStore;
|
||||||
import bisq.core.dao.governance.blindvote.MyBlindVoteList;
|
import bisq.core.dao.governance.blindvote.MyBlindVoteList;
|
||||||
import bisq.core.dao.governance.blindvote.storage.BlindVoteStore;
|
import bisq.core.dao.governance.blindvote.storage.BlindVoteStore;
|
||||||
import bisq.core.dao.governance.bond.reputation.MyReputationList;
|
import bisq.core.dao.governance.bond.reputation.MyReputationList;
|
||||||
|
@ -141,6 +142,8 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
|
||||||
return RemovedPayloadsMap.fromProto(proto.getRemovedPayloadsMap());
|
return RemovedPayloadsMap.fromProto(proto.getRemovedPayloadsMap());
|
||||||
case BSQ_BLOCK_STORE:
|
case BSQ_BLOCK_STORE:
|
||||||
return BsqBlockStore.fromProto(proto.getBsqBlockStore());
|
return BsqBlockStore.fromProto(proto.getBsqBlockStore());
|
||||||
|
case BURNING_MAN_ACCOUNTING_STORE:
|
||||||
|
return BurningManAccountingStore.fromProto(proto.getBurningManAccountingStore());
|
||||||
default:
|
default:
|
||||||
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " +
|
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " +
|
||||||
"messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString());
|
"messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString());
|
||||||
|
|
|
@ -77,7 +77,6 @@ import javax.annotation.Nullable;
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
@Getter
|
@Getter
|
||||||
public final class Dispute implements NetworkPayload, PersistablePayload {
|
public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
NEEDS_UPGRADE,
|
NEEDS_UPGRADE,
|
||||||
NEW,
|
NEW,
|
||||||
|
@ -146,6 +145,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
// Added at v1.6.0
|
// Added at v1.6.0
|
||||||
private Dispute.State disputeState = State.NEW;
|
private Dispute.State disputeState = State.NEW;
|
||||||
|
|
||||||
|
// Added in v 1.9.7
|
||||||
|
@Setter
|
||||||
|
private int burningManSelectionHeight;
|
||||||
|
@Setter
|
||||||
|
private long tradeTxFee;
|
||||||
|
|
||||||
|
|
||||||
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
|
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
|
||||||
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
|
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
|
||||||
// field in a class would break that hash and therefore break the storage mechanism.
|
// field in a class would break that hash and therefore break the storage mechanism.
|
||||||
|
@ -263,7 +269,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
.setIsClosed(this.isClosed())
|
.setIsClosed(this.isClosed())
|
||||||
.setOpeningDate(openingDate)
|
.setOpeningDate(openingDate)
|
||||||
.setState(Dispute.State.toProtoMessage(disputeState))
|
.setState(Dispute.State.toProtoMessage(disputeState))
|
||||||
.setId(id);
|
.setId(id)
|
||||||
|
.setBurningManSelectionHeight(burningManSelectionHeight)
|
||||||
|
.setTradeTxFee(tradeTxFee);
|
||||||
|
|
||||||
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e)));
|
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e)));
|
||||||
Optional.ofNullable(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e)));
|
Optional.ofNullable(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e)));
|
||||||
|
@ -330,6 +338,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
|
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispute.setBurningManSelectionHeight(proto.getBurningManSelectionHeight());
|
||||||
|
dispute.setTradeTxFee(proto.getTradeTxFee());
|
||||||
|
|
||||||
if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
|
if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
|
||||||
// old disputes did not have a state field, so choose an appropriate state:
|
// old disputes did not have a state field, so choose an appropriate state:
|
||||||
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
|
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
|
||||||
|
@ -516,6 +527,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
return cachedDepositTx;
|
return cachedDepositTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dispute agents might receive disputes created before activation date.
|
||||||
|
// By checking if burningManSelectionHeight is > 0 we can detect if the trade was created with
|
||||||
|
// the new burningmen receivers or with legacy BM.
|
||||||
|
public boolean isUsingLegacyBurningMan() {
|
||||||
|
return burningManSelectionHeight == 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Dispute{" +
|
return "Dispute{" +
|
||||||
|
@ -550,6 +568,8 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||||
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
|
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
|
||||||
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
|
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
|
||||||
",\n cachedDepositTx='" + cachedDepositTx + '\'' +
|
",\n cachedDepositTx='" + cachedDepositTx + '\'' +
|
||||||
|
",\n burningManSelectionHeight='" + burningManSelectionHeight + '\'' +
|
||||||
|
",\n tradeTxFee='" + tradeTxFee + '\'' +
|
||||||
"\n}";
|
"\n}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,8 +273,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
List<Dispute> disputes = getDisputeList().getList();
|
List<Dispute> disputes = getDisputeList().getList();
|
||||||
disputes.forEach(dispute -> {
|
disputes.forEach(dispute -> {
|
||||||
try {
|
try {
|
||||||
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
|
||||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||||
|
if (dispute.isUsingLegacyBurningMan()) {
|
||||||
|
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||||
|
}
|
||||||
} catch (DisputeValidation.AddressException | DisputeValidation.NodeAddressException e) {
|
} catch (DisputeValidation.AddressException | DisputeValidation.NodeAddressException e) {
|
||||||
log.error(e.toString());
|
log.error(e.toString());
|
||||||
validationExceptions.add(e);
|
validationExceptions.add(e);
|
||||||
|
@ -371,8 +373,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
DisputeValidation.validateDisputeData(dispute, btcWalletService);
|
DisputeValidation.validateDisputeData(dispute, btcWalletService);
|
||||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||||
DisputeValidation.validateSenderNodeAddress(dispute, openNewDisputeMessage.getSenderNodeAddress());
|
DisputeValidation.validateSenderNodeAddress(dispute, openNewDisputeMessage.getSenderNodeAddress());
|
||||||
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
|
||||||
DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
|
DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
|
||||||
|
if (dispute.isUsingLegacyBurningMan()) {
|
||||||
|
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||||
|
}
|
||||||
} catch (DisputeValidation.ValidationException e) {
|
} catch (DisputeValidation.ValidationException e) {
|
||||||
log.error(e.toString());
|
log.error(e.toString());
|
||||||
validationExceptions.add(e);
|
validationExceptions.add(e);
|
||||||
|
@ -401,13 +405,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
DisputeValidation.validateDisputeData(dispute, btcWalletService);
|
DisputeValidation.validateDisputeData(dispute, btcWalletService);
|
||||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||||
DisputeValidation.validateTradeAndDispute(dispute, trade);
|
DisputeValidation.validateTradeAndDispute(dispute, trade);
|
||||||
DisputeValidation.validateDonationAddress(dispute,
|
|
||||||
Objects.requireNonNull(trade.getDelayedPayoutTx()),
|
|
||||||
btcWalletService.getParams(),
|
|
||||||
daoFacade);
|
|
||||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||||
trade.getDelayedPayoutTx(),
|
trade.getDelayedPayoutTx(),
|
||||||
btcWalletService);
|
btcWalletService);
|
||||||
|
if (dispute.isUsingLegacyBurningMan()) {
|
||||||
|
DisputeValidation.validateDonationAddress(dispute,
|
||||||
|
Objects.requireNonNull(trade.getDelayedPayoutTx()),
|
||||||
|
btcWalletService.getParams());
|
||||||
|
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||||
|
}
|
||||||
} catch (TradeDataValidation.ValidationException | DisputeValidation.ValidationException e) {
|
} catch (TradeDataValidation.ValidationException | DisputeValidation.ValidationException e) {
|
||||||
// The peer sent us an invalid donation address. We do not return here as we don't want to break
|
// The peer sent us an invalid donation address. We do not return here as we don't want to break
|
||||||
// mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get
|
// mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get
|
||||||
|
@ -618,6 +624,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap());
|
dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap());
|
||||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||||
|
dispute.setBurningManSelectionHeight(disputeFromOpener.getBurningManSelectionHeight());
|
||||||
|
dispute.setTradeTxFee(disputeFromOpener.getTradeTxFee());
|
||||||
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,7 @@ public class DisputeValidation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void validateSenderNodeAddress(Dispute dispute,
|
public static void validateSenderNodeAddress(Dispute dispute,
|
||||||
NodeAddress senderNodeAddress) throws NodeAddressException {
|
NodeAddress senderNodeAddress) throws NodeAddressException {
|
||||||
if (!senderNodeAddress.equals(dispute.getContract().getBuyerNodeAddress())
|
if (!senderNodeAddress.equals(dispute.getContract().getBuyerNodeAddress())
|
||||||
|
@ -156,8 +157,7 @@ public class DisputeValidation {
|
||||||
|
|
||||||
public static void validateDonationAddress(Dispute dispute,
|
public static void validateDonationAddress(Dispute dispute,
|
||||||
Transaction delayedPayoutTx,
|
Transaction delayedPayoutTx,
|
||||||
NetworkParameters params,
|
NetworkParameters params)
|
||||||
DaoFacade daoFacade)
|
|
||||||
throws AddressException {
|
throws AddressException {
|
||||||
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
||||||
Address address = output.getScriptPubKey().getToAddress(params);
|
Address address = output.getScriptPubKey().getToAddress(params);
|
||||||
|
@ -167,12 +167,13 @@ public class DisputeValidation {
|
||||||
log.error(delayedPayoutTx.toString());
|
log.error(delayedPayoutTx.toString());
|
||||||
throw new DisputeValidation.AddressException(dispute, errorMsg);
|
throw new DisputeValidation.AddressException(dispute, errorMsg);
|
||||||
}
|
}
|
||||||
String delayedPayoutTxOutputAddress = address.toString();
|
|
||||||
validateDonationAddressMatchesAnyPastParamValues(dispute, delayedPayoutTxOutputAddress, daoFacade);
|
|
||||||
|
|
||||||
// Verify that address in the dispute matches the one in the trade.
|
// Verify that address in the dispute matches the one in the trade.
|
||||||
|
String delayedPayoutTxOutputAddress = address.toString();
|
||||||
checkArgument(delayedPayoutTxOutputAddress.equals(dispute.getDonationAddressOfDelayedPayoutTx()),
|
checkArgument(delayedPayoutTxOutputAddress.equals(dispute.getDonationAddressOfDelayedPayoutTx()),
|
||||||
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx");
|
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx. " +
|
||||||
|
"delayedPayoutTxOutputAddress=" + delayedPayoutTxOutputAddress +
|
||||||
|
"; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void testIfAnyDisputeTriedReplay(List<Dispute> disputeList,
|
public static void testIfAnyDisputeTriedReplay(List<Dispute> disputeList,
|
||||||
|
@ -244,7 +245,6 @@ public class DisputeValidation {
|
||||||
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
|
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
|
||||||
Map<String, Set<String>> disputesPerDepositTxId)
|
Map<String, Set<String>> disputesPerDepositTxId)
|
||||||
throws DisputeReplayException {
|
throws DisputeReplayException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String disputeToTestTradeId = disputeToTest.getTradeId();
|
String disputeToTestTradeId = disputeToTest.getTradeId();
|
||||||
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
|
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
|
||||||
|
@ -312,6 +312,7 @@ public class DisputeValidation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static class AddressException extends ValidationException {
|
public static class AddressException extends ValidationException {
|
||||||
AddressException(Dispute dispute, String msg) {
|
AddressException(Dispute dispute, String msg) {
|
||||||
super(dispute, msg);
|
super(dispute, msg);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import bisq.core.btc.setup.WalletsSetup;
|
||||||
import bisq.core.btc.wallet.BtcWalletService;
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.dao.DaoFacade;
|
import bisq.core.dao.DaoFacade;
|
||||||
|
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
import bisq.core.offer.OpenOffer;
|
import bisq.core.offer.OpenOffer;
|
||||||
import bisq.core.offer.OpenOfferManager;
|
import bisq.core.offer.OpenOfferManager;
|
||||||
|
@ -50,11 +51,13 @@ import bisq.common.app.Version;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.crypto.KeyRing;
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.util.Hex;
|
import bisq.common.util.Hex;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.bitcoinj.core.TransactionInput;
|
import org.bitcoinj.core.TransactionInput;
|
||||||
import org.bitcoinj.core.TransactionOutPoint;
|
import org.bitcoinj.core.TransactionOutPoint;
|
||||||
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
@ -74,8 +77,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
|
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||||
private final MempoolService mempoolService;
|
private final MempoolService mempoolService;
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -89,6 +94,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
ClosedTradableManager closedTradableManager,
|
ClosedTradableManager closedTradableManager,
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
DaoFacade daoFacade,
|
DaoFacade daoFacade,
|
||||||
|
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
|
||||||
KeyRing keyRing,
|
KeyRing keyRing,
|
||||||
RefundDisputeListService refundDisputeListService,
|
RefundDisputeListService refundDisputeListService,
|
||||||
Config config,
|
Config config,
|
||||||
|
@ -96,6 +102,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
MempoolService mempoolService) {
|
MempoolService mempoolService) {
|
||||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||||
openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService);
|
openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService);
|
||||||
|
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||||
|
|
||||||
this.mempoolService = mempoolService;
|
this.mempoolService = mempoolService;
|
||||||
}
|
}
|
||||||
|
@ -308,4 +315,27 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
checkArgument(fundingTxId.equals(depositTx.getTxId().toString()),
|
checkArgument(fundingTxId.equals(depositTx.getTxId().toString()),
|
||||||
"First input at delayedPayoutTx does not connect to depositTx");
|
"First input at delayedPayoutTx does not connect to depositTx");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void verifyDelayedPayoutTxReceivers(Transaction delayedPayoutTx, Dispute dispute) {
|
||||||
|
Transaction depositTx = dispute.findDepositTx(btcWalletService).orElseThrow();
|
||||||
|
long inputAmount = depositTx.getOutput(0).getValue().value;
|
||||||
|
int selectionHeight = dispute.getBurningManSelectionHeight();
|
||||||
|
List<Tuple2<Long, String>> delayedPayoutTxReceivers = delayedPayoutTxReceiverService.getReceivers(
|
||||||
|
selectionHeight,
|
||||||
|
inputAmount,
|
||||||
|
dispute.getTradeTxFee());
|
||||||
|
log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers);
|
||||||
|
checkArgument(delayedPayoutTx.getOutputs().size() == delayedPayoutTxReceivers.size(),
|
||||||
|
"Size of outputs and delayedPayoutTxReceivers must be the same");
|
||||||
|
|
||||||
|
NetworkParameters params = btcWalletService.getParams();
|
||||||
|
for (int i = 0; i < delayedPayoutTx.getOutputs().size(); i++) {
|
||||||
|
TransactionOutput transactionOutput = delayedPayoutTx.getOutputs().get(i);
|
||||||
|
Tuple2<Long, String> receiverTuple = delayedPayoutTxReceivers.get(0);
|
||||||
|
checkArgument(transactionOutput.getScriptPubKey().getToAddress(params).toString().equals(receiverTuple.second),
|
||||||
|
"output address does not match delayedPayoutTxReceivers address. transactionOutput=" + transactionOutput);
|
||||||
|
checkArgument(transactionOutput.getValue().value == receiverTuple.first,
|
||||||
|
"output value does not match delayedPayoutTxReceivers value. transactionOutput=" + transactionOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@ import bisq.network.p2p.P2PService;
|
||||||
import bisq.network.p2p.network.TorNetworkNode;
|
import bisq.network.p2p.network.TorNetworkNode;
|
||||||
|
|
||||||
import bisq.common.ClockWatcher;
|
import bisq.common.ClockWatcher;
|
||||||
|
import bisq.common.app.DevEnv;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.crypto.KeyRing;
|
import bisq.common.crypto.KeyRing;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
|
@ -644,6 +645,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
user,
|
user,
|
||||||
mediatorManager,
|
mediatorManager,
|
||||||
tradeStatisticsManager,
|
tradeStatisticsManager,
|
||||||
|
provider.getDelayedPayoutTxReceiverService(),
|
||||||
isTakerApiUser);
|
isTakerApiUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -729,6 +731,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
clockWatcher.addListener(new ClockWatcher.Listener() {
|
clockWatcher.addListener(new ClockWatcher.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSecondTick() {
|
public void onSecondTick() {
|
||||||
|
if (DevEnv.isDevMode()) {
|
||||||
|
updateTradePeriodState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -743,6 +748,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
|
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
|
||||||
Date halfTradePeriodDate = trade.getHalfTradePeriodDate();
|
Date halfTradePeriodDate = trade.getHalfTradePeriodDate();
|
||||||
|
if (DevEnv.isDevMode()) {
|
||||||
|
TransactionConfidence confidenceForTxId = btcWalletService.getConfidenceForTxId(trade.getDepositTxId());
|
||||||
|
if (confidenceForTxId != null && confidenceForTxId.getDepthInBlocks() > 4) {
|
||||||
|
trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (maxTradePeriodDate != null && halfTradePeriodDate != null) {
|
if (maxTradePeriodDate != null && halfTradePeriodDate != null) {
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
if (now.after(maxTradePeriodDate)) {
|
if (now.after(maxTradePeriodDate)) {
|
||||||
|
|
|
@ -71,12 +71,6 @@ public class TradeDataValidation {
|
||||||
throw new InvalidTxException(errorMsg);
|
throw new InvalidTxException(errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delayedPayoutTx.getOutputs().size() != 1) {
|
|
||||||
errorMsg = "Number of delayedPayoutTx outputs must be 1";
|
|
||||||
log.error(errorMsg);
|
|
||||||
log.error(delayedPayoutTx.toString());
|
|
||||||
throw new InvalidTxException(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
// connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet
|
// connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet
|
||||||
// yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol.
|
// yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol.
|
||||||
|
@ -97,6 +91,14 @@ public class TradeDataValidation {
|
||||||
throw new InvalidLockTimeException(errorMsg);
|
throw new InvalidLockTimeException(errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (trade.isUsingLegacyBurningMan()) {
|
||||||
|
if (delayedPayoutTx.getOutputs().size() != 1) {
|
||||||
|
errorMsg = "Number of delayedPayoutTx outputs must be 1";
|
||||||
|
log.error(errorMsg);
|
||||||
|
log.error(delayedPayoutTx.toString());
|
||||||
|
throw new InvalidTxException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
// Check amount
|
// Check amount
|
||||||
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
||||||
Offer offer = checkNotNull(trade.getOffer());
|
Offer offer = checkNotNull(trade.getOffer());
|
||||||
|
@ -112,11 +114,12 @@ public class TradeDataValidation {
|
||||||
}
|
}
|
||||||
|
|
||||||
NetworkParameters params = btcWalletService.getParams();
|
NetworkParameters params = btcWalletService.getParams();
|
||||||
String delayedPayoutTxOutputAddress = output.getScriptPubKey().getToAddress(params).toString();
|
|
||||||
if (addressConsumer != null) {
|
if (addressConsumer != null) {
|
||||||
|
String delayedPayoutTxOutputAddress = output.getScriptPubKey().getToAddress(params).toString();
|
||||||
addressConsumer.accept(delayedPayoutTxOutputAddress);
|
addressConsumer.accept(delayedPayoutTxOutputAddress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void validatePayoutTxInput(Transaction depositTx,
|
public static void validatePayoutTxInput(Transaction depositTx,
|
||||||
Transaction delayedPayoutTx)
|
Transaction delayedPayoutTx)
|
||||||
|
|
|
@ -1067,6 +1067,12 @@ public abstract class Trade extends TradeModel {
|
||||||
return offer != null && offer.isBsqSwapOffer();
|
return offer != null && offer.isBsqSwapOffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// By checking if burningManSelectionHeight is 0 we can detect if the trade was created with
|
||||||
|
// the new burningmen receivers or with legacy BM.
|
||||||
|
public boolean isUsingLegacyBurningMan() {
|
||||||
|
return processModel.getBurningManSelectionHeight() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Private
|
// Private
|
||||||
|
|
|
@ -23,6 +23,8 @@ import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.btc.wallet.WalletsManager;
|
import bisq.core.btc.wallet.WalletsManager;
|
||||||
import bisq.core.dao.DaoFacade;
|
import bisq.core.dao.DaoFacade;
|
||||||
|
import bisq.core.dao.burningman.BtcFeeReceiverService;
|
||||||
|
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||||
import bisq.core.filter.FilterManager;
|
import bisq.core.filter.FilterManager;
|
||||||
import bisq.core.offer.OpenOfferManager;
|
import bisq.core.offer.OpenOfferManager;
|
||||||
import bisq.core.provider.fee.FeeService;
|
import bisq.core.provider.fee.FeeService;
|
||||||
|
@ -60,6 +62,8 @@ public class Provider {
|
||||||
private final RefundAgentManager refundAgentManager;
|
private final RefundAgentManager refundAgentManager;
|
||||||
private final KeyRing keyRing;
|
private final KeyRing keyRing;
|
||||||
private final FeeService feeService;
|
private final FeeService feeService;
|
||||||
|
private final BtcFeeReceiverService btcFeeReceiverService;
|
||||||
|
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public Provider(OpenOfferManager openOfferManager,
|
public Provider(OpenOfferManager openOfferManager,
|
||||||
|
@ -78,7 +82,9 @@ public class Provider {
|
||||||
MediatorManager mediatorManager,
|
MediatorManager mediatorManager,
|
||||||
RefundAgentManager refundAgentManager,
|
RefundAgentManager refundAgentManager,
|
||||||
KeyRing keyRing,
|
KeyRing keyRing,
|
||||||
FeeService feeService) {
|
FeeService feeService,
|
||||||
|
BtcFeeReceiverService btcFeeReceiverService,
|
||||||
|
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService) {
|
||||||
|
|
||||||
this.openOfferManager = openOfferManager;
|
this.openOfferManager = openOfferManager;
|
||||||
this.p2PService = p2PService;
|
this.p2PService = p2PService;
|
||||||
|
@ -97,5 +103,7 @@ public class Provider {
|
||||||
this.refundAgentManager = refundAgentManager;
|
this.refundAgentManager = refundAgentManager;
|
||||||
this.keyRing = keyRing;
|
this.keyRing = keyRing;
|
||||||
this.feeService = feeService;
|
this.feeService = feeService;
|
||||||
|
this.btcFeeReceiverService = btcFeeReceiverService;
|
||||||
|
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,9 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
||||||
@Nullable
|
@Nullable
|
||||||
private final String takersPaymentMethodId;
|
private final String takersPaymentMethodId;
|
||||||
|
|
||||||
|
// Added in v 1.9.7
|
||||||
|
private final int burningManSelectionHeight;
|
||||||
|
|
||||||
public InputsForDepositTxRequest(String tradeId,
|
public InputsForDepositTxRequest(String tradeId,
|
||||||
NodeAddress senderNodeAddress,
|
NodeAddress senderNodeAddress,
|
||||||
long tradeAmount,
|
long tradeAmount,
|
||||||
|
@ -107,7 +110,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
||||||
byte[] accountAgeWitnessSignatureOfOfferId,
|
byte[] accountAgeWitnessSignatureOfOfferId,
|
||||||
long currentDate,
|
long currentDate,
|
||||||
@Nullable byte[] hashOfTakersPaymentAccountPayload,
|
@Nullable byte[] hashOfTakersPaymentAccountPayload,
|
||||||
@Nullable String takersPaymentMethodId) {
|
@Nullable String takersPaymentMethodId,
|
||||||
|
int burningManSelectionHeight) {
|
||||||
super(messageVersion, tradeId, uid);
|
super(messageVersion, tradeId, uid);
|
||||||
this.senderNodeAddress = senderNodeAddress;
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
this.tradeAmount = tradeAmount;
|
this.tradeAmount = tradeAmount;
|
||||||
|
@ -134,6 +138,7 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
||||||
this.currentDate = currentDate;
|
this.currentDate = currentDate;
|
||||||
this.hashOfTakersPaymentAccountPayload = hashOfTakersPaymentAccountPayload;
|
this.hashOfTakersPaymentAccountPayload = hashOfTakersPaymentAccountPayload;
|
||||||
this.takersPaymentMethodId = takersPaymentMethodId;
|
this.takersPaymentMethodId = takersPaymentMethodId;
|
||||||
|
this.burningManSelectionHeight = burningManSelectionHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -169,7 +174,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
||||||
.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())
|
.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())
|
||||||
.setUid(uid)
|
.setUid(uid)
|
||||||
.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(accountAgeWitnessSignatureOfOfferId))
|
.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(accountAgeWitnessSignatureOfOfferId))
|
||||||
.setCurrentDate(currentDate);
|
.setCurrentDate(currentDate)
|
||||||
|
.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||||
|
|
||||||
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
|
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
|
||||||
Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()));
|
Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()));
|
||||||
|
@ -223,7 +229,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
||||||
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()),
|
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()),
|
||||||
proto.getCurrentDate(),
|
proto.getCurrentDate(),
|
||||||
hashOfTakersPaymentAccountPayload,
|
hashOfTakersPaymentAccountPayload,
|
||||||
ProtoUtil.stringOrNullFromProto(proto.getTakersPayoutMethodId()));
|
ProtoUtil.stringOrNullFromProto(proto.getTakersPayoutMethodId()),
|
||||||
|
proto.getBurningManSelectionHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -253,6 +260,7 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
||||||
",\n currentDate=" + currentDate +
|
",\n currentDate=" + currentDate +
|
||||||
",\n hashOfTakersPaymentAccountPayload=" + Utilities.bytesAsHexString(hashOfTakersPaymentAccountPayload) +
|
",\n hashOfTakersPaymentAccountPayload=" + Utilities.bytesAsHexString(hashOfTakersPaymentAccountPayload) +
|
||||||
",\n takersPaymentMethodId=" + takersPaymentMethodId +
|
",\n takersPaymentMethodId=" + takersPaymentMethodId +
|
||||||
|
",\n burningManSelectionHeight=" + burningManSelectionHeight +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import bisq.core.btc.wallet.BsqWalletService;
|
||||||
import bisq.core.btc.wallet.BtcWalletService;
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.dao.DaoFacade;
|
import bisq.core.dao.DaoFacade;
|
||||||
|
import bisq.core.dao.burningman.BtcFeeReceiverService;
|
||||||
|
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||||
import bisq.core.filter.FilterManager;
|
import bisq.core.filter.FilterManager;
|
||||||
import bisq.core.network.MessageState;
|
import bisq.core.network.MessageState;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
|
@ -176,6 +178,11 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
||||||
@Setter
|
@Setter
|
||||||
private ObjectProperty<MessageState> paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
private ObjectProperty<MessageState> paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
||||||
|
|
||||||
|
// Added in v 1.9.7
|
||||||
|
@Setter
|
||||||
|
@Getter
|
||||||
|
private int burningManSelectionHeight;
|
||||||
|
|
||||||
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
|
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
|
||||||
this(offerId, accountId, pubKeyRing, new TradingPeer());
|
this(offerId, accountId, pubKeyRing, new TradingPeer());
|
||||||
}
|
}
|
||||||
|
@ -217,7 +224,8 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
||||||
.setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong)
|
.setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong)
|
||||||
.setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name())
|
.setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name())
|
||||||
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
|
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
|
||||||
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation);
|
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
|
||||||
|
.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||||
|
|
||||||
Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId);
|
Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId);
|
||||||
Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature)));
|
Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature)));
|
||||||
|
@ -259,6 +267,7 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
||||||
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());
|
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());
|
||||||
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
||||||
processModel.setPaymentStartedMessageState(paymentStartedMessageState);
|
processModel.setPaymentStartedMessageState(paymentStartedMessageState);
|
||||||
|
processModel.setBurningManSelectionHeight(proto.getBurningManSelectionHeight());
|
||||||
|
|
||||||
if (proto.hasPaymentAccount()) {
|
if (proto.hasPaymentAccount()) {
|
||||||
processModel.setPaymentAccount(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver));
|
processModel.setPaymentAccount(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver));
|
||||||
|
@ -377,6 +386,14 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
||||||
return provider.getTradeWalletService();
|
return provider.getTradeWalletService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BtcFeeReceiverService getBtcFeeReceiverService() {
|
||||||
|
return provider.getBtcFeeReceiverService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DelayedPayoutTxReceiverService getDelayedPayoutTxReceiverService() {
|
||||||
|
return provider.getDelayedPayoutTxReceiverService();
|
||||||
|
}
|
||||||
|
|
||||||
public User getUser() {
|
public User getUser() {
|
||||||
return provider.getUser();
|
return provider.getUser();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,16 +17,22 @@
|
||||||
|
|
||||||
package bisq.core.trade.protocol.bisq_v1.tasks.buyer;
|
package bisq.core.trade.protocol.bisq_v1.tasks.buyer;
|
||||||
|
|
||||||
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
||||||
import bisq.core.trade.model.bisq_v1.Trade;
|
import bisq.core.trade.model.bisq_v1.Trade;
|
||||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||||
|
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -40,17 +46,37 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
|
BtcWalletService btcWalletService = processModel.getBtcWalletService();
|
||||||
checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
|
Transaction finalDelayedPayoutTx = trade.getDelayedPayoutTx();
|
||||||
|
checkNotNull(finalDelayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
|
||||||
|
|
||||||
// Check again tx
|
// Check again tx
|
||||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||||
delayedPayoutTx,
|
finalDelayedPayoutTx,
|
||||||
processModel.getBtcWalletService());
|
btcWalletService);
|
||||||
|
|
||||||
// Now as we know the deposit tx we can also verify the input
|
|
||||||
Transaction depositTx = trade.getDepositTx();
|
Transaction depositTx = trade.getDepositTx();
|
||||||
checkNotNull(depositTx, "trade.getDepositTx() must not be null");
|
checkNotNull(depositTx, "trade.getDepositTx() must not be null");
|
||||||
TradeDataValidation.validatePayoutTxInput(depositTx, delayedPayoutTx);
|
// Now as we know the deposit tx we can also verify the input
|
||||||
|
TradeDataValidation.validatePayoutTxInput(depositTx, finalDelayedPayoutTx);
|
||||||
|
|
||||||
|
if (BurningManService.isActivated()) {
|
||||||
|
long inputAmount = depositTx.getOutput(0).getValue().value;
|
||||||
|
long tradeTxFeeAsLong = trade.getTradeTxFeeAsLong();
|
||||||
|
int selectionHeight = processModel.getBurningManSelectionHeight();
|
||||||
|
List<Tuple2<Long, String>> delayedPayoutTxReceivers = processModel.getDelayedPayoutTxReceiverService().getReceivers(
|
||||||
|
selectionHeight,
|
||||||
|
inputAmount,
|
||||||
|
tradeTxFeeAsLong);
|
||||||
|
log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers);
|
||||||
|
long lockTime = trade.getLockTime();
|
||||||
|
Transaction buyersDelayedPayoutTx = processModel.getTradeWalletService().createDelayedUnsignedPayoutTx(
|
||||||
|
depositTx,
|
||||||
|
delayedPayoutTxReceivers,
|
||||||
|
lockTime);
|
||||||
|
checkArgument(buyersDelayedPayoutTx.getTxId().equals(finalDelayedPayoutTx.getTxId()),
|
||||||
|
"TxIds of buyersDelayedPayoutTx and finalDelayedPayoutTx must be the same");
|
||||||
|
}
|
||||||
|
|
||||||
complete();
|
complete();
|
||||||
} catch (TradeDataValidation.ValidationException e) {
|
} catch (TradeDataValidation.ValidationException e) {
|
||||||
|
|
|
@ -17,14 +17,22 @@
|
||||||
|
|
||||||
package bisq.core.trade.protocol.bisq_v1.tasks.buyer;
|
package bisq.core.trade.protocol.bisq_v1.tasks.buyer;
|
||||||
|
|
||||||
|
import bisq.core.btc.wallet.BtcWalletService;
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
||||||
import bisq.core.trade.model.bisq_v1.Trade;
|
import bisq.core.trade.model.bisq_v1.Trade;
|
||||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||||
|
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ -38,19 +46,36 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
var preparedDelayedPayoutTx = processModel.getPreparedDelayedPayoutTx();
|
Transaction sellersPreparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
|
||||||
|
BtcWalletService btcWalletService = processModel.getBtcWalletService();
|
||||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||||
preparedDelayedPayoutTx,
|
sellersPreparedDelayedPayoutTx,
|
||||||
processModel.getBtcWalletService());
|
btcWalletService);
|
||||||
|
|
||||||
|
Transaction preparedDepositTx = btcWalletService.getTxFromSerializedTx(processModel.getPreparedDepositTx());
|
||||||
|
if (BurningManService.isActivated()) {
|
||||||
|
long inputAmount = preparedDepositTx.getOutput(0).getValue().value;
|
||||||
|
long tradeTxFeeAsLong = trade.getTradeTxFeeAsLong();
|
||||||
|
List<Tuple2<Long, String>> delayedPayoutTxReceivers = processModel.getDelayedPayoutTxReceiverService().getReceivers(
|
||||||
|
processModel.getBurningManSelectionHeight(),
|
||||||
|
inputAmount,
|
||||||
|
tradeTxFeeAsLong);
|
||||||
|
|
||||||
|
long lockTime = trade.getLockTime();
|
||||||
|
Transaction buyersPreparedDelayedPayoutTx = processModel.getTradeWalletService().createDelayedUnsignedPayoutTx(
|
||||||
|
preparedDepositTx,
|
||||||
|
delayedPayoutTxReceivers,
|
||||||
|
lockTime);
|
||||||
|
checkArgument(buyersPreparedDelayedPayoutTx.getTxId().equals(sellersPreparedDelayedPayoutTx.getTxId()),
|
||||||
|
"TxIds of buyersPreparedDelayedPayoutTx and sellersPreparedDelayedPayoutTx must be the same");
|
||||||
|
}
|
||||||
|
|
||||||
// If the deposit tx is non-malleable, we already know its final ID, so should check that now
|
// If the deposit tx is non-malleable, we already know its final ID, so should check that now
|
||||||
// before sending any further data to the seller, to provide extra protection for the buyer.
|
// before sending any further data to the seller, to provide extra protection for the buyer.
|
||||||
if (isDepositTxNonMalleable()) {
|
if (isDepositTxNonMalleable()) {
|
||||||
var preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(
|
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, sellersPreparedDelayedPayoutTx);
|
||||||
processModel.getPreparedDepositTx());
|
|
||||||
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, checkNotNull(preparedDelayedPayoutTx));
|
|
||||||
} else {
|
} else {
|
||||||
log.info("Deposit tx is malleable, so we skip preparedDelayedPayoutTx input validation.");
|
log.info("Deposit tx is malleable, so we skip sellersPreparedDelayedPayoutTx input validation.");
|
||||||
}
|
}
|
||||||
|
|
||||||
complete();
|
complete();
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package bisq.core.trade.protocol.bisq_v1.tasks.maker;
|
package bisq.core.trade.protocol.bisq_v1.tasks.maker;
|
||||||
|
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
||||||
|
@ -79,6 +80,16 @@ public class MakerProcessesInputsForDepositTxRequest extends TradeTask {
|
||||||
|
|
||||||
tradingPeer.setAccountId(nonEmptyStringOf(request.getTakerAccountId()));
|
tradingPeer.setAccountId(nonEmptyStringOf(request.getTakerAccountId()));
|
||||||
|
|
||||||
|
if (BurningManService.isActivated()) {
|
||||||
|
int takersBurningManSelectionHeight = request.getBurningManSelectionHeight();
|
||||||
|
checkArgument(takersBurningManSelectionHeight > 0, "takersBurningManSelectionHeight must not be 0");
|
||||||
|
|
||||||
|
int makersBurningManSelectionHeight = processModel.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
|
||||||
|
checkArgument(takersBurningManSelectionHeight == makersBurningManSelectionHeight,
|
||||||
|
"takersBurningManSelectionHeight does no match makersBurningManSelectionHeight");
|
||||||
|
processModel.setBurningManSelectionHeight(makersBurningManSelectionHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// We set the taker fee only in the processModel yet not in the trade as the tx was only created but not
|
// We set the taker fee only in the processModel yet not in the trade as the tx was only created but not
|
||||||
// published yet. Once it was published we move it to trade. The takerFeeTx should be sent in a later
|
// published yet. Once it was published we move it to trade. The takerFeeTx should be sent in a later
|
||||||
// message but that cannot be changed due backward compatibility issues. It is a left over from the
|
// message but that cannot be changed due backward compatibility issues. It is a left over from the
|
||||||
|
|
|
@ -18,16 +18,20 @@
|
||||||
package bisq.core.trade.protocol.bisq_v1.tasks.seller;
|
package bisq.core.trade.protocol.bisq_v1.tasks.seller;
|
||||||
|
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
|
import bisq.core.dao.burningman.BurningManService;
|
||||||
import bisq.core.dao.governance.param.Param;
|
import bisq.core.dao.governance.param.Param;
|
||||||
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
||||||
import bisq.core.trade.model.bisq_v1.Trade;
|
import bisq.core.trade.model.bisq_v1.Trade;
|
||||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||||
|
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
import bisq.common.util.Tuple2;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
@ -44,16 +48,33 @@ public class SellerCreatesDelayedPayoutTx extends TradeTask {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS);
|
|
||||||
Coin minerFee = trade.getTradeTxFee();
|
|
||||||
TradeWalletService tradeWalletService = processModel.getTradeWalletService();
|
TradeWalletService tradeWalletService = processModel.getTradeWalletService();
|
||||||
Transaction depositTx = checkNotNull(processModel.getDepositTx());
|
Transaction depositTx = checkNotNull(processModel.getDepositTx());
|
||||||
|
Transaction preparedDelayedPayoutTx;
|
||||||
|
if (BurningManService.isActivated()) {
|
||||||
|
long inputAmount = depositTx.getOutput(0).getValue().value;
|
||||||
|
long tradeTxFeeAsLong = trade.getTradeTxFeeAsLong();
|
||||||
|
int selectionHeight = processModel.getBurningManSelectionHeight();
|
||||||
|
List<Tuple2<Long, String>> delayedPayoutTxReceivers = processModel.getDelayedPayoutTxReceiverService().getReceivers(
|
||||||
|
selectionHeight,
|
||||||
|
inputAmount,
|
||||||
|
tradeTxFeeAsLong);
|
||||||
|
log.info("Verify delayedPayoutTx using selectionHeight {} and receivers {}", selectionHeight, delayedPayoutTxReceivers);
|
||||||
long lockTime = trade.getLockTime();
|
long lockTime = trade.getLockTime();
|
||||||
Transaction preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(depositTx,
|
preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(
|
||||||
|
depositTx,
|
||||||
|
delayedPayoutTxReceivers,
|
||||||
|
lockTime);
|
||||||
|
} else {
|
||||||
|
String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS);
|
||||||
|
Coin minerFee = trade.getTradeTxFee();
|
||||||
|
long lockTime = trade.getLockTime();
|
||||||
|
preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(depositTx,
|
||||||
donationAddressString,
|
donationAddressString,
|
||||||
minerFee,
|
minerFee,
|
||||||
lockTime);
|
lockTime);
|
||||||
|
}
|
||||||
|
|
||||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||||
preparedDelayedPayoutTx,
|
preparedDelayedPayoutTx,
|
||||||
processModel.getBtcWalletService());
|
processModel.getBtcWalletService());
|
||||||
|
|
|
@ -24,7 +24,6 @@ import bisq.core.btc.wallet.WalletService;
|
||||||
import bisq.core.dao.exceptions.DaoDisabledException;
|
import bisq.core.dao.exceptions.DaoDisabledException;
|
||||||
import bisq.core.trade.model.bisq_v1.Trade;
|
import bisq.core.trade.model.bisq_v1.Trade;
|
||||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||||
import bisq.core.util.FeeReceiverSelector;
|
|
||||||
|
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
|
|
||||||
|
@ -66,7 +65,7 @@ public class CreateTakerFeeTx extends TradeTask {
|
||||||
Transaction transaction;
|
Transaction transaction;
|
||||||
|
|
||||||
if (trade.isCurrencyForTakerFeeBtc()) {
|
if (trade.isCurrencyForTakerFeeBtc()) {
|
||||||
String feeReceiver = FeeReceiverSelector.getAddress(processModel.getFilterManager());
|
String feeReceiver = processModel.getBtcFeeReceiverService().getAddress();
|
||||||
transaction = tradeWalletService.createBtcTradingFeeTx(
|
transaction = tradeWalletService.createBtcTradingFeeTx(
|
||||||
fundingAddress,
|
fundingAddress,
|
||||||
reservedForTradeAddress,
|
reservedForTradeAddress,
|
||||||
|
|
|
@ -104,6 +104,9 @@ public class TakerSendInputsForDepositTxRequest extends TradeTask {
|
||||||
byte[] signatureOfNonce = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(),
|
byte[] signatureOfNonce = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(),
|
||||||
offerId.getBytes(Charsets.UTF_8));
|
offerId.getBytes(Charsets.UTF_8));
|
||||||
|
|
||||||
|
int burningManSelectionHeight = processModel.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
|
||||||
|
processModel.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||||
|
|
||||||
String takersPaymentMethodId = checkNotNull(processModel.getPaymentAccountPayload(trade)).getPaymentMethodId();
|
String takersPaymentMethodId = checkNotNull(processModel.getPaymentAccountPayload(trade)).getPaymentMethodId();
|
||||||
InputsForDepositTxRequest request = new InputsForDepositTxRequest(
|
InputsForDepositTxRequest request = new InputsForDepositTxRequest(
|
||||||
offerId,
|
offerId,
|
||||||
|
@ -133,7 +136,8 @@ public class TakerSendInputsForDepositTxRequest extends TradeTask {
|
||||||
signatureOfNonce,
|
signatureOfNonce,
|
||||||
new Date().getTime(),
|
new Date().getTime(),
|
||||||
hashOfTakersPaymentAccountPayload,
|
hashOfTakersPaymentAccountPayload,
|
||||||
takersPaymentMethodId);
|
takersPaymentMethodId,
|
||||||
|
burningManSelectionHeight);
|
||||||
log.info("Send {} with offerId {} and uid {} to peer {}",
|
log.info("Send {} with offerId {} and uid {} to peer {}",
|
||||||
request.getClass().getSimpleName(), request.getTradeId(),
|
request.getClass().getSimpleName(), request.getTradeId(),
|
||||||
request.getUid(), trade.getTradingPeerNodeAddress());
|
request.getUid(), trade.getTradingPeerNodeAddress());
|
||||||
|
|
|
@ -42,11 +42,26 @@ public class AveragePriceUtil {
|
||||||
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
|
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
|
||||||
TradeStatisticsManager tradeStatisticsManager,
|
TradeStatisticsManager tradeStatisticsManager,
|
||||||
int days) {
|
int days) {
|
||||||
|
return getAveragePriceTuple(preferences, tradeStatisticsManager, days, new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
|
||||||
|
TradeStatisticsManager tradeStatisticsManager,
|
||||||
|
int days,
|
||||||
|
Date date) {
|
||||||
|
Date pastXDays = getPastDate(days, date);
|
||||||
|
return getAveragePriceTuple(preferences, tradeStatisticsManager, pastXDays, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
|
||||||
|
TradeStatisticsManager tradeStatisticsManager,
|
||||||
|
Date pastXDays,
|
||||||
|
Date date) {
|
||||||
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
|
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
|
||||||
Date pastXDays = getPastDate(days);
|
|
||||||
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||||
.filter(e -> e.getCurrency().equals("BSQ"))
|
.filter(e -> e.getCurrency().equals("BSQ"))
|
||||||
.filter(e -> e.getDate().after(pastXDays))
|
.filter(e -> e.getDate().after(pastXDays))
|
||||||
|
.filter(e -> e.getDate().before(date))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
|
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
|
||||||
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
|
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
|
||||||
|
@ -55,6 +70,7 @@ public class AveragePriceUtil {
|
||||||
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||||
.filter(e -> e.getCurrency().equals("USD"))
|
.filter(e -> e.getCurrency().equals("USD"))
|
||||||
.filter(e -> e.getDate().after(pastXDays))
|
.filter(e -> e.getDate().after(pastXDays))
|
||||||
|
.filter(e -> e.getDate().before(date))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
|
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
|
||||||
removeOutliers(usdAllTradePastXDays, percentToTrim) :
|
removeOutliers(usdAllTradePastXDays, percentToTrim) :
|
||||||
|
@ -103,7 +119,7 @@ public class AveragePriceUtil {
|
||||||
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
|
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
|
||||||
|
|
||||||
for (TradeStatistics3 item : bsqList) {
|
for (TradeStatistics3 item : bsqList) {
|
||||||
// Find usdprice for trade item
|
// Find usd price for trade item
|
||||||
usdBTCPrice = usdList.stream()
|
usdBTCPrice = usdList.stream()
|
||||||
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
|
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
|
||||||
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
|
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
|
||||||
|
@ -130,9 +146,9 @@ public class AveragePriceUtil {
|
||||||
return averagePrice;
|
return averagePrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Date getPastDate(int days) {
|
private static Date getPastDate(int days, Date date) {
|
||||||
Calendar cal = new GregorianCalendar();
|
Calendar cal = new GregorianCalendar();
|
||||||
cal.setTime(new Date());
|
cal.setTime(date);
|
||||||
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
|
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
|
||||||
return cal.getTime();
|
return cal.getTime();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,86 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Bisq.
|
|
||||||
*
|
|
||||||
* Bisq is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or (at
|
|
||||||
* your option) any later version.
|
|
||||||
*
|
|
||||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
||||||
* License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package bisq.core.util;
|
|
||||||
|
|
||||||
import bisq.core.filter.FilterManager;
|
|
||||||
|
|
||||||
import bisq.common.config.Config;
|
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class FeeReceiverSelector {
|
|
||||||
public static final String BTC_FEE_RECEIVER_ADDRESS = "38bZBj5peYS3Husdz7AH3gEUiUbYRD951t";
|
|
||||||
|
|
||||||
public static String getMostRecentAddress() {
|
|
||||||
return Config.baseCurrencyNetwork().isMainnet() ? BTC_FEE_RECEIVER_ADDRESS :
|
|
||||||
Config.baseCurrencyNetwork().isTestnet() ? "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV" :
|
|
||||||
"2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAddress(FilterManager filterManager) {
|
|
||||||
return getAddress(filterManager, new Random());
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static String getAddress(FilterManager filterManager, Random rnd) {
|
|
||||||
List<String> feeReceivers = Optional.ofNullable(filterManager.getFilter())
|
|
||||||
.flatMap(f -> Optional.ofNullable(f.getBtcFeeReceiverAddresses()))
|
|
||||||
.orElse(List.of());
|
|
||||||
|
|
||||||
List<Long> amountList = new ArrayList<>();
|
|
||||||
List<String> receiverAddressList = new ArrayList<>();
|
|
||||||
|
|
||||||
feeReceivers.forEach(e -> {
|
|
||||||
try {
|
|
||||||
String[] tokens = e.split("#");
|
|
||||||
amountList.add(Coin.parseCoin(tokens[1]).longValue()); // total amount the victim should receive
|
|
||||||
receiverAddressList.add(tokens[0]); // victim's receiver address
|
|
||||||
} catch (RuntimeException ignore) {
|
|
||||||
// If input format is not as expected we ignore entry
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!amountList.isEmpty()) {
|
|
||||||
return receiverAddressList.get(weightedSelection(amountList, rnd));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no fee address receiver is defined via filter we use the hard coded recent address
|
|
||||||
return getMostRecentAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static int weightedSelection(List<Long> weights, Random rnd) {
|
|
||||||
long sum = weights.stream().mapToLong(n -> n).sum();
|
|
||||||
long target = rnd.longs(0, sum).findFirst().orElseThrow();
|
|
||||||
int i;
|
|
||||||
for (i = 0; i < weights.size() && target >= 0; i++) {
|
|
||||||
target -= weights.get(i);
|
|
||||||
}
|
|
||||||
return i - 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -221,6 +221,10 @@ public class BsqFormatter implements CoinFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String formatCoin(long value) {
|
||||||
|
return formatCoin(Coin.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
public String formatCoin(Coin coin) {
|
public String formatCoin(Coin coin) {
|
||||||
return formatCoin(coin, false);
|
return formatCoin(coin, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import org.bitcoinj.core.Coin;
|
||||||
public interface CoinFormatter {
|
public interface CoinFormatter {
|
||||||
String formatCoin(Coin coin);
|
String formatCoin(Coin coin);
|
||||||
|
|
||||||
|
String formatCoin(long value);
|
||||||
|
|
||||||
String formatCoin(Coin coin, boolean appendCode);
|
String formatCoin(Coin coin, boolean appendCode);
|
||||||
|
|
||||||
String formatCoin(Coin coin, int decimalPlaces);
|
String formatCoin(Coin coin, int decimalPlaces);
|
||||||
|
|
|
@ -53,6 +53,11 @@ public class ImmutableCoinFormatter implements CoinFormatter {
|
||||||
return formatCoin(coin, -1);
|
return formatCoin(coin, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatCoin(long value) {
|
||||||
|
return formatCoin(Coin.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String formatCoin(Coin coin, boolean appendCode) {
|
public String formatCoin(Coin coin, boolean appendCode) {
|
||||||
return appendCode ? formatCoinWithCode(coin) : formatCoin(coin);
|
return appendCode ? formatCoinWithCode(coin) : formatCoin(coin);
|
||||||
|
|
|
@ -43,6 +43,9 @@ public final class BtcAddressValidator extends InputValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValidationResult validateBtcAddress(String input) {
|
private ValidationResult validateBtcAddress(String input) {
|
||||||
|
if (allowEmpty && (input == null || input.trim().isEmpty())) {
|
||||||
|
return new ValidationResult(true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Address.fromString(Config.baseCurrencyNetworkParameters(), input);
|
Address.fromString(Config.baseCurrencyNetworkParameters(), input);
|
||||||
return new ValidationResult(true);
|
return new ValidationResult(true);
|
||||||
|
|
|
@ -24,7 +24,11 @@ import java.math.BigInteger;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
public class InputValidator {
|
public class InputValidator {
|
||||||
|
@Setter
|
||||||
|
public boolean allowEmpty;
|
||||||
|
|
||||||
public ValidationResult validate(String input) {
|
public ValidationResult validate(String input) {
|
||||||
return validateIfNotEmpty(input);
|
return validateIfNotEmpty(input);
|
||||||
|
@ -32,10 +36,13 @@ public class InputValidator {
|
||||||
|
|
||||||
protected ValidationResult validateIfNotEmpty(String input) {
|
protected ValidationResult validateIfNotEmpty(String input) {
|
||||||
//trim added to avoid empty input
|
//trim added to avoid empty input
|
||||||
if (input == null || input.trim().length() == 0)
|
if (allowEmpty) {
|
||||||
return new ValidationResult(false, Res.get("validation.empty"));
|
|
||||||
else
|
|
||||||
return new ValidationResult(true);
|
return new ValidationResult(true);
|
||||||
|
} else if (input == null || input.trim().length() == 0) {
|
||||||
|
return new ValidationResult(false, Res.get("validation.empty"));
|
||||||
|
} else {
|
||||||
|
return new ValidationResult(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ValidationResult {
|
public static class ValidationResult {
|
||||||
|
|
|
@ -975,14 +975,15 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
|
||||||
Your trading peer receives: {1}\n\n\
|
Your trading peer receives: {1}\n\n\
|
||||||
You can accept or reject this suggested payout.\n\n\
|
You can accept or reject this suggested payout.\n\n\
|
||||||
By accepting, you sign the proposed payout transaction. \
|
By accepting, you sign the proposed payout transaction. \
|
||||||
|
Mediation is expected to be the optimal resolution for both traders. \
|
||||||
If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\
|
If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed.\n\n\
|
||||||
If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to open a \
|
If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to reject mediation suggestion, \
|
||||||
second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\
|
which will open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\
|
||||||
The arbitrator may charge a small fee (fee maximum: the trader''s security deposit) as compensation for their work. \
|
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer's security deposit. \
|
||||||
Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \
|
This means the total arbitration payout will be less than the mediation payout. \
|
||||||
exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \
|
Requesting arbitration is meant for exceptional circumstances. such as; \
|
||||||
(or if the other peer is unresponsive).\n\n\
|
one peer not responding, or disputing the mediator made a fair payout suggestion. \n\n\
|
||||||
More details about the new arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
|
More details about the arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
|
||||||
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \
|
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \
|
||||||
but it seems that your trading peer has not accepted it.\n\n\
|
but it seems that your trading peer has not accepted it.\n\n\
|
||||||
Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \
|
Once the lock time is over on {0} (block {1}), you can open a second-round dispute with an arbitrator who will \
|
||||||
|
@ -1967,7 +1968,7 @@ dao.tab.factsAndFigures=Facts & Figures
|
||||||
dao.tab.bsqWallet=BSQ wallet
|
dao.tab.bsqWallet=BSQ wallet
|
||||||
dao.tab.proposals=Governance
|
dao.tab.proposals=Governance
|
||||||
dao.tab.bonding=Bonding
|
dao.tab.bonding=Bonding
|
||||||
dao.tab.proofOfBurn=Asset listing fee/Proof of burn
|
dao.tab.proofOfBurn=Proof of burn
|
||||||
dao.tab.monitor=Network monitor
|
dao.tab.monitor=Network monitor
|
||||||
dao.tab.news=News
|
dao.tab.news=News
|
||||||
|
|
||||||
|
@ -2097,7 +2098,8 @@ dao.param.ASSET_LISTING_FEE_PER_DAY=Asset listing fee per day
|
||||||
dao.param.ASSET_MIN_VOLUME=Min. trade volume for assets
|
dao.param.ASSET_MIN_VOLUME=Min. trade volume for assets
|
||||||
|
|
||||||
# suppress inspection "UnusedProperty"
|
# suppress inspection "UnusedProperty"
|
||||||
dao.param.LOCK_TIME_TRADE_PAYOUT=Lock time for alternative trade payout tx
|
## Was never used and got re-purposed for expected BTC fee revenue per cycle
|
||||||
|
dao.param.LOCK_TIME_TRADE_PAYOUT=BTC fee revenue / cycle as BSQ Satoshi value
|
||||||
# suppress inspection "UnusedProperty"
|
# suppress inspection "UnusedProperty"
|
||||||
dao.param.ARBITRATOR_FEE=Arbitrator fee in BTC
|
dao.param.ARBITRATOR_FEE=Arbitrator fee in BTC
|
||||||
|
|
||||||
|
@ -2261,6 +2263,7 @@ dao.bond.bondedRoleType.BTC_DONATION_ADDRESS_OWNER=BTC donation address owner
|
||||||
dao.burnBsq.assetFee=Asset listing
|
dao.burnBsq.assetFee=Asset listing
|
||||||
dao.burnBsq.menuItem.assetFee=Asset listing fee
|
dao.burnBsq.menuItem.assetFee=Asset listing fee
|
||||||
dao.burnBsq.menuItem.proofOfBurn=Proof of burn
|
dao.burnBsq.menuItem.proofOfBurn=Proof of burn
|
||||||
|
dao.burnBsq.menuItem.burningMan=Burningmen
|
||||||
dao.burnBsq.header=Fee for asset listing
|
dao.burnBsq.header=Fee for asset listing
|
||||||
dao.burnBsq.selectAsset=Select Asset
|
dao.burnBsq.selectAsset=Select Asset
|
||||||
dao.burnBsq.fee=Fee
|
dao.burnBsq.fee=Fee
|
||||||
|
@ -2275,6 +2278,59 @@ dao.burnBsq.assets.trialFee=Fee for trial period
|
||||||
dao.burnBsq.assets.totalFee=Total fees paid
|
dao.burnBsq.assets.totalFee=Total fees paid
|
||||||
dao.burnBsq.assets.days={0} days
|
dao.burnBsq.assets.days={0} days
|
||||||
dao.burnBsq.assets.toFewDays=The asset fee is too low. The min. amount of days for the trial period is {0}.
|
dao.burnBsq.assets.toFewDays=The asset fee is too low. The min. amount of days for the trial period is {0}.
|
||||||
|
dao.burningman.target.header=Burn target
|
||||||
|
dao.burningman.burn.header=Burn BSQ
|
||||||
|
dao.burningman.amount.prompt=Amount to burn
|
||||||
|
dao.burningman.amount.prompt.max=Amount to burn: {0} - {1} BSQ
|
||||||
|
dao.burningman.candidates.table.header=Burningmen candidates
|
||||||
|
dao.burningman.burnOutput.table.header=Burn outputs
|
||||||
|
dao.burningman.balanceEntry.table.header=Revenue
|
||||||
|
dao.burningman.balanceEntry.table.showMonthlyToggle=By month
|
||||||
|
dao.burningman.balanceEntry.table.radio.all=All
|
||||||
|
dao.burningman.balanceEntry.table.radio.fee=BTC fees only
|
||||||
|
dao.burningman.balanceEntry.table.radio.dpt=DPT only
|
||||||
|
dao.burningman.balanceEntry.table.radio.burn=Burned BSQ
|
||||||
|
|
||||||
|
dao.burningman.compensations.table.header=Compensations
|
||||||
|
dao.burningman.reimbursement.table.header=Reimbursement requests
|
||||||
|
dao.burningman.expectedRevenue=Past 3-month average distribution
|
||||||
|
dao.burningman.burnTarget.label=Amount of BSQ to burn
|
||||||
|
dao.burningman.burnTarget.fromTo={0} - {1} BSQ
|
||||||
|
dao.burningman.filter=Filter burningman candidates
|
||||||
|
dao.burningman.toggle=Show only active burningmen
|
||||||
|
dao.burningman.contributorsComboBox.prompt=Select any of my contributors
|
||||||
|
dao.burningman.selectedContributor=Selected contributor
|
||||||
|
dao.burningman.selectedContributorName=Contributor name
|
||||||
|
dao.burningman.selectedContributorAddress=Receiver address
|
||||||
|
dao.burningman.shared.table.height=Block height
|
||||||
|
dao.burningman.shared.table.cycle=Cycle
|
||||||
|
dao.burningman.shared.table.date=Date
|
||||||
|
dao.burningman.table.name=Contributor name
|
||||||
|
dao.burningman.table.burnTarget=Burn target
|
||||||
|
dao.burningman.table.expectedRevenue=Expected to receive
|
||||||
|
dao.burningman.table.burnAmount=Burned amount
|
||||||
|
dao.burningman.table.decayedBurnAmount=Decayed burned amount
|
||||||
|
dao.burningman.table.burnAmountShare.label=Receiver share
|
||||||
|
dao.burningman.table.burnAmountShare.capped={0} (capped from {1})
|
||||||
|
dao.burningman.table.numBurnOutputs=Num. Burns
|
||||||
|
dao.burningman.table.issuanceAmount=Issued amount
|
||||||
|
dao.burningman.table.decayedIssuanceAmount=Decayed issued amount
|
||||||
|
dao.burningman.table.issuanceShare=Issuance share
|
||||||
|
dao.burningman.table.numIssuances=Num. issuances
|
||||||
|
dao.burningman.table.reimbursedAmount=Reimbursed amount
|
||||||
|
dao.burningman.table.balanceEntry.date=Date
|
||||||
|
dao.burningman.table.balanceEntry.receivedBtc=Received BTC
|
||||||
|
dao.burningman.table.balanceEntry.receivedBtcAsBsq=Received BTC as BSQ
|
||||||
|
dao.burningman.table.balanceEntry.burnedBsq=Burned BSQ
|
||||||
|
dao.burningman.table.balanceEntry.revenue=Revenue
|
||||||
|
dao.burningman.table.balanceEntry.price=BSQ/BTC price
|
||||||
|
dao.burningman.table.balanceEntry.type=Type
|
||||||
|
|
||||||
|
# From BalanceEntry.Type enum names
|
||||||
|
dao.burningman.balanceEntry.type.UNDEFINED=Undefined
|
||||||
|
dao.burningman.balanceEntry.type.BTC_TRADE_FEE_TX=Trade fee
|
||||||
|
dao.burningman.balanceEntry.type.DPT_TX=DPT
|
||||||
|
dao.burningman.balanceEntry.type.BURN_TX=Burned BSQ
|
||||||
|
|
||||||
# suppress inspection "UnusedProperty"
|
# suppress inspection "UnusedProperty"
|
||||||
dao.assetState.UNDEFINED=Undefined
|
dao.assetState.UNDEFINED=Undefined
|
||||||
|
@ -2400,6 +2456,7 @@ dao.proposal.display.name=Exact GitHub username
|
||||||
dao.proposal.display.link=Link to detailed info
|
dao.proposal.display.link=Link to detailed info
|
||||||
dao.proposal.display.link.prompt=Link to proposal
|
dao.proposal.display.link.prompt=Link to proposal
|
||||||
dao.proposal.display.requestedBsq=Requested amount in BSQ
|
dao.proposal.display.requestedBsq=Requested amount in BSQ
|
||||||
|
dao.proposal.display.burningManReceiverAddress=Burningman receiver address (optional)
|
||||||
dao.proposal.display.txId=Proposal transaction ID
|
dao.proposal.display.txId=Proposal transaction ID
|
||||||
dao.proposal.display.proposalFee=Proposal fee
|
dao.proposal.display.proposalFee=Proposal fee
|
||||||
dao.proposal.display.myVote=My vote
|
dao.proposal.display.myVote=My vote
|
||||||
|
@ -2459,6 +2516,10 @@ dao.wallet.send.receiverBtcAddress=Receiver's BTC address
|
||||||
dao.wallet.send.setDestinationAddress=Fill in your destination address
|
dao.wallet.send.setDestinationAddress=Fill in your destination address
|
||||||
dao.wallet.send.send=Send BSQ funds
|
dao.wallet.send.send=Send BSQ funds
|
||||||
dao.wallet.send.inputControl=Select inputs
|
dao.wallet.send.inputControl=Select inputs
|
||||||
|
dao.wallet.send.addOpReturn=Add data
|
||||||
|
dao.wallet.send.preImage=Pre-image
|
||||||
|
dao.wallet.send.opReturnAsHex=Op-Return data Hex encoded
|
||||||
|
dao.wallet.send.opReturnAsHash=Hash of Op-Return data
|
||||||
dao.wallet.send.sendBtc=Send BTC funds
|
dao.wallet.send.sendBtc=Send BTC funds
|
||||||
dao.wallet.send.sendFunds.headline=Confirm withdrawal request
|
dao.wallet.send.sendFunds.headline=Confirm withdrawal request
|
||||||
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
|
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
|
||||||
|
@ -2758,7 +2819,7 @@ disputeSummaryWindow.requestingTxs=Requesting blockchain transactions from block
|
||||||
disputeSummaryWindow.requestTransactionsError=Requesting the 4 trade transactions failed. Error message: {0}.\n\n\
|
disputeSummaryWindow.requestTransactionsError=Requesting the 4 trade transactions failed. Error message: {0}.\n\n\
|
||||||
Please verify the transactions manually before closing the dispute.
|
Please verify the transactions manually before closing the dispute.
|
||||||
disputeSummaryWindow.delayedPayoutTxVerificationFailed=Verification of the delayed payout transaction failed. Error message: {0}.\n\n\
|
disputeSummaryWindow.delayedPayoutTxVerificationFailed=Verification of the delayed payout transaction failed. Error message: {0}.\n\n\
|
||||||
Please do not make the payout but get in touch with developers to clearify the case.
|
Please do not make the payout but get in touch with developers to clarify the case.
|
||||||
|
|
||||||
# dynamic values are not recognized by IntelliJ
|
# dynamic values are not recognized by IntelliJ
|
||||||
# suppress inspection "UnusedProperty"
|
# suppress inspection "UnusedProperty"
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class BtcFeeReceiverServiceTest {
|
||||||
|
@Test
|
||||||
|
public void testGetRandomIndex() {
|
||||||
|
Random rnd = new Random(456);
|
||||||
|
assertEquals(4, BtcFeeReceiverService.getRandomIndex(Longs.asList(0, 0, 0, 3, 3), rnd));
|
||||||
|
assertEquals(3, BtcFeeReceiverService.getRandomIndex(Longs.asList(0, 0, 0, 6, 0, 0, 0, 0, 0), rnd));
|
||||||
|
|
||||||
|
assertEquals(-1, BtcFeeReceiverService.getRandomIndex(Longs.asList(), rnd));
|
||||||
|
assertEquals(-1, BtcFeeReceiverService.getRandomIndex(Longs.asList(0), rnd));
|
||||||
|
assertEquals(-1, BtcFeeReceiverService.getRandomIndex(Longs.asList(0, 0), rnd));
|
||||||
|
|
||||||
|
int[] selections = new int[3];
|
||||||
|
for (int i = 0; i < 6000; i++) {
|
||||||
|
selections[BtcFeeReceiverService.getRandomIndex(Longs.asList(1, 2, 3), rnd)]++;
|
||||||
|
}
|
||||||
|
// selections with new Random(456) are: [986, 1981, 3033]
|
||||||
|
assertEquals(1000.0, selections[0], 100);
|
||||||
|
assertEquals(2000.0, selections[1], 100);
|
||||||
|
assertEquals(3000.0, selections[2], 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFindIndex() {
|
||||||
|
List<Long> weights = Longs.asList(1, 2, 3);
|
||||||
|
assertEquals(0, BtcFeeReceiverService.findIndex(weights, 1));
|
||||||
|
assertEquals(1, BtcFeeReceiverService.findIndex(weights, 2));
|
||||||
|
assertEquals(1, BtcFeeReceiverService.findIndex(weights, 3));
|
||||||
|
assertEquals(2, BtcFeeReceiverService.findIndex(weights, 4));
|
||||||
|
assertEquals(2, BtcFeeReceiverService.findIndex(weights, 5));
|
||||||
|
assertEquals(2, BtcFeeReceiverService.findIndex(weights, 6));
|
||||||
|
|
||||||
|
// invalid values return index 0
|
||||||
|
assertEquals(0, BtcFeeReceiverService.findIndex(weights, 0));
|
||||||
|
assertEquals(0, BtcFeeReceiverService.findIndex(weights, 7));
|
||||||
|
|
||||||
|
assertEquals(0, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 2, 3), 0));
|
||||||
|
assertEquals(0, BtcFeeReceiverService.findIndex(Longs.asList(1, 2, 3), 0));
|
||||||
|
assertEquals(0, BtcFeeReceiverService.findIndex(Longs.asList(1, 2, 3), 1));
|
||||||
|
assertEquals(1, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 2, 3), 1));
|
||||||
|
assertEquals(2, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 2, 3), 2));
|
||||||
|
assertEquals(1, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 0, 2, 3), 1));
|
||||||
|
assertEquals(3, BtcFeeReceiverService.findIndex(Longs.asList(0, 1, 0, 2, 3), 2));
|
||||||
|
assertEquals(3, BtcFeeReceiverService.findIndex(Longs.asList(0, 0, 0, 1, 2, 3), 1));
|
||||||
|
assertEquals(4, BtcFeeReceiverService.findIndex(Longs.asList(0, 0, 0, 1, 2, 3), 2));
|
||||||
|
assertEquals(6, BtcFeeReceiverService.findIndex(Longs.asList(0, 0, 0, 1, 0, 0, 2, 3), 2));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class BurningManPresentationServiceTest {
|
||||||
|
@Test
|
||||||
|
public void testGetRandomIndex() {
|
||||||
|
long total = 100;
|
||||||
|
long myAmount = 40;
|
||||||
|
double myTargetShare = 0.75;
|
||||||
|
// Initial state:
|
||||||
|
// Mine: 40
|
||||||
|
// Others: 60
|
||||||
|
// Total: 100
|
||||||
|
// Current myShare: 0.4
|
||||||
|
|
||||||
|
// Target state:
|
||||||
|
// Mine: 40 + 140 = 180
|
||||||
|
// Others: 60
|
||||||
|
// Total: 240
|
||||||
|
// Target myShare: 0.75
|
||||||
|
|
||||||
|
assertEquals(140, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
|
||||||
|
|
||||||
|
total = 60;
|
||||||
|
myAmount = 0;
|
||||||
|
myTargetShare = 0.4;
|
||||||
|
// Initial state:
|
||||||
|
// Mine: 0
|
||||||
|
// Others: 60
|
||||||
|
// Total: 60
|
||||||
|
// Current myShare: 0
|
||||||
|
|
||||||
|
// Target state:
|
||||||
|
// Mine: 0 + 40 = 40
|
||||||
|
// Others: 60
|
||||||
|
// Total: 100
|
||||||
|
// Target myShare: 0.4
|
||||||
|
|
||||||
|
assertEquals(40, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
|
||||||
|
|
||||||
|
total = 40;
|
||||||
|
myAmount = 40;
|
||||||
|
myTargetShare = 1;
|
||||||
|
// Initial state:
|
||||||
|
// Mine: 40
|
||||||
|
// Others: 0
|
||||||
|
// Total: 40
|
||||||
|
// Current myShare: 1
|
||||||
|
|
||||||
|
// Target state:
|
||||||
|
// Mine: 40 -40 = 0
|
||||||
|
// Others: 0
|
||||||
|
// Total: 0
|
||||||
|
// Target myShare: 1
|
||||||
|
|
||||||
|
assertEquals(-40, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
|
||||||
|
|
||||||
|
total = 100;
|
||||||
|
myAmount = 99;
|
||||||
|
myTargetShare = 0;
|
||||||
|
// Initial state:
|
||||||
|
// Mine: 99
|
||||||
|
// Others: 1
|
||||||
|
// Total: 100
|
||||||
|
// Current myShare: 0.99
|
||||||
|
|
||||||
|
// Target state:
|
||||||
|
// Mine: 99 - 99 = 0
|
||||||
|
// Others: 1
|
||||||
|
// Total: 100
|
||||||
|
// Target myShare: 0
|
||||||
|
|
||||||
|
assertEquals(-99, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
|
||||||
|
|
||||||
|
total = 100;
|
||||||
|
myAmount = 1;
|
||||||
|
myTargetShare = 0.5;
|
||||||
|
// Initial state:
|
||||||
|
// Mine: 1
|
||||||
|
// Others: 99
|
||||||
|
// Total: 100
|
||||||
|
// Current myShare: 0.01
|
||||||
|
|
||||||
|
// Target state:
|
||||||
|
// Mine: 1 + 98 = 99
|
||||||
|
// Others: 99
|
||||||
|
// Total: 198
|
||||||
|
// Target myShare: 0.5
|
||||||
|
|
||||||
|
assertEquals(98, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
|
||||||
|
|
||||||
|
|
||||||
|
total = 110;
|
||||||
|
myAmount = 0;
|
||||||
|
myTargetShare = 0.6;
|
||||||
|
// Initial state:
|
||||||
|
// Mine: 0
|
||||||
|
// Others: 110
|
||||||
|
// Total: 110
|
||||||
|
// Current myShare: 0
|
||||||
|
|
||||||
|
// Target state:
|
||||||
|
// Mine: 165
|
||||||
|
// Others: 110
|
||||||
|
// Total: 275
|
||||||
|
// Target myShare: 0.6
|
||||||
|
|
||||||
|
assertEquals(165, BurningManPresentationService.getMissingAmountToReachTargetShare(total, myAmount, myTargetShare));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class BurningManServiceTest {
|
||||||
|
@Test
|
||||||
|
public void testGetDecayedAmount() {
|
||||||
|
long amount = 100;
|
||||||
|
int currentBlockHeight = 1400;
|
||||||
|
int fromBlockHeight = 1000;
|
||||||
|
assertEquals(0, BurningManService.getDecayedAmount(amount, 1000, currentBlockHeight, fromBlockHeight));
|
||||||
|
assertEquals(25, BurningManService.getDecayedAmount(amount, 1100, currentBlockHeight, fromBlockHeight));
|
||||||
|
assertEquals(50, BurningManService.getDecayedAmount(amount, 1200, currentBlockHeight, fromBlockHeight));
|
||||||
|
assertEquals(75, BurningManService.getDecayedAmount(amount, 1300, currentBlockHeight, fromBlockHeight));
|
||||||
|
|
||||||
|
// cycles with 100 blocks, issuance at block 20, look-back period 3 cycles
|
||||||
|
assertEquals(40, BurningManService.getDecayedAmount(amount, 120, 300, 0));
|
||||||
|
assertEquals(33, BurningManService.getDecayedAmount(amount, 120, 320, 20));
|
||||||
|
assertEquals(27, BurningManService.getDecayedAmount(amount, 120, 340, 40));
|
||||||
|
assertEquals(20, BurningManService.getDecayedAmount(amount, 120, 360, 60));
|
||||||
|
assertEquals(13, BurningManService.getDecayedAmount(amount, 120, 380, 80));
|
||||||
|
assertEquals(7, BurningManService.getDecayedAmount(amount, 120, 399, 99));
|
||||||
|
assertEquals(7, BurningManService.getDecayedAmount(amount, 120, 400, 100));
|
||||||
|
assertEquals(3, BurningManService.getDecayedAmount(amount, 120, 410, 110));
|
||||||
|
assertEquals(40, BurningManService.getDecayedAmount(amount, 220, 400, 100));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.dao.burningman;
|
||||||
|
|
||||||
|
|
||||||
|
import org.mockito.junit.MockitoJUnitRunner;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
@RunWith(MockitoJUnitRunner.class)
|
||||||
|
public class DelayedPayoutTxReceiverServiceTest {
|
||||||
|
@Test
|
||||||
|
public void testGetSnapshotHeight() {
|
||||||
|
// up to genesis + 3* grid we use genesis
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 0, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 100, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 102, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 119, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 120, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 121, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 130, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 131, 10));
|
||||||
|
assertEquals(102, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 132, 10));
|
||||||
|
|
||||||
|
assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 133, 10));
|
||||||
|
assertEquals(120, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 134, 10));
|
||||||
|
|
||||||
|
assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 135, 10));
|
||||||
|
assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 136, 10));
|
||||||
|
assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 139, 10));
|
||||||
|
assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 140, 10));
|
||||||
|
assertEquals(130, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 141, 10));
|
||||||
|
|
||||||
|
assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 149, 10));
|
||||||
|
assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 150, 10));
|
||||||
|
assertEquals(140, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 151, 10));
|
||||||
|
|
||||||
|
assertEquals(150, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 159, 10));
|
||||||
|
|
||||||
|
assertEquals(990, DelayedPayoutTxReceiverService.getSnapshotHeight(102, 1000, 10));
|
||||||
|
}
|
||||||
|
}
|
|
@ -67,6 +67,8 @@ public class OpenOfferManagerTest {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
persistenceManager
|
persistenceManager
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,6 +116,8 @@ public class OpenOfferManagerTest {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
persistenceManager
|
persistenceManager
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -153,6 +157,8 @@ public class OpenOfferManagerTest {
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
persistenceManager
|
persistenceManager
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import bisq.core.dao.state.DaoStateService;
|
||||||
import bisq.core.filter.Filter;
|
import bisq.core.filter.Filter;
|
||||||
import bisq.core.filter.FilterManager;
|
import bisq.core.filter.FilterManager;
|
||||||
import bisq.core.trade.DelayedPayoutAddressProvider;
|
import bisq.core.trade.DelayedPayoutAddressProvider;
|
||||||
import bisq.core.util.FeeReceiverSelector;
|
|
||||||
import bisq.core.util.ParsingUtils;
|
import bisq.core.util.ParsingUtils;
|
||||||
import bisq.core.util.coin.BsqFormatter;
|
import bisq.core.util.coin.BsqFormatter;
|
||||||
|
|
||||||
|
@ -65,7 +64,7 @@ public class TxValidatorTest {
|
||||||
btcFeeReceivers.add("13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR");
|
btcFeeReceivers.add("13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR");
|
||||||
btcFeeReceivers.add("19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c");
|
btcFeeReceivers.add("19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c");
|
||||||
btcFeeReceivers.add("19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq");
|
btcFeeReceivers.add("19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq");
|
||||||
btcFeeReceivers.add(FeeReceiverSelector.BTC_FEE_RECEIVER_ADDRESS);
|
btcFeeReceivers.add("38bZBj5peYS3Husdz7AH3gEUiUbYRD951t");
|
||||||
btcFeeReceivers.add(DelayedPayoutAddressProvider.BM2019_ADDRESS);
|
btcFeeReceivers.add(DelayedPayoutAddressProvider.BM2019_ADDRESS);
|
||||||
btcFeeReceivers.add("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7");
|
btcFeeReceivers.add("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7");
|
||||||
btcFeeReceivers.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV");
|
btcFeeReceivers.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV");
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
/*
|
|
||||||
* This file is part of Bisq.
|
|
||||||
*
|
|
||||||
* Bisq is free software: you can redistribute it and/or modify it
|
|
||||||
* under the terms of the GNU Affero General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or (at
|
|
||||||
* your option) any later version.
|
|
||||||
*
|
|
||||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
|
||||||
* License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package bisq.core.util;
|
|
||||||
|
|
||||||
import bisq.core.filter.Filter;
|
|
||||||
import bisq.core.filter.FilterManager;
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.common.primitives.Longs;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.MockitoJUnitRunner;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner.class)
|
|
||||||
public class FeeReceiverSelectorTest {
|
|
||||||
@Mock
|
|
||||||
private FilterManager filterManager;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAddress() {
|
|
||||||
Random rnd = new Random(123);
|
|
||||||
when(filterManager.getFilter()).thenReturn(filterWithReceivers(
|
|
||||||
List.of("", "foo#0.001", "ill-formed", "bar#0.002", "baz#0.001", "partial#bad")));
|
|
||||||
|
|
||||||
Map<String, Integer> selectionCounts = new HashMap<>();
|
|
||||||
for (int i = 0; i < 400; i++) {
|
|
||||||
String address = FeeReceiverSelector.getAddress(filterManager, rnd);
|
|
||||||
selectionCounts.compute(address, (k, n) -> n != null ? n + 1 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(3, selectionCounts.size());
|
|
||||||
|
|
||||||
// Check within 2 std. of the expected values (95% confidence each):
|
|
||||||
assertEquals(100.0, selectionCounts.get("foo"), 18);
|
|
||||||
assertEquals(200.0, selectionCounts.get("bar"), 20);
|
|
||||||
assertEquals(100.0, selectionCounts.get("baz"), 18);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAddress_noValidReceivers_nullFilter() {
|
|
||||||
when(filterManager.getFilter()).thenReturn(null);
|
|
||||||
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAddress_noValidReceivers_filterWithNullList() {
|
|
||||||
when(filterManager.getFilter()).thenReturn(filterWithReceivers(null));
|
|
||||||
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAddress_noValidReceivers_filterWithEmptyList() {
|
|
||||||
when(filterManager.getFilter()).thenReturn(filterWithReceivers(List.of()));
|
|
||||||
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetAddress_noValidReceivers_filterWithIllFormedList() {
|
|
||||||
when(filterManager.getFilter()).thenReturn(filterWithReceivers(List.of("ill-formed")));
|
|
||||||
assertEquals(FeeReceiverSelector.getMostRecentAddress(), FeeReceiverSelector.getAddress(filterManager));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testWeightedSelection() {
|
|
||||||
Random rnd = new Random(456);
|
|
||||||
|
|
||||||
int[] selections = new int[3];
|
|
||||||
for (int i = 0; i < 6000; i++) {
|
|
||||||
selections[FeeReceiverSelector.weightedSelection(Longs.asList(1, 2, 3), rnd)]++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check within 2 std. of the expected values (95% confidence each):
|
|
||||||
assertEquals(1000.0, selections[0], 58);
|
|
||||||
assertEquals(2000.0, selections[1], 74);
|
|
||||||
assertEquals(3000.0, selections[2], 78);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Filter filterWithReceivers(List<String> btcFeeReceiverAddresses) {
|
|
||||||
return new Filter(Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
false,
|
|
||||||
Lists.newArrayList(),
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
Lists.newArrayList(),
|
|
||||||
btcFeeReceiverAddresses,
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
Lists.newArrayList(),
|
|
||||||
new HashSet<>(),
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
Lists.newArrayList(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -93,7 +93,8 @@ public class BondsView extends ActivatableView<GridPane, Void> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.bond.allBonds.header"), "last");
|
tableView = FormBuilder.<BondListItem>addTableViewWithHeader(root, ++gridRow,
|
||||||
|
Res.get("dao.bond.allBonds.header"), "last").first;
|
||||||
tableView.setItems(sortedList);
|
tableView.setItems(sortedList);
|
||||||
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
||||||
addColumns();
|
addColumns();
|
||||||
|
|
|
@ -141,7 +141,8 @@ public class MyReputationView extends ActivatableView<GridPane, Void> implements
|
||||||
|
|
||||||
lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bond.reputation.lockupButton"));
|
lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bond.reputation.lockupButton"));
|
||||||
|
|
||||||
tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.bond.reputation.table.header"), 20, "last");
|
tableView = FormBuilder.<MyReputationListItem>addTableViewWithHeader(root, ++gridRow,
|
||||||
|
Res.get("dao.bond.reputation.table.header"), 20, "last").first;
|
||||||
createColumns();
|
createColumns();
|
||||||
tableView.setItems(sortedList);
|
tableView.setItems(sortedList);
|
||||||
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
||||||
|
|
|
@ -23,9 +23,9 @@ import bisq.desktop.components.AutoTooltipButton;
|
||||||
import bisq.desktop.components.AutoTooltipTableColumn;
|
import bisq.desktop.components.AutoTooltipTableColumn;
|
||||||
import bisq.desktop.components.ExternalHyperlink;
|
import bisq.desktop.components.ExternalHyperlink;
|
||||||
import bisq.desktop.components.HyperlinkWithIcon;
|
import bisq.desktop.components.HyperlinkWithIcon;
|
||||||
import bisq.desktop.main.dao.bonding.BondingViewUtils;
|
|
||||||
import bisq.desktop.main.dao.MessageSignatureWindow;
|
import bisq.desktop.main.dao.MessageSignatureWindow;
|
||||||
import bisq.desktop.main.dao.MessageVerificationWindow;
|
import bisq.desktop.main.dao.MessageVerificationWindow;
|
||||||
|
import bisq.desktop.main.dao.bonding.BondingViewUtils;
|
||||||
import bisq.desktop.util.FormBuilder;
|
import bisq.desktop.util.FormBuilder;
|
||||||
import bisq.desktop.util.GUIUtil;
|
import bisq.desktop.util.GUIUtil;
|
||||||
|
|
||||||
|
@ -92,7 +92,8 @@ public class RolesView extends ActivatableView<GridPane, Void> {
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
int gridRow = 0;
|
int gridRow = 0;
|
||||||
tableView = FormBuilder.addTableViewWithHeader(root, gridRow, Res.get("dao.bond.bondedRoles"), "last");
|
tableView = FormBuilder.<RolesListItem>addTableViewWithHeader(root, gridRow,
|
||||||
|
Res.get("dao.bond.bondedRoles"), "last").first;
|
||||||
createColumns();
|
createColumns();
|
||||||
tableView.setItems(sortedList);
|
tableView.setItems(sortedList);
|
||||||
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
||||||
|
@ -294,6 +295,7 @@ public class RolesView extends ActivatableView<GridPane, Void> {
|
||||||
public TableCell<RolesListItem, RolesListItem> call(TableColumn<RolesListItem, RolesListItem> column) {
|
public TableCell<RolesListItem, RolesListItem> call(TableColumn<RolesListItem, RolesListItem> column) {
|
||||||
return new TableCell<>() {
|
return new TableCell<>() {
|
||||||
HBox hbox;
|
HBox hbox;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(final RolesListItem item, boolean empty) {
|
public void updateItem(final RolesListItem item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
@ -315,13 +317,15 @@ public class RolesView extends ActivatableView<GridPane, Void> {
|
||||||
if (item.isLockupButtonVisible()) {
|
if (item.isLockupButtonVisible()) {
|
||||||
AutoTooltipButton buttonLockup = new AutoTooltipButton(Res.get("dao.bond.table.button.lockup"));
|
AutoTooltipButton buttonLockup = new AutoTooltipButton(Res.get("dao.bond.table.button.lockup"));
|
||||||
buttonLockup.setMinWidth(70);
|
buttonLockup.setMinWidth(70);
|
||||||
buttonLockup.setOnAction(e -> bondingViewUtils.lockupBondForBondedRole(item.getRole(), txId -> {}));
|
buttonLockup.setOnAction(e -> bondingViewUtils.lockupBondForBondedRole(item.getRole(), txId -> {
|
||||||
|
}));
|
||||||
hbox.getChildren().add(buttonLockup);
|
hbox.getChildren().add(buttonLockup);
|
||||||
}
|
}
|
||||||
if (item.isRevokeButtonVisible()) {
|
if (item.isRevokeButtonVisible()) {
|
||||||
AutoTooltipButton buttonRevoke = new AutoTooltipButton(Res.get("dao.bond.table.button.revoke"));
|
AutoTooltipButton buttonRevoke = new AutoTooltipButton(Res.get("dao.bond.table.button.revoke"));
|
||||||
buttonRevoke.setMinWidth(70);
|
buttonRevoke.setMinWidth(70);
|
||||||
buttonRevoke.setOnAction(e -> bondingViewUtils.unLock(item.getLockupTxId(), txId -> {}));
|
buttonRevoke.setOnAction(e -> bondingViewUtils.unLock(item.getLockupTxId(), txId -> {
|
||||||
|
}));
|
||||||
hbox.getChildren().add(buttonRevoke);
|
hbox.getChildren().add(buttonRevoke);
|
||||||
}
|
}
|
||||||
hbox.setMinWidth(hbox.getChildren().size() * 70);
|
hbox.setMinWidth(hbox.getChildren().size() * 70);
|
||||||
|
|
|
@ -28,6 +28,7 @@ import bisq.desktop.components.MenuItem;
|
||||||
import bisq.desktop.main.MainView;
|
import bisq.desktop.main.MainView;
|
||||||
import bisq.desktop.main.dao.DaoView;
|
import bisq.desktop.main.dao.DaoView;
|
||||||
import bisq.desktop.main.dao.burnbsq.assetfee.AssetFeeView;
|
import bisq.desktop.main.dao.burnbsq.assetfee.AssetFeeView;
|
||||||
|
import bisq.desktop.main.dao.burnbsq.burningman.BurningManView;
|
||||||
import bisq.desktop.main.dao.burnbsq.proofofburn.ProofOfBurnView;
|
import bisq.desktop.main.dao.burnbsq.proofofburn.ProofOfBurnView;
|
||||||
|
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -49,7 +50,7 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
|
||||||
private final ViewLoader viewLoader;
|
private final ViewLoader viewLoader;
|
||||||
private final Navigation navigation;
|
private final Navigation navigation;
|
||||||
|
|
||||||
private MenuItem assetFee, proofOfBurn;
|
private MenuItem proofOfBurn, burningMan, assetFee;
|
||||||
private Navigation.Listener listener;
|
private Navigation.Listener listener;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
@ -77,26 +78,28 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleGroup = new ToggleGroup();
|
toggleGroup = new ToggleGroup();
|
||||||
final List<Class<? extends View>> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class);
|
List<Class<? extends View>> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class);
|
||||||
assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"),
|
|
||||||
AssetFeeView.class, baseNavPath);
|
|
||||||
proofOfBurn = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.proofOfBurn"),
|
proofOfBurn = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.proofOfBurn"),
|
||||||
ProofOfBurnView.class, baseNavPath);
|
ProofOfBurnView.class, baseNavPath);
|
||||||
|
burningMan = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.burningMan"),
|
||||||
leftVBox.getChildren().addAll(assetFee, proofOfBurn);
|
BurningManView.class, baseNavPath);
|
||||||
|
assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"),
|
||||||
|
AssetFeeView.class, baseNavPath);
|
||||||
|
leftVBox.getChildren().addAll(burningMan, proofOfBurn, assetFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void activate() {
|
protected void activate() {
|
||||||
assetFee.activate();
|
|
||||||
proofOfBurn.activate();
|
proofOfBurn.activate();
|
||||||
|
burningMan.activate();
|
||||||
|
assetFee.activate();
|
||||||
|
|
||||||
navigation.addListener(listener);
|
navigation.addListener(listener);
|
||||||
ViewPath viewPath = navigation.getCurrentPath();
|
ViewPath viewPath = navigation.getCurrentPath();
|
||||||
if (viewPath.size() == 3 && viewPath.indexOf(BurnBsqView.class) == 2 ||
|
if (viewPath.size() == 3 && viewPath.indexOf(BurnBsqView.class) == 2 ||
|
||||||
viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) {
|
viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) {
|
||||||
if (selectedViewClass == null)
|
if (selectedViewClass == null)
|
||||||
selectedViewClass = AssetFeeView.class;
|
selectedViewClass = BurningManView.class;
|
||||||
|
|
||||||
loadView(selectedViewClass);
|
loadView(selectedViewClass);
|
||||||
|
|
||||||
|
@ -111,15 +114,17 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
|
||||||
protected void deactivate() {
|
protected void deactivate() {
|
||||||
navigation.removeListener(listener);
|
navigation.removeListener(listener);
|
||||||
|
|
||||||
assetFee.deactivate();
|
|
||||||
proofOfBurn.deactivate();
|
proofOfBurn.deactivate();
|
||||||
|
burningMan.deactivate();
|
||||||
|
assetFee.deactivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadView(Class<? extends View> viewClass) {
|
private void loadView(Class<? extends View> viewClass) {
|
||||||
View view = viewLoader.load(viewClass);
|
View view = viewLoader.load(viewClass);
|
||||||
content.getChildren().setAll(view.getRoot());
|
content.getChildren().setAll(view.getRoot());
|
||||||
|
|
||||||
if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee);
|
if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn);
|
||||||
else if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn);
|
else if (view instanceof BurningManView) toggleGroup.selectToggle(burningMan);
|
||||||
|
else if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue