mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-03 10:46:54 +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 DAO_NODE_API_URL = "daoNodeApiUrl";
|
||||
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";
|
||||
|
||||
// Default values for certain options
|
||||
|
@ -221,6 +224,9 @@ public class Config {
|
|||
public final boolean bypassMempoolValidation;
|
||||
public final String daoNodeApiUrl;
|
||||
public final int daoNodeApiPort;
|
||||
public final boolean isBmFullNode;
|
||||
public final String bmOracleNodePubKey;
|
||||
public final String bmOracleNodePrivKey;
|
||||
public final String seedNodeReportingServerUrl;
|
||||
|
||||
// Properties derived from options but not exposed as options themselves
|
||||
|
@ -681,6 +687,23 @@ public class Config {
|
|||
.withRequiredArg()
|
||||
.ofType(Integer.class)
|
||||
.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 =
|
||||
parser.accepts(SEED_NODE_REPORTING_SERVER_URL, "URL of seed node reporting server")
|
||||
.withRequiredArg()
|
||||
|
@ -806,6 +829,9 @@ public class Config {
|
|||
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
|
||||
this.daoNodeApiUrl = options.valueOf(daoNodeApiUrlOpt);
|
||||
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);
|
||||
} catch (OptionException ex) {
|
||||
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) {
|
||||
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
|
||||
return tx;
|
||||
} catch (InsufficientMoneyException e) {
|
||||
log.error("getPreparedSendTx: tx={}", tx.toString());
|
||||
log.error("getPreparedSendTx: tx={}", tx);
|
||||
log.error(e.toString());
|
||||
throw new InsufficientBsqException(e.missing);
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||
|
||||
public class TradeWalletService {
|
||||
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 Preferences preferences;
|
||||
|
@ -696,16 +696,31 @@ public class TradeWalletService {
|
|||
// 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,
|
||||
String donationAddressString,
|
||||
Coin minerFee,
|
||||
long lockTime)
|
||||
throws AddressFormatException, TransactionVerificationException {
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
TransactionOutput depositTxOutput = depositTx.getOutput(0);
|
||||
Transaction delayedPayoutTx = new Transaction(params);
|
||||
delayedPayoutTx.addInput(hashedMultiSigOutput);
|
||||
delayedPayoutTx.addInput(depositTxOutput);
|
||||
applyLockTime(lockTime, delayedPayoutTx);
|
||||
Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee);
|
||||
Coin outputAmount = depositTxOutput.getValue().subtract(minerFee);
|
||||
delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString));
|
||||
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", 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
|
||||
public ProposalWithTransaction getCompensationProposalWithTransaction(String name,
|
||||
String link,
|
||||
Coin requestedBsq)
|
||||
Coin requestedBsq,
|
||||
Optional<String> burningManReceiverAddress)
|
||||
throws ProposalValidationException, InsufficientMoneyException, TxException {
|
||||
return compensationProposalFactory.createProposalWithTransaction(name,
|
||||
link,
|
||||
requestedBsq);
|
||||
requestedBsq,
|
||||
burningManReceiverAddress);
|
||||
}
|
||||
|
||||
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.DUMP_BLOCKCHAIN_DATA)).to(config.dumpBlockchainData);
|
||||
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;
|
||||
|
||||
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.ballot.BallotListService;
|
||||
import bisq.core.dao.governance.blindvote.BlindVoteListService;
|
||||
|
@ -54,9 +57,11 @@ import java.util.function.Consumer;
|
|||
public class DaoSetup {
|
||||
private final BsqNode bsqNode;
|
||||
private final List<DaoSetupService> daoSetupServices = new ArrayList<>();
|
||||
private final AccountingNode accountingNode;
|
||||
|
||||
@Inject
|
||||
public DaoSetup(BsqNodeProvider bsqNodeProvider,
|
||||
AccountingNodeProvider accountingNodeProvider,
|
||||
DaoStateService daoStateService,
|
||||
CycleService cycleService,
|
||||
BallotListService ballotListService,
|
||||
|
@ -79,9 +84,11 @@ public class DaoSetup {
|
|||
DaoStateMonitoringService daoStateMonitoringService,
|
||||
ProposalStateMonitoringService proposalStateMonitoringService,
|
||||
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
|
||||
DaoStateSnapshotService daoStateSnapshotService) {
|
||||
DaoStateSnapshotService daoStateSnapshotService,
|
||||
BurningManAccountingService burningManAccountingService) {
|
||||
|
||||
bsqNode = bsqNodeProvider.getBsqNode();
|
||||
accountingNode = accountingNodeProvider.getAccountingNode();
|
||||
|
||||
// We need to take care of order of execution.
|
||||
daoSetupServices.add(daoStateService);
|
||||
|
@ -107,8 +114,10 @@ public class DaoSetup {
|
|||
daoSetupServices.add(proposalStateMonitoringService);
|
||||
daoSetupServices.add(blindVoteStateMonitoringService);
|
||||
daoSetupServices.add(daoStateSnapshotService);
|
||||
daoSetupServices.add(burningManAccountingService);
|
||||
|
||||
daoSetupServices.add(bsqNodeProvider.getBsqNode());
|
||||
daoSetupServices.add(bsqNode);
|
||||
daoSetupServices.add(accountingNode);
|
||||
}
|
||||
|
||||
public void onAllServicesInitialized(Consumer<String> errorMessageHandler,
|
||||
|
@ -116,6 +125,9 @@ public class DaoSetup {
|
|||
bsqNode.setErrorMessageHandler(errorMessageHandler);
|
||||
bsqNode.setWarnMessageHandler(warnMessageHandler);
|
||||
|
||||
accountingNode.setErrorMessageHandler(errorMessageHandler);
|
||||
accountingNode.setWarnMessageHandler(warnMessageHandler);
|
||||
|
||||
// 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
|
||||
// listeners are set before we call start as that might trigger state change
|
||||
|
@ -126,5 +138,6 @@ public class DaoSetup {
|
|||
|
||||
public void 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.
|
||||
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
|
||||
|
||||
// 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.
|
||||
|
||||
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.
|
||||
|
|
|
@ -40,6 +40,8 @@ import org.bitcoinj.core.Transaction;
|
|||
import javax.inject.Inject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
@ -48,9 +50,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||
*/
|
||||
@Slf4j
|
||||
public class CompensationProposalFactory extends BaseProposalFactory<CompensationProposal> {
|
||||
|
||||
private Coin requestedBsq;
|
||||
private String bsqAddress;
|
||||
private Optional<String> burningManReceiverAddress;
|
||||
|
||||
@Inject
|
||||
public CompensationProposalFactory(BsqWalletService bsqWalletService,
|
||||
|
@ -65,9 +67,11 @@ public class CompensationProposalFactory extends BaseProposalFactory<Compensatio
|
|||
|
||||
public ProposalWithTransaction createProposalWithTransaction(String name,
|
||||
String link,
|
||||
Coin requestedBsq)
|
||||
Coin requestedBsq,
|
||||
Optional<String> burningManReceiverAddress)
|
||||
throws ProposalValidationException, InsufficientMoneyException, TxException {
|
||||
this.requestedBsq = requestedBsq;
|
||||
this.burningManReceiverAddress = burningManReceiverAddress;
|
||||
this.bsqAddress = bsqWalletService.getUnusedBsqAddressAsString();
|
||||
|
||||
return super.createProposalWithTransaction(name, link);
|
||||
|
@ -75,12 +79,17 @@ public class CompensationProposalFactory extends BaseProposalFactory<Compensatio
|
|||
|
||||
@Override
|
||||
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(
|
||||
name,
|
||||
link,
|
||||
requestedBsq,
|
||||
bsqAddress,
|
||||
new HashMap<>());
|
||||
extraDataMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
package bisq.core.dao.node.full;
|
||||
|
||||
class RpcException extends Exception {
|
||||
public class RpcException extends Exception {
|
||||
RpcException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
|
|
@ -56,6 +56,8 @@ import java.io.IOException;
|
|||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -91,6 +93,14 @@ public class RpcService {
|
|||
// Keep that for optimization after measuring performance differences
|
||||
private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("RpcService");
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
ListenableFuture<Void> future = executor.submit(() -> {
|
||||
try {
|
||||
|
@ -159,7 +182,11 @@ public class RpcService {
|
|||
daemon = new BitcoindDaemon(rpcBlockHost, rpcBlockPort, throwable -> {
|
||||
log.error(throwable.toString());
|
||||
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);
|
||||
|
@ -173,11 +200,20 @@ public class RpcService {
|
|||
|
||||
Futures.addCallback(future, new FutureCallback<>() {
|
||||
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) {
|
||||
UserThread.execute(() -> errorHandler.accept(throwable));
|
||||
UserThread.execute(() -> {
|
||||
setupErrorHandlers.forEach(handler -> handler.accept(throwable));
|
||||
setupErrorHandlers.clear();
|
||||
setupResultHandlers.clear();
|
||||
});
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
} catch (Exception e) {
|
||||
|
@ -219,21 +255,49 @@ public class RpcService {
|
|||
|
||||
void addNewDtoBlockHandler(Consumer<RawBlock> dtoBlockHandler,
|
||||
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 -> {
|
||||
try {
|
||||
var rawDtoBlock = client.getBlock(blockHash, 2);
|
||||
log.info("New block received: height={}, id={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash());
|
||||
RawDtoBlock rawDtoBlock = client.getBlock(blockHash, 2);
|
||||
log.info("New rawDtoBlock received: height={}, hash={}", rawDtoBlock.getHeight(), rawDtoBlock.getHash());
|
||||
|
||||
var block = getBlockFromRawDtoBlock(rawDtoBlock);
|
||||
UserThread.execute(() -> dtoBlockHandler.accept(block));
|
||||
if (rawBlockHandler != null) {
|
||||
RawBlock rawBlock = getRawBlockFromRawDtoBlock(rawDtoBlock);
|
||||
UserThread.execute(() -> rawBlockHandler.accept(rawBlock));
|
||||
}
|
||||
if (rawDtoBlockHandler != null) {
|
||||
UserThread.execute(() -> rawDtoBlockHandler.accept(rawDtoBlock));
|
||||
}
|
||||
} catch (Throwable 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 {
|
||||
ListenableFuture<Integer> future = executor.submit(client::getBlockCount);
|
||||
Futures.addCallback(future, new FutureCallback<>() {
|
||||
|
@ -262,7 +326,7 @@ public class RpcService {
|
|||
long startTs = System.currentTimeMillis();
|
||||
String blockHash = client.getBlockHash(blockHeight);
|
||||
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",
|
||||
blockHeight, block.getRawTxs().size(), System.currentTimeMillis() - startTs);
|
||||
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 static RawBlock getBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) {
|
||||
private static RawBlock getRawBlockFromRawDtoBlock(RawDtoBlock rawDtoBlock) {
|
||||
List<RawTx> txList = rawDtoBlock.getTx().stream()
|
||||
.map(e -> getTxFromRawTransaction(e, rawDtoBlock))
|
||||
.collect(Collectors.toList());
|
||||
|
|
|
@ -201,6 +201,32 @@ public class DaoStateService implements DaoSetupService {
|
|||
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
|
||||
|
@ -440,7 +466,7 @@ public class DaoStateService implements DaoSetupService {
|
|||
// TxOutput
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Stream<TxOutput> getUnorderedTxOutputStream() {
|
||||
public Stream<TxOutput> getUnorderedTxOutputStream() {
|
||||
return getUnorderedTxStream()
|
||||
.flatMap(tx -> tx.getTxOutputs().stream());
|
||||
}
|
||||
|
|
|
@ -30,11 +30,13 @@ import org.bitcoinj.core.Coin;
|
|||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
|
@ -42,6 +44,9 @@ import javax.annotation.concurrent.Immutable;
|
|||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
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 String bsqAddress;
|
||||
|
||||
|
@ -49,7 +54,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
|||
String link,
|
||||
Coin requestedBsq,
|
||||
String bsqAddress,
|
||||
Map<String, String> extraDataMap) {
|
||||
@Nullable Map<String, String> extraDataMap) {
|
||||
this(name,
|
||||
link,
|
||||
bsqAddress,
|
||||
|
@ -72,7 +77,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
|||
byte version,
|
||||
long creationDate,
|
||||
String txId,
|
||||
Map<String, String> extraDataMap) {
|
||||
@Nullable Map<String, String> extraDataMap) {
|
||||
super(name,
|
||||
link,
|
||||
version,
|
||||
|
@ -135,6 +140,11 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
|||
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
|
||||
public Proposal cloneProposalAndAddTxId(String txId) {
|
||||
return new CompensationProposal(getName(),
|
||||
|
@ -152,6 +162,7 @@ public final class CompensationProposal extends Proposal implements IssuanceProp
|
|||
return "CompensationProposal{" +
|
||||
"\n requestedBsq=" + requestedBsq +
|
||||
",\n bsqAddress='" + bsqAddress + '\'' +
|
||||
",\n burningManReceiverAddress='" + getBurningManReceiverAddress() + '\'' +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ public abstract class Proposal implements PersistablePayload, NetworkPayload, Co
|
|||
"\n txId='" + txId + '\'' +
|
||||
",\n name='" + name + '\'' +
|
||||
",\n link='" + link + '\'' +
|
||||
",\n txId='" + txId + '\'' +
|
||||
",\n extraDataMap='" + extraDataMap + '\'' +
|
||||
",\n version=" + version +
|
||||
",\n creationDate=" + new Date(creationDate) +
|
||||
"\n}";
|
||||
|
|
|
@ -22,6 +22,9 @@ import bisq.core.btc.wallet.BsqWalletService;
|
|||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
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.filter.FilterManager;
|
||||
import bisq.core.locale.Res;
|
||||
|
@ -122,6 +125,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
private final RefundAgentManager refundAgentManager;
|
||||
private final DaoFacade daoFacade;
|
||||
private final FilterManager filterManager;
|
||||
private final BtcFeeReceiverService btcFeeReceiverService;
|
||||
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||
private final Broadcaster broadcaster;
|
||||
private final PersistenceManager<TradableList<OpenOffer>> persistenceManager;
|
||||
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
|
||||
|
@ -155,6 +160,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
FilterManager filterManager,
|
||||
BtcFeeReceiverService btcFeeReceiverService,
|
||||
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
|
||||
Broadcaster broadcaster,
|
||||
PersistenceManager<TradableList<OpenOffer>> persistenceManager) {
|
||||
this.coreContext = coreContext;
|
||||
|
@ -175,6 +182,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
this.refundAgentManager = refundAgentManager;
|
||||
this.daoFacade = daoFacade;
|
||||
this.filterManager = filterManager;
|
||||
this.btcFeeReceiverService = btcFeeReceiverService;
|
||||
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||
this.broadcaster = broadcaster;
|
||||
this.persistenceManager = persistenceManager;
|
||||
|
||||
|
@ -389,6 +398,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
arbitratorManager,
|
||||
tradeStatisticsManager,
|
||||
daoFacade,
|
||||
btcFeeReceiverService,
|
||||
user,
|
||||
filterManager);
|
||||
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
|
||||
|
@ -663,6 +673,22 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
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 {
|
||||
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
|
||||
AvailabilityResult availabilityResult;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package bisq.core.offer.availability;
|
||||
|
||||
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.availability.messages.OfferAvailabilityResponse;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
|
@ -70,12 +71,17 @@ public class OfferAvailabilityModel implements Model {
|
|||
@Getter
|
||||
private final boolean isTakerApiUser;
|
||||
|
||||
// Added in v 1.9.7
|
||||
@Getter
|
||||
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||
|
||||
public OfferAvailabilityModel(Offer offer,
|
||||
PubKeyRing pubKeyRing,
|
||||
P2PService p2PService,
|
||||
User user,
|
||||
MediatorManager mediatorManager,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
|
||||
boolean isTakerApiUser) {
|
||||
this.offer = offer;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
|
@ -83,6 +89,7 @@ public class OfferAvailabilityModel implements Model {
|
|||
this.user = user;
|
||||
this.mediatorManager = mediatorManager;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||
this.isTakerApiUser = isTakerApiUser;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,17 +44,23 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
|||
private final Capabilities supportedCapabilities;
|
||||
private final boolean isTakerApiUser;
|
||||
|
||||
// Added in v 1.9.7
|
||||
private final int burningManSelectionHeight;
|
||||
|
||||
public OfferAvailabilityRequest(String offerId,
|
||||
PubKeyRing pubKeyRing,
|
||||
long takersTradePrice,
|
||||
boolean isTakerApiUser) {
|
||||
boolean isTakerApiUser,
|
||||
int burningManSelectionHeight) {
|
||||
this(offerId,
|
||||
pubKeyRing,
|
||||
takersTradePrice,
|
||||
isTakerApiUser,
|
||||
burningManSelectionHeight,
|
||||
Capabilities.app,
|
||||
Version.getP2PMessageVersion(),
|
||||
UUID.randomUUID().toString());
|
||||
UUID.randomUUID().toString()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
@ -66,6 +72,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
|||
PubKeyRing pubKeyRing,
|
||||
long takersTradePrice,
|
||||
boolean isTakerApiUser,
|
||||
int burningManSelectionHeight,
|
||||
@Nullable Capabilities supportedCapabilities,
|
||||
int messageVersion,
|
||||
@Nullable String uid) {
|
||||
|
@ -73,6 +80,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
|||
this.pubKeyRing = pubKeyRing;
|
||||
this.takersTradePrice = takersTradePrice;
|
||||
this.isTakerApiUser = isTakerApiUser;
|
||||
this.burningManSelectionHeight = burningManSelectionHeight;
|
||||
this.supportedCapabilities = supportedCapabilities;
|
||||
}
|
||||
|
||||
|
@ -82,7 +90,8 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
|||
.setOfferId(offerId)
|
||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||
.setTakersTradePrice(takersTradePrice)
|
||||
.setIsTakerApiUser(isTakerApiUser);
|
||||
.setIsTakerApiUser(isTakerApiUser)
|
||||
.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||
|
||||
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
||||
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
|
||||
|
@ -97,6 +106,7 @@ public final class OfferAvailabilityRequest extends OfferMessage implements Supp
|
|||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getTakersTradePrice(),
|
||||
proto.getIsTakerApiUser(),
|
||||
proto.getBurningManSelectionHeight(),
|
||||
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
|
||||
messageVersion,
|
||||
proto.getUid().isEmpty() ? null : proto.getUid());
|
||||
|
|
|
@ -39,8 +39,12 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
|||
try {
|
||||
runInterceptHook();
|
||||
|
||||
int burningManSelectionHeight = model.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
|
||||
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 {}",
|
||||
message.getClass().getSimpleName(), message.getOfferId(),
|
||||
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.TradeWalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.dao.burningman.BtcFeeReceiverService;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferBookService;
|
||||
|
@ -51,6 +52,7 @@ public class PlaceOfferModel implements Model {
|
|||
private final ArbitratorManager arbitratorManager;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
private final DaoFacade daoFacade;
|
||||
private final BtcFeeReceiverService btcFeeReceiverService;
|
||||
private final User user;
|
||||
@Getter
|
||||
private final FilterManager filterManager;
|
||||
|
@ -71,6 +73,7 @@ public class PlaceOfferModel implements Model {
|
|||
ArbitratorManager arbitratorManager,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
DaoFacade daoFacade,
|
||||
BtcFeeReceiverService btcFeeReceiverService,
|
||||
User user,
|
||||
FilterManager filterManager) {
|
||||
this.offer = offer;
|
||||
|
@ -83,6 +86,7 @@ public class PlaceOfferModel implements Model {
|
|||
this.arbitratorManager = arbitratorManager;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
this.daoFacade = daoFacade;
|
||||
this.btcFeeReceiverService = btcFeeReceiverService;
|
||||
this.user = user;
|
||||
this.filterManager = filterManager;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import bisq.core.dao.exceptions.DaoDisabledException;
|
|||
import bisq.core.dao.state.model.blockchain.TxType;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.placeoffer.bisq_v1.PlaceOfferModel;
|
||||
import bisq.core.util.FeeReceiverSelector;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.taskrunner.Task;
|
||||
|
@ -66,9 +65,8 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
|
|||
|
||||
TradeWalletService tradeWalletService = model.getTradeWalletService();
|
||||
|
||||
String feeReceiver = FeeReceiverSelector.getAddress(model.getFilterManager());
|
||||
|
||||
if (offer.isCurrencyForMakerFeeBtc()) {
|
||||
String feeReceiver = model.getBtcFeeReceiverService().getAddress();
|
||||
tradeWalletService.createBtcTradingFeeTx(
|
||||
fundingAddress,
|
||||
reservedForTradeAddress,
|
||||
|
|
|
@ -19,6 +19,9 @@ package bisq.core.proto.network;
|
|||
|
||||
import bisq.core.alert.Alert;
|
||||
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.proposal.storage.temp.TempProposalPayload;
|
||||
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
|
||||
|
@ -254,6 +257,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
|
|||
case GET_INVENTORY_RESPONSE:
|
||||
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:
|
||||
throw new ProtobufferException("Unknown proto message case (PB.NetworkEnvelope). messageCase=" +
|
||||
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.btc.model.AddressEntryList;
|
||||
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.storage.BlindVoteStore;
|
||||
import bisq.core.dao.governance.bond.reputation.MyReputationList;
|
||||
|
@ -141,6 +142,8 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
|
|||
return RemovedPayloadsMap.fromProto(proto.getRemovedPayloadsMap());
|
||||
case BSQ_BLOCK_STORE:
|
||||
return BsqBlockStore.fromProto(proto.getBsqBlockStore());
|
||||
case BURNING_MAN_ACCOUNTING_STORE:
|
||||
return BurningManAccountingStore.fromProto(proto.getBurningManAccountingStore());
|
||||
default:
|
||||
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " +
|
||||
"messageCase=" + proto.getMessageCase() + "; proto raw data=" + proto.toString());
|
||||
|
|
|
@ -77,7 +77,6 @@ import javax.annotation.Nullable;
|
|||
@EqualsAndHashCode
|
||||
@Getter
|
||||
public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
|
||||
public enum State {
|
||||
NEEDS_UPGRADE,
|
||||
NEW,
|
||||
|
@ -146,6 +145,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
// Added at v1.6.0
|
||||
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
|
||||
// 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.
|
||||
|
@ -263,7 +269,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
.setIsClosed(this.isClosed())
|
||||
.setOpeningDate(openingDate)
|
||||
.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(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e)));
|
||||
|
@ -330,6 +338,9 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
|
||||
}
|
||||
|
||||
dispute.setBurningManSelectionHeight(proto.getBurningManSelectionHeight());
|
||||
dispute.setTradeTxFee(proto.getTradeTxFee());
|
||||
|
||||
if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
|
||||
// old disputes did not have a state field, so choose an appropriate state:
|
||||
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
|
||||
|
@ -516,6 +527,13 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
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
|
||||
public String toString() {
|
||||
return "Dispute{" +
|
||||
|
@ -550,6 +568,8 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
|
||||
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
|
||||
",\n cachedDepositTx='" + cachedDepositTx + '\'' +
|
||||
",\n burningManSelectionHeight='" + burningManSelectionHeight + '\'' +
|
||||
",\n tradeTxFee='" + tradeTxFee + '\'' +
|
||||
"\n}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -273,8 +273,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
List<Dispute> disputes = getDisputeList().getList();
|
||||
disputes.forEach(dispute -> {
|
||||
try {
|
||||
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||
if (dispute.isUsingLegacyBurningMan()) {
|
||||
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||
}
|
||||
} catch (DisputeValidation.AddressException | DisputeValidation.NodeAddressException e) {
|
||||
log.error(e.toString());
|
||||
validationExceptions.add(e);
|
||||
|
@ -371,8 +373,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
DisputeValidation.validateDisputeData(dispute, btcWalletService);
|
||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||
DisputeValidation.validateSenderNodeAddress(dispute, openNewDisputeMessage.getSenderNodeAddress());
|
||||
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||
DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
|
||||
if (dispute.isUsingLegacyBurningMan()) {
|
||||
DisputeValidation.validateDonationAddressMatchesAnyPastParamValues(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
|
||||
}
|
||||
} catch (DisputeValidation.ValidationException e) {
|
||||
log.error(e.toString());
|
||||
validationExceptions.add(e);
|
||||
|
@ -401,13 +405,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
DisputeValidation.validateDisputeData(dispute, btcWalletService);
|
||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||
DisputeValidation.validateTradeAndDispute(dispute, trade);
|
||||
DisputeValidation.validateDonationAddress(dispute,
|
||||
Objects.requireNonNull(trade.getDelayedPayoutTx()),
|
||||
btcWalletService.getParams(),
|
||||
daoFacade);
|
||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||
trade.getDelayedPayoutTx(),
|
||||
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) {
|
||||
// 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
|
||||
|
@ -618,6 +624,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
dispute.setExtraDataMap(disputeFromOpener.getExtraDataMap());
|
||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||
dispute.setBurningManSelectionHeight(disputeFromOpener.getBurningManSelectionHeight());
|
||||
dispute.setTradeTxFee(disputeFromOpener.getTradeTxFee());
|
||||
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
|
||||
|
|
|
@ -115,6 +115,7 @@ public class DisputeValidation {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static void validateSenderNodeAddress(Dispute dispute,
|
||||
NodeAddress senderNodeAddress) throws NodeAddressException {
|
||||
if (!senderNodeAddress.equals(dispute.getContract().getBuyerNodeAddress())
|
||||
|
@ -156,8 +157,7 @@ public class DisputeValidation {
|
|||
|
||||
public static void validateDonationAddress(Dispute dispute,
|
||||
Transaction delayedPayoutTx,
|
||||
NetworkParameters params,
|
||||
DaoFacade daoFacade)
|
||||
NetworkParameters params)
|
||||
throws AddressException {
|
||||
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
||||
Address address = output.getScriptPubKey().getToAddress(params);
|
||||
|
@ -167,12 +167,13 @@ public class DisputeValidation {
|
|||
log.error(delayedPayoutTx.toString());
|
||||
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.
|
||||
String delayedPayoutTxOutputAddress = address.toString();
|
||||
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,
|
||||
|
@ -244,7 +245,6 @@ public class DisputeValidation {
|
|||
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
|
||||
Map<String, Set<String>> disputesPerDepositTxId)
|
||||
throws DisputeReplayException {
|
||||
|
||||
try {
|
||||
String disputeToTestTradeId = disputeToTest.getTradeId();
|
||||
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
|
||||
|
@ -312,6 +312,7 @@ public class DisputeValidation {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static class AddressException extends ValidationException {
|
||||
AddressException(Dispute dispute, String 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.TradeWalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.dao.burningman.DelayedPayoutTxReceiverService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
|
@ -50,11 +51,13 @@ import bisq.common.app.Version;
|
|||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.util.Hex;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionInput;
|
||||
import org.bitcoinj.core.TransactionOutPoint;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
@ -74,8 +77,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||
@Slf4j
|
||||
@Singleton
|
||||
public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||
private final MempoolService mempoolService;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -89,6 +94,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
DaoFacade daoFacade,
|
||||
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService,
|
||||
KeyRing keyRing,
|
||||
RefundDisputeListService refundDisputeListService,
|
||||
Config config,
|
||||
|
@ -96,6 +102,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
MempoolService mempoolService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService);
|
||||
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||
|
||||
this.mempoolService = mempoolService;
|
||||
}
|
||||
|
@ -308,4 +315,27 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
checkArgument(fundingTxId.equals(depositTx.getTxId().toString()),
|
||||
"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.common.ClockWatcher;
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
|
@ -644,6 +645,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
user,
|
||||
mediatorManager,
|
||||
tradeStatisticsManager,
|
||||
provider.getDelayedPayoutTxReceiverService(),
|
||||
isTakerApiUser);
|
||||
}
|
||||
|
||||
|
@ -729,6 +731,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
clockWatcher.addListener(new ClockWatcher.Listener() {
|
||||
@Override
|
||||
public void onSecondTick() {
|
||||
if (DevEnv.isDevMode()) {
|
||||
updateTradePeriodState();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -743,6 +748,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
if (!trade.isPayoutPublished()) {
|
||||
Date maxTradePeriodDate = trade.getMaxTradePeriodDate();
|
||||
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) {
|
||||
Date now = new Date();
|
||||
if (now.after(maxTradePeriodDate)) {
|
||||
|
|
|
@ -71,12 +71,6 @@ public class TradeDataValidation {
|
|||
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
|
||||
// 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);
|
||||
}
|
||||
|
||||
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
|
||||
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
||||
Offer offer = checkNotNull(trade.getOffer());
|
||||
|
@ -112,11 +114,12 @@ public class TradeDataValidation {
|
|||
}
|
||||
|
||||
NetworkParameters params = btcWalletService.getParams();
|
||||
String delayedPayoutTxOutputAddress = output.getScriptPubKey().getToAddress(params).toString();
|
||||
if (addressConsumer != null) {
|
||||
String delayedPayoutTxOutputAddress = output.getScriptPubKey().getToAddress(params).toString();
|
||||
addressConsumer.accept(delayedPayoutTxOutputAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void validatePayoutTxInput(Transaction depositTx,
|
||||
Transaction delayedPayoutTx)
|
||||
|
|
|
@ -1067,6 +1067,12 @@ public abstract class Trade extends TradeModel {
|
|||
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
|
||||
|
|
|
@ -23,6 +23,8 @@ import bisq.core.btc.wallet.BtcWalletService;
|
|||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
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.offer.OpenOfferManager;
|
||||
import bisq.core.provider.fee.FeeService;
|
||||
|
@ -60,6 +62,8 @@ public class Provider {
|
|||
private final RefundAgentManager refundAgentManager;
|
||||
private final KeyRing keyRing;
|
||||
private final FeeService feeService;
|
||||
private final BtcFeeReceiverService btcFeeReceiverService;
|
||||
private final DelayedPayoutTxReceiverService delayedPayoutTxReceiverService;
|
||||
|
||||
@Inject
|
||||
public Provider(OpenOfferManager openOfferManager,
|
||||
|
@ -78,7 +82,9 @@ public class Provider {
|
|||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
KeyRing keyRing,
|
||||
FeeService feeService) {
|
||||
FeeService feeService,
|
||||
BtcFeeReceiverService btcFeeReceiverService,
|
||||
DelayedPayoutTxReceiverService delayedPayoutTxReceiverService) {
|
||||
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.p2PService = p2PService;
|
||||
|
@ -97,5 +103,7 @@ public class Provider {
|
|||
this.refundAgentManager = refundAgentManager;
|
||||
this.keyRing = keyRing;
|
||||
this.feeService = feeService;
|
||||
this.btcFeeReceiverService = btcFeeReceiverService;
|
||||
this.delayedPayoutTxReceiverService = delayedPayoutTxReceiverService;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,9 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
|||
@Nullable
|
||||
private final String takersPaymentMethodId;
|
||||
|
||||
// Added in v 1.9.7
|
||||
private final int burningManSelectionHeight;
|
||||
|
||||
public InputsForDepositTxRequest(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
long tradeAmount,
|
||||
|
@ -107,7 +110,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
|||
byte[] accountAgeWitnessSignatureOfOfferId,
|
||||
long currentDate,
|
||||
@Nullable byte[] hashOfTakersPaymentAccountPayload,
|
||||
@Nullable String takersPaymentMethodId) {
|
||||
@Nullable String takersPaymentMethodId,
|
||||
int burningManSelectionHeight) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.tradeAmount = tradeAmount;
|
||||
|
@ -134,6 +138,7 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
|||
this.currentDate = currentDate;
|
||||
this.hashOfTakersPaymentAccountPayload = hashOfTakersPaymentAccountPayload;
|
||||
this.takersPaymentMethodId = takersPaymentMethodId;
|
||||
this.burningManSelectionHeight = burningManSelectionHeight;
|
||||
}
|
||||
|
||||
|
||||
|
@ -169,7 +174,8 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
|||
.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())
|
||||
.setUid(uid)
|
||||
.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(accountAgeWitnessSignatureOfOfferId))
|
||||
.setCurrentDate(currentDate);
|
||||
.setCurrentDate(currentDate)
|
||||
.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||
|
||||
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
|
||||
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()),
|
||||
proto.getCurrentDate(),
|
||||
hashOfTakersPaymentAccountPayload,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getTakersPayoutMethodId()));
|
||||
ProtoUtil.stringOrNullFromProto(proto.getTakersPayoutMethodId()),
|
||||
proto.getBurningManSelectionHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -253,6 +260,7 @@ public final class InputsForDepositTxRequest extends TradeMessage implements Dir
|
|||
",\n currentDate=" + currentDate +
|
||||
",\n hashOfTakersPaymentAccountPayload=" + Utilities.bytesAsHexString(hashOfTakersPaymentAccountPayload) +
|
||||
",\n takersPaymentMethodId=" + takersPaymentMethodId +
|
||||
",\n burningManSelectionHeight=" + burningManSelectionHeight +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import bisq.core.btc.wallet.BsqWalletService;
|
|||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
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.network.MessageState;
|
||||
import bisq.core.offer.Offer;
|
||||
|
@ -176,6 +178,11 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
|||
@Setter
|
||||
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) {
|
||||
this(offerId, accountId, pubKeyRing, new TradingPeer());
|
||||
}
|
||||
|
@ -217,7 +224,8 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
|||
.setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong)
|
||||
.setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name())
|
||||
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
|
||||
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation);
|
||||
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
|
||||
.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||
|
||||
Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId);
|
||||
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());
|
||||
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
||||
processModel.setPaymentStartedMessageState(paymentStartedMessageState);
|
||||
processModel.setBurningManSelectionHeight(proto.getBurningManSelectionHeight());
|
||||
|
||||
if (proto.hasPaymentAccount()) {
|
||||
processModel.setPaymentAccount(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver));
|
||||
|
@ -377,6 +386,14 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
|||
return provider.getTradeWalletService();
|
||||
}
|
||||
|
||||
public BtcFeeReceiverService getBtcFeeReceiverService() {
|
||||
return provider.getBtcFeeReceiverService();
|
||||
}
|
||||
|
||||
public DelayedPayoutTxReceiverService getDelayedPayoutTxReceiverService() {
|
||||
return provider.getDelayedPayoutTxReceiverService();
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return provider.getUser();
|
||||
}
|
||||
|
|
|
@ -17,16 +17,22 @@
|
|||
|
||||
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.model.bisq_v1.Trade;
|
||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||
|
||||
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 static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
|
@ -40,17 +46,37 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask {
|
|||
try {
|
||||
runInterceptHook();
|
||||
|
||||
Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
|
||||
checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
|
||||
BtcWalletService btcWalletService = processModel.getBtcWalletService();
|
||||
Transaction finalDelayedPayoutTx = trade.getDelayedPayoutTx();
|
||||
checkNotNull(finalDelayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
|
||||
|
||||
// Check again tx
|
||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||
delayedPayoutTx,
|
||||
processModel.getBtcWalletService());
|
||||
finalDelayedPayoutTx,
|
||||
btcWalletService);
|
||||
|
||||
// Now as we know the deposit tx we can also verify the input
|
||||
Transaction depositTx = trade.getDepositTx();
|
||||
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();
|
||||
} catch (TradeDataValidation.ValidationException e) {
|
||||
|
|
|
@ -17,14 +17,22 @@
|
|||
|
||||
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.model.bisq_v1.Trade;
|
||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||
|
||||
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 static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
|
@ -38,19 +46,36 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
|
|||
try {
|
||||
runInterceptHook();
|
||||
|
||||
var preparedDelayedPayoutTx = processModel.getPreparedDelayedPayoutTx();
|
||||
Transaction sellersPreparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx());
|
||||
BtcWalletService btcWalletService = processModel.getBtcWalletService();
|
||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||
preparedDelayedPayoutTx,
|
||||
processModel.getBtcWalletService());
|
||||
sellersPreparedDelayedPayoutTx,
|
||||
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
|
||||
// before sending any further data to the seller, to provide extra protection for the buyer.
|
||||
if (isDepositTxNonMalleable()) {
|
||||
var preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(
|
||||
processModel.getPreparedDepositTx());
|
||||
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, checkNotNull(preparedDelayedPayoutTx));
|
||||
TradeDataValidation.validatePayoutTxInput(preparedDepositTx, sellersPreparedDelayedPayoutTx);
|
||||
} 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();
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package bisq.core.trade.protocol.bisq_v1.tasks.maker;
|
||||
|
||||
import bisq.core.dao.burningman.BurningManService;
|
||||
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
||||
|
@ -79,6 +80,16 @@ public class MakerProcessesInputsForDepositTxRequest extends TradeTask {
|
|||
|
||||
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
|
||||
// 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
|
||||
|
|
|
@ -18,16 +18,20 @@
|
|||
package bisq.core.trade.protocol.bisq_v1.tasks.seller;
|
||||
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.dao.burningman.BurningManService;
|
||||
import bisq.core.dao.governance.param.Param;
|
||||
import bisq.core.trade.bisq_v1.TradeDataValidation;
|
||||
import bisq.core.trade.model.bisq_v1.Trade;
|
||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
@ -44,16 +48,33 @@ public class SellerCreatesDelayedPayoutTx extends TradeTask {
|
|||
try {
|
||||
runInterceptHook();
|
||||
|
||||
String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS);
|
||||
Coin minerFee = trade.getTradeTxFee();
|
||||
TradeWalletService tradeWalletService = processModel.getTradeWalletService();
|
||||
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();
|
||||
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,
|
||||
minerFee,
|
||||
lockTime);
|
||||
}
|
||||
|
||||
TradeDataValidation.validateDelayedPayoutTx(trade,
|
||||
preparedDelayedPayoutTx,
|
||||
processModel.getBtcWalletService());
|
||||
|
|
|
@ -24,7 +24,6 @@ import bisq.core.btc.wallet.WalletService;
|
|||
import bisq.core.dao.exceptions.DaoDisabledException;
|
||||
import bisq.core.trade.model.bisq_v1.Trade;
|
||||
import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
|
||||
import bisq.core.util.FeeReceiverSelector;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
|
@ -66,7 +65,7 @@ public class CreateTakerFeeTx extends TradeTask {
|
|||
Transaction transaction;
|
||||
|
||||
if (trade.isCurrencyForTakerFeeBtc()) {
|
||||
String feeReceiver = FeeReceiverSelector.getAddress(processModel.getFilterManager());
|
||||
String feeReceiver = processModel.getBtcFeeReceiverService().getAddress();
|
||||
transaction = tradeWalletService.createBtcTradingFeeTx(
|
||||
fundingAddress,
|
||||
reservedForTradeAddress,
|
||||
|
|
|
@ -104,6 +104,9 @@ public class TakerSendInputsForDepositTxRequest extends TradeTask {
|
|||
byte[] signatureOfNonce = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(),
|
||||
offerId.getBytes(Charsets.UTF_8));
|
||||
|
||||
int burningManSelectionHeight = processModel.getDelayedPayoutTxReceiverService().getBurningManSelectionHeight();
|
||||
processModel.setBurningManSelectionHeight(burningManSelectionHeight);
|
||||
|
||||
String takersPaymentMethodId = checkNotNull(processModel.getPaymentAccountPayload(trade)).getPaymentMethodId();
|
||||
InputsForDepositTxRequest request = new InputsForDepositTxRequest(
|
||||
offerId,
|
||||
|
@ -133,7 +136,8 @@ public class TakerSendInputsForDepositTxRequest extends TradeTask {
|
|||
signatureOfNonce,
|
||||
new Date().getTime(),
|
||||
hashOfTakersPaymentAccountPayload,
|
||||
takersPaymentMethodId);
|
||||
takersPaymentMethodId,
|
||||
burningManSelectionHeight);
|
||||
log.info("Send {} with offerId {} and uid {} to peer {}",
|
||||
request.getClass().getSimpleName(), request.getTradeId(),
|
||||
request.getUid(), trade.getTradingPeerNodeAddress());
|
||||
|
|
|
@ -42,11 +42,26 @@ public class AveragePriceUtil {
|
|||
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
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));
|
||||
Date pastXDays = getPastDate(days);
|
||||
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrency().equals("BSQ"))
|
||||
.filter(e -> e.getDate().after(pastXDays))
|
||||
.filter(e -> e.getDate().before(date))
|
||||
.collect(Collectors.toList());
|
||||
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
|
||||
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
|
||||
|
@ -55,6 +70,7 @@ public class AveragePriceUtil {
|
|||
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrency().equals("USD"))
|
||||
.filter(e -> e.getDate().after(pastXDays))
|
||||
.filter(e -> e.getDate().before(date))
|
||||
.collect(Collectors.toList());
|
||||
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
|
||||
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
|
||||
|
||||
for (TradeStatistics3 item : bsqList) {
|
||||
// Find usdprice for trade item
|
||||
// Find usd price for trade item
|
||||
usdBTCPrice = usdList.stream()
|
||||
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
|
||||
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
|
||||
|
@ -130,9 +146,9 @@ public class AveragePriceUtil {
|
|||
return averagePrice;
|
||||
}
|
||||
|
||||
private static Date getPastDate(int days) {
|
||||
private static Date getPastDate(int days, Date date) {
|
||||
Calendar cal = new GregorianCalendar();
|
||||
cal.setTime(new Date());
|
||||
cal.setTime(date);
|
||||
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
|
||||
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) {
|
||||
return formatCoin(coin, false);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import org.bitcoinj.core.Coin;
|
|||
public interface CoinFormatter {
|
||||
String formatCoin(Coin coin);
|
||||
|
||||
String formatCoin(long value);
|
||||
|
||||
String formatCoin(Coin coin, boolean appendCode);
|
||||
|
||||
String formatCoin(Coin coin, int decimalPlaces);
|
||||
|
|
|
@ -53,6 +53,11 @@ public class ImmutableCoinFormatter implements CoinFormatter {
|
|||
return formatCoin(coin, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatCoin(long value) {
|
||||
return formatCoin(Coin.valueOf(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatCoin(Coin coin, boolean appendCode) {
|
||||
return appendCode ? formatCoinWithCode(coin) : formatCoin(coin);
|
||||
|
|
|
@ -43,6 +43,9 @@ public final class BtcAddressValidator extends InputValidator {
|
|||
}
|
||||
|
||||
private ValidationResult validateBtcAddress(String input) {
|
||||
if (allowEmpty && (input == null || input.trim().isEmpty())) {
|
||||
return new ValidationResult(true);
|
||||
}
|
||||
try {
|
||||
Address.fromString(Config.baseCurrencyNetworkParameters(), input);
|
||||
return new ValidationResult(true);
|
||||
|
|
|
@ -24,7 +24,11 @@ import java.math.BigInteger;
|
|||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
public class InputValidator {
|
||||
@Setter
|
||||
public boolean allowEmpty;
|
||||
|
||||
public ValidationResult validate(String input) {
|
||||
return validateIfNotEmpty(input);
|
||||
|
@ -32,10 +36,13 @@ public class InputValidator {
|
|||
|
||||
protected ValidationResult validateIfNotEmpty(String input) {
|
||||
//trim added to avoid empty input
|
||||
if (input == null || input.trim().length() == 0)
|
||||
return new ValidationResult(false, Res.get("validation.empty"));
|
||||
else
|
||||
if (allowEmpty) {
|
||||
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 {
|
||||
|
|
|
@ -975,14 +975,15 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
|
|||
Your trading peer receives: {1}\n\n\
|
||||
You can accept or reject this suggested payout.\n\n\
|
||||
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 one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to 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. \
|
||||
Both traders agreeing to the mediator''s suggestion is the happy path—requesting arbitration is meant for \
|
||||
exceptional circumstances, such as if a trader is sure the mediator did not make a fair payout suggestion \
|
||||
(or if the other peer is unresponsive).\n\n\
|
||||
More details about the new arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
|
||||
If one or both of you reject the suggestion, you will have to wait until {2} (block {3}) to reject mediation suggestion, \
|
||||
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\
|
||||
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer's security deposit. \
|
||||
This means the total arbitration payout will be less than the mediation payout. \
|
||||
Requesting arbitration is meant for exceptional circumstances. such as; \
|
||||
one peer not responding, or disputing the mediator made a fair payout suggestion. \n\n\
|
||||
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 \
|
||||
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 \
|
||||
|
@ -1967,7 +1968,7 @@ dao.tab.factsAndFigures=Facts & Figures
|
|||
dao.tab.bsqWallet=BSQ wallet
|
||||
dao.tab.proposals=Governance
|
||||
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.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
|
||||
|
||||
# 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"
|
||||
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.menuItem.assetFee=Asset listing fee
|
||||
dao.burnBsq.menuItem.proofOfBurn=Proof of burn
|
||||
dao.burnBsq.menuItem.burningMan=Burningmen
|
||||
dao.burnBsq.header=Fee for asset listing
|
||||
dao.burnBsq.selectAsset=Select Asset
|
||||
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.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.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"
|
||||
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.prompt=Link to proposal
|
||||
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.proposalFee=Proposal fee
|
||||
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.send=Send BSQ funds
|
||||
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.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?
|
||||
|
@ -2758,7 +2819,7 @@ disputeSummaryWindow.requestingTxs=Requesting blockchain transactions from block
|
|||
disputeSummaryWindow.requestTransactionsError=Requesting the 4 trade transactions failed. Error message: {0}.\n\n\
|
||||
Please verify the transactions manually before closing the dispute.
|
||||
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
|
||||
# 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,
|
||||
persistenceManager
|
||||
);
|
||||
|
||||
|
@ -114,6 +116,8 @@ public class OpenOfferManagerTest {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
persistenceManager
|
||||
);
|
||||
|
||||
|
@ -153,6 +157,8 @@ public class OpenOfferManagerTest {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
persistenceManager
|
||||
);
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import bisq.core.dao.state.DaoStateService;
|
|||
import bisq.core.filter.Filter;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.trade.DelayedPayoutAddressProvider;
|
||||
import bisq.core.util.FeeReceiverSelector;
|
||||
import bisq.core.util.ParsingUtils;
|
||||
import bisq.core.util.coin.BsqFormatter;
|
||||
|
||||
|
@ -65,7 +64,7 @@ public class TxValidatorTest {
|
|||
btcFeeReceivers.add("13sxMq8mTw7CTSqgGiMPfwo6ZDsVYrHLmR");
|
||||
btcFeeReceivers.add("19qA2BVPoyXDfHKVMovKG7SoxGY7xrBV8c");
|
||||
btcFeeReceivers.add("19BNi5EpZhgBBWAt5ka7xWpJpX2ZWJEYyq");
|
||||
btcFeeReceivers.add(FeeReceiverSelector.BTC_FEE_RECEIVER_ADDRESS);
|
||||
btcFeeReceivers.add("38bZBj5peYS3Husdz7AH3gEUiUbYRD951t");
|
||||
btcFeeReceivers.add(DelayedPayoutAddressProvider.BM2019_ADDRESS);
|
||||
btcFeeReceivers.add("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7");
|
||||
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
|
||||
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);
|
||||
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
||||
addColumns();
|
||||
|
|
|
@ -141,7 +141,8 @@ public class MyReputationView extends ActivatableView<GridPane, Void> implements
|
|||
|
||||
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();
|
||||
tableView.setItems(sortedList);
|
||||
GridPane.setVgrow(tableView, Priority.ALWAYS);
|
||||
|
|
|
@ -23,9 +23,9 @@ import bisq.desktop.components.AutoTooltipButton;
|
|||
import bisq.desktop.components.AutoTooltipTableColumn;
|
||||
import bisq.desktop.components.ExternalHyperlink;
|
||||
import bisq.desktop.components.HyperlinkWithIcon;
|
||||
import bisq.desktop.main.dao.bonding.BondingViewUtils;
|
||||
import bisq.desktop.main.dao.MessageSignatureWindow;
|
||||
import bisq.desktop.main.dao.MessageVerificationWindow;
|
||||
import bisq.desktop.main.dao.bonding.BondingViewUtils;
|
||||
import bisq.desktop.util.FormBuilder;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
|
@ -92,7 +92,8 @@ public class RolesView extends ActivatableView<GridPane, Void> {
|
|||
@Override
|
||||
public void initialize() {
|
||||
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();
|
||||
tableView.setItems(sortedList);
|
||||
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) {
|
||||
return new TableCell<>() {
|
||||
HBox hbox;
|
||||
|
||||
@Override
|
||||
public void updateItem(final RolesListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
@ -315,13 +317,15 @@ public class RolesView extends ActivatableView<GridPane, Void> {
|
|||
if (item.isLockupButtonVisible()) {
|
||||
AutoTooltipButton buttonLockup = new AutoTooltipButton(Res.get("dao.bond.table.button.lockup"));
|
||||
buttonLockup.setMinWidth(70);
|
||||
buttonLockup.setOnAction(e -> bondingViewUtils.lockupBondForBondedRole(item.getRole(), txId -> {}));
|
||||
buttonLockup.setOnAction(e -> bondingViewUtils.lockupBondForBondedRole(item.getRole(), txId -> {
|
||||
}));
|
||||
hbox.getChildren().add(buttonLockup);
|
||||
}
|
||||
if (item.isRevokeButtonVisible()) {
|
||||
AutoTooltipButton buttonRevoke = new AutoTooltipButton(Res.get("dao.bond.table.button.revoke"));
|
||||
buttonRevoke.setMinWidth(70);
|
||||
buttonRevoke.setOnAction(e -> bondingViewUtils.unLock(item.getLockupTxId(), txId -> {}));
|
||||
buttonRevoke.setOnAction(e -> bondingViewUtils.unLock(item.getLockupTxId(), txId -> {
|
||||
}));
|
||||
hbox.getChildren().add(buttonRevoke);
|
||||
}
|
||||
hbox.setMinWidth(hbox.getChildren().size() * 70);
|
||||
|
|
|
@ -28,6 +28,7 @@ import bisq.desktop.components.MenuItem;
|
|||
import bisq.desktop.main.MainView;
|
||||
import bisq.desktop.main.dao.DaoView;
|
||||
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.core.locale.Res;
|
||||
|
@ -49,7 +50,7 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
|
|||
private final ViewLoader viewLoader;
|
||||
private final Navigation navigation;
|
||||
|
||||
private MenuItem assetFee, proofOfBurn;
|
||||
private MenuItem proofOfBurn, burningMan, assetFee;
|
||||
private Navigation.Listener listener;
|
||||
|
||||
@FXML
|
||||
|
@ -77,26 +78,28 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
|
|||
};
|
||||
|
||||
toggleGroup = new ToggleGroup();
|
||||
final 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);
|
||||
List<Class<? extends View>> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class);
|
||||
proofOfBurn = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.proofOfBurn"),
|
||||
ProofOfBurnView.class, baseNavPath);
|
||||
|
||||
leftVBox.getChildren().addAll(assetFee, proofOfBurn);
|
||||
burningMan = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.burningMan"),
|
||||
BurningManView.class, baseNavPath);
|
||||
assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"),
|
||||
AssetFeeView.class, baseNavPath);
|
||||
leftVBox.getChildren().addAll(burningMan, proofOfBurn, assetFee);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate() {
|
||||
assetFee.activate();
|
||||
proofOfBurn.activate();
|
||||
burningMan.activate();
|
||||
assetFee.activate();
|
||||
|
||||
navigation.addListener(listener);
|
||||
ViewPath viewPath = navigation.getCurrentPath();
|
||||
if (viewPath.size() == 3 && viewPath.indexOf(BurnBsqView.class) == 2 ||
|
||||
viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) {
|
||||
if (selectedViewClass == null)
|
||||
selectedViewClass = AssetFeeView.class;
|
||||
selectedViewClass = BurningManView.class;
|
||||
|
||||
loadView(selectedViewClass);
|
||||
|
||||
|
@ -111,15 +114,17 @@ public class BurnBsqView extends ActivatableView<AnchorPane, Void> {
|
|||
protected void deactivate() {
|
||||
navigation.removeListener(listener);
|
||||
|
||||
assetFee.deactivate();
|
||||
proofOfBurn.deactivate();
|
||||
burningMan.deactivate();
|
||||
assetFee.deactivate();
|
||||
}
|
||||
|
||||
private void loadView(Class<? extends View> viewClass) {
|
||||
View view = viewLoader.load(viewClass);
|
||||
content.getChildren().setAll(view.getRoot());
|
||||
|
||||
if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee);
|
||||
else if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn);
|
||||
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