Merge remote-tracking branch 'bisq-network/hotfix/v1.5.4' into upgrade-javafax-14

This commit is contained in:
cd2357 2021-01-13 19:44:12 +01:00
commit 47c4e09d69
No known key found for this signature in database
GPG key ID: F26C56748514D0D3
268 changed files with 9225 additions and 3508 deletions

View file

@ -154,7 +154,8 @@ public class Scaffold {
try {
log.info("Shutting down executor service ...");
executor.shutdownNow();
executor.awaitTermination(config.supportingApps.size() * 2000, MILLISECONDS);
//noinspection ResultOfMethodCallIgnored
executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS);
SetupTask[] orderedTasks = new SetupTask[]{
bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask};
@ -218,20 +219,25 @@ public class Scaffold {
if (copyBitcoinRegtestDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install bitcoin regtest dir");
String aliceDataDir = daoSetupDir + "/" + alicedaemon.appName;
BashCommand copyAliceDataDir = new BashCommand(
"cp -rf " + daoSetupDir + "/" + alicedaemon.appName
+ " " + config.rootAppDataDir);
"cp -rf " + aliceDataDir + " " + config.rootAppDataDir);
if (copyAliceDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install alice data dir");
String bobDataDir = daoSetupDir + "/" + bobdaemon.appName;
BashCommand copyBobDataDir = new BashCommand(
"cp -rf " + daoSetupDir + "/" + bobdaemon.appName
+ " " + config.rootAppDataDir);
"cp -rf " + bobDataDir + " " + config.rootAppDataDir);
if (copyBobDataDir.run().getExitStatus() != 0)
throw new IllegalStateException("Could not install bob data dir");
log.info("Installed dao-setup files into {}", buildDataDir);
if (!config.callRateMeteringConfigPath.isEmpty()) {
installCallRateMeteringConfiguration(aliceDataDir);
installCallRateMeteringConfiguration(bobDataDir);
}
// Copy the blocknotify script from the src resources dir to the build
// resources dir. Users may want to edit comment out some lines when all
// of the default block notifcation ports being will not be used (to avoid
@ -287,6 +293,25 @@ public class Scaffold {
}
}
private void installCallRateMeteringConfiguration(String dataDir) throws IOException, InterruptedException {
File testRateMeteringFile = new File(config.callRateMeteringConfigPath);
if (!testRateMeteringFile.exists())
throw new FileNotFoundException(
format("Call rate metering config file '%s' not found", config.callRateMeteringConfigPath));
BashCommand copyRateMeteringConfigFile = new BashCommand(
"cp -rf " + config.callRateMeteringConfigPath + " " + dataDir);
if (copyRateMeteringConfigFile.run().getExitStatus() != 0)
throw new IllegalStateException(
format("Could not install %s file in %s",
testRateMeteringFile.getAbsolutePath(), dataDir));
Path destPath = Paths.get(dataDir, testRateMeteringFile.getName());
String chmod700Perms = "rwx------";
Files.setPosixFilePermissions(destPath, PosixFilePermissions.fromString(chmod700Perms));
log.info("Installed {} with perms {}.", destPath.toString(), chmod700Perms);
}
private void installShutdownHook() {
// Background apps can be left running until the jvm is manually shutdown,
// so we add a shutdown hook for that use case.

View file

@ -71,6 +71,7 @@ public class ApiTestConfig {
static final String SKIP_TESTS = "skipTests";
static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests";
static final String SUPPORTING_APPS = "supportingApps";
static final String CALL_RATE_METERING_CONFIG_PATH = "callRateMeteringConfigPath";
static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging";
// Default values for certain options
@ -102,6 +103,7 @@ public class ApiTestConfig {
public final boolean skipTests;
public final boolean shutdownAfterTests;
public final List<String> supportingApps;
public final String callRateMeteringConfigPath;
public final boolean enableBisqDebugging;
// Immutable system configurations set in the constructor.
@ -228,6 +230,12 @@ public class ApiTestConfig {
.ofType(String.class)
.defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon");
ArgumentAcceptingOptionSpec<String> callRateMeteringConfigPathOpt =
parser.accepts(CALL_RATE_METERING_CONFIG_PATH,
"Install a ratemeters.json file to configure call rate metering interceptors")
.withRequiredArg()
.defaultsTo(EMPTY);
ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt =
parser.accepts(ENABLE_BISQ_DEBUGGING,
"Start Bisq apps with remote debug options")
@ -289,6 +297,7 @@ public class ApiTestConfig {
this.skipTests = options.valueOf(skipTestsOpt);
this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt);
this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(","));
this.callRateMeteringConfigPath = options.valueOf(callRateMeteringConfigPathOpt);
this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt);
// Assign values to special-case static fields.

View file

@ -59,7 +59,7 @@ public class BitcoinDaemon extends AbstractLinuxProcess implements LinuxProcess
+ " -rpcport=" + config.bitcoinRpcPort
+ " -rpcuser=" + config.bitcoinRpcUser
+ " -rpcpassword=" + config.bitcoinRpcPassword
+ " -blocknotify=" + config.bitcoinDatadir + "/blocknotify";
+ " -blocknotify=" + "\"" + config.bitcoinDatadir + "/blocknotify" + " %s\"";
BashCommand cmd = new BashCommand(bitcoindCmd).run();
log.info("Starting ...\n$ {}", cmd.getCommand());

View file

@ -19,6 +19,7 @@ package bisq.apitest;
import java.net.InetAddress;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
@ -72,6 +73,16 @@ public class ApiTestCase {
// gRPC service stubs are used by method & scenario tests, but not e2e tests.
private static final Map<BisqAppConfig, GrpcStubs> grpcStubsCache = new HashMap<>();
public static void setUpScaffold(File callRateMeteringConfigFile,
Enum<?>... supportingApps)
throws InterruptedException, ExecutionException, IOException {
scaffold = new Scaffold(stream(supportingApps).map(Enum::name)
.collect(Collectors.joining(",")))
.setUp();
config = scaffold.config;
bitcoinCli = new BitcoinCliHelper((config));
}
public static void setUpScaffold(Enum<?>... supportingApps)
throws InterruptedException, ExecutionException, IOException {
scaffold = new Scaffold(stream(supportingApps).map(Enum::name)

View file

@ -0,0 +1,129 @@
/*
* 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.apitest.method;
import io.grpc.StatusRuntimeException;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import bisq.daemon.grpc.GrpcVersionService;
import bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CallRateMeteringInterceptorTest extends MethodTest {
private static final GetVersionTest getVersionTest = new GetVersionTest();
@BeforeAll
public static void setUp() {
File callRateMeteringConfigFile = buildInterceptorConfigFile();
startSupportingApps(callRateMeteringConfigFile,
false,
false,
bitcoind, alicedaemon);
}
@BeforeEach
public void sleep200Milliseconds() {
sleep(200);
}
@Test
@Order(1)
public void testGetVersionCall1IsAllowed() {
getVersionTest.testGetVersion();
}
@Test
@Order(2)
public void testGetVersionCall2ShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
exception.getMessage());
}
@Test
@Order(3)
public void testGetVersionCall3ShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
exception.getMessage());
}
@Test
@Order(4)
public void testGetVersionCall4IsAllowed() {
sleep(1100); // Let the server's rate meter reset the call count.
getVersionTest.testGetVersion();
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
public static File buildInterceptorConfigFile() {
GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"getVersion",
1,
SECONDS);
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"shouldNotBreakAnything",
1000,
DAYS);
// Only GrpcVersionService is @VisibleForTesting, so we hardcode the class names.
builder.addCallRateMeter("GrpcOffersService",
"createOffer",
5,
MINUTES);
builder.addCallRateMeter("GrpcOffersService",
"takeOffer",
10,
DAYS);
builder.addCallRateMeter("GrpcTradesService",
"withdrawFunds",
3,
HOURS);
return builder.build();
}
}

View file

@ -39,6 +39,7 @@ import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.KeepFundsRequest;
@ -48,10 +49,12 @@ import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
@ -64,6 +67,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
@ -96,37 +100,62 @@ public class MethodTest extends ApiTestCase {
private static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver();
private static final Function<Enum<?>[], String> toNameList = (enums) ->
stream(enums).map(Enum::name).collect(Collectors.joining(","));
public static void startSupportingApps(File callRateMeteringConfigFile,
boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
try {
setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps),
"--callRateMeteringConfigPath", callRateMeteringConfigFile.getAbsolutePath(),
"--enableBisqDebugging", "false"
});
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
} catch (Exception ex) {
fail(ex);
}
}
public static void startSupportingApps(boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
try {
// To run Bisq apps in debug mode, use the other setUpScaffold method:
// setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon",
// "--enableBisqDebugging", "true"});
setUpScaffold(supportingApps);
if (registerDisputeAgents) {
registerDisputeAgents(arbdaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(alicedaemon.name()))) {
aliceStubs = grpcStubs(alicedaemon);
alicesDummyAcct = getDefaultPerfectDummyPaymentAccount(alicedaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(bobdaemon.name()))) {
bobStubs = grpcStubs(bobdaemon);
bobsDummyAcct = getDefaultPerfectDummyPaymentAccount(bobdaemon);
}
// Generate 1 regtest block for alice's and/or bob's wallet to
// show 10 BTC balance, and allow time for daemons parse the new block.
if (generateBtcBlock)
genBtcBlocksThenWait(1, 1500);
setUpScaffold(new String[]{
"--supportingApps", toNameList.apply(supportingApps),
"--enableBisqDebugging", "false"
});
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
} catch (Exception ex) {
fail(ex);
}
}
private static void doPostStartup(boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
if (registerDisputeAgents) {
registerDisputeAgents(arbdaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(alicedaemon.name()))) {
aliceStubs = grpcStubs(alicedaemon);
alicesDummyAcct = getDefaultPerfectDummyPaymentAccount(alicedaemon);
}
if (stream(supportingApps).map(Enum::name).anyMatch(name -> name.equals(bobdaemon.name()))) {
bobStubs = grpcStubs(bobdaemon);
bobsDummyAcct = getDefaultPerfectDummyPaymentAccount(bobdaemon);
}
// Generate 1 regtest block for alice's and/or bob's wallet to
// show 10 BTC balance, and allow time for daemons parse the new block.
if (generateBtcBlock)
genBtcBlocksThenWait(1, 1500);
}
// Convenience methods for building gRPC request objects
protected final GetBalancesRequest createGetBalancesRequest(String currencyCode) {
return GetBalancesRequest.newBuilder().setCurrencyCode(currencyCode).build();
@ -160,8 +189,26 @@ public class MethodTest extends ApiTestCase {
return GetUnusedBsqAddressRequest.newBuilder().build();
}
protected final SendBsqRequest createSendBsqRequest(String address, String amount) {
return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build();
protected final SendBsqRequest createSendBsqRequest(String address,
String amount,
String txFeeRate) {
return SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
}
protected final SendBtcRequest createSendBtcRequest(String address,
String amount,
String txFeeRate,
String memo) {
return SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
@ -208,10 +255,13 @@ public class MethodTest extends ApiTestCase {
.build();
}
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, String address) {
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId,
String address,
String memo) {
return WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
}
@ -247,9 +297,36 @@ public class MethodTest extends ApiTestCase {
return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress();
}
protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, String amount) {
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount) {
return sendBsq(bisqAppConfig, address, amount, "");
}
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount));
return grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address,
amount,
txFeeRate))
.getTxInfo();
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig, String address, String amount) {
return sendBtc(bisqAppConfig, address, amount, "", "");
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate,
String memo) {
//noinspection ResultOfMethodCallIgnored
return grpcStubs(bisqAppConfig).walletsService.sendBtc(
createSendBtcRequest(address, amount, txFeeRate, memo))
.getTxInfo();
}
protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) {
@ -354,8 +431,11 @@ public class MethodTest extends ApiTestCase {
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void withdrawFunds(BisqAppConfig bisqAppConfig, String tradeId, String address) {
var req = createWithdrawFundsRequest(tradeId, address);
protected final void withdrawFunds(BisqAppConfig bisqAppConfig,
String tradeId,
String address,
String memo) {
var req = createWithdrawFundsRequest(tradeId, address, memo);
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
}
@ -379,6 +459,11 @@ public class MethodTest extends ApiTestCase {
grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxInfo getTransaction(BisqAppConfig bisqAppConfig, String txId) {
var req = GetTransactionRequest.newBuilder().setTxId(txId).build();
return grpcStubs(bisqAppConfig).walletsService.getTransaction(req).getTxInfo();
}
// Static conveniences for test methods and test case fixture setups.
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {

View file

@ -65,9 +65,12 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.core.locale.CurrencyUtil.*;
import static bisq.core.locale.CurrencyUtil.getAllAdvancedCashCurrencies;
import static bisq.core.locale.CurrencyUtil.getAllMoneyGramCurrencies;
import static bisq.core.locale.CurrencyUtil.getAllRevolutCurrencies;
import static bisq.core.locale.CurrencyUtil.getAllUpholdCurrencies;
import static bisq.core.payment.payload.PaymentMethod.*;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@ -746,7 +749,10 @@ public class CreatePaymentAccountTest extends AbstractPaymentAccountTest {
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount);
// As per commit 88f26f93241af698ae689bf081205d0f9dc929fa
// Do not autofill all currencies by default but keep all unselected.
// verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount);
assertEquals(0, paymentAccount.getTradeCurrencies().size());
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);

View file

@ -64,9 +64,11 @@ public class AbstractTradeTest extends AbstractOfferTest {
TestInfo testInfo,
String description,
TradeInfo trade) {
log.info(String.format("%s %s%n%s",
testName(testInfo),
description.toUpperCase(),
format(trade)));
if (log.isDebugEnabled()) {
log.debug(String.format("%s %s%n%s",
testName(testInfo),
description.toUpperCase(),
format(trade)));
}
}
}

View file

@ -52,6 +52,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = "btc";
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
@Test
@Order(1)
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
@ -147,7 +149,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
String toAddress = bitcoinCli.getNewBtcAddress();
withdrawFunds(bobdaemon, tradeId, toAddress);
withdrawFunds(bobdaemon, tradeId, toAddress, WITHDRAWAL_TX_MEMO);
genBtcBlocksThenWait(1, 2250);
@ -158,7 +160,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
log.info("{} Bob's current available balance: {} BTC",
log.debug("{} Bob's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(currentBalance.getAvailableBalance()));
}

View file

@ -108,7 +108,7 @@ public class BsqWalletTest extends MethodTest {
@Order(3)
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT);
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT, "100");
sleep(2000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);

View file

@ -1,6 +1,7 @@
package bisq.apitest.method.wallet;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.TxInfo;
import lombok.extern.slf4j.Slf4j;
@ -20,6 +21,8 @@ import static bisq.cli.TableFormat.formatAddressBalanceTbl;
import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
@ -31,6 +34,8 @@ import bisq.apitest.method.MethodTest;
@TestMethodOrder(OrderAnnotation.class)
public class BtcWalletTest extends MethodTest {
private static final String TX_MEMO = "tx memo";
// All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets
// are initialized with 10 BTC during the scaffolding setup.
private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
@ -92,6 +97,50 @@ public class BtcWalletTest extends MethodTest {
formatBtcBalanceInfoTbl(btcBalanceInfo));
}
@Test
@Order(3)
public void testAliceSendBTCToBob(TestInfo testInfo) {
String bobsBtcAddress = getUnusedBtcAddress(bobdaemon);
log.debug("Sending 5.5 BTC From Alice to Bob @ {}", bobsBtcAddress);
TxInfo txInfo = sendBtc(alicedaemon,
bobsBtcAddress,
"5.50",
"100",
TX_MEMO);
assertTrue(txInfo.getIsPending());
// Note that the memo is not set on the tx yet.
assertTrue(txInfo.getMemo().isEmpty());
genBtcBlocksThenWait(1, 3000);
// Fetch the tx and check for confirmation and memo.
txInfo = getTransaction(alicedaemon, txInfo.getTxId());
assertFalse(txInfo.getIsPending());
assertEquals(TX_MEMO, txInfo.getMemo());
BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon);
log.debug("{} Alice's BTC Balances:\n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(alicesBalances));
bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances =
bisq.core.api.model.BtcBalanceInfo.valueOf(700000000,
0,
700000000,
0);
verifyBtcBalances(alicesExpectedBalances, alicesBalances);
BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon);
log.debug("{} Bob's BTC Balances:\n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(bobsBalances));
// We cannot (?) predict the exact tx size and calculate how much in tx fees were
// deducted from the 5.5 BTC sent to Bob, but we do know Bob should have something
// between 15.49978000 and 15.49978100 BTC.
assertTrue(bobsBalances.getAvailableBalance() >= 1549978000);
assertTrue(bobsBalances.getAvailableBalance() <= 1549978100);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();

View file

@ -17,6 +17,8 @@
package bisq.apitest.scenario;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
@ -30,10 +32,12 @@ import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.apitest.method.CallRateMeteringInterceptorTest.buildInterceptorConfigFile;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.method.CallRateMeteringInterceptorTest;
import bisq.apitest.method.GetVersionTest;
import bisq.apitest.method.MethodTest;
import bisq.apitest.method.RegisterDisputeAgentsTest;
@ -46,7 +50,11 @@ public class StartupTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon);
File callRateMeteringConfigFile = buildInterceptorConfigFile();
startSupportingApps(callRateMeteringConfigFile,
false,
false,
bitcoind, seednode, arbdaemon, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
@ -54,13 +62,27 @@ public class StartupTest extends MethodTest {
@Test
@Order(1)
public void testCallRateMeteringInterceptor() {
CallRateMeteringInterceptorTest test = new CallRateMeteringInterceptorTest();
test.testGetVersionCall1IsAllowed();
test.sleep200Milliseconds();
test.testGetVersionCall2ShouldThrowException();
test.sleep200Milliseconds();
test.testGetVersionCall3ShouldThrowException();
test.sleep200Milliseconds();
test.testGetVersionCall4IsAllowed();
sleep(1000); // Wait 1 second before calling getversion in next test.
}
@Test
@Order(2)
public void testGetVersion() {
GetVersionTest test = new GetVersionTest();
test.testGetVersion();
}
@Test
@Order(2)
@Order(3)
public void testRegisterDisputeAgents() {
RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest();
test.testRegisterArbitratorShouldThrowException();

View file

@ -67,6 +67,7 @@ public class WalletTest extends MethodTest {
btcWalletTest.testInitialBtcBalances(testInfo);
btcWalletTest.testFundAlicesBtcWallet(testInfo);
btcWalletTest.testAliceSendBTCToBob(testInfo);
}
@Test

View file

@ -33,7 +33,7 @@ configure(subprojects) {
ext { // in alphabetical order
bcVersion = '1.63'
bitcoinjVersion = 'dcf8af0'
bitcoinjVersion = '2a80db4'
btcdCli4jVersion = '27b94333'
codecVersion = '1.13'
easybindVersion = '1.0.3'
@ -392,7 +392,7 @@ configure(project(':desktop')) {
apply from: '../gradle/witness/gradle-witness.gradle'
apply from: 'package/package.gradle'
version = '1.5.1-SNAPSHOT'
version = '1.5.4-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,
@ -602,6 +602,12 @@ configure(project(':daemon')) {
compileOnly "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
}
}

View file

@ -31,6 +31,7 @@ import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
@ -40,9 +41,11 @@ import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
@ -110,9 +113,11 @@ public class CliMain {
getfundingaddresses,
getunusedbsqaddress,
sendbsq,
sendbtc,
gettxfeerate,
settxfeerate,
unsettxfeerate,
gettransaction,
lockwallet,
unlockwallet,
removewalletpassword,
@ -259,19 +264,56 @@ public class CliMain {
throw new IllegalArgumentException("no bsq amount specified");
var amount = nonOptionArgs.get(2);
verifyStringIsValidDecimal(amount);
try {
Double.parseDouble(amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", amount));
}
var txFeeRate = nonOptionArgs.size() == 4 ? nonOptionArgs.get(3) : "";
if (!txFeeRate.isEmpty())
verifyStringIsValidLong(txFeeRate);
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
walletsService.sendBsq(request);
out.printf("%s BSQ sent to %s%n", amount, address);
var reply = walletsService.sendBsq(request);
TxInfo txInfo = reply.getTxInfo();
out.printf("%s bsq sent to %s in tx %s%n",
amount,
address,
txInfo.getTxId());
return;
}
case sendbtc: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no btc address specified");
var address = nonOptionArgs.get(1);
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("no btc amount specified");
var amount = nonOptionArgs.get(2);
verifyStringIsValidDecimal(amount);
// TODO Find a better way to handle the two optional parameters.
var txFeeRate = nonOptionArgs.size() >= 4 ? nonOptionArgs.get(3) : "";
if (!txFeeRate.isEmpty())
verifyStringIsValidLong(txFeeRate);
var memo = nonOptionArgs.size() == 5 ? nonOptionArgs.get(4) : "";
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
var reply = walletsService.sendBtc(request);
TxInfo txInfo = reply.getTxInfo();
out.printf("%s btc sent to %s in tx %s%n",
amount,
address,
txInfo.getTxId());
return;
}
case gettxfeerate: {
@ -284,13 +326,7 @@ public class CliMain {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no tx fee rate specified");
long txFeeRate;
try {
txFeeRate = Long.parseLong(nonOptionArgs.get(2));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
}
var txFeeRate = toLong(nonOptionArgs.get(2));
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
@ -304,6 +340,18 @@ public class CliMain {
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
return;
}
case gettransaction: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no tx id specified");
var txId = nonOptionArgs.get(1);
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
var reply = walletsService.getTransaction(request);
out.println(TransactionFormat.format(reply.getTxInfo()));
return;
}
case createoffer: {
if (nonOptionArgs.size() < 9)
throw new IllegalArgumentException("incorrect parameter count,"
@ -413,7 +461,7 @@ public class CliMain {
return;
}
case gettrade: {
// TODO make short-id a valid argument
// TODO make short-id a valid argument?
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("incorrect parameter count, "
+ " expecting trade id [,showcontract = true|false]");
@ -472,16 +520,21 @@ public class CliMain {
case withdrawfunds: {
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("incorrect parameter count, "
+ " expecting trade id, bitcoin wallet address");
+ " expecting trade id, bitcoin wallet address [,\"memo\"]");
var tradeId = nonOptionArgs.get(1);
var address = nonOptionArgs.get(2);
// A multi-word memo must be double quoted.
var memo = nonOptionArgs.size() == 4
? nonOptionArgs.get(3)
: "";
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
tradesService.withdrawFunds(request);
out.printf("funds from trade %s sent to btc address %s%n", tradeId, address);
out.printf("trade %s funds sent to btc address %s%n", tradeId, address);
return;
}
case getpaymentmethods: {
@ -560,12 +613,7 @@ public class CliMain {
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("no unlock timeout specified");
long timeout;
try {
timeout = Long.parseLong(nonOptionArgs.get(2));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
}
var timeout = toLong(nonOptionArgs.get(2));
var request = UnlockWalletRequest.newBuilder()
.setPassword(nonOptionArgs.get(1))
.setTimeout(timeout).build();
@ -627,6 +675,30 @@ public class CliMain {
return Method.valueOf(methodName.toLowerCase());
}
private static void verifyStringIsValidDecimal(String param) {
try {
Double.parseDouble(param);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("'%s' is not a number", param));
}
}
private static void verifyStringIsValidLong(String param) {
try {
Long.parseLong(param);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("'%s' is not a number", param));
}
}
private static long toLong(String param) {
try {
return Long.parseLong(param);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("'%s' is not a number", param));
}
}
private static File saveFileToDisk(String prefix,
@SuppressWarnings("SameParameterValue") String suffix,
String text) {
@ -663,10 +735,12 @@ public class CliMain {
stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance");
stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses");
stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address");
stream.format(rowFormat, "sendbsq", "address, amount", "Send BSQ");
stream.format(rowFormat, "sendbsq", "address, amount [,tx fee rate (sats/byte)]", "Send BSQ");
stream.format(rowFormat, "sendbtc", "address, amount [,tx fee rate (sats/byte), \"memo\"]", "Send BTC");
stream.format(rowFormat, "gettxfeerate", "", "Get current tx fee rate in sats/byte");
stream.format(rowFormat, "settxfeerate", "satoshis (per byte)", "Set custom tx fee rate in sats/byte");
stream.format(rowFormat, "unsettxfeerate", "", "Unset custom tx fee rate");
stream.format(rowFormat, "gettransaction", "transaction id", "Get transaction with id");
stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer");
stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", "");
stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), security deposit (%) \\", "");
@ -679,7 +753,8 @@ public class CliMain {
stream.format(rowFormat, "confirmpaymentstarted", "trade id", "Confirm payment started");
stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received");
stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet");
stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address", "Withdraw received funds to external wallet address");
stream.format(rowFormat, "withdrawfunds", "trade id, bitcoin wallet address [,\"memo\"]",
"Withdraw received funds to external wallet address");
stream.format(rowFormat, "getpaymentmethods", "", "Get list of supported payment account method ids");
stream.format(rowFormat, "getpaymentacctform", "payment method id", "Get a new payment account form");
stream.format(rowFormat, "createpaymentacct", "path to payment account form", "Create a new payment account");

View file

@ -59,6 +59,16 @@ class ColumnHeaderConstants {
static final String COL_HEADER_TRADE_SHORT_ID = "ID";
static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)";
static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)";
static final String COL_HEADER_TRADE_WITHDRAWAL_TX_ID = "Withdrawal TX ID";
static final String COL_HEADER_TX_ID = "Tx ID";
static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)";
static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)";
static final String COL_HEADER_TX_FEE = "Tx Fee (BTC)";
static final String COL_HEADER_TX_SIZE = "Tx Size (Bytes)";
static final String COL_HEADER_TX_IS_CONFIRMED = "Is Confirmed";
static final String COL_HEADER_TX_MEMO = "Memo";
static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' ');
static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');
}

View file

@ -66,18 +66,19 @@ public class TradeFormat {
? String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode, baseCurrencyCode)
: String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode);
String colDataFormat = "%-" + shortIdColWidth + "s" // left justify
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left justify
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // right justify
+ "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // right justify
+ "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // right justify
+ takerFeeHeader.get() // right justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // left justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // left justify
+ " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // left justify
+ " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // left justify
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // left justify
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // left justify
String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify
+ "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify
+ "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify
+ takerFeeHeader.get() // rt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // lt justify
return headerLine +
(isTaker

View file

@ -0,0 +1,59 @@
/*
* 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.cli;
import bisq.proto.grpc.TxInfo;
import com.google.common.annotations.VisibleForTesting;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static com.google.common.base.Strings.padEnd;
@VisibleForTesting
public class TransactionFormat {
public static String format(TxInfo txInfo) {
String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER
+ COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER
+ COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER
+ COL_HEADER_TX_FEE + COL_HEADER_DELIMITER
+ COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER
+ (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER)
+ "\n";
String colDataFormat = "%-" + txInfo.getTxId().length() + "s"
+ " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s"
+ " %" + COL_HEADER_TX_INPUT_SUM.length() + "s"
+ " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s"
+ " %" + COL_HEADER_TX_FEE.length() + "s"
+ " %" + COL_HEADER_TX_SIZE.length() + "s"
+ " %s";
return headerLine
+ String.format(colDataFormat,
txInfo.getTxId(),
txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed
formatSatoshis(txInfo.getInputSum()),
formatSatoshis(txInfo.getOutputSum()),
formatSatoshis(txInfo.getFee()),
txInfo.getSize(),
txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo());
}
}

View file

@ -22,25 +22,26 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AsciiLogo {
public static void showAsciiLogo() {
log.info("\n\n" +
" ........ ...... \n" +
" .............. ...... \n" +
" ................. ...... \n" +
" ...... .......... .. ...... \n" +
" ...... ...... ...... ............... ..... ......... .......... \n" +
" ....... ........ .................. ..... ............. ............... \n" +
" ...... ........ .......... ....... ..... ...... ... ........ ....... \n" +
" ...... ..... ....... ..... ..... ..... ..... ...... \n" +
" ...... ... ... ...... ...... ..... ........... ...... ...... \n" +
" ...... ..... .... ...... ...... ..... ............ ..... ...... \n" +
" ...... ..... ...... ..... ........ ...... ...... \n" +
" ...... .... ... ...... ...... ..... .. ...... ...... ........ \n" +
" ........ .. ....... ................. ..... .............. ................... \n" +
" .......... ......... ............. ..... ............ ................. \n" +
" ...................... ..... .... .... ...... \n" +
" ................ ...... \n" +
" .... ...... \n" +
" ...... \n" +
"\n\n");
String ls = System.lineSeparator();
log.info(ls + ls +
" ........ ...... " + ls +
" .............. ...... " + ls +
" ................. ...... " + ls +
" ...... .......... .. ...... " + ls +
" ...... ...... ...... ............... ..... ......... .......... " + ls +
" ....... ........ .................. ..... ............. ............... " + ls +
" ...... ........ .......... ....... ..... ...... ... ........ ....... " + ls +
" ...... ..... ....... ..... ..... ..... ..... ...... " + ls +
" ...... ... ... ...... ...... ..... ........... ...... ...... " + ls +
" ...... ..... .... ...... ...... ..... ............ ..... ...... " + ls +
" ...... ..... ...... ..... ........ ...... ...... " + ls +
" ...... .... ... ...... ...... ..... .. ...... ...... ........ " + ls +
" ........ .. ....... ................. ..... .............. ................... " + ls +
" .......... ......... ............. ..... ............ ................. " + ls +
" ...................... ..... .... .... ...... " + ls +
" ................ ...... " + ls +
" .... ...... " + ls +
" ...... " + ls +
ls + ls);
}
}

View file

@ -30,14 +30,14 @@ public class Version {
// VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update
// Therefore all sub versions start again with 1
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.5.1";
public static final String VERSION = "1.5.4";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
* This must not contain each version but only those where we add new version-tagged resource files for
* historical data stores.
*/
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0");
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0", "1.5.2");
public static int getMajorVersion(String version) {
return getSubVersion(version, 0);

View file

@ -375,7 +375,10 @@ public class PersistenceManager<T extends PersistableEnvelope> {
// reference to the persistable object.
getWriteToDiskExecutor().execute(() -> writeToDisk(serialized, completeHandler));
log.info("Serializing {} took {} msec", fileName, System.currentTimeMillis() - ts);
long duration = System.currentTimeMillis() - ts;
if (duration > 100) {
log.info("Serializing {} took {} msec", fileName, duration);
}
} catch (Throwable e) {
log.error("Error in saveToFile toProtoMessage: {}, {}", persistable.getClass().getSimpleName(), fileName);
e.printStackTrace();
@ -437,7 +440,10 @@ public class PersistenceManager<T extends PersistableEnvelope> {
e.printStackTrace();
log.error("Cannot close resources." + e.getMessage());
}
log.info("Writing the serialized {} completed in {} msec", fileName, System.currentTimeMillis() - ts);
long duration = System.currentTimeMillis() - ts;
if (duration > 100) {
log.info("Writing the serialized {} completed in {} msec", fileName, duration);
}
persistenceRequested = false;
if (completeHandler != null) {
UserThread.execute(completeHandler);

View file

@ -51,6 +51,7 @@ import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@ -61,7 +62,6 @@ import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ -74,11 +74,15 @@ public class SignedWitnessService {
private final P2PService p2PService;
private final ArbitratorManager arbitratorManager;
private final User user;
@Getter
private final Map<P2PDataStorage.ByteArray, SignedWitness> signedWitnessMap = new HashMap<>();
private final FilterManager filterManager;
private final Map<P2PDataStorage.ByteArray, SignedWitness> signedWitnessMap = new HashMap<>();
// This map keeps all SignedWitnesses with the same AccountAgeWitnessHash in a Set.
// This avoids iterations over the signedWitnessMap for getting the set of such SignedWitnesses.
private final Map<P2PDataStorage.ByteArray, Set<SignedWitness>> signedWitnessSetByAccountAgeWitnessHash = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -142,6 +146,10 @@ public class SignedWitnessService {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public Collection<SignedWitness> getSignedWitnessMapValues() {
return signedWitnessMap.values();
}
/**
* List of dates as long when accountAgeWitness was signed
*
@ -199,7 +207,7 @@ public class SignedWitnessService {
@VisibleForTesting
public Set<SignedWitness> getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey) {
return signedWitnessMap.values().stream()
return getSignedWitnessMapValues().stream()
.filter(e -> Arrays.equals(e.getWitnessOwnerPubKey(), ownerPubKey))
.collect(Collectors.toSet());
}
@ -344,30 +352,27 @@ public class SignedWitnessService {
}
}
private Set<SignedWitness> getSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
.filter(e -> Arrays.equals(e.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.collect(Collectors.toSet());
public Set<SignedWitness> getSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
P2PDataStorage.ByteArray key = new P2PDataStorage.ByteArray(accountAgeWitness.getHash());
return signedWitnessSetByAccountAgeWitnessHash.getOrDefault(key, new HashSet<>());
}
// SignedWitness objects signed by arbitrators
public Set<SignedWitness> getArbitratorsSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
return getSignedWitnessSet(accountAgeWitness).stream()
.filter(SignedWitness::isSignedByArbitrator)
.filter(e -> Arrays.equals(e.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.collect(Collectors.toSet());
}
// SignedWitness objects signed by any other peer
public Set<SignedWitness> getTrustedPeerSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
return getSignedWitnessSet(accountAgeWitness).stream()
.filter(e -> !e.isSignedByArbitrator())
.filter(e -> Arrays.equals(e.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.collect(Collectors.toSet());
}
public Set<SignedWitness> getRootSignedWitnessSet(boolean includeSignedByArbitrator) {
return signedWitnessMap.values().stream()
return getSignedWitnessMapValues().stream()
.filter(witness -> getSignedWitnessSetByOwnerPubKey(witness.getSignerPubKey(), new Stack<>()).isEmpty())
.filter(witness -> includeSignedByArbitrator ||
witness.getVerificationMethod() != SignedWitness.VerificationMethod.ARBITRATOR)
@ -388,7 +393,7 @@ public class SignedWitnessService {
// witnessOwnerPubKey
private Set<SignedWitness> getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey,
Stack<P2PDataStorage.ByteArray> excluded) {
return signedWitnessMap.values().stream()
return getSignedWitnessMapValues().stream()
.filter(e -> Arrays.equals(e.getWitnessOwnerPubKey(), ownerPubKey))
.filter(e -> !excluded.contains(new P2PDataStorage.ByteArray(e.getSignerPubKey())))
.collect(Collectors.toSet());
@ -487,8 +492,12 @@ public class SignedWitnessService {
///////////////////////////////////////////////////////////////////////////////////////////
@VisibleForTesting
void addToMap(SignedWitness signedWitness) {
public void addToMap(SignedWitness signedWitness) {
signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness);
P2PDataStorage.ByteArray accountAgeWitnessHash = new P2PDataStorage.ByteArray(signedWitness.getAccountAgeWitnessHash());
signedWitnessSetByAccountAgeWitnessHash.putIfAbsent(accountAgeWitnessHash, new HashSet<>());
signedWitnessSetByAccountAgeWitnessHash.get(accountAgeWitnessHash).add(signedWitness);
}
private void publishSignedWitness(SignedWitness signedWitness) {
@ -501,7 +510,22 @@ public class SignedWitnessService {
}
private void doRepublishAllSignedWitnesses() {
signedWitnessMap.forEach((e, signedWitness) -> p2PService.addPersistableNetworkPayload(signedWitness, true));
getSignedWitnessMapValues()
.forEach(signedWitness -> p2PService.addPersistableNetworkPayload(signedWitness, true));
}
@VisibleForTesting
public void removeSignedWitness(SignedWitness signedWitness) {
signedWitnessMap.remove(signedWitness.getHashAsByteArray());
P2PDataStorage.ByteArray accountAgeWitnessHash = new P2PDataStorage.ByteArray(signedWitness.getAccountAgeWitnessHash());
if (signedWitnessSetByAccountAgeWitnessHash.containsKey(accountAgeWitnessHash)) {
Set<SignedWitness> set = signedWitnessSetByAccountAgeWitnessHash.get(accountAgeWitnessHash);
set.remove(signedWitness);
if (set.isEmpty()) {
signedWitnessSetByAccountAgeWitnessHash.remove(accountAgeWitnessHash);
}
}
}
// Remove SignedWitnesses that are signed by TRADE that also have an ARBITRATOR signature

View file

@ -51,6 +51,7 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Coin;
@ -63,6 +64,8 @@ import com.google.common.annotations.VisibleForTesting;
import java.security.PublicKey;
import java.time.Clock;
import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
@ -73,6 +76,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -102,12 +106,12 @@ public class AccountAgeWitnessService {
PEER_SIGNER(Res.get("offerbook.timeSinceSigning.info.signer")),
BANNED(Res.get("offerbook.timeSinceSigning.info.banned"));
private String presentation;
private String displayString;
private String hash = "";
private long daysUntilLimitLifted = 0;
SignState(String presentation) {
this.presentation = presentation;
SignState(String displayString) {
this.displayString = displayString;
}
public SignState addHash(String hash) {
@ -120,11 +124,11 @@ public class AccountAgeWitnessService {
return this;
}
public String getPresentation() {
public String getDisplayString() {
if (!hash.isEmpty()) { // Only showing in DEBUG mode
return presentation + " " + hash;
return displayString + " " + hash;
}
return String.format(presentation, daysUntilLimitLifted);
return String.format(displayString, daysUntilLimitLifted);
}
}
@ -134,13 +138,19 @@ public class AccountAgeWitnessService {
private final User user;
private final SignedWitnessService signedWitnessService;
private final ChargeBackRisk chargeBackRisk;
private final AccountAgeWitnessStorageService accountAgeWitnessStorageService;
private final Clock clock;
private final FilterManager filterManager;
@Getter
private final AccountAgeWitnessUtils accountAgeWitnessUtils;
@Getter
private final Map<P2PDataStorage.ByteArray, AccountAgeWitness> accountAgeWitnessMap = new HashMap<>();
// The accountAgeWitnessMap is very large (70k items) and access is a bit expensive. We usually only access less
// than 100 items, those who have offers online. So we use a cache for a fast lookup and only if
// not found there we use the accountAgeWitnessMap and put then the new item into our cache.
private final Map<P2PDataStorage.ByteArray, AccountAgeWitness> accountAgeWitnessCache = new ConcurrentHashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -155,12 +165,15 @@ public class AccountAgeWitnessService {
ChargeBackRisk chargeBackRisk,
AccountAgeWitnessStorageService accountAgeWitnessStorageService,
AppendOnlyDataStoreService appendOnlyDataStoreService,
Clock clock,
FilterManager filterManager) {
this.keyRing = keyRing;
this.p2PService = p2PService;
this.user = user;
this.signedWitnessService = signedWitnessService;
this.chargeBackRisk = chargeBackRisk;
this.accountAgeWitnessStorageService = accountAgeWitnessStorageService;
this.clock = clock;
this.filterManager = filterManager;
accountAgeWitnessUtils = new AccountAgeWitnessUtils(
@ -184,10 +197,10 @@ public class AccountAgeWitnessService {
});
// At startup the P2PDataStorage initializes earlier, otherwise we get the listener called.
p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().forEach(e -> {
if (e instanceof AccountAgeWitness)
addToMap((AccountAgeWitness) e);
});
accountAgeWitnessStorageService.getMapOfAllData().values().stream()
.filter(e -> e instanceof AccountAgeWitness)
.map(e -> (AccountAgeWitness) e)
.forEach(this::addToMap);
if (p2PService.isBootstrapped()) {
onBootStrapped();
@ -211,13 +224,18 @@ public class AccountAgeWitnessService {
private void republishAllFiatAccounts() {
if (user.getPaymentAccounts() != null)
user.getPaymentAccounts().stream()
.filter(e -> !(e instanceof AssetAccount))
.forEach(e -> {
// We delay with a random interval of 20-60 sec to ensure to be better connected and don't
// stress the P2P network with publishing all at once at startup time.
final int delayInSec = 20 + new Random().nextInt(40);
UserThread.runAfter(() -> p2PService.addPersistableNetworkPayload(getMyWitness(
e.getPaymentAccountPayload()), true), delayInSec);
.filter(account -> !(account instanceof AssetAccount))
.forEach(account -> {
AccountAgeWitness myWitness = getMyWitness(account.getPaymentAccountPayload());
// We only publish if the date of our witness is inside the date tolerance.
// It would be rejected otherwise from the peers.
if (myWitness.isDateInTolerance(clock)) {
// We delay with a random interval of 20-60 sec to ensure to be better connected and don't
// stress the P2P network with publishing all at once at startup time.
int delayInSec = 20 + new Random().nextInt(40);
UserThread.runAfter(() ->
p2PService.addPersistableNetworkPayload(myWitness, true), delayInSec);
}
});
}
@ -233,8 +251,17 @@ public class AccountAgeWitnessService {
public void publishMyAccountAgeWitness(PaymentAccountPayload paymentAccountPayload) {
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccountPayload);
if (!accountAgeWitnessMap.containsKey(accountAgeWitness.getHashAsByteArray()))
P2PDataStorage.ByteArray hash = accountAgeWitness.getHashAsByteArray();
// We use first our fast lookup cache. If its in accountAgeWitnessCache it is also in accountAgeWitnessMap
// and we do not publish.
if (accountAgeWitnessCache.containsKey(hash)) {
return;
}
if (!accountAgeWitnessMap.containsKey(hash)) {
p2PService.addPersistableNetworkPayload(accountAgeWitness, false);
}
}
public byte[] getPeerAccountAgeWitnessHash(Trade trade) {
@ -265,7 +292,7 @@ public class AccountAgeWitnessService {
return getWitnessByHash(hash);
}
private Optional<AccountAgeWitness> findWitness(Offer offer) {
public Optional<AccountAgeWitness> findWitness(Offer offer) {
final Optional<String> accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex();
return accountAgeWitnessHash.isPresent() ?
getWitnessByHashAsHex(accountAgeWitnessHash.get()) :
@ -284,12 +311,21 @@ public class AccountAgeWitnessService {
private Optional<AccountAgeWitness> getWitnessByHash(byte[] hash) {
P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash);
final boolean containsKey = accountAgeWitnessMap.containsKey(hashAsByteArray);
if (!containsKey)
log.debug("hash not found in accountAgeWitnessMap");
// First we look up in our fast lookup cache
if (accountAgeWitnessCache.containsKey(hashAsByteArray)) {
return Optional.of(accountAgeWitnessCache.get(hashAsByteArray));
}
return accountAgeWitnessMap.containsKey(hashAsByteArray) ?
Optional.of(accountAgeWitnessMap.get(hashAsByteArray)) : Optional.empty();
if (accountAgeWitnessMap.containsKey(hashAsByteArray)) {
AccountAgeWitness accountAgeWitness = accountAgeWitnessMap.get(hashAsByteArray);
// We add it to our fast lookup cache
accountAgeWitnessCache.put(hashAsByteArray, accountAgeWitness);
return Optional.of(accountAgeWitness);
}
return Optional.empty();
}
private Optional<AccountAgeWitness> getWitnessByHashAsHex(String hashAsHex) {
@ -613,7 +649,10 @@ public class AccountAgeWitnessService {
if (!result) {
String msg = "The peers trade limit is less than the traded amount.\n" +
"tradeAmount=" + tradeAmount.toFriendlyString() +
"\nPeers trade limit=" + Coin.valueOf(peersCurrentTradeLimit).toFriendlyString();
"\nPeers trade limit=" + Coin.valueOf(peersCurrentTradeLimit).toFriendlyString() +
"\nOffer ID=" + offer.getShortId() +
"\nPaymentMethod=" + offer.getPaymentMethod().getId() +
"\nCurrencyCode=" + offer.getCurrencyCode();
log.warn(msg);
errorMessageHandler.handleErrorMessage(msg);
}
@ -653,16 +692,20 @@ public class AccountAgeWitnessService {
}
public String arbitratorSignOrphanWitness(AccountAgeWitness accountAgeWitness,
ECKey key,
ECKey ecKey,
long time) {
// Find AccountAgeWitness as signedwitness
var signedWitness = signedWitnessService.getSignedWitnessMap().values().stream()
.filter(sw -> Arrays.equals(sw.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
// TODO Is not found signedWitness considered an error case?
// Previous code version was throwing an exception in case no signedWitness was found...
// signAndPublishAccountAgeWitness returns an empty string in success case and error otherwise
return signedWitnessService.getSignedWitnessSet(accountAgeWitness).stream()
.findAny()
.orElse(null);
checkNotNull(signedWitness);
return signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, key,
signedWitness.getWitnessOwnerPubKey(), time);
.map(SignedWitness::getWitnessOwnerPubKey)
.map(witnessOwnerPubKey ->
signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, ecKey,
witnessOwnerPubKey, time)
)
.orElse("No signedWitness found");
}
public String arbitratorSignOrphanPubKey(ECKey key,
@ -877,4 +920,29 @@ public class AccountAgeWitnessService {
!peerHasSignedWitness(trade) &&
tradeAmountIsSufficient(trade.getTradeAmount());
}
public String getSignInfoFromAccount(PaymentAccount paymentAccount) {
var pubKey = keyRing.getSignatureKeyPair().getPublic();
var witness = getMyWitness(paymentAccount.getPaymentAccountPayload());
return Utilities.bytesAsHexString(witness.getHash()) + "," + Utilities.bytesAsHexString(pubKey.getEncoded());
}
public Tuple2<AccountAgeWitness, byte[]> getSignInfoFromString(String signInfo) {
var parts = signInfo.split(",");
if (parts.length != 2) {
return null;
}
byte[] pubKeyHash;
Optional<AccountAgeWitness> accountAgeWitness;
try {
var accountAgeWitnessHash = Utilities.decodeFromHex(parts[0]);
pubKeyHash = Utilities.decodeFromHex(parts[1]);
accountAgeWitness = getWitnessByHash(accountAgeWitnessHash);
return accountAgeWitness
.map(ageWitness -> new Tuple2<>(ageWitness, pubKeyHash))
.orElse(null);
} catch (Exception e) {
return null;
}
}
}

View file

@ -17,9 +17,8 @@
package bisq.core.account.witness;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.network.p2p.storage.persistence.MapStoreService;
import bisq.network.p2p.storage.persistence.HistoricalDataStoreService;
import bisq.common.config.Config;
import bisq.common.persistence.PersistenceManager;
@ -29,12 +28,10 @@ import javax.inject.Named;
import java.io.File;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AccountAgeWitnessStorageService extends MapStoreService<AccountAgeWitnessStore, PersistableNetworkPayload> {
public class AccountAgeWitnessStorageService extends HistoricalDataStoreService<AccountAgeWitnessStore> {
private static final String FILE_NAME = "AccountAgeWitnessStore";
@ -48,23 +45,19 @@ public class AccountAgeWitnessStorageService extends MapStoreService<AccountAgeW
super(storageDir, persistenceManager);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void initializePersistenceManager() {
persistenceManager.initialize(store, PersistenceManager.Source.NETWORK);
}
@Override
public String getFileName() {
return FILE_NAME;
}
@Override
public Map<P2PDataStorage.ByteArray, PersistableNetworkPayload> getMap() {
return store.getMap();
protected void initializePersistenceManager() {
persistenceManager.initialize(store, PersistenceManager.Source.NETWORK);
}
@Override

View file

@ -30,6 +30,7 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Utilities;
import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import java.util.Stack;
@ -67,12 +68,11 @@ public class AccountAgeWitnessUtils {
}
private void logChild(SignedWitness sigWit, String initString, Stack<P2PDataStorage.ByteArray> excluded) {
var allSig = signedWitnessService.getSignedWitnessMap();
log.info("{}AEW: {} PKH: {} time: {}", initString,
Utilities.bytesAsHexString(sigWit.getAccountAgeWitnessHash()).substring(0, 7),
Utilities.bytesAsHexString(Hash.getRipemd160hash(sigWit.getWitnessOwnerPubKey())).substring(0, 7),
sigWit.getDate());
allSig.values().forEach(w -> {
signedWitnessService.getSignedWitnessMapValues().forEach(w -> {
if (!excluded.contains(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())) &&
Arrays.equals(w.getSignerPubKey(), sigWit.getWitnessOwnerPubKey())) {
excluded.push(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey()));
@ -85,10 +85,10 @@ public class AccountAgeWitnessUtils {
// Log signers per
public void logSigners() {
log.info("Signers per AEW");
var allSig = signedWitnessService.getSignedWitnessMap();
allSig.values().forEach(w -> {
Collection<SignedWitness> signedWitnessMapValues = signedWitnessService.getSignedWitnessMapValues();
signedWitnessMapValues.forEach(w -> {
log.info("AEW {}", Utilities.bytesAsHexString(w.getAccountAgeWitnessHash()));
allSig.values().forEach(ww -> {
signedWitnessMapValues.forEach(ww -> {
if (Arrays.equals(w.getSignerPubKey(), ww.getWitnessOwnerPubKey())) {
log.info(" {}", Utilities.bytesAsHexString(ww.getAccountAgeWitnessHash()));
}
@ -123,7 +123,7 @@ public class AccountAgeWitnessUtils {
AccountAgeWitnessService.SignState signState =
accountAgeWitnessService.getSignState(accountAgeWitness.get());
return signState.name() + " " + signState.getPresentation() +
return signState.name() + " " + signState.getDisplayString() +
"\n" + accountAgeWitness.toString();
}

View file

@ -31,22 +31,25 @@ import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.util.concurrent.FutureCallback;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/**
* Provides high level interface to functionality of core Bisq features.
* E.g. useful for different APIs to access data of different domains of Bisq.
@ -55,6 +58,8 @@ import javax.annotation.Nullable;
@Slf4j
public class CoreApi {
@Getter
private final Config config;
private final CoreDisputeAgentsService coreDisputeAgentsService;
private final CoreOffersService coreOffersService;
private final CorePaymentAccountsService paymentAccountsService;
@ -64,13 +69,15 @@ public class CoreApi {
private final TradeStatisticsManager tradeStatisticsManager;
@Inject
public CoreApi(CoreDisputeAgentsService coreDisputeAgentsService,
public CoreApi(Config config,
CoreDisputeAgentsService coreDisputeAgentsService,
CoreOffersService coreOffersService,
CorePaymentAccountsService paymentAccountsService,
CorePriceService corePriceService,
CoreTradesService coreTradesService,
CoreWalletsService walletsService,
TradeStatisticsManager tradeStatisticsManager) {
this.config = config;
this.coreDisputeAgentsService = coreDisputeAgentsService;
this.coreOffersService = coreOffersService;
this.paymentAccountsService = paymentAccountsService;
@ -210,7 +217,7 @@ public class CoreApi {
coreTradesService.keepFunds(tradeId);
}
public void withdrawFunds(String tradeId, String address, @Nullable String memo) {
public void withdrawFunds(String tradeId, String address, String memo) {
coreTradesService.withdrawFunds(tradeId, address, memo);
}
@ -246,8 +253,19 @@ public class CoreApi {
return walletsService.getUnusedBsqAddress();
}
public void sendBsq(String address, String amount, TxBroadcaster.Callback callback) {
walletsService.sendBsq(address, amount, callback);
public void sendBsq(String address,
String amount,
String txFeeRate,
TxBroadcaster.Callback callback) {
walletsService.sendBsq(address, amount, txFeeRate, callback);
}
public void sendBtc(String address,
String amount,
String txFeeRate,
String memo,
FutureCallback<Transaction> callback) {
walletsService.sendBtc(address, amount, txFeeRate, memo, callback);
}
public void getTxFeeRate(ResultHandler resultHandler) {
@ -267,6 +285,10 @@ public class CoreApi {
return walletsService.getMostRecentTxFeeRateInfo();
}
public Transaction getTransaction(String txId) {
return walletsService.getTransaction(txId);
}
public void setWalletPassword(String password, String newPassword) {
walletsService.setWalletPassword(password, newPassword);
}

View file

@ -182,9 +182,11 @@ class CoreOffersService {
double buyerSecurityDeposit,
boolean useSavingsWallet,
Consumer<Transaction> resultHandler) {
// TODO add support for triggerPrice parameter. If value is 0 it is interpreted as not used. Its an optional value
openOfferManager.placeOffer(offer,
buyerSecurityDeposit,
useSavingsWallet,
0,
resultHandler::accept,
log::error);

View file

@ -41,8 +41,6 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
import static java.lang.String.format;
@ -85,6 +83,8 @@ class CoreTradesService {
String paymentAccountId,
String takerFeeCurrencyCode,
Consumer<Trade> resultHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode);
@ -149,6 +149,9 @@ class CoreTradesService {
}
void keepFunds(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
@ -156,8 +159,10 @@ class CoreTradesService {
tradeManager.onTradeCompleted(trade);
}
void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) {
// An encrypted wallet must be unlocked for this operation.
void withdrawFunds(String tradeId, String toAddress, String memo) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
@ -172,21 +177,21 @@ class CoreTradesService {
var receiverAmount = amount.subtract(fee);
log.info(format("Withdrawing funds received from trade %s:"
+ "%n From %s%n To %s%n Amt %s%n Tx Fee %s%n Receiver Amt %s",
+ "%n From %s%n To %s%n Amt %s%n Tx Fee %s%n Receiver Amt %s%n Memo %s%n",
tradeId,
fromAddressEntry.getAddressString(),
toAddress,
amount.toFriendlyString(),
fee.toFriendlyString(),
receiverAmount.toFriendlyString()));
receiverAmount.toFriendlyString(),
memo));
tradeManager.onWithdrawRequest(
toAddress,
amount,
fee,
coreWalletsService.getKey(),
trade,
memo,
memo.isEmpty() ? null : memo,
() -> {
},
(errorMessage, throwable) -> {
@ -196,10 +201,14 @@ class CoreTradesService {
}
String getTradeRole(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return tradeUtil.getRole(getTrade(tradeId));
}
Trade getTrade(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return getOpenTrade(tradeId).orElseGet(() ->
getClosedTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId))

View file

@ -23,7 +23,9 @@ import bisq.core.api.model.BsqBalanceInfo;
import bisq.core.api.model.BtcBalanceInfo;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.btc.Balances;
import bisq.core.btc.exceptions.AddressEntryException;
import bisq.core.btc.exceptions.BsqChangeBelowDustException;
import bisq.core.btc.exceptions.InsufficientFundsException;
import bisq.core.btc.exceptions.TransactionVerificationException;
import bisq.core.btc.exceptions.WalletException;
import bisq.core.btc.model.AddressEntry;
@ -35,7 +37,9 @@ import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.btc.wallet.WalletsManager;
import bisq.core.provider.fee.FeeService;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.Timer;
import bisq.common.UserThread;
@ -46,10 +50,12 @@ import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import javax.inject.Inject;
import javax.inject.Named;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
@ -64,6 +70,7 @@ import org.bouncycastle.crypto.params.KeyParameter;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -85,6 +92,7 @@ class CoreWalletsService {
private final BsqTransferService bsqTransferService;
private final BsqFormatter bsqFormatter;
private final BtcWalletService btcWalletService;
private final CoinFormatter btcFormatter;
private final FeeService feeService;
private final Preferences preferences;
@ -103,6 +111,7 @@ class CoreWalletsService {
BsqTransferService bsqTransferService,
BsqFormatter bsqFormatter,
BtcWalletService btcWalletService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
FeeService feeService,
Preferences preferences) {
this.balances = balances;
@ -111,6 +120,7 @@ class CoreWalletsService {
this.bsqTransferService = bsqTransferService;
this.bsqFormatter = bsqFormatter;
this.btcWalletService = btcWalletService;
this.btcFormatter = btcFormatter;
this.feeService = feeService;
this.preferences = preferences;
}
@ -189,13 +199,27 @@ class CoreWalletsService {
void sendBsq(String address,
String amount,
String txFeeRate,
TxBroadcaster.Callback callback) {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
try {
LegacyAddress legacyAddress = getValidBsqLegacyAddress(address);
Coin receiverAmount = getValidBsqTransferAmount(amount);
BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount);
Coin receiverAmount = getValidTransferAmount(amount, bsqFormatter);
Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate);
BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress,
receiverAmount,
txFeePerVbyte);
log.info("Sending {} BSQ to {} with tx fee rate {} sats/byte.",
amount,
address,
txFeePerVbyte.value);
bsqTransferService.sendFunds(model, callback);
} catch (InsufficientMoneyException
} catch (InsufficientMoneyException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send bsq due to insufficient funds", ex);
} catch (NumberFormatException
| BsqChangeBelowDustException
| TransactionVerificationException
| WalletException ex) {
@ -204,6 +228,61 @@ class CoreWalletsService {
}
}
void sendBtc(String address,
String amount,
String txFeeRate,
String memo,
FutureCallback<Transaction> callback) {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
try {
Set<String> fromAddresses = btcWalletService.getAddressEntriesForAvailableBalanceStream()
.map(AddressEntry::getAddressString)
.collect(Collectors.toSet());
Coin receiverAmount = getValidTransferAmount(amount, btcFormatter);
Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate);
// TODO Support feeExcluded (or included), default is fee included.
// See WithdrawalView # onWithdraw (and refactor).
Transaction feeEstimationTransaction =
btcWalletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses,
receiverAmount,
txFeePerVbyte);
if (feeEstimationTransaction == null)
throw new IllegalStateException("could not estimate the transaction fee");
Coin dust = btcWalletService.getDust(feeEstimationTransaction);
Coin fee = feeEstimationTransaction.getFee().add(dust);
if (dust.isPositive()) {
fee = feeEstimationTransaction.getFee().add(dust);
log.info("Dust txo ({} sats) was detected, the dust amount has been added to the fee (was {}, now {})",
dust.value,
feeEstimationTransaction.getFee(),
fee.value);
}
log.info("Sending {} BTC to {} with tx fee of {} sats (fee rate {} sats/byte).",
amount,
address,
fee.value,
txFeePerVbyte.value);
btcWalletService.sendFundsForMultipleAddresses(fromAddresses,
address,
receiverAmount,
fee,
null,
tempAesKey,
memo.isEmpty() ? null : memo,
callback);
} catch (AddressEntryException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send btc from any addresses in wallet", ex);
} catch (InsufficientFundsException | InsufficientMoneyException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send btc due to insufficient funds", ex);
}
}
void getTxFeeRate(ResultHandler resultHandler) {
try {
@SuppressWarnings({"unchecked", "Convert2MethodRef"})
@ -252,6 +331,26 @@ class CoreWalletsService {
feeService.getLastRequest());
}
Transaction getTransaction(String txId) {
if (txId.length() != 64)
throw new IllegalArgumentException(format("%s is not a transaction id", txId));
try {
Transaction tx = btcWalletService.getTransaction(txId);
if (tx == null)
throw new IllegalArgumentException(format("tx with id %s not found", txId));
else
return tx;
} catch (IllegalArgumentException ex) {
log.error("", ex);
throw new IllegalArgumentException(
format("could not get transaction with id %s%ncause: %s",
txId,
ex.getMessage().toLowerCase()));
}
}
int getNumConfirmationsForMostRecentTransaction(String addressString) {
Address address = getAddressEntry(addressString).getAddress();
TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address);
@ -342,13 +441,13 @@ class CoreWalletsService {
}
// Throws a RuntimeException if wallets are not available (encrypted or not).
private void verifyWalletsAreAvailable() {
void verifyWalletsAreAvailable() {
if (!walletsManager.areWalletsAvailable())
throw new IllegalStateException("wallet is not yet available");
}
// Throws a RuntimeException if wallets are not available or not encrypted.
private void verifyWalletIsAvailableAndEncrypted() {
void verifyWalletIsAvailableAndEncrypted() {
if (!walletsManager.areWalletsAvailable())
throw new IllegalStateException("wallet is not yet available");
@ -357,7 +456,7 @@ class CoreWalletsService {
}
// Throws a RuntimeException if wallets are encrypted and locked.
private void verifyEncryptedWalletIsUnlocked() {
void verifyEncryptedWalletIsUnlocked() {
if (walletsManager.areWalletsEncrypted() && tempAesKey == null)
throw new IllegalStateException("wallet is locked");
}
@ -423,15 +522,22 @@ class CoreWalletsService {
}
}
// Returns a Coin for the amount string, or a RuntimeException if invalid.
private Coin getValidBsqTransferAmount(String amount) {
Coin amountAsCoin = parseToCoin(amount, bsqFormatter);
// Returns a Coin for the transfer amount string, or a RuntimeException if invalid.
private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) {
Coin amountAsCoin = parseToCoin(amount, coinFormatter);
if (amountAsCoin.isLessThan(getMinNonDustOutput()))
throw new IllegalStateException(format("%s bsq is an invalid send amount", amount));
throw new IllegalStateException(format("%s is an invalid transfer amount", amount));
return amountAsCoin;
}
private Coin getTxFeeRateFromParamOrPreferenceOrFeeService(String txFeeRate) {
// A non txFeeRate String value overrides the fee service and custom fee.
return txFeeRate.isEmpty()
? btcWalletService.getTxFeeForWithdrawalPerVbyte()
: Coin.valueOf(Long.parseLong(txFeeRate));
}
private KeyCrypterScrypt getKeyCrypterScrypt() {
KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt();
if (keyCrypterScrypt == null)

View file

@ -0,0 +1,160 @@
/*
* 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.api.model;
import bisq.common.Payload;
import org.bitcoinj.core.Transaction;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode
@Getter
public class TxInfo implements Payload {
// The client cannot see an instance of an org.bitcoinj.core.Transaction. We use the
// lighter weight TxInfo proto wrapper instead, containing just enough fields to
// view some transaction details. A block explorer or bitcoin-core client can be
// used to see more detail.
private final String txId;
private final long inputSum;
private final long outputSum;
private final long fee;
private final int size;
private final boolean isPending;
private final String memo;
public TxInfo(TxInfo.TxInfoBuilder builder) {
this.txId = builder.txId;
this.inputSum = builder.inputSum;
this.outputSum = builder.outputSum;
this.fee = builder.fee;
this.size = builder.size;
this.isPending = builder.isPending;
this.memo = builder.memo;
}
public static TxInfo toTxInfo(Transaction transaction) {
if (transaction == null)
throw new IllegalStateException("server created a null transaction");
return new TxInfo.TxInfoBuilder()
.withTxId(transaction.getTxId().toString())
.withInputSum(transaction.getInputSum().value)
.withOutputSum(transaction.getOutputSum().value)
.withFee(transaction.getFee().value)
.withSize(transaction.getMessageSize())
.withIsPending(transaction.isPending())
.withMemo(transaction.getMemo())
.build();
}
//////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
//////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.TxInfo toProtoMessage() {
return bisq.proto.grpc.TxInfo.newBuilder()
.setTxId(txId)
.setInputSum(inputSum)
.setOutputSum(outputSum)
.setFee(fee)
.setSize(size)
.setIsPending(isPending)
.setMemo(memo == null ? "" : memo)
.build();
}
@SuppressWarnings("unused")
public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) {
return new TxInfo.TxInfoBuilder()
.withTxId(proto.getTxId())
.withInputSum(proto.getInputSum())
.withOutputSum(proto.getOutputSum())
.withFee(proto.getFee())
.withSize(proto.getSize())
.withIsPending(proto.getIsPending())
.withMemo(proto.getMemo())
.build();
}
public static class TxInfoBuilder {
private String txId;
private long inputSum;
private long outputSum;
private long fee;
private int size;
private boolean isPending;
private String memo;
public TxInfo.TxInfoBuilder withTxId(String txId) {
this.txId = txId;
return this;
}
public TxInfo.TxInfoBuilder withInputSum(long inputSum) {
this.inputSum = inputSum;
return this;
}
public TxInfo.TxInfoBuilder withOutputSum(long outputSum) {
this.outputSum = outputSum;
return this;
}
public TxInfo.TxInfoBuilder withFee(long fee) {
this.fee = fee;
return this;
}
public TxInfo.TxInfoBuilder withSize(int size) {
this.size = size;
return this;
}
public TxInfo.TxInfoBuilder withIsPending(boolean isPending) {
this.isPending = isPending;
return this;
}
public TxInfo.TxInfoBuilder withMemo(String memo) {
this.memo = memo;
return this;
}
public TxInfo build() {
return new TxInfo(this);
}
}
@Override
public String toString() {
return "TxInfo{" + "\n" +
" txId='" + txId + '\'' + "\n" +
", inputSum=" + inputSum + "\n" +
", outputSum=" + outputSum + "\n" +
", fee=" + fee + "\n" +
", size=" + size + "\n" +
", isPending=" + isPending + "\n" +
", memo='" + memo + '\'' + "\n" +
'}';
}
}

View file

@ -23,6 +23,7 @@ import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoSetup;
import bisq.core.dao.node.full.RpcService;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.setup.CorePersistedDataHost;
import bisq.core.setup.CoreSetup;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
@ -227,12 +228,14 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
}
try {
injector.getInstance(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(XmrTxProofService.class).shutDown();
injector.getInstance(RpcService.class).shutDown();
injector.getInstance(DaoSetup.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown();
log.info("OpenOfferManager shutdown started");
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
log.info("OpenOfferManager shutdown completed");
@ -265,7 +268,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
// Wait max 20 sec.
UserThread.runAfter(() -> {
log.warn("Timeout triggered resultHandler");
log.warn("Graceful shut down not completed in 20 sec. We trigger our timeout handler.");
if (!hasDowngraded) {
// If user tried to downgrade we do not write the persistable data to avoid data corruption
PersistenceManager.flushAllDataToDisk(() -> {

View file

@ -97,6 +97,12 @@ public class BisqHeadlessApp implements HeadlessApp {
bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported",
lastVersion, Version.VERSION));
bisqSetup.setDaoRequiresRestartHandler(() -> {
log.info("There was a problem with synchronizing the DAO state. " +
"A restart of the application is required to fix the issue.");
gracefulShutDownHandler.gracefulShutDown(() -> {
});
});
corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files));
tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("onTakeOfferRequestErrorMessageHandler"));

View file

@ -27,6 +27,7 @@ import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.WalletsManager;
import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster;
import bisq.core.dao.governance.voteresult.VoteResultException;
import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService;
import bisq.core.locale.Res;
@ -41,6 +42,7 @@ import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.Socks5ProxyProvider;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
@ -49,6 +51,7 @@ import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.app.Log;
import bisq.common.app.Version;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.util.InvalidVersionException;
import bisq.common.util.Utilities;
@ -180,6 +183,9 @@ public class BisqSetup {
private Runnable qubesOSInfoHandler;
@Setter
@Nullable
private Runnable daoRequiresRestartHandler;
@Setter
@Nullable
private Consumer<String> downGradePreventionHandler;
@Getter
@ -210,7 +216,8 @@ public class BisqSetup {
TorSetup torSetup,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
LocalBitcoinNode localBitcoinNode,
AppStartupState appStartupState) {
AppStartupState appStartupState,
Socks5ProxyProvider socks5ProxyProvider) {
this.domainInitialisation = domainInitialisation;
this.p2PNetworkSetup = p2PNetworkSetup;
this.walletAppSetup = walletAppSetup;
@ -230,6 +237,8 @@ public class BisqSetup {
this.formatter = formatter;
this.localBitcoinNode = localBitcoinNode;
this.appStartupState = appStartupState;
MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -265,7 +274,8 @@ public class BisqSetup {
public void start() {
// If user tried to downgrade we require a shutdown
if (hasDowngraded(downGradePreventionHandler)) {
if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.BTC_MAINNET &&
hasDowngraded(downGradePreventionHandler)) {
return;
}
@ -438,7 +448,8 @@ public class BisqSetup {
daoWarnMessageHandler,
filterWarningHandler,
voteResultExceptionHandler,
revolutAccountsUpdateHandler);
revolutAccountsUpdateHandler,
daoRequiresRestartHandler);
if (walletsSetup.downloadPercentageProperty().get() == 1) {
checkForLockedUpFunds();

View file

@ -25,6 +25,7 @@ import bisq.core.btc.Balances;
import bisq.core.dao.DaoSetup;
import bisq.core.dao.governance.voteresult.VoteResultException;
import bisq.core.dao.governance.voteresult.VoteResultService;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.filter.FilterManager;
import bisq.core.notifications.MobileNotificationService;
import bisq.core.notifications.alerts.DisputeMsgEvents;
@ -33,6 +34,7 @@ import bisq.core.notifications.alerts.TradeEvents;
import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOfferManager;
import bisq.core.offer.TriggerPriceService;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.TradeLimits;
import bisq.core.provider.fee.FeeService;
@ -104,6 +106,8 @@ public class DomainInitialisation {
private final PriceAlert priceAlert;
private final MarketAlerts marketAlerts;
private final User user;
private final DaoStateSnapshotService daoStateSnapshotService;
private final TriggerPriceService triggerPriceService;
@Inject
public DomainInitialisation(ClockWatcher clockWatcher,
@ -138,7 +142,9 @@ public class DomainInitialisation {
DisputeMsgEvents disputeMsgEvents,
PriceAlert priceAlert,
MarketAlerts marketAlerts,
User user) {
User user,
DaoStateSnapshotService daoStateSnapshotService,
TriggerPriceService triggerPriceService) {
this.clockWatcher = clockWatcher;
this.tradeLimits = tradeLimits;
this.arbitrationManager = arbitrationManager;
@ -172,6 +178,8 @@ public class DomainInitialisation {
this.priceAlert = priceAlert;
this.marketAlerts = marketAlerts;
this.user = user;
this.daoStateSnapshotService = daoStateSnapshotService;
this.triggerPriceService = triggerPriceService;
}
public void initDomainServices(Consumer<String> rejectedTxErrorMessageHandler,
@ -180,7 +188,8 @@ public class DomainInitialisation {
Consumer<String> daoWarnMessageHandler,
Consumer<String> filterWarningHandler,
Consumer<VoteResultException> voteResultExceptionHandler,
Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler) {
Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler,
Runnable daoRequiresRestartHandler) {
clockWatcher.start();
tradeLimits.onAllServicesInitialized();
@ -222,6 +231,8 @@ public class DomainInitialisation {
if (daoWarnMessageHandler != null)
daoWarnMessageHandler.accept(warningMessage);
});
daoStateSnapshotService.setDaoRequiresRestartHandler(daoRequiresRestartHandler);
}
tradeStatisticsManager.onAllServicesInitialized();
@ -247,6 +258,7 @@ public class DomainInitialisation {
disputeMsgEvents.onAllServicesInitialized();
priceAlert.onAllServicesInitialized();
marketAlerts.onAllServicesInitialized();
triggerPriceService.onAllServicesInitialized();
if (revolutAccountsUpdateHandler != null) {
revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream()

View file

@ -107,7 +107,7 @@ public class WalletAppSetup {
Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) {
log.info("Initialize WalletAppSetup with BitcoinJ version {} and hash of BitcoinJ commit {}",
VersionMessage.BITCOINJ_VERSION, "dcf8af0");
VersionMessage.BITCOINJ_VERSION, "2a80db4");
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(),

View file

@ -26,7 +26,6 @@ import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.NonBsqCoinSelector;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.provider.PriceNodeHttpClient;
import bisq.core.provider.ProvidersRepository;
import bisq.core.provider.fee.FeeProvider;
import bisq.core.provider.fee.FeeService;
@ -95,8 +94,6 @@ public class BitcoinModule extends AppModule {
bind(BtcNodes.class).in(Singleton.class);
bind(Balances.class).in(Singleton.class);
bind(PriceNodeHttpClient.class).in(Singleton.class);
bind(ProvidersRepository.class).in(Singleton.class);
bind(FeeProvider.class).in(Singleton.class);
bind(PriceFeedService.class).in(Singleton.class);

View file

@ -30,11 +30,16 @@ import javax.annotation.concurrent.Immutable;
@EqualsAndHashCode
@Immutable
public final class RawTransactionInput implements NetworkPayload, PersistablePayload {
// Payload
public final long index;
public final byte[] parentTransaction;
public final long index; // Index of spending txo
public final byte[] parentTransaction; // Spending tx (fromTx)
public final long value;
/**
* Holds the relevant data for the connected output for a tx input.
* @param index the index of the parentTransaction
* @param parentTransaction the spending output tx, not the parent tx of the input
* @param value the number of satoshis being spent
*/
public RawTransactionInput(long index, byte[] parentTransaction, long value) {
this.index = index;
this.parentTransaction = parentTransaction;

View file

@ -72,7 +72,7 @@ public class BtcNodes {
new BtcNode("btc2.bisq.services", "qxjrxmhyqp5vy5hj.onion", "173.255.240.205", BtcNode.DEFAULT_PORT, "@devinbileck"),
// m52go
new BtcNode("btc.bisq.cc", "4nnuyxm5k5tlyjq3.onion", "167.71.168.194", BtcNode.DEFAULT_PORT, "@m52go"),
new BtcNode(null, "4nnuyxm5k5tlyjq3.onion", null, BtcNode.DEFAULT_PORT, "@m52go"),
// wiz
new BtcNode("node100.hnl.wiz.biz", "m3yqzythryowgedc.onion", "103.99.168.100", BtcNode.DEFAULT_PORT, "@wiz"),

View file

@ -242,14 +242,16 @@ public class WalletsSetup {
return message;
});
chainHeight.set(chain.getBestChainHeight());
chain.addNewBestBlockListener(block -> {
connectedPeers.set(peerGroup.getConnectedPeers());
chainHeight.set(block.getHeight());
UserThread.execute(() -> {
connectedPeers.set(peerGroup.getConnectedPeers());
chainHeight.set(block.getHeight());
});
});
// Map to user thread
UserThread.execute(() -> {
chainHeight.set(chain.getBestChainHeight());
addressEntryList.onWalletReady(walletConfig.btcWallet());
timeoutTimer.stop();
setupCompletedHandlers.forEach(Runnable::run);

View file

@ -33,14 +33,15 @@ public class BsqTransferService {
}
public BsqTransferModel getBsqTransferModel(LegacyAddress address,
Coin receiverAmount)
Coin receiverAmount,
Coin txFeePerVbyte)
throws TransactionVerificationException,
WalletException,
BsqChangeBelowDustException,
InsufficientMoneyException {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, txFeePerVbyte);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
return new BsqTransferModel(address,

View file

@ -24,6 +24,7 @@ import bisq.core.btc.exceptions.WalletException;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.model.AddressEntryList;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster;
import bisq.core.provider.fee.FeeService;
import bisq.core.user.Preferences;
@ -439,8 +440,7 @@ public class BtcWalletService extends WalletService {
// Add fee input to prepared BSQ send tx
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean isSendTx) throws
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
// preparedBsqTx has following structure:
// inputs [1-n] BSQ inputs
@ -454,13 +454,26 @@ public class BtcWalletService extends WalletService {
// outputs [0-1] BSQ change output
// outputs [0-1] BTC change output
// mining fee: BTC mining fee
return completePreparedBsqTx(preparedBsqTx, isSendTx, null);
Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte();
return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte);
}
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, Coin txFeePerVbyte) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte);
}
public Transaction completePreparedBsqTx(Transaction preparedBsqTx,
boolean useCustomTxFee,
@Nullable byte[] opReturnData) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte();
return completePreparedBsqTx(preparedBsqTx, opReturnData, txFeePerVbyte);
}
public Transaction completePreparedBsqTx(Transaction preparedBsqTx,
@Nullable byte[] opReturnData,
Coin txFeePerVbyte) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
// preparedBsqTx has following structure:
// inputs [1-n] BSQ inputs
@ -487,8 +500,6 @@ public class BtcWalletService extends WalletService {
int sigSizePerInput = 106;
// typical size for a tx with 2 inputs
int txVsizeWithUnsignedInputs = 203;
// If useCustomTxFee we allow overriding the estimated fee from preferences
Coin txFeePerVbyte = useCustomTxFee ? getTxFeeForWithdrawalPerVbyte() : feeService.getTxFeePerVbyte();
// In case there are no change outputs we force a change by adding min dust to the BTC input
Coin forcedChangeValue = Coin.ZERO;
@ -957,13 +968,17 @@ public class BtcWalletService extends WalletService {
try {
sendResult = wallet.sendCoins(sendRequest);
printTx("FeeEstimationTransaction", newTransaction);
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
} catch (InsufficientMoneyException e2) {
errorMessageHandler.handleErrorMessage("We did not get the correct fee calculated. " + (e2.missing != null ? e2.missing.toFriendlyString() : ""));
}
}
if (sendResult != null) {
log.info("Broadcasting double spending transaction. " + sendResult.tx);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(Transaction result) {
log.info("Double spending transaction published. " + result);
@ -1043,6 +1058,14 @@ public class BtcWalletService extends WalletService {
public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses,
Coin amount)
throws AddressFormatException, AddressEntryException, InsufficientFundsException {
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
return getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amount, txFeeForWithdrawalPerVbyte);
}
public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses,
Coin amount,
Coin txFeeForWithdrawalPerVbyte)
throws AddressFormatException, AddressEntryException, InsufficientFundsException {
Set<AddressEntry> addressEntries = fromAddresses.stream()
.map(address -> {
Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
@ -1065,7 +1088,6 @@ public class BtcWalletService extends WalletService {
int counter = 0;
int txVsize = 0;
Transaction tx;
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
@ -1092,7 +1114,11 @@ public class BtcWalletService extends WalletService {
}
private boolean feeEstimationNotSatisfied(int counter, Transaction tx) {
long targetFee = getTxFeeForWithdrawalPerVbyte().multiply(tx.getVsize()).value;
return feeEstimationNotSatisfied(counter, tx, getTxFeeForWithdrawalPerVbyte());
}
private boolean feeEstimationNotSatisfied(int counter, Transaction tx, Coin txFeeForWithdrawalPerVbyte) {
long targetFee = txFeeForWithdrawalPerVbyte.multiply(tx.getVsize()).value;
return counter < 10 &&
(tx.getFee().value < targetFee ||
tx.getFee().value - targetFee > 1000);
@ -1139,7 +1165,11 @@ public class BtcWalletService extends WalletService {
if (memo != null) {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
return sendResult.tx.getTxId().toString();
}
@ -1160,6 +1190,11 @@ public class BtcWalletService extends WalletService {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
return sendResult.tx;
}
@ -1199,7 +1234,7 @@ public class BtcWalletService extends WalletService {
Coin fee,
@Nullable String changeAddress,
@Nullable KeyParameter aesKey) throws
AddressFormatException, AddressEntryException, InsufficientMoneyException {
AddressFormatException, AddressEntryException {
Transaction tx = new Transaction(params);
final Coin netValue = amount.subtract(fee);
checkArgument(Restrictions.isAboveDust(netValue),
@ -1232,12 +1267,12 @@ public class BtcWalletService extends WalletService {
sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries),
preferences.getIgnoreDustThreshold());
Optional<AddressEntry> addressEntryOptional = Optional.<AddressEntry>empty();
AddressEntry changeAddressAddressEntry = null;
Optional<AddressEntry> addressEntryOptional = Optional.empty();
if (changeAddress != null)
addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE);
changeAddressAddressEntry = addressEntryOptional.orElseGet(() -> getFreshAddressEntry());
AddressEntry changeAddressAddressEntry = addressEntryOptional.orElseGet(this::getFreshAddressEntry);
checkNotNull(changeAddressAddressEntry, "change address must not be null");
sendRequest.changeAddress = changeAddressAddressEntry.getAddress();
return sendRequest;

View file

@ -30,6 +30,7 @@ import bisq.core.locale.Res;
import bisq.core.user.Preferences;
import bisq.common.config.Config;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
@ -1098,30 +1099,19 @@ public class TradeWalletService {
// Emergency payoutTx
///////////////////////////////////////////////////////////////////////////////////////////
public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex,
Coin buyerPayoutAmount,
Coin sellerPayoutAmount,
Coin txFee,
String buyerAddressString,
String sellerAddressString,
String buyerPrivateKeyAsHex,
String sellerPrivateKeyAsHex,
String buyerPubKeyAsHex,
String sellerPubKeyAsHex,
boolean hashedMultiSigOutputIsLegacy,
TxBroadcaster.Callback callback)
throws AddressFormatException, TransactionVerificationException, WalletException {
public Tuple2<String, String> emergencyBuildPayoutTxFrom2of2MultiSig(String depositTxHex,
Coin buyerPayoutAmount,
Coin sellerPayoutAmount,
Coin txFee,
String buyerAddressString,
String sellerAddressString,
String buyerPubKeyAsHex,
String sellerPubKeyAsHex,
boolean hashedMultiSigOutputIsLegacy) {
byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey();
byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey();
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey,
hashedMultiSigOutputIsLegacy);
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee);
TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, null, msOutputValue, hashedMultiSigOutputScript.getProgram());
Transaction depositTx = new Transaction(params);
depositTx.addOutput(hashedMultiSigOutput);
Transaction payoutTx = new Transaction(params);
Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex);
payoutTx.addInput(new TransactionInput(params, payoutTx, new byte[]{}, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue));
@ -1133,27 +1123,44 @@ public class TradeWalletService {
payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString));
}
// take care of sorting!
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
String redeemScriptHex = Utils.HEX.encode(redeemScript.getProgram());
String unsignedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy));
return new Tuple2<>(redeemScriptHex, unsignedTxHex);
}
public String emergencyGenerateSignature(String rawTxHex, String redeemScriptHex, Coin inputValue, String myPrivKeyAsHex)
throws IllegalArgumentException {
boolean hashedMultiSigOutputIsLegacy = true;
if (rawTxHex.startsWith("010000000001"))
hashedMultiSigOutputIsLegacy = false;
byte[] payload = Utils.HEX.decode(rawTxHex);
Transaction payoutTx = new Transaction(params, payload, null, params.getDefaultSerializer(), payload.length);
Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex));
Sha256Hash sigHash;
if (hashedMultiSigOutputIsLegacy) {
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
} else {
Coin inputValue = msOutputValue;
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
inputValue, Transaction.SigHash.ALL, false);
}
ECKey buyerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(buyerPrivateKeyAsHex));
checkNotNull(buyerPrivateKey, "key must not be null");
ECKey.ECDSASignature buyerECDSASignature = buyerPrivateKey.sign(sigHash, aesKey).toCanonicalised();
ECKey myPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(myPrivKeyAsHex));
checkNotNull(myPrivateKey, "key must not be null");
ECKey.ECDSASignature myECDSASignature = myPrivateKey.sign(sigHash, aesKey).toCanonicalised();
TransactionSignature myTxSig = new TransactionSignature(myECDSASignature, Transaction.SigHash.ALL, false);
return Utils.HEX.encode(myTxSig.encodeToBitcoin());
}
ECKey sellerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(sellerPrivateKeyAsHex));
checkNotNull(sellerPrivateKey, "key must not be null");
ECKey.ECDSASignature sellerECDSASignature = sellerPrivateKey.sign(sigHash, aesKey).toCanonicalised();
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
public Tuple2<String, String> emergencyApplySignatureToPayoutTxFrom2of2MultiSig(String unsignedTxHex,
String redeemScriptHex,
String buyerSignatureAsHex,
String sellerSignatureAsHex,
boolean hashedMultiSigOutputIsLegacy)
throws AddressFormatException, SignatureDecodeException {
Transaction payoutTx = new Transaction(params, Utils.HEX.decode(unsignedTxHex));
TransactionSignature buyerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(buyerSignatureAsHex), true, true);
TransactionSignature sellerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(sellerSignatureAsHex), true, true);
Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex));
TransactionInput input = payoutTx.getInput(0);
if (hashedMultiSigOutputIsLegacy) {
@ -1165,7 +1172,14 @@ public class TradeWalletService {
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
input.setWitness(witness);
}
String txId = payoutTx.getTxId().toString();
String signedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy));
return new Tuple2<>(txId, signedTxHex);
}
public void emergencyPublishPayoutTxFrom2of2MultiSig(String signedTxHex, TxBroadcaster.Callback callback)
throws AddressFormatException, TransactionVerificationException, WalletException {
Transaction payoutTx = new Transaction(params, Utils.HEX.decode(signedTxHex));
WalletService.printTx("payoutTx", payoutTx);
WalletService.verifyTransaction(payoutTx);
WalletService.checkWalletConsistency(wallet);

View file

@ -19,6 +19,7 @@ package bisq.core.btc.wallet;
import bisq.core.btc.exceptions.TxBroadcastException;
import bisq.core.btc.exceptions.TxBroadcastTimeoutException;
import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster;
import bisq.common.Timer;
import bisq.common.UserThread;
@ -135,6 +136,10 @@ public class TxBroadcaster {
"the peerGroup.broadcastTransaction callback.", throwable)));
}
}, MoreExecutors.directExecutor());
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(tx);
}
private static void stopAndRemoveTimer(String txId) {

View file

@ -23,6 +23,7 @@ import bisq.core.btc.listeners.AddressConfidenceListener;
import bisq.core.btc.listeners.BalanceListener;
import bisq.core.btc.listeners.TxConfidenceListener;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster;
import bisq.core.provider.fee.FeeService;
import bisq.core.user.Preferences;
@ -535,7 +536,12 @@ public abstract class WalletService {
sendRequest.aesKey = aesKey;
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
printTx("empty btc wallet", sendResult.tx);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(Transaction result) {
log.info("emptyBtcWallet onSuccess Transaction=" + result);
@ -671,6 +677,9 @@ public abstract class WalletService {
@Nullable
public Transaction getTransaction(String txId) {
if (txId == null) {
return null;
}
return getTransaction(Sha256Hash.wrap(txId));
}

View file

@ -0,0 +1,156 @@
/*
* 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.btc.wallet.http;
import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.core.user.Preferences;
import bisq.network.Socks5ProxyProvider;
import bisq.network.http.HttpException;
import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Utils;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class MemPoolSpaceTxBroadcaster {
private static Socks5ProxyProvider socks5ProxyProvider;
private static Preferences preferences;
private static LocalBitcoinNode localBitcoinNode;
private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
"MemPoolSpaceTxBroadcaster", 3, 5, 10 * 60);
public static void init(Socks5ProxyProvider socks5ProxyProvider,
Preferences preferences,
LocalBitcoinNode localBitcoinNode) {
MemPoolSpaceTxBroadcaster.socks5ProxyProvider = socks5ProxyProvider;
MemPoolSpaceTxBroadcaster.preferences = preferences;
MemPoolSpaceTxBroadcaster.localBitcoinNode = localBitcoinNode;
}
public static void broadcastTx(Transaction tx) {
if (!Config.baseCurrencyNetwork().isMainnet()) {
log.info("MemPoolSpaceTxBroadcaster only supports mainnet");
return;
}
if (localBitcoinNode.shouldBeUsed()) {
log.info("A localBitcoinNode is detected and used. For privacy reasons we do not use the tx " +
"broadcast to mempool nodes in that case.");
return;
}
if (socks5ProxyProvider == null) {
log.warn("We got broadcastTx called before init was called.");
return;
}
String txIdToSend = tx.getTxId().toString();
String rawTx = Utils.HEX.encode(tx.bitcoinSerialize(true));
List<String> txBroadcastServices = new ArrayList<>(preferences.getDefaultTxBroadcastServices());
// Broadcast to first service
String serviceAddress = broadcastTx(txIdToSend, rawTx, txBroadcastServices);
if (serviceAddress != null) {
// Broadcast to second service
txBroadcastServices.remove(serviceAddress);
broadcastTx(txIdToSend, rawTx, txBroadcastServices);
}
}
@Nullable
private static String broadcastTx(String txIdToSend, String rawTx, List<String> txBroadcastServices) {
String serviceAddress = getRandomServiceAddress(txBroadcastServices);
if (serviceAddress == null) {
log.warn("We don't have a serviceAddress available. txBroadcastServices={}", txBroadcastServices);
return null;
}
broadcastTx(serviceAddress, txIdToSend, rawTx);
return serviceAddress;
}
private static void broadcastTx(String serviceAddress, String txIdToSend, String rawTx) {
TxBroadcastHttpClient httpClient = new TxBroadcastHttpClient(socks5ProxyProvider);
httpClient.setBaseUrl(serviceAddress);
httpClient.setIgnoreSocks5Proxy(false);
log.info("We broadcast rawTx {} to {}", rawTx, serviceAddress);
ListenableFuture<String> future = executorService.submit(() -> {
Thread.currentThread().setName("MemPoolSpaceTxBroadcaster @ " + serviceAddress);
return httpClient.post(rawTx, "User-Agent", "bisq/" + Version.VERSION);
});
Futures.addCallback(future, new FutureCallback<>() {
public void onSuccess(String txId) {
if (txId.equals(txIdToSend)) {
log.info("Broadcast of raw tx with txId {} to {} was successful. rawTx={}",
txId, serviceAddress, rawTx);
} else {
log.error("The txId we got returned from the service does not match " +
"out tx of the sending tx. txId={}; txIdToSend={}",
txId, txIdToSend);
}
}
public void onFailure(@NotNull Throwable throwable) {
Throwable cause = throwable.getCause();
if (cause instanceof HttpException) {
int responseCode = ((HttpException) cause).getResponseCode();
String message = cause.getMessage();
// See all error codes at: https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h
if (responseCode == 400 && message.contains("code\":-27")) {
log.info("Broadcast of raw tx to {} failed as transaction {} is already confirmed",
serviceAddress, txIdToSend);
} else {
log.info("Broadcast of raw tx to {} failed for transaction {}. responseCode={}, error={}",
serviceAddress, txIdToSend, responseCode, message);
}
} else {
log.warn("Broadcast of raw tx with txId {} to {} failed. Error={}",
txIdToSend, serviceAddress, throwable.toString());
}
}
}, MoreExecutors.directExecutor());
}
@Nullable
private static String getRandomServiceAddress(List<String> txBroadcastServices) {
List<String> list = checkNotNull(txBroadcastServices);
return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null;
}
}

View file

@ -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.btc.wallet.http;
import bisq.core.trade.txproof.AssetTxProofHttpClient;
import bisq.network.Socks5ProxyProvider;
import bisq.network.http.HttpClientImpl;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class TxBroadcastHttpClient extends HttpClientImpl implements AssetTxProofHttpClient {
TxBroadcastHttpClient(Socks5ProxyProvider socks5ProxyProvider) {
super(socks5ProxyProvider);
}
}

View file

@ -631,7 +631,7 @@ public class DaoFacade implements DaoSetupService {
}
public int getNumIssuanceTransactions(IssuanceType issuanceType) {
return daoStateService.getIssuanceSet(issuanceType).size();
return daoStateService.getIssuanceSetForType(issuanceType).size();
}
public Set<Tx> getBurntFeeTxs() {

View file

@ -286,7 +286,7 @@ public class MyBlindVoteListService implements PersistedDataHost, DaoStateListen
.filter(txId -> periodService.isTxInPastCycle(txId, periodService.getChainHeight()))
.collect(Collectors.toSet());
return new MeritList(daoStateService.getIssuanceSet(IssuanceType.COMPENSATION).stream()
return new MeritList(daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream()
.map(issuance -> {
checkArgument(issuance.getIssuanceType() == IssuanceType.COMPENSATION,
"IssuanceType must be COMPENSATION for MeritList");

View file

@ -102,7 +102,7 @@ public final class RepublishGovernanceDataHandler {
connectToNextNode();
} else {
log.warn("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by an previous networkNode.sendMessage.onFailure.");
"Might be caused by a previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT);
@ -118,7 +118,7 @@ public final class RepublishGovernanceDataHandler {
stop();
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by an previous timeout.");
"Might be caused by a previous timeout.");
}
}
@ -133,7 +133,7 @@ public final class RepublishGovernanceDataHandler {
connectToNextNode();
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " +
"Might be caused by an previous timeout.");
"Might be caused by a previous timeout.");
}
}
}, MoreExecutors.directExecutor());

View file

@ -103,7 +103,7 @@ public class LockupTxService {
throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException {
byte[] opReturnData = BondConsensus.getLockupOpReturnData(lockTime, lockupReason, hash);
Transaction preparedTx = bsqWalletService.getPreparedLockupTx(lockupAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, true, opReturnData);
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, opReturnData);
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
log.info("Lockup tx: " + transaction);
return transaction;

View file

@ -103,7 +103,7 @@ public class UnlockTxService {
checkArgument(optionalLockupTxOutput.isPresent(), "lockupTxOutput must be present");
TxOutput lockupTxOutput = optionalLockupTxOutput.get();
Transaction preparedTx = bsqWalletService.getPreparedUnlockTx(lockupTxOutput);
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, true, null);
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, null);
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
log.info("Unlock tx: " + transaction);
return transaction;

View file

@ -371,7 +371,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
if (this.isInConflictWithSeedNode)
log.warn("Conflict with seed nodes: {}", conflictMsg);
else if (this.isInConflictWithNonSeedNode)
log.info("Conflict with non-seed nodes: {}", conflictMsg);
log.debug("Conflict with non-seed nodes: {}", conflictMsg);
}

View file

@ -116,7 +116,7 @@ abstract class RequestStateHashesHandler<Req extends GetStateHashesRequest, Res
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
} else {
log.trace("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by an previous networkNode.sendMessage.onFailure.");
"Might be caused by a previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT);
@ -134,7 +134,7 @@ abstract class RequestStateHashesHandler<Req extends GetStateHashesRequest, Res
nodeAddress.getFullAddress());
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by an previous timeout.");
"Might be caused by a previous timeout.");
}
}
@ -149,7 +149,7 @@ abstract class RequestStateHashesHandler<Req extends GetStateHashesRequest, Res
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE);
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " +
"Might be caused by an previous timeout.");
"Might be caused by a previous timeout.");
}
}
}, MoreExecutors.directExecutor());

View file

@ -189,7 +189,7 @@ public abstract class BsqNode implements DaoSetupService {
if (chainHeight > genesisBlockHeight)
startBlockHeight = chainHeight + 1;
log.info("Start parse blocks:\n" +
log.info("getStartBlockHeight:\n" +
" Start block height={}\n" +
" Genesis txId={}\n" +
" Genesis block height={}\n" +
@ -223,15 +223,14 @@ public abstract class BsqNode implements DaoSetupService {
// height we have no block but chainHeight is initially set to genesis height (bad design ;-( but a bit tricky
// to change now as it used in many areas.)
if (daoStateService.getBlockAtHeight(rawBlock.getHeight()).isPresent()) {
log.debug("We have already a block with the height of the new block. Height of new block={}", rawBlock.getHeight());
log.info("We have already a block with the height of the new block. Height of new block={}", rawBlock.getHeight());
return Optional.empty();
}
try {
Block block = blockParser.parseBlock(rawBlock);
if (pendingBlocks.contains(rawBlock))
pendingBlocks.remove(rawBlock);
pendingBlocks.remove(rawBlock);
// 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

View file

@ -43,7 +43,7 @@ enum JsonTxOutputType {
INVALID_OUTPUT("Invalid");
@Getter
private String displayString;
private final String displayString;
JsonTxOutputType(String displayString) {
this.displayString = displayString;

View file

@ -40,7 +40,7 @@ enum JsonTxType {
IRREGULAR("Irregular");
@Getter
private String displayString;
private final String displayString;
JsonTxType(String displayString) {
this.displayString = displayString;

View file

@ -136,51 +136,62 @@ public class FullNodeNetworkService implements MessageListener, PeerManager.List
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof GetBlocksRequest) {
// We received a GetBlocksRequest from a liteNode
if (!stopped) {
final String uid = connection.getUid();
if (!getBlocksRequestHandlers.containsKey(uid)) {
GetBlocksRequestHandler requestHandler = new GetBlocksRequestHandler(networkNode,
daoStateService,
new GetBlocksRequestHandler.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);
peerManager.handleConnectionFault(connection);
} else {
log.warn("We have stopped already. We ignore that getDataRequestHandler.handle.onFault call.");
}
}
});
getBlocksRequestHandlers.put(uid, requestHandler);
requestHandler.onGetBlocksRequest((GetBlocksRequest) networkEnvelope, connection);
} else {
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)) {
GetBlocksRequestHandler handler = getBlocksRequestHandlers.get(uid);
handler.stop();
getBlocksRequestHandlers.remove(uid);
}
}, CLEANUP_TIMER);
}
} else {
log.warn("We have stopped already. We ignore that onMessage call.");
}
handleGetBlocksRequest((GetBlocksRequest) networkEnvelope, connection);
} else if (networkEnvelope instanceof RepublishGovernanceDataRequest) {
log.warn("We received a RepublishGovernanceDataRequest and re-published all proposalPayloads and " +
"blindVotePayloads to the P2P network.");
missingDataRequestService.reRepublishAllGovernanceData();
handleRepublishGovernanceDataRequest();
}
}
private void handleGetBlocksRequest(GetBlocksRequest getBlocksRequest, Connection connection) {
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)) {
GetBlocksRequestHandler handler = getBlocksRequestHandlers.get(uid);
handler.stop();
getBlocksRequestHandlers.remove(uid);
}
}, CLEANUP_TIMER);
return;
}
GetBlocksRequestHandler requestHandler = new GetBlocksRequestHandler(networkNode,
daoStateService,
new GetBlocksRequestHandler.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 void handleRepublishGovernanceDataRequest() {
log.warn("We received a RepublishGovernanceDataRequest and re-published all proposalPayloads and " +
"blindVotePayloads to the P2P network.");
missingDataRequestService.reRepublishAllGovernanceData();
}
}

View file

@ -49,7 +49,7 @@ import org.jetbrains.annotations.NotNull;
*/
@Slf4j
class GetBlocksRequestHandler {
private static final long TIMEOUT = 120;
private static final long TIMEOUT_MIN = 3;
///////////////////////////////////////////////////////////////////////////////////////////
@ -89,22 +89,28 @@ class GetBlocksRequestHandler {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onGetBlocksRequest(GetBlocksRequest getBlocksRequest, final Connection connection) {
public void onGetBlocksRequest(GetBlocksRequest getBlocksRequest, Connection connection) {
long ts = System.currentTimeMillis();
// We limit number of blocks to 6000 which is about 1.5 month.
List<Block> blocks = new LinkedList<>(daoStateService.getBlocksFromBlockHeight(getBlocksRequest.getFromBlockHeight(), 6000));
List<RawBlock> rawBlocks = blocks.stream().map(RawBlock::fromBlock).collect(Collectors.toList());
GetBlocksResponse getBlocksResponse = new GetBlocksResponse(rawBlocks, getBlocksRequest.getNonce());
log.info("Received GetBlocksRequest from {} for blocks from height {}",
connection.getPeersNodeAddressOptional(), getBlocksRequest.getFromBlockHeight());
if (timeoutTimer == null) {
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, TimeUnit.SECONDS);
log.info("Received GetBlocksRequest from {} for blocks from height {}. " +
"Building GetBlocksResponse with {} blocks took {} ms.",
connection.getPeersNodeAddressOptional(), getBlocksRequest.getFromBlockHeight(),
rawBlocks.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<>() {
@ -145,7 +151,7 @@ class GetBlocksRequestHandler {
private void handleFault(String errorMessage, CloseConnectionReason closeConnectionReason, Connection connection) {
if (!stopped) {
log.debug(errorMessage + "\n\tcloseConnectionReason=" + closeConnectionReason);
log.warn("{}, closeConnectionReason={}", errorMessage, closeConnectionReason);
cleanup();
listener.onFault(errorMessage, connection);
} else {

View file

@ -17,6 +17,7 @@
package bisq.core.dao.node.lite;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.dao.node.BsqNode;
import bisq.core.dao.node.explorer.ExportJsonFilesService;
@ -37,6 +38,8 @@ import bisq.common.UserThread;
import com.google.inject.Inject;
import javafx.beans.value.ChangeListener;
import java.util.ArrayList;
import java.util.List;
@ -54,7 +57,9 @@ public class LiteNode extends BsqNode {
private final LiteNodeNetworkService liteNodeNetworkService;
private final BsqWalletService bsqWalletService;
private final WalletsSetup walletsSetup;
private Timer checkForBlockReceivedTimer;
private final ChangeListener<Number> blockDownloadListener;
///////////////////////////////////////////////////////////////////////////////////////////
@ -69,11 +74,19 @@ public class LiteNode extends BsqNode {
P2PService p2PService,
LiteNodeNetworkService liteNodeNetworkService,
BsqWalletService bsqWalletService,
WalletsSetup walletsSetup,
ExportJsonFilesService exportJsonFilesService) {
super(blockParser, daoStateService, daoStateSnapshotService, p2PService, exportJsonFilesService);
this.liteNodeNetworkService = liteNodeNetworkService;
this.bsqWalletService = bsqWalletService;
this.walletsSetup = walletsSetup;
blockDownloadListener = (observable, oldValue, newValue) -> {
if ((double) newValue == 1) {
setupWalletBestBlockListener();
}
};
}
@ -87,7 +100,18 @@ public class LiteNode extends BsqNode {
liteNodeNetworkService.start();
bsqWalletService.addNewBestBlockListener(block -> {
// We wait until the wallet is synced before using it for triggering requests
if (walletsSetup.isDownloadComplete()) {
setupWalletBestBlockListener();
} else {
walletsSetup.downloadPercentageProperty().addListener(blockDownloadListener);
}
}
private void setupWalletBestBlockListener() {
walletsSetup.downloadPercentageProperty().removeListener(blockDownloadListener);
bsqWalletService.addNewBestBlockListener(blockFromWallet -> {
// Check if we are done with parsing
if (!daoStateService.isParseBlockChainComplete())
return;
@ -97,18 +121,18 @@ public class LiteNode extends BsqNode {
checkForBlockReceivedTimer.stop();
}
int height = block.getHeight();
log.info("New block at height {} from bsqWalletService", height);
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 chainHeight = daoStateService.getChainHeight();
if (chainHeight < height) {
log.warn("We did not receive a block from the network {} seconds after we saw the new block in BicoinJ. " +
int daoChainHeight = daoStateService.getChainHeight();
if (daoChainHeight < 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, chainHeight + 1);
liteNodeNetworkService.requestBlocks(chainHeight + 1);
CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC, daoChainHeight + 1);
liteNodeNetworkService.requestBlocks(daoChainHeight + 1);
}
}, CHECK_FOR_BLOCK_RECEIVED_DELAY_SEC);
});
@ -157,7 +181,6 @@ public class LiteNode extends BsqNode {
// First we request the blocks from a full node
@Override
protected void startParseBlocks() {
log.info("startParseBlocks");
liteNodeNetworkService.requestBlocks(getStartBlockHeight());
}
@ -199,8 +222,12 @@ public class LiteNode extends BsqNode {
runDelayedBatchProcessing(new ArrayList<>(blockList),
() -> {
log.debug("Parsing {} blocks took {} seconds.", blockList.size(), (System.currentTimeMillis() - ts) / 1000d);
if (daoStateService.getChainHeight() < bsqWalletService.getBestChainHeight()) {
log.info("runDelayedBatchProcessing Parsing {} blocks took {} seconds.", blockList.size(),
(System.currentTimeMillis() - ts) / 1000d);
// We only request again if wallet is synced, otherwise we would get repeated calls we want to avoid.
// We deal with that case at the setupWalletBestBlockListener method above.
if (walletsSetup.isDownloadComplete() &&
daoStateService.getChainHeight() < bsqWalletService.getBestChainHeight()) {
liteNodeNetworkService.requestBlocks(getStartBlockHeight());
} else {
onParsingComplete.run();
@ -229,11 +256,13 @@ public class LiteNode extends BsqNode {
// We received a new block
private void onNewBlockReceived(RawBlock block) {
int blockHeight = block.getHeight();
log.debug("onNewBlockReceived: block at height {}, hash={}", blockHeight, block.getHash());
log.info("onNewBlockReceived: block at height {}, hash={}. Our DAO chainHeight={}",
blockHeight, block.getHash(), chainTipHeight);
// We only update chainTipHeight if we get a newer block
if (blockHeight > chainTipHeight)
if (blockHeight > chainTipHeight) {
chainTipHeight = blockHeight;
}
try {
doParseBlock(block);

View file

@ -99,7 +99,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
private final Map<Tuple2<NodeAddress, Integer>, RequestBlocksHandler> requestBlocksHandlerMap = new HashMap<>();
private Timer retryTimer;
private boolean stopped;
private Set<String> receivedBlocks = new HashSet<>();
private final Set<String> receivedBlocks = new HashSet<>();
///////////////////////////////////////////////////////////////////////////////////////////
@ -129,7 +129,6 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
peerManager.addListener(this);
}
@SuppressWarnings("Duplicates")
public void shutDown() {
stopped = true;
stopRetryTimer();
@ -152,19 +151,21 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
Optional<Connection> connectionToSeedNodeOptional = networkNode.getConfirmedConnections().stream()
.filter(peerManager::isSeedNode)
.findAny();
if (connectionToSeedNodeOptional.isPresent() &&
connectionToSeedNodeOptional.get().getPeersNodeAddressOptional().isPresent()) {
requestBlocks(connectionToSeedNodeOptional.get().getPeersNodeAddressOptional().get(), startBlockHeight);
} else {
tryWithNewSeedNode(startBlockHeight);
}
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(RequestBlocksHandler::cancel);
requestBlocksHandlerMap.values().forEach(RequestBlocksHandler::terminate);
}
@ -202,7 +203,6 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
closeAllHandlers();
stopRetryTimer();
stopped = true;
tryWithNewSeedNode(lastRequestedBlockHeight);
}
@ -218,8 +218,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
log.info("onAwakeFromStandby");
closeAllHandlers();
stopped = false;
if (!networkNode.getAllConnections().isEmpty())
tryWithNewSeedNode(lastRequestedBlockHeight);
tryWithNewSeedNode(lastRequestedBlockHeight);
}
@ -232,17 +231,20 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
if (networkEnvelope instanceof NewBlockBroadcastMessage) {
NewBlockBroadcastMessage newBlockBroadcastMessage = (NewBlockBroadcastMessage) networkEnvelope;
// We combine blockHash and txId list in case we receive blocks with different transactions.
List<String> txIds = newBlockBroadcastMessage.getBlock().getRawTxs().stream().map(BaseTx::getId).collect(Collectors.toList());
String extBlockId = newBlockBroadcastMessage.getBlock().getHash() + ":" + txIds;
if (!receivedBlocks.contains(extBlockId)) {
log.debug("We received a new message from peer {} and broadcast it to our peers. extBlockId={}",
connection.getPeersNodeAddressOptional().orElse(null), extBlockId);
receivedBlocks.add(extBlockId);
broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null));
listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage));
} else {
log.debug("We had that message already and do not further broadcast it. extBlockId={}", extBlockId);
List<String> txIds = newBlockBroadcastMessage.getBlock().getRawTxs().stream()
.map(BaseTx::getId)
.collect(Collectors.toList());
String blockUid = newBlockBroadcastMessage.getBlock().getHash() + ":" + txIds;
if (receivedBlocks.contains(blockUid)) {
log.debug("We had that message already and do not further broadcast it. blockUid={}", blockUid);
return;
}
log.info("We received a NewBlockBroadcastMessage from peer {} and broadcast it to our peers. blockUid={}",
connection.getPeersNodeAddressOptional().orElse(null), blockUid);
receivedBlocks.add(blockUid);
broadcaster.broadcast(newBlockBroadcastMessage, connection.getPeersNodeAddressOptional().orElse(null));
listeners.forEach(listener -> listener.onNewBlockReceived(newBlockBroadcastMessage));
}
}
@ -252,78 +254,85 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
///////////////////////////////////////////////////////////////////////////////////////////
private void requestBlocks(NodeAddress peersNodeAddress, int startBlockHeight) {
if (!stopped) {
final Tuple2<NodeAddress, Integer> key = new Tuple2<>(peersNodeAddress, startBlockHeight);
if (!requestBlocksHandlerMap.containsKey(key)) {
if (startBlockHeight >= lastReceivedBlockHeight) {
RequestBlocksHandler requestBlocksHandler = new RequestBlocksHandler(networkNode,
peerManager,
peersNodeAddress,
startBlockHeight,
new RequestBlocksHandler.Listener() {
@Override
public void onComplete(GetBlocksResponse getBlocksResponse) {
log.debug("requestBlocksHandler of outbound connection complete. nodeAddress={}",
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,
() -> {
// After we received the blocks we allow to disconnect seed nodes.
// We delay 20 seconds to allow multiple requests to finish.
UserThread.runAfter(() -> peerManager.setAllowDisconnectSeedNodes(true), 20);
}));
} else {
log.warn("We got a response which is already obsolete because we receive a " +
"response from a request with a higher block height. " +
"This could theoretically happen, but is very unlikely.");
}
}
@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);
listeners.forEach(listener -> listener.onFault(errorMessage, connection));
tryWithNewSeedNode(startBlockHeight);
}
});
requestBlocksHandlerMap.put(key, requestBlocksHandler);
log.info("requestBlocks with startBlockHeight={} from peer {}", startBlockHeight, peersNodeAddress);
requestBlocksHandler.requestBlocks();
} else {
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);
}
} else {
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)) {
RequestBlocksHandler handler = requestBlocksHandlerMap.get(key);
handler.stop();
requestBlocksHandlerMap.remove(key);
}
}, CLEANUP_TIMER);
}
} else {
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)) {
RequestBlocksHandler 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;
}
// In case we would have had an earlier request and had set allowDisconnectSeedNodes to true we un-do that
// if we get a repeated request.
peerManager.setAllowDisconnectSeedNodes(false);
RequestBlocksHandler requestBlocksHandler = new RequestBlocksHandler(networkNode,
peerManager,
peersNodeAddress,
startBlockHeight,
new RequestBlocksHandler.Listener() {
@Override
public void onComplete(GetBlocksResponse 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,
() -> {
// After we received the blocks we allow to disconnect seed nodes.
// We delay 20 seconds to allow multiple requests to finish.
UserThread.runAfter(() -> peerManager.setAllowDisconnectSeedNodes(true), 20);
}));
} else {
log.warn("We got a response which is already obsolete because we received a " +
"response from a request with a higher block height. " +
"This could theoretically happen, but is very unlikely.");
}
}
@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);
listeners.forEach(listener -> listener.onFault(errorMessage, connection));
// We allow now to disconnect from that seed.
peerManager.setAllowDisconnectSeedNodes(true);
tryWithNewSeedNode(startBlockHeight);
}
});
requestBlocksHandlerMap.put(key, requestBlocksHandler);
requestBlocksHandler.requestBlocks();
}
@ -332,37 +341,52 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
///////////////////////////////////////////////////////////////////////////////////////////
private void tryWithNewSeedNode(int startBlockHeight) {
if (retryTimer == null) {
retryCounter++;
if (retryCounter <= MAX_RETRY) {
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 with {}", nextCandidate);
requestBlocks(nextCandidate, startBlockHeight);
} else {
log.warn("No more seed nodes available we could try.");
listeners.forEach(Listener::onNoSeedNodeAvailable);
}
},
RETRY_DELAY_SEC);
} else {
log.warn("We tried {} times but could not connect to a seed node.", retryCounter);
listeners.forEach(Listener::onNoSeedNodeAvailable);
}
} else {
log.warn("We have a retry timer already running.");
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);
listeners.forEach(Listener::onNoSeedNodeAvailable);
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.");
listeners.forEach(Listener::onNoSeedNodeAvailable);
}
},
RETRY_DELAY_SEC);
}
private void stopRetryTimer() {
@ -386,17 +410,14 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
requestBlocksHandlerMap.entrySet().stream()
.filter(e -> e.getKey().first.equals(nodeAddress))
.findAny()
.map(Map.Entry::getValue)
.ifPresent(handler -> {
final Tuple2<NodeAddress, Integer> key = new Tuple2<>(handler.getNodeAddress(), handler.getStartBlockHeight());
requestBlocksHandlerMap.get(key).cancel();
requestBlocksHandlerMap.remove(key);
.ifPresent(e -> {
e.getValue().terminate();
requestBlocksHandlerMap.remove(e.getKey());
});
}
private void closeAllHandlers() {
requestBlocksHandlerMap.values().forEach(RequestBlocksHandler::cancel);
requestBlocksHandlerMap.values().forEach(RequestBlocksHandler::terminate);
requestBlocksHandlerMap.clear();
}
}

View file

@ -36,7 +36,9 @@ 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;
@ -44,14 +46,12 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Sends a GetBlocksRequest to a full node and listens on corresponding GetBlocksResponse from the full node.
*/
@Slf4j
public class RequestBlocksHandler implements MessageListener {
private static final long TIMEOUT = 120;
private static final long TIMEOUT_MIN = 3;
///////////////////////////////////////////////////////////////////////////////////////////
@ -98,66 +98,61 @@ public class RequestBlocksHandler implements MessageListener {
this.listener = listener;
}
public void cancel() {
cleanup();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void requestBlocks() {
if (!stopped) {
GetBlocksRequest getBlocksRequest = new GetBlocksRequest(startBlockHeight, nonce, networkNode.getNodeAddress());
log.debug("getBlocksRequest " + getBlocksRequest);
if (timeoutTimer == null) {
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(errorMessage + " / RequestDataHandler=" + RequestBlocksHandler.this);
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_TIMEOUT);
} else {
log.trace("We have stopped already. We ignore that timeoutTimer.run call. " +
"Might be caused by an previous networkNode.sendMessage.onFailure.");
}
},
TIMEOUT);
if (stopped) {
log.warn("We have stopped already. We ignore that requestData call.");
return;
}
GetBlocksRequest getBlocksRequest = new GetBlocksRequest(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, RequestBlocksHandler.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 GetBlocksRequest message to peer {} succeeded.", nodeAddress.getFullAddress());
}
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) {
if (!stopped) {
log.info("Sending of GetBlocksRequest message to peer {} succeeded.", nodeAddress.getFullAddress());
} else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by a previous timeout.");
}
@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.");
}
@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.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " +
"Might be caused by a previous timeout.");
}
}
}, MoreExecutors.directExecutor());
} else {
log.warn("We have stopped already. We ignore that requestData call.");
}
}
}, MoreExecutors.directExecutor());
}
@ -168,56 +163,60 @@ public class RequestBlocksHandler implements MessageListener {
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
if (networkEnvelope instanceof GetBlocksResponse) {
if (connection.getPeersNodeAddressOptional().isPresent() && connection.getPeersNodeAddressOptional().get().equals(nodeAddress)) {
if (!stopped) {
GetBlocksResponse getBlocksResponse = (GetBlocksResponse) networkEnvelope;
if (getBlocksResponse.getRequestNonce() == nonce) {
stopTimeoutTimer();
checkArgument(connection.getPeersNodeAddressOptional().isPresent(),
"RequestDataHandler.onMessage: connection.getPeersNodeAddressOptional() must be present " +
"at that moment");
cleanup();
log.info("We received from peer {} a BlocksResponse with {} blocks",
nodeAddress.getFullAddress(), getBlocksResponse.getBlocks().size());
listener.onComplete(getBlocksResponse);
} else {
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());
}
} else {
log.warn("We have stopped already. We ignore that onDataRequest call.");
}
} else {
log.warn("We got a message from ourselves. That should never happen.");
if (stopped) {
log.warn("We have stopped already. We ignore that onDataRequest call.");
return;
}
Optional<NodeAddress> optionalNodeAddress = connection.getPeersNodeAddressOptional();
if (!optionalNodeAddress.isPresent()) {
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;
}
GetBlocksResponse getBlocksResponse = (GetBlocksResponse) 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 stop() {
cleanup();
public void terminate() {
stopped = true;
networkNode.removeMessageListener(this);
stopTimeoutTimer();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("UnusedParameters")
private void handleFault(String errorMessage, NodeAddress nodeAddress, CloseConnectionReason closeConnectionReason) {
cleanup();
private void handleFault(String errorMessage,
NodeAddress nodeAddress,
CloseConnectionReason closeConnectionReason) {
terminate();
peerManager.handleConnectionFault(nodeAddress);
listener.onFault(errorMessage, null);
}
private void cleanup() {
stopped = true;
networkNode.removeMessageListener(this);
stopTimeoutTimer();
}
private void stopTimeoutTimer() {
if (timeoutTimer != null) {
timeoutTimer.stop();

View file

@ -27,7 +27,8 @@ import com.google.common.collect.ImmutableList;
import java.util.Objects;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nullable;
@ -36,7 +37,8 @@ import javax.annotation.Nullable;
* After parsing it will get cloned to the immutable Tx.
* We don't need to implement the ProtoBuffer methods as it is not persisted or sent over the wire.
*/
@Data
@Getter
@Setter
public class TempTx extends BaseTx {
static TempTx fromRawTx(RawTx rawTx) {
return new TempTx(rawTx.getTxVersion(),

View file

@ -24,7 +24,8 @@ import bisq.core.dao.state.model.blockchain.TxOutputType;
import java.util.Objects;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nullable;
@ -32,7 +33,8 @@ import javax.annotation.Nullable;
* Contains mutable BSQ specific data (TxOutputType) and used only during tx parsing.
* Will get converted to immutable TxOutput after tx parsing is completed.
*/
@Data
@Getter
@Setter
public class TempTxOutput extends BaseTxOutput {
static TempTxOutput fromRawTxOutput(RawTxOutput txOutput) {
return new TempTxOutput(txOutput.getIndex(),
@ -78,6 +80,10 @@ public class TempTxOutput extends BaseTxOutput {
this.unlockBlockHeight = unlockBlockHeight;
}
public boolean isOpReturnOutput() {
// We do not check for pubKeyScript.scriptType.NULL_DATA because that is only set if dumpBlockchainData is true
return getOpReturnData() != null;
}
@Override
public String toString() {
@ -88,12 +94,6 @@ public class TempTxOutput extends BaseTxOutput {
"\n} " + super.toString();
}
public boolean isOpReturnOutput() {
// We do not check for pubKeyScript.scriptType.NULL_DATA because that is only set if dumpBlockchainData is true
return getOpReturnData() != null;
}
// Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)!
// The equals and hashCode methods cannot be overwritten in Enums.
@Override

View file

@ -87,9 +87,9 @@ import static com.google.common.base.Preconditions.checkNotNull;
*/
@Slf4j
class TxOutputParser {
private static int ACTIVATE_HARD_FORK_1_HEIGHT_MAINNET = 605000;
private static int ACTIVATE_HARD_FORK_1_HEIGHT_TESTNET = 1583054;
private static int ACTIVATE_HARD_FORK_1_HEIGHT_REGTEST = 1;
private static final int ACTIVATE_HARD_FORK_1_HEIGHT_MAINNET = 605000;
private static final int ACTIVATE_HARD_FORK_1_HEIGHT_TESTNET = 1583054;
private static final int ACTIVATE_HARD_FORK_1_HEIGHT_REGTEST = 1;
private final DaoStateService daoStateService;
// Setters

View file

@ -24,7 +24,7 @@ import lombok.Getter;
@Getter
public class BlockHashNotConnectingException extends Exception {
private RawBlock rawBlock;
private final RawBlock rawBlock;
public BlockHashNotConnectingException(RawBlock rawBlock) {
this.rawBlock = rawBlock;

View file

@ -24,7 +24,7 @@ import lombok.Getter;
@Getter
public class BlockHeightNotConnectingException extends Exception {
private RawBlock rawBlock;
private final RawBlock rawBlock;
public BlockHeightNotConnectingException(RawBlock rawBlock) {
this.rawBlock = rawBlock;

View file

@ -24,7 +24,7 @@ import lombok.Getter;
@Getter
public class RequiredReorgFromSnapshotException extends Exception {
private RawBlock rawBlock;
private final RawBlock rawBlock;
public RequiredReorgFromSnapshotException(RawBlock rawBlock) {
this.rawBlock = rawBlock;

View file

@ -597,23 +597,18 @@ public class DaoStateService implements DaoSetupService {
daoState.getIssuanceMap().put(issuance.getTxId(), issuance);
}
public Set<Issuance> getIssuanceSet(IssuanceType issuanceType) {
public Set<Issuance> getIssuanceSetForType(IssuanceType issuanceType) {
return daoState.getIssuanceMap().values().stream()
.filter(issuance -> issuance.getIssuanceType() == issuanceType)
.collect(Collectors.toSet());
}
public Optional<Issuance> getIssuance(String txId, IssuanceType issuanceType) {
return daoState.getIssuanceMap().values().stream()
.filter(issuance -> issuance.getTxId().equals(txId))
.filter(issuance -> issuance.getIssuanceType() == issuanceType)
.findAny();
return getIssuance(txId).filter(issuance -> issuance.getIssuanceType() == issuanceType);
}
public Optional<Issuance> getIssuance(String txId) {
return daoState.getIssuanceMap().values().stream()
.filter(issuance -> issuance.getTxId().equals(txId))
.findAny();
return Optional.ofNullable(daoState.getIssuanceMap().get(txId));
}
public boolean isIssuanceTx(String txId) {

View file

@ -17,21 +17,29 @@
package bisq.core.dao.state;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.state.model.DaoState;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.storage.DaoStateStorageService;
import bisq.common.config.Config;
import javax.inject.Inject;
import javax.inject.Named;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/**
* Manages periodical snapshots of the DaoState.
* At startup we apply a snapshot if available.
@ -45,13 +53,16 @@ public class DaoStateSnapshotService {
private final DaoStateService daoStateService;
private final GenesisTxInfo genesisTxInfo;
private final CycleService cycleService;
private final DaoStateStorageService daoStateStorageService;
private final DaoStateMonitoringService daoStateMonitoringService;
private final File storageDir;
private DaoState daoStateSnapshotCandidate;
private LinkedList<DaoStateHash> daoStateHashChainSnapshotCandidate = new LinkedList<>();
private int chainHeightOfLastApplySnapshot;
@Setter
@Nullable
private Runnable daoRequiresRestartHandler;
///////////////////////////////////////////////////////////////////////////////////////////
@ -61,14 +72,14 @@ public class DaoStateSnapshotService {
@Inject
public DaoStateSnapshotService(DaoStateService daoStateService,
GenesisTxInfo genesisTxInfo,
CycleService cycleService,
DaoStateStorageService daoStateStorageService,
DaoStateMonitoringService daoStateMonitoringService) {
DaoStateMonitoringService daoStateMonitoringService,
@Named(Config.STORAGE_DIR) File storageDir) {
this.daoStateService = daoStateService;
this.genesisTxInfo = genesisTxInfo;
this.cycleService = cycleService;
this.daoStateStorageService = daoStateStorageService;
this.daoStateMonitoringService = daoStateMonitoringService;
this.storageDir = storageDir;
}
@ -128,15 +139,19 @@ public class DaoStateSnapshotService {
} else {
// The reorg might have been caused by the previous parsing which might contains a range of
// blocks.
log.warn("We applied already a snapshot with chainHeight {}. We will reset the daoState and " +
"start over from the genesis transaction again.", chainHeightOfLastApplySnapshot);
applyEmptySnapshot();
log.warn("We applied already a snapshot with chainHeight {}. " +
"We remove all dao store files and shutdown. After a restart resource files will " +
"be applied if available.",
chainHeightOfLastApplySnapshot);
resyncDaoStateFromResources();
}
}
} else if (fromReorg) {
log.info("We got a reorg and we want to apply the snapshot but it is empty. That is expected in the first blocks until the " +
"first snapshot has been created. We use our applySnapshot method and restart from the genesis tx");
applyEmptySnapshot();
log.info("We got a reorg and we want to apply the snapshot but it is empty. " +
"That is expected in the first blocks until the first snapshot has been created. " +
"We remove all dao store files and shutdown. " +
"After a restart resource files will be applied if available.");
resyncDaoStateFromResources();
}
} else {
log.info("Try to apply snapshot but no stored snapshot available. That is expected at first blocks.");
@ -152,16 +167,17 @@ public class DaoStateSnapshotService {
return heightOfLastBlock >= genesisTxInfo.getGenesisBlockHeight();
}
private void applyEmptySnapshot() {
DaoState emptyDaoState = new DaoState();
int genesisBlockHeight = genesisTxInfo.getGenesisBlockHeight();
emptyDaoState.setChainHeight(genesisBlockHeight);
chainHeightOfLastApplySnapshot = genesisBlockHeight;
daoStateService.applySnapshot(emptyDaoState);
// In case we apply an empty snapshot we need to trigger the cycleService.addFirstCycle method
cycleService.addFirstCycle();
private void resyncDaoStateFromResources() {
log.info("resyncDaoStateFromResources called");
try {
daoStateStorageService.resyncDaoStateFromResources(storageDir);
daoStateMonitoringService.applySnapshot(new LinkedList<>());
if (daoRequiresRestartHandler != null) {
daoRequiresRestartHandler.run();
}
} catch (IOException e) {
log.error("Error at resyncDaoStateFromResources: {}", e.toString());
}
}
@VisibleForTesting

View file

@ -81,6 +81,8 @@ public class DaoState implements PersistablePayload {
private final LinkedList<Cycle> cycles;
// These maps represent mutual data which can get changed at parsing a transaction
// We use TreeMaps instead of HashMaps because we need deterministic sorting of the maps for the hashChains
// used for the DAO monitor.
@Getter
private final TreeMap<TxOutputKey, TxOutput> unspentTxOutputMap;
@Getter

View file

@ -46,6 +46,9 @@ public final class Role implements PersistablePayload, NetworkPayload, BondedAss
private final String link;
private final BondedRoleType bondedRoleType;
// Only used as cache
transient private final byte[] hash;
/**
* @param name Full name or nickname
* @param link GitHub account or forum account of user
@ -74,6 +77,8 @@ public final class Role implements PersistablePayload, NetworkPayload, BondedAss
this.name = name;
this.link = link;
this.bondedRoleType = bondedRoleType;
hash = Hash.getSha256Ripemd160hash(toProtoMessage().toByteArray());
}
@Override
@ -100,8 +105,7 @@ public final class Role implements PersistablePayload, NetworkPayload, BondedAss
@Override
public byte[] getHash() {
byte[] bytes = toProtoMessage().toByteArray();
return Hash.getSha256Ripemd160hash(bytes);
return hash;
}
@Override

View file

@ -466,7 +466,11 @@ public class FilterManager {
Filter currentFilter = getFilter();
if (!isFilterPublicKeyInList(newFilter)) {
log.warn("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex={}", newFilter.getSignerPubKeyAsHex());
if (newFilter.getSignerPubKeyAsHex() != null && !newFilter.getSignerPubKeyAsHex().isEmpty()) {
log.warn("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex={}", newFilter.getSignerPubKeyAsHex());
} else {
log.info("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex not set (expected case for pre v1.3.9 filter)");
}
return;
}
if (!isSignatureValid(newFilter)) {
@ -593,7 +597,7 @@ public class FilterManager {
private boolean isFilterPublicKeyInList(Filter filter) {
String signerPubKeyAsHex = filter.getSignerPubKeyAsHex();
if (!isPublicKeyInList(signerPubKeyAsHex)) {
log.warn("Invalid filter (expected case for pre v1.3.9 filter as we still keep that in the network " +
log.info("Invalid filter (expected case for pre v1.3.9 filter as we still keep that in the network " +
"but the new version does not recognize it as valid filter): " +
"signerPubKeyAsHex from filter is not part of our pub key list. " +
"signerPubKeyAsHex={}, publicKeys={}, filterCreationDate={}",
@ -606,7 +610,7 @@ public class FilterManager {
private boolean isPublicKeyInList(String pubKeyAsHex) {
boolean isPublicKeyInList = publicKeys.contains(pubKeyAsHex);
if (!isPublicKeyInList) {
log.warn("pubKeyAsHex is not part of our pub key list. pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys);
log.info("pubKeyAsHex is not part of our pub key list (expected case for pre v1.3.9 filter). pubKeyAsHex={}, publicKeys={}", pubKeyAsHex, publicKeys);
}
return isPublicKeyInList;
}

View file

@ -42,6 +42,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -53,7 +54,6 @@ import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
public class CurrencyUtil {
public static void setup() {
setBaseCurrencyCode(Config.baseCurrencyNetwork().getCurrencyCode());
}
@ -62,6 +62,14 @@ public class CurrencyUtil {
private static String baseCurrencyCode = "BTC";
// Calls to isFiatCurrency and isCryptoCurrency are very frequent so we use a cache of the results.
// The main improvement was already achieved with using memoize for the source maps, but
// the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50%
// for isFiatCurrency calls (1777 ms vs 3467 ms).
// See: https://github.com/bisq-network/bisq/pull/4955#issuecomment-745302802
private static final Map<String, Boolean> isFiatCurrencyMap = new ConcurrentHashMap<>();
private static final Map<String, Boolean> isCryptoCurrencyMap = new ConcurrentHashMap<>();
private static Supplier<Map<String, FiatCurrency>> fiatCurrencyMapSupplier = Suppliers.memoize(
CurrencyUtil::createFiatCurrencyMap)::get;
private static Supplier<Map<String, CryptoCurrency>> cryptoCurrencyMapSupplier = Suppliers.memoize(
@ -293,7 +301,6 @@ public class CurrencyUtil {
new FiatCurrency("MAD"),
new FiatCurrency("NPR"),
new FiatCurrency("NZD"),
new FiatCurrency("NGN"),
new FiatCurrency("NOK"),
new FiatCurrency("PKR"),
new FiatCurrency("PEN"),
@ -391,12 +398,25 @@ public class CurrencyUtil {
}
public static boolean isFiatCurrency(String currencyCode) {
if (currencyCode != null && isFiatCurrencyMap.containsKey(currencyCode)) {
return isFiatCurrencyMap.get(currencyCode);
}
try {
return currencyCode != null
boolean isFiatCurrency = currencyCode != null
&& !currencyCode.isEmpty()
&& !isCryptoCurrency(currencyCode)
&& Currency.getInstance(currencyCode) != null;
if (currencyCode != null) {
isFiatCurrencyMap.put(currencyCode, isFiatCurrency);
}
return isFiatCurrency;
} catch (Throwable t) {
if (currencyCode != null) {
isFiatCurrencyMap.put(currencyCode, false);
}
return false;
}
}
@ -417,28 +437,37 @@ public class CurrencyUtil {
* contains 3 entries (CryptoCurrency, Fiat, Undefined).
*/
public static boolean isCryptoCurrency(String currencyCode) {
// Some tests call that method with null values. Should be fixed in the tests but to not break them return false.
if (currencyCode == null)
return false;
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode)) {
return isCryptoCurrencyMap.get(currencyCode);
}
// BTC is not part of our assetRegistry so treat it extra here. Other old base currencies (LTC, DOGE, DASH)
// are not supported anymore so we can ignore that case.
if (currencyCode.equals("BTC"))
return true;
boolean isCryptoCurrency;
if (currencyCode == null) {
// Some tests call that method with null values. Should be fixed in the tests but to not break them return false.
isCryptoCurrency = false;
} else if (currencyCode.equals("BTC")) {
// BTC is not part of our assetRegistry so treat it extra here. Other old base currencies (LTC, DOGE, DASH)
// are not supported anymore so we can ignore that case.
isCryptoCurrency = true;
} else if (getCryptoCurrency(currencyCode).isPresent()) {
// If we find the code in our assetRegistry we return true.
// It might be that an asset was removed from the assetsRegistry, we deal with such cases below by checking if
// it is a fiat currency
isCryptoCurrency = true;
} else if (!getFiatCurrency(currencyCode).isPresent()) {
// In case the code is from a removed asset we cross check if there exist a fiat currency with that code,
// if we don't find a fiat currency we treat it as a crypto currency.
isCryptoCurrency = true;
} else {
// If we would have found a fiat currency we return false
isCryptoCurrency = false;
}
// If we find the code in our assetRegistry we return true.
// It might be that an asset was removed from the assetsRegistry, we deal with such cases below by checking if
// it is a fiat currency
if (getCryptoCurrency(currencyCode).isPresent())
return true;
if (currencyCode != null) {
isCryptoCurrencyMap.put(currencyCode, isCryptoCurrency);
}
// In case the code is from a removed asset we cross check if there exist a fiat currency with that code,
// if we don't find a fiat currency we treat it as a crypto currency.
if (!getFiatCurrency(currencyCode).isPresent())
return true;
// If we would have found a fiat currency we return false
return false;
return isCryptoCurrency;
}
public static Optional<CryptoCurrency> getCryptoCurrency(String currencyCode) {
@ -527,7 +556,9 @@ public class CurrencyUtil {
return new CryptoCurrency(asset.getTickerSymbol(), asset.getName(), asset instanceof Token);
}
private static boolean isNotBsqOrBsqTradingActivated(Asset asset, BaseCurrencyNetwork baseCurrencyNetwork, boolean daoTradingActivated) {
private static boolean isNotBsqOrBsqTradingActivated(Asset asset,
BaseCurrencyNetwork baseCurrencyNetwork,
boolean daoTradingActivated) {
return !(asset instanceof BSQ) ||
daoTradingActivated && assetMatchesNetwork(asset, baseCurrencyNetwork);
}
@ -582,7 +613,8 @@ public class CurrencyUtil {
}
// Excludes all assets which got removed by DAO voting
public static List<CryptoCurrency> getActiveSortedCryptoCurrencies(AssetService assetService, FilterManager filterManager) {
public static List<CryptoCurrency> getActiveSortedCryptoCurrencies(AssetService assetService,
FilterManager filterManager) {
return getAllSortedCryptoCurrencies().stream()
.filter(e -> e.getCode().equals("BSQ") || assetService.isActive(e.getCode()))
.filter(e -> !filterManager.isCurrencyBanned(e.getCode()))

View file

@ -256,6 +256,11 @@ public class MobileNotificationService {
boolean useSound,
Consumer<String> resultHandler,
Consumer<Throwable> errorHandler) throws Exception {
if (httpClient.hasPendingRequest()) {
log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient);
return;
}
String msg;
if (mobileModel.getOs() == null)
throw new RuntimeException("No mobileModel OS set");
@ -297,7 +302,7 @@ public class MobileNotificationService {
String threadName = "sendMobileNotification-" + msgAsHex.substring(0, 5) + "...";
ListenableFuture<String> future = executorService.submit(() -> {
Thread.currentThread().setName(threadName);
String result = httpClient.requestWithGET(param, "User-Agent",
String result = httpClient.get(param, "User-Agent",
"bisq/" + Version.VERSION + ", uid:" + httpClient.getUid());
log.info("sendMobileNotification result: " + result);
checkArgument(result.equals(SUCCESS), "Result was not 'success'. result=" + result);

View file

@ -55,8 +55,9 @@ public class MyOfferTakenEvents {
}
private void onOpenOfferRemoved(OpenOffer openOffer) {
log.info("We got a offer removed. id={}, state={}", openOffer.getId(), openOffer.getState());
if (openOffer.getState() == OpenOffer.State.RESERVED) {
OpenOffer.State state = openOffer.getState();
if (state == OpenOffer.State.RESERVED) {
log.info("We got a offer removed. id={}, state={}", openOffer.getId(), state);
String shortId = openOffer.getShortId();
MobileMessage message = new MobileMessage(Res.get("account.notifications.offer.message.title"),
Res.get("account.notifications.offer.message.msg", shortId),

View file

@ -109,6 +109,11 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Setter
transient private PriceFeedService priceFeedService;
// Used only as cache
@Nullable
@JsonExclude
transient private String currencyCode;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -346,8 +351,9 @@ public class Offer implements NetworkPayload, PersistablePayload {
public Optional<String> getAccountAgeWitnessHashAsHex() {
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.ACCOUNT_AGE_WITNESS_HASH))
return Optional.of(getExtraDataMap().get(OfferPayload.ACCOUNT_AGE_WITNESS_HASH));
Map<String, String> extraDataMap = getExtraDataMap();
if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.ACCOUNT_AGE_WITNESS_HASH))
return Optional.of(extraDataMap.get(OfferPayload.ACCOUNT_AGE_WITNESS_HASH));
else
return Optional.empty();
}
@ -421,9 +427,14 @@ public class Offer implements NetworkPayload, PersistablePayload {
}
public String getCurrencyCode() {
return offerPayload.getBaseCurrencyCode().equals("BTC") ?
if (currencyCode != null) {
return currencyCode;
}
currencyCode = offerPayload.getBaseCurrencyCode().equals("BTC") ?
offerPayload.getCounterCurrencyCode() :
offerPayload.getBaseCurrencyCode();
return currencyCode;
}
public long getProtocolVersion() {

View file

@ -203,7 +203,6 @@ public class OfferBookService {
}
public void removeOfferAtShutDown(OfferPayload offerPayload) {
log.debug("removeOfferAtShutDown " + offerPayload);
removeOffer(offerPayload, null, null);
}

View file

@ -19,6 +19,7 @@ package bisq.core.offer;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.filter.FilterManager;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
@ -44,6 +45,9 @@ import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
@ -373,14 +377,72 @@ public class OfferUtil {
tradeStatisticsManager,
30);
Price bsqPrice = tuple.second;
String inputValue = bsqFormatter.formatCoin(makerFee);
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
Volume volumeByAmount = userCurrencyPrice.getVolumeByAmount(requiredBtc);
return Optional.of(volumeByAmount);
if (bsqPrice.isPositive()) {
String inputValue = bsqFormatter.formatCoin(makerFee);
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
Volume volumeByAmount = userCurrencyPrice.getVolumeByAmount(requiredBtc);
return Optional.of(volumeByAmount);
} else {
return Optional.empty();
}
}
} else {
return Optional.empty();
}
}
public static Optional<String> getInvalidMakerFeeTxErrorMessage(Offer offer, BtcWalletService btcWalletService) {
String offerFeePaymentTxId = offer.getOfferFeePaymentTxId();
if (offerFeePaymentTxId == null) {
return Optional.empty();
}
Transaction makerFeeTx = btcWalletService.getTransaction(offerFeePaymentTxId);
if (makerFeeTx == null) {
return Optional.empty();
}
String errorMsg = null;
String header = "The offer with offer ID '" + offer.getShortId() +
"' has an invalid maker fee transaction.\n\n";
String spendingTransaction = null;
String extraString = "\nYou have to remove that offer to avoid failed trades.\n" +
"If this happened because of a bug please contact the Bisq developers " +
"and you can request reimbursement for the lost maker fee.";
if (makerFeeTx.getOutputs().size() > 1) {
// Our output to fund the deposit tx is at index 1
TransactionOutput output = makerFeeTx.getOutput(1);
TransactionInput spentByTransactionInput = output.getSpentBy();
if (spentByTransactionInput != null) {
spendingTransaction = spentByTransactionInput.getConnectedTransaction() != null ?
spentByTransactionInput.getConnectedTransaction().toString() :
"null";
// We this is an exceptional case we do not translate that error msg.
errorMsg = "The output of the maker fee tx is already spent.\n" +
extraString +
"\n\nTransaction input which spent the reserved funds for that offer: '" +
spentByTransactionInput.getConnectedTransaction().getTxId().toString() + ":" +
(spentByTransactionInput.getConnectedOutput() != null ?
spentByTransactionInput.getConnectedOutput().getIndex() + "'" :
"null'");
log.error("spentByTransactionInput {}", spentByTransactionInput);
}
} else {
errorMsg = "The maker fee tx is invalid as it does not has at least 2 outputs." + extraString +
"\nMakerFeeTx=" + makerFeeTx.toString();
}
if (errorMsg == null) {
return Optional.empty();
}
errorMsg = header + errorMsg;
log.error(errorMsg);
if (spendingTransaction != null) {
log.error("Spending transaction: {}", spendingTransaction);
}
return Optional.of(errorMsg);
}
}

View file

@ -69,9 +69,18 @@ public final class OpenOffer implements Tradable {
@Nullable
private NodeAddress refundAgentNodeAddress;
// Added in v1.5.3.
// If market price reaches that trigger price the offer gets deactivated
@Getter
private final long triggerPrice;
public OpenOffer(Offer offer) {
this(offer, 0);
}
public OpenOffer(Offer offer, long triggerPrice) {
this.offer = offer;
this.triggerPrice = triggerPrice;
state = State.AVAILABLE;
}
@ -83,12 +92,14 @@ public final class OpenOffer implements Tradable {
State state,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress) {
@Nullable NodeAddress refundAgentNodeAddress,
long triggerPrice) {
this.offer = offer;
this.state = state;
this.arbitratorNodeAddress = arbitratorNodeAddress;
this.mediatorNodeAddress = mediatorNodeAddress;
this.refundAgentNodeAddress = refundAgentNodeAddress;
this.triggerPrice = triggerPrice;
if (this.state == State.RESERVED)
setState(State.AVAILABLE);
@ -98,6 +109,7 @@ public final class OpenOffer implements Tradable {
public protobuf.Tradable toProtoMessage() {
protobuf.OpenOffer.Builder builder = protobuf.OpenOffer.newBuilder()
.setOffer(offer.toProtoMessage())
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()));
Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage()));
@ -112,7 +124,8 @@ public final class OpenOffer implements Tradable {
ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()),
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null,
proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null);
proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null,
proto.getTriggerPrice());
}
@ -178,6 +191,7 @@ public final class OpenOffer implements Tradable {
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n mediatorNodeAddress=" + mediatorNodeAddress +
",\n refundAgentNodeAddress=" + refundAgentNodeAddress +
",\n triggerPrice=" + triggerPrice +
"\n}";
}
}

View file

@ -49,6 +49,7 @@ import bisq.network.p2p.DecryptedMessageWithPubKey;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendDirectMessageListener;
import bisq.network.p2p.peers.Broadcaster;
import bisq.network.p2p.peers.PeerManager;
import bisq.common.Timer;
@ -63,11 +64,13 @@ import bisq.common.handlers.ResultHandler;
import bisq.common.persistence.PersistenceManager;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.persistable.PersistedDataHost;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import javax.inject.Inject;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
@ -82,6 +85,8 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
@ -113,11 +118,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private final RefundAgentManager refundAgentManager;
private final DaoFacade daoFacade;
private final FilterManager filterManager;
private final Broadcaster broadcaster;
private final PersistenceManager<TradableList<OpenOffer>> persistenceManager;
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
private final TradableList<OpenOffer> openOffers = new TradableList<>();
private boolean stopped;
private Timer periodicRepublishOffersTimer, periodicRefreshOffersTimer, retryRepublishOffersTimer;
@Getter
private final ObservableList<Tuple2<OpenOffer, String>> invalidOffers = FXCollections.observableArrayList();
///////////////////////////////////////////////////////////////////////////////////////////
@ -142,6 +150,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
FilterManager filterManager,
Broadcaster broadcaster,
PersistenceManager<TradableList<OpenOffer>> persistenceManager) {
this.createOfferService = createOfferService;
this.keyRing = keyRing;
@ -160,6 +169,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.refundAgentManager = refundAgentManager;
this.daoFacade = daoFacade;
this.filterManager = filterManager;
this.broadcaster = broadcaster;
this.persistenceManager = persistenceManager;
this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE);
@ -190,6 +200,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
cleanUpAddressEntries();
openOffers.stream()
.forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
.ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
}
private void cleanUpAddressEntries() {
@ -204,10 +218,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
});
}
private void shutDown() {
shutDown(null);
}
public void shutDown(@Nullable Runnable completeHandler) {
stopped = true;
p2PService.getPeerManager().removeListener(this);
@ -225,8 +235,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
UserThread.execute(() -> openOffers.forEach(
openOffer -> offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload())
));
if (completeHandler != null)
UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS);
// Force broadcaster to send out immediately, otherwise we could have a 2 sec delay until the
// bundled messages sent out.
broadcaster.flush();
if (completeHandler != null) {
// For typical number of offers we are tolerant with delay to give enough time to broadcast.
// If number of offers is very high we limit to 3 sec. to not delay other shutdown routines.
int delay = Math.min(3000, size * 200 + 500);
UserThread.runAfter(completeHandler, delay, TimeUnit.MILLISECONDS);
}
} else {
if (completeHandler != null)
completeHandler.run();
@ -344,6 +363,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void placeOffer(Offer offer,
double buyerSecurityDeposit,
boolean useSavingsWallet,
long triggerPrice,
TransactionResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer.getMakerFee(), "makerFee must not be null");
@ -368,7 +388,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
model,
transaction -> {
OpenOffer openOffer = new OpenOffer(offer);
OpenOffer openOffer = new OpenOffer(offer, triggerPrice);
openOffers.add(openOffer);
requestPersistence();
resultHandler.handleResult(transaction);
@ -472,6 +492,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
public void editOpenOfferPublish(Offer editedOffer,
long triggerPrice,
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
@ -484,7 +505,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setState(OpenOffer.State.CANCELED);
openOffers.remove(openOffer);
OpenOffer editedOpenOffer = new OpenOffer(editedOffer);
OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice);
editedOpenOffer.setState(originalState);
openOffers.add(editedOpenOffer);
@ -841,7 +862,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
updatedOffer.setPriceFeedService(priceFeedService);
updatedOffer.setState(originalOfferState);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice());
updatedOpenOffer.setState(originalOpenOfferState);
openOffers.add(updatedOpenOffer);
requestPersistence();
@ -857,41 +878,53 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
private void republishOffers() {
int size = openOffers.size();
final ArrayList<OpenOffer> openOffersList = new ArrayList<>(openOffers.getList());
if (!stopped) {
stopPeriodicRefreshOffersTimer();
for (int i = 0; i < size; i++) {
// we delay to avoid reaching throttle limits
if (stopped) {
return;
}
long delay = 700;
final long minDelay = (i + 1) * delay;
final long maxDelay = (i + 2) * delay;
final OpenOffer openOffer = openOffersList.get(i);
UserThread.runAfterRandomDelay(() -> {
if (openOffers.contains(openOffer)) {
String id = openOffer.getId();
if (id != null && !openOffer.isDeactivated())
republishOffer(openOffer);
}
stopPeriodicRefreshOffersTimer();
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
}
List<OpenOffer> openOffersList = new ArrayList<>(openOffers.getList());
processListForRepublishOffers(openOffersList);
}
private void processListForRepublishOffers(List<OpenOffer> list) {
if (list.isEmpty()) {
return;
}
OpenOffer openOffer = list.remove(0);
if (openOffers.contains(openOffer) && !openOffer.isDeactivated()) {
// TODO It is not clear yet if it is better for the node and the network to send out all add offer
// messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have
// some significant impact to user experience and the network
republishOffer(openOffer, () -> processListForRepublishOffers(list));
/* republishOffer(openOffer,
() -> UserThread.runAfter(() -> processListForRepublishOffers(list),
30, TimeUnit.MILLISECONDS));*/
} else {
log.debug("We have stopped already. We ignore that republishOffers call.");
// If the offer was removed in the meantime or if its deactivated we skip and call
// processListForRepublishOffers again with the list where we removed the offer already.
processListForRepublishOffers(list);
}
}
private void republishOffer(OpenOffer openOffer) {
republishOffer(openOffer, null);
}
private void republishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
offerBookService.addOffer(openOffer.getOffer(),
() -> {
if (!stopped) {
log.debug("Successfully added offer to P2P network.");
// Refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.)
if (periodicRefreshOffersTimer == null)
if (periodicRefreshOffersTimer == null) {
startPeriodicRefreshOffersTimer();
} else {
log.debug("We have stopped already. We ignore that offerBookService.republishOffers.onSuccess call.");
}
if (completeHandler != null) {
completeHandler.run();
}
}
},
errorMessage -> {
@ -900,26 +933,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
stopRetryRepublishOffersTimer();
retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers,
RETRY_REPUBLISH_DELAY_SEC);
} else {
log.debug("We have stopped already. We ignore that offerBookService.republishOffers.onFault call.");
if (completeHandler != null) {
completeHandler.run();
}
}
});
}
private void startPeriodicRepublishOffersTimer() {
stopped = false;
if (periodicRepublishOffersTimer == null)
if (periodicRepublishOffersTimer == null) {
periodicRepublishOffersTimer = UserThread.runPeriodically(() -> {
if (!stopped) {
republishOffers();
} else {
log.debug("We have stopped already. We ignore that periodicRepublishOffersTimer.run call.");
}
},
REPUBLISH_INTERVAL_MS,
TimeUnit.MILLISECONDS);
else
log.trace("periodicRepublishOffersTimer already stated");
}
}
private void startPeriodicRefreshOffersTimer() {

View file

@ -0,0 +1,163 @@
/*
* 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.offer;
import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.common.util.MathUtils;
import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.collections.ListChangeListener;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import static bisq.common.util.MathUtils.roundDoubleToLong;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
@Slf4j
@Singleton
public class TriggerPriceService {
private final OpenOfferManager openOfferManager;
private final PriceFeedService priceFeedService;
private final Map<String, Set<OpenOffer>> openOffersByCurrency = new HashMap<>();
@Inject
public TriggerPriceService(OpenOfferManager openOfferManager, PriceFeedService priceFeedService) {
this.openOfferManager = openOfferManager;
this.priceFeedService = priceFeedService;
}
public void onAllServicesInitialized() {
openOfferManager.getObservableList().addListener((ListChangeListener<OpenOffer>) c -> {
c.next();
if (c.wasAdded()) {
onAddedOpenOffers(c.getAddedSubList());
}
if (c.wasRemoved()) {
onRemovedOpenOffers(c.getRemoved());
}
});
onAddedOpenOffers(openOfferManager.getObservableList());
priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> onPriceFeedChanged());
onPriceFeedChanged();
}
private void onPriceFeedChanged() {
openOffersByCurrency.keySet().stream()
.map(priceFeedService::getMarketPrice)
.filter(Objects::nonNull)
.filter(marketPrice -> openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
.forEach(marketPrice -> {
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
.filter(openOffer -> !openOffer.isDeactivated())
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
});
}
public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
Price price = openOffer.getOffer().getPrice();
if (price == null) {
return false;
}
String currencyCode = openOffer.getOffer().getCurrencyCode();
boolean cryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
int smallestUnitExponent = cryptoCurrency ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long marketPriceAsLong = roundDoubleToLong(
scaleUpByPowerOf10(marketPrice.getPrice(), smallestUnitExponent));
long triggerPrice = openOffer.getTriggerPrice();
if (triggerPrice <= 0) {
return false;
}
OfferPayload.Direction direction = openOffer.getOffer().getDirection();
boolean isSellOffer = direction == OfferPayload.Direction.SELL;
boolean condition = isSellOffer && !cryptoCurrency || !isSellOffer && cryptoCurrency;
return condition ?
marketPriceAsLong < triggerPrice :
marketPriceAsLong > triggerPrice;
}
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
if (wasTriggered(marketPrice, openOffer)) {
String currencyCode = openOffer.getOffer().getCurrencyCode();
int smallestUnitExponent = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long triggerPrice = openOffer.getTriggerPrice();
log.info("Market price exceeded the trigger price of the open offer.\n" +
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}",
openOffer.getOffer().getShortId(),
currencyCode,
openOffer.getOffer().getDirection(),
marketPrice.getPrice(),
MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
);
openOfferManager.deactivateOpenOffer(openOffer, () -> {
}, errorMessage -> {
});
}
}
private void onAddedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode();
openOffersByCurrency.putIfAbsent(currencyCode, new HashSet<>());
openOffersByCurrency.get(currencyCode).add(openOffer);
MarketPrice marketPrice = priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode());
if (marketPrice != null) {
checkPriceThreshold(marketPrice, openOffer);
}
});
}
private void onRemovedOpenOffers(List<? extends OpenOffer> openOffers) {
openOffers.forEach(openOffer -> {
String currencyCode = openOffer.getOffer().getCurrencyCode();
if (openOffersByCurrency.containsKey(currencyCode)) {
Set<OpenOffer> set = openOffersByCurrency.get(currencyCode);
set.remove(openOffer);
if (set.isEmpty()) {
openOffersByCurrency.remove(currencyCode);
}
}
});
}
}

View file

@ -97,12 +97,25 @@ public abstract class PaymentAccount implements PersistablePayload {
}
public static PaymentAccount fromProto(protobuf.PaymentAccount proto, CoreProtoResolver coreProtoResolver) {
PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethodById(proto.getPaymentMethod().getId()));
String paymentMethodId = proto.getPaymentMethod().getId();
List<TradeCurrency> tradeCurrencies = proto.getTradeCurrenciesList().stream()
.map(TradeCurrency::fromProto)
.collect(Collectors.toList());
// We need to remove NGN for Transferwise
Optional<TradeCurrency> ngnTwOptional = tradeCurrencies.stream()
.filter(e -> paymentMethodId.equals(PaymentMethod.TRANSFERWISE_ID))
.filter(e -> e.getCode().equals("NGN"))
.findAny();
// We cannot remove it in the stream as it would cause a concurrentModificationException
ngnTwOptional.ifPresent(tradeCurrencies::remove);
PaymentAccount account = PaymentAccountFactory.getPaymentAccount(PaymentMethod.getPaymentMethodById(paymentMethodId));
account.getTradeCurrencies().clear();
account.setId(proto.getId());
account.setCreationDate(proto.getCreationDate());
account.setAccountName(proto.getAccountName());
account.getTradeCurrencies().addAll(proto.getTradeCurrenciesList().stream().map(TradeCurrency::fromProto).collect(Collectors.toList()));
account.getTradeCurrencies().addAll(tradeCurrencies);
account.setPaymentAccountPayload(coreProtoResolver.fromProto(proto.getPaymentAccountPayload()));
if (proto.hasSelectedTradeCurrency())

View file

@ -17,7 +17,6 @@
package bisq.core.payment;
import bisq.core.locale.CurrencyUtil;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.payment.payload.TransferwiseAccountPayload;
@ -28,8 +27,6 @@ import lombok.EqualsAndHashCode;
public final class TransferwiseAccount extends PaymentAccount {
public TransferwiseAccount() {
super(PaymentMethod.TRANSFERWISE);
tradeCurrencies.addAll(CurrencyUtil.getAllTransferwiseCurrencies());
}
@Override

View file

@ -332,7 +332,11 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
@Override
public int compareTo(@NotNull PaymentMethod other) {
return id.compareTo(other.id);
return Res.get(id).compareTo(Res.get(other.id));
}
public String getDisplayString() {
return Res.get(id);
}
public boolean isAsset() {

View file

@ -40,8 +40,6 @@ public class TradePresentation {
long numPendingTrades = (long) newValue;
if (numPendingTrades > 0)
this.numPendingTrades.set(String.valueOf(numPendingTrades));
if (numPendingTrades > 9)
this.numPendingTrades.set("");
showPendingTradesNotification.set(numPendingTrades > 0);
});

View file

@ -21,12 +21,14 @@ import bisq.network.Socks5ProxyProvider;
import bisq.network.http.HttpClientImpl;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.annotation.Nullable;
public class PriceNodeHttpClient extends HttpClientImpl {
@Singleton
public class FeeHttpClient extends HttpClientImpl {
@Inject
public PriceNodeHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) {
public FeeHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) {
super(socks5ProxyProvider);
}
}

View file

@ -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.provider;
import bisq.network.Socks5ProxyProvider;
import bisq.network.http.HttpClientImpl;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.annotation.Nullable;
@Singleton
public class PriceHttpClient extends HttpClientImpl {
@Inject
public PriceHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) {
super(socks5ProxyProvider);
}
}

View file

@ -79,10 +79,8 @@ public class ProvidersRepository {
fillProviderList();
selectNextProviderBaseUrl();
if (bannedNodes == null) {
log.info("Selected provider baseUrl={}, providerList={}", baseUrl, providerList);
} else if (!bannedNodes.isEmpty()) {
log.warn("We have banned provider nodes: bannedNodes={}, selected provider baseUrl={}, providerList={}",
if (bannedNodes != null && !bannedNodes.isEmpty()) {
log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}",
bannedNodes, baseUrl, providerList);
}
}

View file

@ -17,10 +17,12 @@
package bisq.core.provider.fee;
import bisq.core.provider.FeeHttpClient;
import bisq.core.provider.HttpClientProvider;
import bisq.core.provider.PriceNodeHttpClient;
import bisq.core.provider.ProvidersRepository;
import bisq.network.http.HttpClient;
import bisq.common.app.Version;
import bisq.common.util.Tuple2;
@ -40,12 +42,12 @@ import lombok.extern.slf4j.Slf4j;
public class FeeProvider extends HttpClientProvider {
@Inject
public FeeProvider(PriceNodeHttpClient httpClient, ProvidersRepository providersRepository) {
public FeeProvider(FeeHttpClient httpClient, ProvidersRepository providersRepository) {
super(httpClient, providersRepository.getBaseUrl(), false);
}
public Tuple2<Map<String, Long>, Map<String, Long>> getFees() throws IOException {
String json = httpClient.requestWithGET("getFees", "User-Agent", "bisq/" + Version.VERSION);
String json = httpClient.get("getFees", "User-Agent", "bisq/" + Version.VERSION);
LinkedTreeMap<?, ?> linkedTreeMap = new Gson().fromJson(json, LinkedTreeMap.class);
Map<String, Long> tsMap = new HashMap<>();
@ -64,4 +66,8 @@ public class FeeProvider extends HttpClientProvider {
}
return new Tuple2<>(tsMap, map);
}
public HttpClient getHttpClient() {
return httpClient;
}
}

View file

@ -45,7 +45,7 @@ public class FeeRequest {
public SettableFuture<Tuple2<Map<String, Long>, Map<String, Long>>> getFees(FeeProvider provider) {
final SettableFuture<Tuple2<Map<String, Long>, Map<String, Long>>> resultFuture = SettableFuture.create();
ListenableFuture<Tuple2<Map<String, Long>, Map<String, Long>>> future = executorService.submit(() -> {
Thread.currentThread().setName("FeeRequest-" + provider.toString());
Thread.currentThread().setName("FeeRequest @ " + provider.getHttpClient().getBaseUrl());
return provider.getFees();
});

View file

@ -137,6 +137,11 @@ public class FeeService {
}
public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) {
if (feeProvider.getHttpClient().hasPendingRequest()) {
log.warn("We have a pending request open. We ignore that request. httpClient {}", feeProvider.getHttpClient());
return;
}
long now = Instant.now().getEpochSecond();
// We all requests only each 2 minutes
if (now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) {

View file

@ -21,7 +21,7 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.provider.PriceNodeHttpClient;
import bisq.core.provider.PriceHttpClient;
import bisq.core.provider.ProvidersRepository;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.user.Preferences;
@ -92,6 +92,8 @@ public class PriceFeedService {
private String baseUrlOfRespondingProvider;
@Nullable
private Timer requestTimer;
@Nullable
private PriceRequest priceRequest;
///////////////////////////////////////////////////////////////////////////////////////////
@ -99,7 +101,7 @@ public class PriceFeedService {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public PriceFeedService(@SuppressWarnings("SameParameterValue") PriceNodeHttpClient httpClient,
public PriceFeedService(PriceHttpClient httpClient,
@SuppressWarnings("SameParameterValue") ProvidersRepository providersRepository,
@SuppressWarnings("SameParameterValue") Preferences preferences) {
this.httpClient = httpClient;
@ -115,10 +117,20 @@ public class PriceFeedService {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void shutDown() {
if (requestTimer != null) {
requestTimer.stop();
requestTimer = null;
}
if (priceRequest != null) {
priceRequest.shutDown();
}
}
public void setCurrencyCodeOnInit() {
if (getCurrencyCode() == null) {
final TradeCurrency preferredTradeCurrency = preferences.getPreferredTradeCurrency();
final String code = preferredTradeCurrency != null ? preferredTradeCurrency.getCode() : "USD";
TradeCurrency preferredTradeCurrency = preferences.getPreferredTradeCurrency();
String code = preferredTradeCurrency != null ? preferredTradeCurrency.getCode() : "USD";
setCurrencyCode(code);
}
}
@ -180,11 +192,11 @@ public class PriceFeedService {
}
}, (errorMessage, throwable) -> {
if (throwable instanceof PriceRequestException) {
final String baseUrlOfFaultyRequest = ((PriceRequestException) throwable).priceProviderBaseUrl;
final String baseUrlOfCurrentRequest = priceProvider.getBaseUrl();
if (baseUrlOfFaultyRequest != null && baseUrlOfCurrentRequest.equals(baseUrlOfFaultyRequest)) {
log.warn("We received an error: baseUrlOfCurrentRequest={}, baseUrlOfFaultyRequest={}",
baseUrlOfCurrentRequest, baseUrlOfFaultyRequest);
String baseUrlOfFaultyRequest = ((PriceRequestException) throwable).priceProviderBaseUrl;
String baseUrlOfCurrentRequest = priceProvider.getBaseUrl();
if (baseUrlOfCurrentRequest.equals(baseUrlOfFaultyRequest)) {
log.warn("We received an error: baseUrlOfCurrentRequest={}, baseUrlOfFaultyRequest={}, error={}",
baseUrlOfCurrentRequest, baseUrlOfFaultyRequest, throwable.toString());
retryWithNewProvider();
} else {
log.debug("We received an error from an earlier request. We have started a new request already so we ignore that error. " +
@ -192,7 +204,7 @@ public class PriceFeedService {
baseUrlOfCurrentRequest, baseUrlOfFaultyRequest);
}
} else {
log.warn("We received an error with throwable={}", throwable);
log.warn("We received an error with throwable={}", throwable.toString());
retryWithNewProvider();
}
@ -223,7 +235,7 @@ public class PriceFeedService {
UserThread.runAfter(() -> {
retryDelay = Math.min(retryDelay + 5, PERIOD_SEC);
final String oldBaseUrl = priceProvider.getBaseUrl();
String oldBaseUrl = priceProvider.getBaseUrl();
setNewPriceProvider();
log.warn("We received an error at the request from provider {}. " +
"We select the new provider {} and use that for a new request. retryDelay was {} sec.", oldBaseUrl, priceProvider.getBaseUrl(), retryDelay);
@ -381,15 +393,20 @@ public class PriceFeedService {
}
private void requestAllPrices(PriceProvider provider, Runnable resultHandler, FaultHandler faultHandler) {
PriceRequest priceRequest = new PriceRequest();
if (httpClient.hasPendingRequest()) {
log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient);
return;
}
priceRequest = new PriceRequest();
SettableFuture<Tuple2<Map<String, Long>, Map<String, MarketPrice>>> future = priceRequest.requestAllPrices(provider);
Futures.addCallback(future, new FutureCallback<Tuple2<Map<String, Long>, Map<String, MarketPrice>>>() {
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Tuple2<Map<String, Long>, Map<String, MarketPrice>> result) {
UserThread.execute(() -> {
checkNotNull(result, "Result must not be null at requestAllPrices");
// Each currency rate has a different timestamp, depending on when
// the pricenode aggregate rate was calculated
// the priceNode aggregate rate was calculated
// However, the request timestamp is when the pricenode was queried
epochInMillisAtLastRequest = System.currentTimeMillis();

View file

@ -41,18 +41,24 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PriceProvider extends HttpClientProvider {
private boolean shutDownRequested;
// Do not use Guice here as we might create multiple instances
public PriceProvider(HttpClient httpClient, String baseUrl) {
super(httpClient, baseUrl, false);
}
public Tuple2<Map<String, Long>, Map<String, MarketPrice>> getAll() throws IOException {
if (shutDownRequested) {
return new Tuple2<>(new HashMap<>(), new HashMap<>());
}
Map<String, MarketPrice> marketPriceMap = new HashMap<>();
String hsVersion = "";
if (P2PService.getMyNodeAddress() != null)
hsVersion = P2PService.getMyNodeAddress().getHostName().length() > 22 ? ", HSv3" : ", HSv2";
String json = httpClient.requestWithGET("getAllMarketPrices", "User-Agent", "bisq/"
String json = httpClient.get("getAllMarketPrices", "User-Agent", "bisq/"
+ Version.VERSION + hsVersion);
@ -66,10 +72,10 @@ public class PriceProvider extends HttpClientProvider {
list.forEach(obj -> {
try {
LinkedTreeMap<?, ?> treeMap = (LinkedTreeMap<?, ?>) obj;
final String currencyCode = (String) treeMap.get("currencyCode");
final double price = (Double) treeMap.get("price");
String currencyCode = (String) treeMap.get("currencyCode");
double price = (Double) treeMap.get("price");
// json uses double for our timestampSec long value...
final long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec"));
long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec"));
marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true));
} catch (Throwable t) {
log.error(t.toString());
@ -83,4 +89,9 @@ public class PriceProvider extends HttpClientProvider {
public String getBaseUrl() {
return httpClient.getBaseUrl();
}
public void shutDown() {
shutDownRequested = true;
httpClient.shutDown();
}
}

View file

@ -28,37 +28,64 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Slf4j
public class PriceRequest {
private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService("PriceRequest", 3, 5, 10 * 60);
@Nullable
private PriceProvider provider;
private boolean shutDownRequested;
public PriceRequest() {
}
public SettableFuture<Tuple2<Map<String, Long>, Map<String, MarketPrice>>> requestAllPrices(PriceProvider provider) {
final String baseUrl = provider.getBaseUrl();
final SettableFuture<Tuple2<Map<String, Long>, Map<String, MarketPrice>>> resultFuture = SettableFuture.create();
this.provider = provider;
String baseUrl = provider.getBaseUrl();
SettableFuture<Tuple2<Map<String, Long>, Map<String, MarketPrice>>> resultFuture = SettableFuture.create();
ListenableFuture<Tuple2<Map<String, Long>, Map<String, MarketPrice>>> future = executorService.submit(() -> {
Thread.currentThread().setName("PriceRequest-" + provider.getBaseUrl());
Thread.currentThread().setName("PriceRequest @ " + baseUrl);
return provider.getAll();
});
Futures.addCallback(future, new FutureCallback<Tuple2<Map<String, Long>, Map<String, MarketPrice>>>() {
Futures.addCallback(future, new FutureCallback<>() {
public void onSuccess(Tuple2<Map<String, Long>, Map<String, MarketPrice>> marketPriceTuple) {
log.trace("Received marketPriceTuple of {}\nfrom provider {}", marketPriceTuple, provider);
resultFuture.set(marketPriceTuple);
if (!shutDownRequested) {
resultFuture.set(marketPriceTuple);
}
}
public void onFailure(@NotNull Throwable throwable) {
resultFuture.setException(new PriceRequestException(throwable, baseUrl));
if (!shutDownRequested) {
resultFuture.setException(new PriceRequestException(throwable, baseUrl));
}
}
}, MoreExecutors.directExecutor());
return resultFuture;
}
public void shutDown() {
shutDownRequested = true;
if (provider != null) {
provider.shutDown();
}
executorService.shutdown();
try {
if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
}
}

View file

@ -47,6 +47,8 @@ public class CoreNetworkCapabilities {
if (config.daoActivated) {
maybeApplyDaoFullMode(config);
}
log.info(Capabilities.app.prettyPrint());
}
public static void maybeApplyDaoFullMode(Config config) {
@ -54,12 +56,10 @@ public class CoreNetworkCapabilities {
// bit later than we call that method so we have to add DAO_FULL_NODE Capability at preferences as well to
// be sure it is set in both cases.
if (config.fullDaoNode) {
log.info("Set Capability.DAO_FULL_NODE");
Capabilities.app.addAll(Capability.DAO_FULL_NODE);
} else {
// A lite node has the capability to receive bsq blocks. We do not want to send BSQ blocks to full nodes
// as they ignore them anyway.
log.info("Set Capability.RECEIVE_BSQ_BLOCK");
Capabilities.app.addAll(Capability.RECEIVE_BSQ_BLOCK);
}
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.agent;
import bisq.core.locale.Res;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class DisputeAgentLookupMap {
// See also: https://bisq.wiki/Finding_your_mediator
@Nullable
public static String getKeyBaseUserName(String fullAddress) {
switch (fullAddress) {
case "sjlho4zwp3gecspf.onion:9999":
return "leo816";
case "wizbisqzd7ku25di7p2ztsajioabihlnyp5lq5av66tmu7do2dke2tid.onion:9999":
return "wiz";
case "apbp7ubuyezav4hy.onion:9999":
return "bisq_knight";
case "a56olqlmmpxrn5q34itq5g5tb5d3fg7vxekpbceq7xqvfl3cieocgsyd.onion:9999":
return "leo816";
case "3z5jnirlccgxzoxc6zwkcgwj66bugvqplzf6z2iyd5oxifiaorhnanqd.onion:9999":
return "refundagent2";
default:
log.warn("No user name for dispute agent with address {} found.", fullAddress);
return Res.get("shared.na");
}
}
}

View file

@ -1039,6 +1039,7 @@ public abstract class Trade implements Tradable, Model {
return offer.getOfferFeePaymentTxId() == null ||
getTakerFeeTxId() == null ||
getDepositTxId() == null ||
getDepositTx() == null ||
getDelayedPayoutTxBytes() == null;
}

View file

@ -82,23 +82,21 @@ public class CleanupMailboxMessages {
private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey,
List<Trade> trades) {
trades.forEach(trade -> handleDecryptedMessageWithPubKey(decryptedMessageWithPubKey, trade));
trades.stream()
.filter(trade -> isMessageForTrade(decryptedMessageWithPubKey, trade))
.filter(trade -> isPubKeyValid(decryptedMessageWithPubKey, trade))
.forEach(trade -> removeEntryFromMailbox(decryptedMessageWithPubKey, trade));
}
private void handleDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey,
Trade trade) {
private boolean isMessageForTrade(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
if (!isPubKeyValid(decryptedMessageWithPubKey, trade)) {
return;
}
if (networkEnvelope instanceof TradeMessage &&
isMyMessage((TradeMessage) networkEnvelope, trade)) {
removeEntryFromMailbox(decryptedMessageWithPubKey, trade);
} else if (networkEnvelope instanceof AckMessage &&
isMyMessage((AckMessage) networkEnvelope, trade)) {
removeEntryFromMailbox(decryptedMessageWithPubKey, trade);
if (networkEnvelope instanceof TradeMessage) {
return isMyMessage((TradeMessage) networkEnvelope, trade);
} else if (networkEnvelope instanceof AckMessage) {
return isMyMessage((AckMessage) networkEnvelope, trade);
}
// Instance must be TradeMessage or AckMessage.
return false;
}
private void removeEntryFromMailbox(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) {
@ -124,7 +122,7 @@ public class CleanupMailboxMessages {
if (peersPubKeyRing != null &&
!message.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) {
isValid = false;
log.error("SignaturePubKey in message does not match the SignaturePubKey we have set for our trading peer.");
log.warn("SignaturePubKey in message does not match the SignaturePubKey we have set for our trading peer.");
}
return isValid;
}

View file

@ -286,8 +286,10 @@ public class FluentProtocol {
log.info(info);
return Result.VALID.info(info);
} else {
String info = MessageFormat.format("We received a {0} but we are are not in the expected phase. " +
"Expected phases={1}, Trade phase={2}, Trade state= {3}, tradeId={4}",
String info = MessageFormat.format("We received a {0} but we are are not in the expected phase.\n" +
"This can be an expected case if we get a repeated CounterCurrencyTransferStartedMessage " +
"after we have already received one as the peer re-sends that message at each startup.\n" +
"Expected phases={1},\nTrade phase={2},\nTrade state= {3},\ntradeId={4}",
trigger,
expectedPhases,
trade.getPhase(),

View file

@ -196,7 +196,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
.condition(condition)
.resultHandler(result -> {
if (!result.isValid()) {
log.error(result.getInfo());
log.warn(result.getInfo());
handleTaskRunnerFault(null,
result.name(),
result.getInfo());

Some files were not shown because too many files have changed in this diff Show more