Merge branch 'master' of github.com:bisq-network/bisq into hotfix/v1.5.3

# Conflicts:
#	build.gradle
#	desktop/package/linux/Dockerfile
#	desktop/package/linux/package.sh
#	desktop/package/linux/release.sh
#	desktop/package/macosx/create_app.sh
#	desktop/package/macosx/finalize.sh
#	desktop/package/macosx/insert_snapshot_version.sh
#	desktop/package/windows/package.bat
#	desktop/package/windows/release.bat
#	relay/src/main/resources/version.txt
This commit is contained in:
Christoph Atteneder 2020-12-30 15:15:06 +01:00
commit 0c83a9b0cd
No known key found for this signature in database
GPG Key ID: CD5DC1C529CDFD3B
134 changed files with 4223 additions and 1362 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

@ -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

@ -592,6 +592,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

@ -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

@ -74,6 +74,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;
@ -139,9 +140,14 @@ public class AccountAgeWitnessService {
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
@ -235,8 +241,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) {
@ -286,12 +301,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) {
@ -658,16 +682,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,

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()));
}

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

@ -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

@ -51,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;
@ -182,6 +183,9 @@ public class BisqSetup {
private Runnable qubesOSInfoHandler;
@Setter
@Nullable
private Runnable daoRequiresRestartHandler;
@Setter
@Nullable
private Consumer<String> downGradePreventionHandler;
@Getter
@ -270,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;
}
@ -443,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

@ -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

@ -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

@ -440,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
@ -455,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
@ -488,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;
@ -968,7 +978,7 @@ public class BtcWalletService extends WalletService {
}
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);
@ -1048,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);
@ -1070,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);
@ -1097,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);
@ -1213,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),
@ -1246,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

@ -1111,15 +1111,7 @@ public class TradeWalletService {
byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey();
byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey();
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey,
hashedMultiSigOutputIsLegacy);
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));

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

@ -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

@ -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;
@ -117,6 +118,7 @@ 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<>();
@ -148,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;
@ -166,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);
@ -214,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);
@ -235,6 +235,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
UserThread.execute(() -> openOffers.forEach(
openOffer -> offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload())
));
// 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.
@ -358,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");
@ -382,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);
@ -486,6 +492,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
public void editOpenOfferPublish(Offer editedOffer,
long triggerPrice,
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
@ -498,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);
@ -855,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();
@ -871,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 -> {
@ -914,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

@ -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

@ -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,8 +122,7 @@ public class CleanupMailboxMessages {
if (peersPubKeyRing != null &&
!message.getSignaturePubKey().equals(peersPubKeyRing.getSignaturePubKey())) {
isValid = false;
// We iterate over all trades so it is expected that the msg which are not assigned to that trade fails.
log.debug("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

@ -0,0 +1,63 @@
/*
* 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.user;
import bisq.common.proto.ProtoUtil;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
/**
* Serves as flexible container for persisting UI states, layout,...
* Should not be over-used for domain specific data where type safety and data integrity is important.
*/
public class Cookie extends HashMap<CookieKey, String> {
public void putAsDouble(CookieKey key, double value) {
put(key, String.valueOf(value));
}
public Optional<Double> getAsOptionalDouble(CookieKey key) {
try {
return containsKey(key) ?
Optional.of(Double.parseDouble(get(key))) :
Optional.empty();
} catch (Throwable t) {
return Optional.empty();
}
}
public Map<String, String> toProtoMessage() {
Map<String, String> protoMap = new HashMap<>();
this.forEach((key, value) -> protoMap.put(key.name(), value));
return protoMap;
}
public static Cookie fromProto(@Nullable Map<String, String> protoMap) {
Cookie cookie = new Cookie();
if (protoMap != null) {
protoMap.forEach((key, value) -> cookie.put(ProtoUtil.enumFromProto(CookieKey.class, key), value));
}
return cookie;
}
}

View File

@ -0,0 +1,26 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.user;
// Used for persistence of Cookie. Entries must not be changes or removed. Only adding entries is permitted.
public enum CookieKey {
STAGE_X,
STAGE_Y,
STAGE_W,
STAGE_H
}

View File

@ -514,4 +514,8 @@ public class User implements PersistedDataHost {
private boolean paymentAccountExists(PaymentAccount paymentAccount) {
return getPaymentAccountsAsObservable().stream().anyMatch(e -> e.equals(paymentAccount));
}
public Cookie getCookie() {
return userPayload.getCookie();
}
}

View File

@ -80,6 +80,11 @@ public class UserPayload implements PersistableEnvelope {
@Nullable
private List<RefundAgent> acceptedRefundAgents = new ArrayList<>();
// Added at 1.5.3
// Generic map for persisting various UI states. We keep values un-typed as string to
// provide sufficient flexibility.
private Cookie cookie = new Cookie();
public UserPayload() {
}
@ -118,6 +123,7 @@ public class UserPayload implements PersistableEnvelope {
Optional.ofNullable(acceptedRefundAgents)
.ifPresent(e -> builder.addAllAcceptedRefundAgents(ProtoUtil.collectionToProto(acceptedRefundAgents,
message -> ((protobuf.StoragePayload) message).getRefundAgent())));
Optional.ofNullable(cookie).ifPresent(e -> builder.putAllCookie(cookie.toProtoMessage()));
return protobuf.PersistableEnvelope.newBuilder().setUserPayload(builder).build();
}
@ -147,7 +153,8 @@ public class UserPayload implements PersistableEnvelope {
proto.hasRegisteredRefundAgent() ? RefundAgent.fromProto(proto.getRegisteredRefundAgent()) : null,
proto.getAcceptedRefundAgentsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedRefundAgentsList().stream()
.map(RefundAgent::fromProto)
.collect(Collectors.toList()))
.collect(Collectors.toList())),
Cookie.fromProto(proto.getCookieMap())
);
}
}

View File

@ -171,7 +171,7 @@ public class FormattingUtils {
return formatMarketPrice(price, 8);
}
private static String formatMarketPrice(double price, int precision) {
public static String formatMarketPrice(double price, int precision) {
return formatRoundedDoubleWithPrecision(price, precision);
}

View File

@ -105,7 +105,6 @@ shared.selectTradingAccount=Select trading account
shared.fundFromSavingsWalletButton=Transfer funds from Bisq wallet
shared.fundFromExternalWalletButton=Open your external wallet for funding
shared.openDefaultWalletFailed=Failed to open a Bitcoin wallet application. Are you sure you have one installed?
shared.distanceInPercent=Distance in % from market price
shared.belowInPercent=Below % from market price
shared.aboveInPercent=Above % from market price
shared.enterPercentageValue=Enter % value
@ -192,7 +191,7 @@ shared.tradeWalletBalance=Trade wallet balance
shared.makerTxFee=Maker: {0}
shared.takerTxFee=Taker: {0}
shared.iConfirm=I confirm
shared.tradingFeeInBsqInfo=equivalent to {0} used as trading fee
shared.tradingFeeInBsqInfo=≈ {0}
shared.openURL=Open {0}
shared.fiat=Fiat
shared.crypto=Crypto
@ -454,15 +453,27 @@ createOffer.warning.sellBelowMarketPrice=You will always get {0}% less than the
createOffer.warning.buyAboveMarketPrice=You will always pay {0}% more than the current market price as the price of your offer will be continuously updated.
createOffer.tradeFee.descriptionBTCOnly=Trade fee
createOffer.tradeFee.descriptionBSQEnabled=Select trade fee currency
createOffer.tradeFee.fiatAndPercent=≈ {0} / {1} of trade amount
createOffer.triggerPrice.prompt=Set optional trigger price
createOffer.triggerPrice.label=Deactivate offer if market price is {0}
createOffer.triggerPrice.tooltip=As protecting against drastic price movements you can set a trigger price which \
deactivates the offer if the market price reaches that value.
createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0}
createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0}
# new entries
createOffer.placeOfferButton=Review: Place offer to {0} bitcoin
createOffer.alreadyFunded=You had already funded that offer.\nYour funds have been moved to your local Bisq wallet and are available for withdrawal in the \"Funds/Send funds\" screen.
createOffer.createOfferFundWalletInfo.headline=Fund your offer
# suppress inspection "TrailingSpacesInProperty"
createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n
createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\nThose funds are reserved in your local wallet and will get locked into the multisig deposit address once someone takes your offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3}\n- Mining fee: {4}\n\nYou can choose between two options when funding your trade:\n- Use your Bisq wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup.
createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\n\
Those funds are reserved in your local wallet and will get locked into the multisig deposit address once someone takes your offer.\n\n\
The amount is the sum of:\n\
{1}\
- Your security deposit: {2}\n\
- Trading fee: {3}\n\
- Mining fee: {4}\n\n\
You can choose between two options when funding your trade:\n- Use your Bisq wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup.
# only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!)
createOffer.amountPriceBox.error.message=An error occurred when placing the offer:\n\n{0}\n\n\
@ -519,7 +530,6 @@ takeOffer.error.message=An error occurred when taking the offer.\n\n{0}
# new entries
takeOffer.takeOfferButton=Review: Take offer to {0} bitcoin
takeOffer.noPriceFeedAvailable=You cannot take that offer as it uses a percentage price based on the market price but there is no price feed available.
takeOffer.alreadyFunded.movedFunds=You had already funded that offer.\nYour funds have been moved to your local Bisq wallet and are available for withdrawal in the \"Funds/Send funds\" screen.
takeOffer.takeOfferFundWalletInfo.headline=Fund your trade
# suppress inspection "TrailingSpacesInProperty"
takeOffer.takeOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n
@ -547,6 +557,11 @@ takeOffer.tac=With taking this offer I agree to the trade conditions as defined
# Offerbook / Edit offer
####################################################################
openOffer.header.triggerPrice=Trigger price
openOffer.triggerPrice=Trigger price {0}
openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\
Please edit the offer to define a new trigger price
editOffer.setPrice=Set price
editOffer.confirmEdit=Confirm: Edit offer
editOffer.publishOffer=Publishing your offer.
@ -1110,6 +1125,7 @@ support.error=Receiver could not process message. Error: {0}
support.buyerAddress=BTC buyer address
support.sellerAddress=BTC seller address
support.role=Role
support.agent=Support agent
support.state=State
support.closed=Closed
support.open=Open
@ -1398,17 +1414,15 @@ account.menu.notifications=Notifications
account.menu.walletInfo.balance.headLine=Wallet balances
account.menu.walletInfo.balance.info=This shows the internal wallet balance including unconfirmed transactions.\n\
For Bitcoin the sum of the 'available balance' and the 'reserved for offers balance' must match the internal wallet balance \
displayed here.
For BTC, the internal wallet balance shown below should match the sum of the 'Available' and 'Reserved' balances shown in the top right of this window.
account.menu.walletInfo.xpub.headLine=Watch keys (xpub keys)
account.menu.walletInfo.walletSelector={0} {1} wallet
account.menu.walletInfo.path.headLine=HD keychain paths
account.menu.walletInfo.path.info=If you import the seed words in another wallet (like Electrum) you need to define the \
path. Use that only in emergency cases when you lost access to the Bisq wallet and the data directory.\n\
Spending funds from another wallet can easily screw up the Bisq internal data structures associated with the wallet \
data and can lead to failed trades.\n\
Do NEVER send BSQ from another wallet as that lead very likely to an invalid BSQ transaction and your \
BSQ get burned.
account.menu.walletInfo.path.info=If you import seed words into another wallet (like Electrum), you'll need to define the \
path. This should only be done in emergency cases when you lose access to the Bisq wallet and data directory.\n\
Keep in mind that spending funds from a non-Bisq wallet can bungle the internal Bisq data structures associated with the wallet \
data, which can lead to failed trades.\n\n\
NEVER send BSQ from a non-Bisq wallet, as it will probably lead to an invalid BSQ transaction and losing your BSQ.
account.menu.walletInfo.openDetails=Show raw wallet details and private keys
@ -2702,6 +2716,8 @@ feeOptionWindow.info=You can choose to pay the trade fee in BSQ or in BTC. If yo
feeOptionWindow.optionsLabel=Choose currency for trade fee payment
feeOptionWindow.useBTC=Use BTC
feeOptionWindow.fee={0} (≈ {1})
feeOptionWindow.btcFeeWithFiatAndPercentage={0} (≈ {1} / {2})
feeOptionWindow.btcFeeWithPercentage={0} ({1})
####################################################################
@ -2850,15 +2866,18 @@ popup.info.shutDownWithOpenOffers=Bisq is being shut down, but there are open of
popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\n\
Please make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes].
popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version.
popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue.
popup.privateNotification.headline=Important private notification!
popup.securityRecommendation.headline=Important security recommendation
popup.securityRecommendation.msg=We would like to remind you to consider using password protection for your wallet if you have not already enabled that.\n\nIt is also highly recommended to write down the wallet seed words. Those seed words are like a master password for recovering your Bitcoin wallet.\nAt the \"Wallet Seed\" section you find more information.\n\nAdditionally you should backup the complete application data folder at the \"Backup\" section.
popup.bitcoinLocalhostNode.msg=Bisq detected a locally running Bitcoin Core node (at localhost).\n\
Please make sure that this node is fully synced before you start Bisq and that it is not running in pruned mode.
popup.bitcoinLocalhostNode.additionalRequirements=\n\nFor a well configured node, the requirements are for the node to have pruning disabled and bloom filters enabled.
popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\n\
Please ensure:\n\
- the node is fully synced before starting Bisq\n\
- pruning is disabled ('prune=0' in bitcoin.conf)\n\
- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf)
popup.shutDownInProgress.headline=Shut down in progress
popup.shutDownInProgress.msg=Shutting down application can take a few seconds.\nPlease don't interrupt this process.

View File

@ -310,12 +310,12 @@ public class AccountAgeWitnessServiceTest {
// Remove SignedWitness signed by arbitrator
@SuppressWarnings("OptionalGetWithoutIsPresent")
var signedWitnessArb = signedWitnessService.getSignedWitnessMap().values().stream()
var signedWitnessArb = signedWitnessService.getSignedWitnessMapValues().stream()
.filter(sw -> sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR)
.findAny()
.get();
signedWitnessService.getSignedWitnessMap().remove(signedWitnessArb.getHashAsByteArray());
assertEquals(signedWitnessService.getSignedWitnessMap().size(), 2);
signedWitnessService.removeSignedWitness(signedWitnessArb);
assertEquals(signedWitnessService.getSignedWitnessMapValues().size(), 2);
// Check that no account age witness is a signer
assertFalse(service.accountIsSigner(aew1));
@ -354,7 +354,7 @@ public class AccountAgeWitnessServiceTest {
witnessOwnerPubKey.getEncoded(),
time,
SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING.value);
signedWitnessService.getSignedWitnessMap().putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness);
signedWitnessService.addToMap(signedWitness);
}
}

View File

@ -17,7 +17,6 @@
package bisq.core.dao.state;
import bisq.core.dao.governance.period.CycleService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.state.storage.DaoStateStorageService;
@ -37,9 +36,9 @@ public class DaoStateSnapshotServiceTest {
public void setup() {
daoStateSnapshotService = new DaoStateSnapshotService(mock(DaoStateService.class),
mock(GenesisTxInfo.class),
mock(CycleService.class),
mock(DaoStateStorageService.class),
mock(DaoStateMonitoringService.class));
mock(DaoStateMonitoringService.class),
null);
}
@Test

View File

@ -49,7 +49,7 @@ public class OpenOfferManagerTest {
final OpenOfferManager manager = new OpenOfferManager(null, null, null, p2PService,
null, null, null, offerBookService,
null, null, null,
null, null, null, null, null, null,
null, null, null, null, null, null, null,
persistenceManager);
AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false);
@ -81,7 +81,7 @@ public class OpenOfferManagerTest {
final OpenOfferManager manager = new OpenOfferManager(null, null, null, p2PService,
null, null, null, offerBookService,
null, null, null,
null, null, null, null, null, null,
null, null, null, null, null, null, null,
persistenceManager);
AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false);
@ -106,7 +106,7 @@ public class OpenOfferManagerTest {
final OpenOfferManager manager = new OpenOfferManager(null, null, null, p2PService,
null, null, null, offerBookService,
null, null, null,
null, null, null, null, null, null,
null, null, null, null, null, null, null,
persistenceManager);
AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false);

View File

@ -6,8 +6,6 @@ import bisq.proto.grpc.DisputeAgentsGrpc;
import bisq.proto.grpc.RegisterDisputeAgentReply;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@ -18,10 +16,12 @@ import lombok.extern.slf4j.Slf4j;
class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcDisputeAgentsService(CoreApi coreApi) {
public GrpcDisputeAgentsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
@ -32,14 +32,8 @@ class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
var reply = RegisterDisputeAgentReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -0,0 +1,93 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
import static io.grpc.Status.INVALID_ARGUMENT;
import static io.grpc.Status.UNKNOWN;
/**
* The singleton instance of this class handles any expected core api Throwable by
* wrapping its message in a gRPC StatusRuntimeException and sending it to the client.
* An unexpected Throwable's message will be replaced with an 'unexpected' error message.
*/
@Singleton
@Slf4j
class GrpcExceptionHandler {
private final Predicate<Throwable> isExpectedException = (t) ->
t instanceof IllegalStateException || t instanceof IllegalArgumentException;
@Inject
public GrpcExceptionHandler() {
}
public void handleException(Throwable t, StreamObserver<?> responseObserver) {
// Log the core api error (this is last chance to do that), wrap it in a new
// gRPC StatusRuntimeException, then send it to the client in the gRPC response.
log.error("", t);
var grpcStatusRuntimeException = wrapException(t);
responseObserver.onError(grpcStatusRuntimeException);
throw grpcStatusRuntimeException;
}
private StatusRuntimeException wrapException(Throwable t) {
// We want to be careful about what kinds of exception messages we send to the
// client. Expected core exceptions should be wrapped in an IllegalStateException
// or IllegalArgumentException, with a consistently styled and worded error
// message. But only a small number of the expected error types are currently
// handled this way; there is much work to do to handle the variety of errors
// that can occur in the api. In the meantime, we take care to not pass full,
// unexpected error messages to the client. If the exception type is unexpected,
// we omit details from the gRPC exception sent to the client.
if (isExpectedException.test(t)) {
if (t.getCause() != null)
return new StatusRuntimeException(mapGrpcErrorStatus(t.getCause(), t.getCause().getMessage()));
else
return new StatusRuntimeException(mapGrpcErrorStatus(t, t.getMessage()));
} else {
return new StatusRuntimeException(mapGrpcErrorStatus(t, "unexpected error on server"));
}
}
private Status mapGrpcErrorStatus(Throwable t, String description) {
// We default to the UNKNOWN status, except were the mapping of a core api
// exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy
// to support RESTful clients, we will need to have more specific mappings
// to support correct HTTP 1.1. status codes.
//noinspection SwitchStatementWithTooFewBranches
switch (t.getClass().getSimpleName()) {
// We go ahead and use a switch statement instead of if, in anticipation
// of more, specific exception mappings.
case "IllegalArgumentException":
return INVALID_ARGUMENT.withDescription(description);
default:
return UNKNOWN.withDescription(description);
}
}
}

View File

@ -16,22 +16,27 @@ import java.util.stream.Collectors;
class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcGetTradeStatisticsService(CoreApi coreApi) {
public GrpcGetTradeStatisticsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void getTradeStatistics(GetTradeStatisticsRequest req,
StreamObserver<GetTradeStatisticsReply> responseObserver) {
try {
var tradeStatistics = coreApi.getTradeStatistics().stream()
.map(TradeStatistics3::toProtoTradeStatistics3)
.collect(Collectors.toList());
var tradeStatistics = coreApi.getTradeStatistics().stream()
.map(TradeStatistics3::toProtoTradeStatistics3)
.collect(Collectors.toList());
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -31,8 +31,6 @@ import bisq.proto.grpc.GetOffersReply;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OffersGrpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@ -48,10 +46,12 @@ import static bisq.core.api.model.OfferInfo.toOfferInfo;
class GrpcOffersService extends OffersGrpc.OffersImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcOffersService(CoreApi coreApi) {
public GrpcOffersService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
@ -64,26 +64,28 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@Override
public void getOffers(GetOffersRequest req,
StreamObserver<GetOffersReply> responseObserver) {
List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
.stream().map(OfferInfo::toOfferInfo)
.collect(Collectors.toList());
var reply = GetOffersReply.newBuilder()
.addAllOffers(result.stream()
.map(OfferInfo::toProtoMessage)
.collect(Collectors.toList()))
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
try {
List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
.stream().map(OfferInfo::toOfferInfo)
.collect(Collectors.toList());
var reply = GetOffersReply.newBuilder()
.addAllOffers(result.stream()
.map(OfferInfo::toProtoMessage)
.collect(Collectors.toList()))
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@Override
@ -111,10 +113,8 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -126,10 +126,8 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
var reply = CancelOfferReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -31,8 +31,6 @@ import bisq.proto.grpc.GetPaymentMethodsReply;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.PaymentAccountsGrpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@ -43,10 +41,12 @@ import java.util.stream.Collectors;
class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcPaymentAccountsService(CoreApi coreApi) {
public GrpcPaymentAccountsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
@ -59,14 +59,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -81,14 +75,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
.addAllPaymentAccounts(paymentAccounts).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -103,14 +91,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
.addAllPaymentMethods(paymentMethods).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -124,14 +106,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -23,8 +23,6 @@ import bisq.proto.grpc.MarketPriceReply;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.PriceGrpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@ -35,10 +33,12 @@ import lombok.extern.slf4j.Slf4j;
class GrpcPriceService extends PriceGrpc.PriceImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcPriceService(CoreApi coreApi) {
public GrpcPriceService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
@ -49,10 +49,8 @@ class GrpcPriceService extends PriceGrpc.PriceImplBase {
var reply = MarketPriceReply.newBuilder().setPrice(price).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -17,6 +17,7 @@
package bisq.daemon.grpc;
import bisq.common.UserThread;
import bisq.common.config.Config;
import io.grpc.Server;
@ -30,6 +31,12 @@ import java.io.UncheckedIOException;
import lombok.extern.slf4j.Slf4j;
import static io.grpc.ServerInterceptors.interceptForward;
import bisq.daemon.grpc.interceptor.PasswordAuthInterceptor;
@Singleton
@Slf4j
public class GrpcServer {
@ -48,13 +55,14 @@ public class GrpcServer {
GrpcTradesService tradesService,
GrpcWalletsService walletsService) {
this.server = ServerBuilder.forPort(config.apiPort)
.executor(UserThread.getExecutor())
.addService(disputeAgentsService)
.addService(offersService)
.addService(paymentAccountsService)
.addService(priceService)
.addService(tradeStatisticsService)
.addService(tradesService)
.addService(versionService)
.addService(interceptForward(versionService, versionService.interceptors()))
.addService(walletsService)
.intercept(passwordAuthInterceptor)
.build();

View File

@ -35,8 +35,6 @@ import bisq.proto.grpc.TradesGrpc;
import bisq.proto.grpc.WithdrawFundsReply;
import bisq.proto.grpc.WithdrawFundsRequest;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@ -49,10 +47,12 @@ import static bisq.core.api.model.TradeInfo.toTradeInfo;
class GrpcTradesService extends TradesGrpc.TradesImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcTradesService(CoreApi coreApi) {
public GrpcTradesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
@ -66,10 +66,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -88,10 +86,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -103,10 +99,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
var reply = ConfirmPaymentStartedReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -118,10 +112,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
var reply = ConfirmPaymentReceivedReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -133,10 +125,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
var reply = KeepFundsReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -144,15 +134,12 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
public void withdrawFunds(WithdrawFundsRequest req,
StreamObserver<WithdrawFundsReply> responseObserver) {
try {
//TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI)
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null);
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), req.getMemo());
var reply = WithdrawFundsReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException | IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -1,3 +1,20 @@
/*
* 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.daemon.grpc;
import bisq.core.api.CoreApi;
@ -6,23 +23,64 @@ import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionReply;
import bisq.proto.grpc.GetVersionRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@VisibleForTesting
@Slf4j
public class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcVersionService(CoreApi coreApi) {
public GrpcVersionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void getVersion(GetVersionRequest req, StreamObserver<GetVersionReply> responseObserver) {
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
try {
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
@SuppressWarnings("unused") // Defined as a usage example.
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getVersion", new GrpcCallRateMeter(100, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(Optional::empty /* Optional.of(defaultCallRateMeteringInterceptor) */);
}
}

View File

@ -29,6 +29,8 @@ import bisq.proto.grpc.GetBalancesReply;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesReply;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetTransactionReply;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateReply;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressReply;
@ -39,6 +41,8 @@ import bisq.proto.grpc.RemoveWalletPasswordReply;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqReply;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcReply;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceReply;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordReply;
@ -49,27 +53,33 @@ import bisq.proto.grpc.UnsetTxFeeRatePreferenceReply;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import com.google.common.util.concurrent.FutureCallback;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import static bisq.core.api.model.TxInfo.toTxInfo;
@Slf4j
class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcWalletsService(CoreApi coreApi) {
public GrpcWalletsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
@ -81,10 +91,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -97,10 +105,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
.setAddressBalanceInfo(balanceInfo.toProtoMessage()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -117,10 +123,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -134,10 +138,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -145,28 +147,69 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
public void sendBsq(SendBsqRequest req,
StreamObserver<SendBsqReply> responseObserver) {
try {
coreApi.sendBsq(req.getAddress(), req.getAmount(), new TxBroadcaster.Callback() {
@Override
public void onSuccess(Transaction tx) {
log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes",
tx.getTxId().toString(),
tx.getOutputSum(),
tx.getFee(),
tx.getMessageSize());
var reply = SendBsqReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
coreApi.sendBsq(req.getAddress(),
req.getAmount(),
req.getTxFeeRate(),
new TxBroadcaster.Callback() {
@Override
public void onSuccess(Transaction tx) {
log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes",
tx.getTxId().toString(),
tx.getOutputSum(),
tx.getFee(),
tx.getMessageSize());
var reply = SendBsqReply.newBuilder()
.setTxInfo(toTxInfo(tx).toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void onFailure(TxBroadcastException ex) {
throw new IllegalStateException(ex);
}
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
@Override
public void onFailure(TxBroadcastException ex) {
throw new IllegalStateException(ex);
}
});
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@Override
public void sendBtc(SendBtcRequest req,
StreamObserver<SendBtcReply> responseObserver) {
try {
coreApi.sendBtc(req.getAddress(),
req.getAmount(),
req.getTxFeeRate(),
req.getMemo(),
new FutureCallback<>() {
@Override
public void onSuccess(Transaction tx) {
if (tx != null) {
log.info("Successfully published BTC tx: id {}, output sum {} sats, fee {} sats, size {} bytes",
tx.getTxId().toString(),
tx.getOutputSum(),
tx.getFee(),
tx.getMessageSize());
var reply = SendBtcReply.newBuilder()
.setTxInfo(toTxInfo(tx).toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} else {
throw new IllegalStateException("btc transaction is null");
}
}
@Override
public void onFailure(@NotNull Throwable t) {
log.error("", t);
throw new IllegalStateException(t);
}
});
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -182,10 +225,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -201,10 +242,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -220,10 +259,23 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@Override
public void getTransaction(GetTransactionRequest req,
StreamObserver<GetTransactionReply> responseObserver) {
try {
Transaction tx = coreApi.getTransaction(req.getTxId());
var reply = GetTransactionReply.newBuilder()
.setTxInfo(toTxInfo(tx).toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -235,10 +287,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
var reply = SetWalletPasswordReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -250,10 +300,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
var reply = RemoveWalletPasswordReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -265,10 +313,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
var reply = LockWalletReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
@ -280,10 +326,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
var reply = UnlockWalletReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

@ -0,0 +1,127 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc.interceptor;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.StatusRuntimeException;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static io.grpc.Status.PERMISSION_DENIED;
import static java.lang.String.format;
import static java.util.stream.Collectors.joining;
@Slf4j
public final class CallRateMeteringInterceptor implements ServerInterceptor {
// Maps the gRPC server method names to rate meters. This allows one interceptor
// instance to handle rate metering for any or all the methods in a Grpc*Service.
protected final Map<String, GrpcCallRateMeter> serviceCallRateMeters;
public CallRateMeteringInterceptor(Map<String, GrpcCallRateMeter> serviceCallRateMeters) {
this.serviceCallRateMeters = serviceCallRateMeters;
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
Metadata headers,
ServerCallHandler<ReqT, RespT> serverCallHandler) {
Optional<Map.Entry<String, GrpcCallRateMeter>> rateMeterKV = getRateMeterKV(serverCall);
rateMeterKV.ifPresentOrElse(
(kv) -> checkRateMeterAndMaybeCloseCall(kv, serverCall),
() -> handleMissingRateMeterConfiguration(serverCall));
// We leave it to the gRPC framework to clean up if the server call was closed
// above. But we still have to invoke startCall here because the method must
// return a ServerCall.Listener<RequestT>.
return serverCallHandler.startCall(serverCall, headers);
}
private void checkRateMeterAndMaybeCloseCall(Map.Entry<String, GrpcCallRateMeter> rateMeterKV,
ServerCall<?, ?> serverCall) {
String methodName = rateMeterKV.getKey();
GrpcCallRateMeter rateMeter = rateMeterKV.getValue();
if (!rateMeter.checkAndIncrement())
handlePermissionDeniedWarningAndCloseCall(methodName, rateMeter, serverCall);
else
log.info(rateMeter.getCallsCountProgress(methodName));
}
private void handleMissingRateMeterConfiguration(ServerCall<?, ?> serverCall)
throws StatusRuntimeException {
log.debug("The gRPC service's call rate metering interceptor does not"
+ " meter the {} method.",
getRateMeterKey(serverCall));
}
private void handlePermissionDeniedWarningAndCloseCall(String methodName,
GrpcCallRateMeter rateMeter,
ServerCall<?, ?> serverCall)
throws StatusRuntimeException {
String msg = getDefaultRateExceededError(methodName, rateMeter);
log.warn(StringUtils.capitalize(msg) + ".");
serverCall.close(PERMISSION_DENIED.withDescription(msg), new Metadata());
}
private String getDefaultRateExceededError(String methodName,
GrpcCallRateMeter rateMeter) {
// The derived method name may not be an exact match to CLI's method name.
String timeUnitName = StringUtils.chop(rateMeter.getTimeUnit().name().toLowerCase());
return format("the maximum allowed number of %s calls (%d/%s) has been exceeded",
methodName.toLowerCase(),
rateMeter.getAllowedCallsPerTimeWindow(),
timeUnitName);
}
private Optional<Map.Entry<String, GrpcCallRateMeter>> getRateMeterKV(ServerCall<?, ?> serverCall) {
String rateMeterKey = getRateMeterKey(serverCall);
return serviceCallRateMeters.entrySet().stream()
.filter((e) -> e.getKey().equals(rateMeterKey)).findFirst();
}
private String getRateMeterKey(ServerCall<?, ?> serverCall) {
// Get the rate meter map key from the full rpc service name. The key name
// is hard coded in the Grpc*Service interceptors() method.
String fullServiceName = serverCall.getMethodDescriptor().getServiceName();
return StringUtils.uncapitalize(Objects.requireNonNull(fullServiceName)
.substring("io.bisq.protobuffer.".length()));
}
@Override
public String toString() {
String rateMetersString =
serviceCallRateMeters.entrySet()
.stream()
.map(Object::toString)
.collect(joining("\n\t\t"));
return "CallRateMeteringInterceptor {" + "\n\t" +
"serviceCallRateMeters {" + "\n\t\t" +
rateMetersString + "\n\t" + "}" + "\n"
+ "}";
}
}

View File

@ -0,0 +1,92 @@
package bisq.daemon.grpc.interceptor;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayDeque;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
@Slf4j
public class GrpcCallRateMeter {
@Getter
private final int allowedCallsPerTimeWindow;
@Getter
private final TimeUnit timeUnit;
@Getter
private final int numTimeUnits;
@Getter
private transient final long timeUnitIntervalInMilliseconds;
private transient final ArrayDeque<Long> callTimestamps;
public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit) {
this(allowedCallsPerTimeWindow, timeUnit, 1);
}
public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit, int numTimeUnits) {
this.allowedCallsPerTimeWindow = allowedCallsPerTimeWindow;
this.timeUnit = timeUnit;
this.numTimeUnits = numTimeUnits;
this.timeUnitIntervalInMilliseconds = timeUnit.toMillis(1) * numTimeUnits;
this.callTimestamps = new ArrayDeque<>();
}
public boolean checkAndIncrement() {
if (getCallsCount() < allowedCallsPerTimeWindow) {
incrementCallsCount();
return true;
} else {
return false;
}
}
public int getCallsCount() {
removeStaleCallTimestamps();
return callTimestamps.size();
}
public String getCallsCountProgress(String calledMethodName) {
String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase());
return format("%s has been called %d time%s in the last %s, rate limit is %d/%s",
calledMethodName,
callTimestamps.size(),
callTimestamps.size() == 1 ? "" : "s",
shortTimeUnitName,
allowedCallsPerTimeWindow,
shortTimeUnitName);
}
private void incrementCallsCount() {
callTimestamps.add(currentTimeMillis());
}
private void removeStaleCallTimestamps() {
while (!callTimestamps.isEmpty() && isStale.test(callTimestamps.peek())) {
callTimestamps.remove();
}
}
private final Predicate<Long> isStale = (t) -> {
long stale = currentTimeMillis() - this.getTimeUnitIntervalInMilliseconds();
// Is the given timestamp before the current time minus 1 timeUnit in millis?
return t < stale;
};
@Override
public String toString() {
return "GrpcCallRateMeter{" +
"allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow +
", timeUnit=" + timeUnit.name() +
", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds +
", callsCount=" + callTimestamps.size() +
'}';
}
}

View File

@ -0,0 +1,287 @@
/*
* 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.daemon.grpc.interceptor;
import io.grpc.ServerInterceptor;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.common.annotations.VisibleForTesting;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static bisq.common.file.FileUtil.deleteFileIfExists;
import static bisq.common.file.FileUtil.renameFile;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
@VisibleForTesting
@Slf4j
public class GrpcServiceRateMeteringConfig {
public static final String RATE_METERS_CONFIG_FILENAME = "ratemeters.json";
private static final String KEY_GRPC_SERVICE_CLASS_NAME = "grpcServiceClassName";
private static final String KEY_METHOD_RATE_METERS = "methodRateMeters";
private static final String KEY_ALLOWED_CALL_PER_TIME_WINDOW = "allowedCallsPerTimeWindow";
private static final String KEY_TIME_UNIT = "timeUnit";
private static final String KEY_NUM_TIME_UNITS = "numTimeUnits";
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final List<Map<String, GrpcCallRateMeter>> methodRateMeters;
private final String grpcServiceClassName;
public GrpcServiceRateMeteringConfig(String grpcServiceClassName) {
this(grpcServiceClassName, new ArrayList<>());
}
public GrpcServiceRateMeteringConfig(String grpcServiceClassName,
List<Map<String, GrpcCallRateMeter>> methodRateMeters) {
this.grpcServiceClassName = grpcServiceClassName;
this.methodRateMeters = methodRateMeters;
}
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
int maxCalls,
TimeUnit timeUnit) {
return addMethodCallRateMeter(methodName, maxCalls, timeUnit, 1);
}
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
int maxCalls,
TimeUnit timeUnit,
int numTimeUnits) {
methodRateMeters.add(new LinkedHashMap<>() {{
put(methodName, new GrpcCallRateMeter(maxCalls, timeUnit, numTimeUnits));
}});
return this;
}
public boolean isConfigForGrpcService(Class<?> clazz) {
return isConfigForGrpcService(clazz.getSimpleName());
}
public boolean isConfigForGrpcService(String grpcServiceClassSimpleName) {
return this.grpcServiceClassName.equals(grpcServiceClassSimpleName);
}
@Override
public String toString() {
return "GrpcServiceRateMeteringConfig{" + "\n" +
" grpcServiceClassName='" + grpcServiceClassName + '\'' + "\n" +
", methodRateMeters=" + methodRateMeters + "\n" +
'}';
}
public static Optional<ServerInterceptor> getCustomRateMeteringInterceptor(File installationDir,
Class<?> grpcServiceClass) {
File configFile = new File(installationDir, RATE_METERS_CONFIG_FILENAME);
return configFile.exists()
? toServerInterceptor(configFile, grpcServiceClass)
: Optional.empty();
}
public static Optional<ServerInterceptor> toServerInterceptor(File configFile, Class<?> grpcServiceClass) {
// From a global rate metering config file, create a specific gRPC service
// interceptor configuration in the form of an interceptor constructor argument,
// a map<method-name, rate-meter>.
// Transforming json into the List<Map<String, GrpcCallRateMeter>> is a bit
// convoluted due to Gson's loss of generic type information during deserialization.
Optional<GrpcServiceRateMeteringConfig> grpcServiceConfig = getAllDeserializedConfigs(configFile)
.stream().filter(x -> x.isConfigForGrpcService(grpcServiceClass)).findFirst();
if (grpcServiceConfig.isPresent()) {
Map<String, GrpcCallRateMeter> serviceCallRateMeters = new HashMap<>();
for (Map<String, GrpcCallRateMeter> methodToRateMeterMap : grpcServiceConfig.get().methodRateMeters) {
Map.Entry<String, GrpcCallRateMeter> entry = methodToRateMeterMap.entrySet().stream().findFirst().orElseThrow(()
-> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map."));
serviceCallRateMeters.put(entry.getKey(), entry.getValue());
}
return Optional.of(new CallRateMeteringInterceptor(serviceCallRateMeters));
} else {
return Optional.empty();
}
}
@SuppressWarnings("unchecked")
private static List<Map<String, GrpcCallRateMeter>> getMethodRateMetersMap(Map<String, Object> gsonMap) {
List<Map<String, GrpcCallRateMeter>> rateMeters = new ArrayList<>();
// Each gsonMap is a Map<String, Object> with a single entry:
// {getVersion={allowedCallsPerTimeUnit=8.0, timeUnit=SECONDS, callsCount=0.0, isRunning=false}}
// Convert it to a multiple entry Map<String, GrpcCallRateMeter>, where the key
// is a method name.
for (Map<String, Object> singleEntryRateMeterMap : (List<Map<String, Object>>) gsonMap.get(KEY_METHOD_RATE_METERS)) {
log.debug("Gson's single entry {} {}<String, Object> = {}",
gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME),
singleEntryRateMeterMap.getClass().getSimpleName(),
singleEntryRateMeterMap);
Map.Entry<String, Object> entry = singleEntryRateMeterMap.entrySet().stream().findFirst().orElseThrow(()
-> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map."));
String methodName = entry.getKey();
GrpcCallRateMeter rateMeter = getGrpcCallRateMeter(entry);
rateMeters.add(new LinkedHashMap<>() {{
put(methodName, rateMeter);
}});
}
return rateMeters;
}
@SuppressWarnings({"rawtypes", "unchecked"})
public static List<GrpcServiceRateMeteringConfig> deserialize(File configFile) {
verifyConfigFile(configFile);
List<GrpcServiceRateMeteringConfig> serviceMethodConfigurations = new ArrayList<>();
// Gson cannot deserialize a json string to List<GrpcServiceRateMeteringConfig>
// so easily for us, so we do it here before returning the list of configurations.
List rawConfigList = gson.fromJson(toJson(configFile), ArrayList.class);
// Gson gave us a list of maps with keys grpcServiceClassName, methodRateMeters:
// String grpcServiceClassName
// List<Map> methodRateMeters
for (Object rawConfig : rawConfigList) {
Map<String, Object> gsonMap = (Map<String, Object>) rawConfig;
String grpcServiceClassName = (String) gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME);
List<Map<String, GrpcCallRateMeter>> rateMeters = getMethodRateMetersMap(gsonMap);
serviceMethodConfigurations.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName, rateMeters));
}
return serviceMethodConfigurations;
}
@SuppressWarnings("unchecked")
private static GrpcCallRateMeter getGrpcCallRateMeter(Map.Entry<String, Object> gsonEntry) {
Map<String, Object> valueMap = (Map<String, Object>) gsonEntry.getValue();
int allowedCallsPerTimeWindow = ((Number) valueMap.get(KEY_ALLOWED_CALL_PER_TIME_WINDOW)).intValue();
TimeUnit timeUnit = TimeUnit.valueOf((String) valueMap.get(KEY_TIME_UNIT));
int numTimeUnits = ((Number) valueMap.get(KEY_NUM_TIME_UNITS)).intValue();
return new GrpcCallRateMeter(allowedCallsPerTimeWindow, timeUnit, numTimeUnits);
}
private static void verifyConfigFile(File configFile) {
if (configFile == null)
throw new IllegalStateException("Cannot read null json config file.");
if (!configFile.exists())
throw new IllegalStateException(format("cannot find json config file %s", configFile.getAbsolutePath()));
}
private static String toJson(File configFile) {
try {
return new String(readAllBytes(Paths.get(configFile.getAbsolutePath())));
} catch (IOException ex) {
throw new IllegalStateException(format("Cannot read json string from file %s.",
configFile.getAbsolutePath()));
}
}
private static List<GrpcServiceRateMeteringConfig> allDeserializedConfigs;
private static List<GrpcServiceRateMeteringConfig> getAllDeserializedConfigs(File configFile) {
// We deserialize once, not for each gRPC service wanting an interceptor.
if (allDeserializedConfigs == null)
allDeserializedConfigs = deserialize(configFile);
return allDeserializedConfigs;
}
@VisibleForTesting
public static class Builder {
private final List<GrpcServiceRateMeteringConfig> rateMeterConfigs = new ArrayList<>();
public void addCallRateMeter(String grpcServiceClassName,
String methodName,
int maxCalls,
TimeUnit timeUnit) {
addCallRateMeter(grpcServiceClassName,
methodName,
maxCalls,
timeUnit,
1);
}
public void addCallRateMeter(String grpcServiceClassName,
String methodName,
int maxCalls,
TimeUnit timeUnit,
int numTimeUnits) {
log.info("Adding call rate metering definition {}.{} ({}/{}ms).",
grpcServiceClassName,
methodName,
maxCalls,
timeUnit.toMillis(1) * numTimeUnits);
rateMeterConfigs.stream().filter(c -> c.isConfigForGrpcService(grpcServiceClassName))
.findFirst().ifPresentOrElse(
(config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits),
() -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName)
.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits)));
}
public File build() {
File tmpFile = serializeRateMeterDefinitions();
File configFile = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toFile();
try {
deleteFileIfExists(configFile);
renameFile(tmpFile, configFile);
} catch (IOException ex) {
throw new IllegalStateException(format("Could not create config file %s.",
configFile.getAbsolutePath()), ex);
}
return configFile;
}
private File serializeRateMeterDefinitions() {
String json = gson.toJson(rateMeterConfigs);
File file = createTmpFile();
try (OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) {
outputStreamWriter.write(json);
} catch (Exception ex) {
throw new IllegalStateException(format("Cannot write file for json string %s.", json), ex);
}
return file;
}
private File createTmpFile() {
File file;
try {
file = File.createTempFile("ratemeters_",
".tmp",
Paths.get(getProperty("java.io.tmpdir")).toFile());
} catch (IOException ex) {
throw new IllegalStateException("Cannot create tmp ratemeters json file.", ex);
}
return file;
}
}
}

View File

@ -15,7 +15,7 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc;
package bisq.daemon.grpc.interceptor;
import bisq.common.config.Config;
@ -38,7 +38,7 @@ import static java.lang.String.format;
*
* @see bisq.common.config.Config#apiPassword
*/
class PasswordAuthInterceptor implements ServerInterceptor {
public class PasswordAuthInterceptor implements ServerInterceptor {
private static final String PASSWORD_KEY = "password";
@ -50,7 +50,8 @@ class PasswordAuthInterceptor implements ServerInterceptor {
}
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata headers,
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
Metadata headers,
ServerCallHandler<ReqT, RespT> serverCallHandler) {
var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER));

View File

@ -0,0 +1,190 @@
/*
* 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.daemon.grpc.interceptor;
import io.grpc.ServerInterceptor;
import java.nio.file.Paths;
import java.io.File;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.lang.System.getProperty;
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import bisq.daemon.grpc.GrpcVersionService;
@Slf4j
public class GrpcServiceRateMeteringConfigTest {
private static final GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
private static File configFile;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Optional<ServerInterceptor> versionServiceInterceptor;
@BeforeClass
public static void setup() {
// This is the tested rate meter, it allows 3 calls every 2 seconds.
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"getVersion",
3,
SECONDS,
2);
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"badMethodNameDoesNotBreakAnything",
100,
DAYS);
// The other Grpc*Service classes are not @VisibleForTesting, so we hardcode
// the simple class name.
builder.addCallRateMeter("GrpcOffersService",
"createOffer",
5,
MINUTES);
builder.addCallRateMeter("GrpcOffersService",
"takeOffer",
10,
DAYS);
builder.addCallRateMeter("GrpcWalletsService",
"sendBtc",
3,
HOURS);
}
@Before
public void buildConfigFile() {
if (configFile == null)
configFile = builder.build();
}
@Test
public void testConfigFileBuild() {
assertNotNull(configFile);
assertTrue(configFile.exists());
assertTrue(configFile.length() > 0);
String expectedConfigFilePath = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toString();
assertEquals(expectedConfigFilePath, configFile.getAbsolutePath());
}
@Test
public void testGetVersionCallRateMeter() {
// Check the interceptor has 2 rate meters, for getVersion and badMethodNameDoesNotBreakAnything.
CallRateMeteringInterceptor versionServiceInterceptor = buildInterceptor();
assertEquals(2, versionServiceInterceptor.serviceCallRateMeters.size());
// Check the rate meter config.
GrpcCallRateMeter rateMeter = versionServiceInterceptor.serviceCallRateMeters.get("getVersion");
assertEquals(3, rateMeter.getAllowedCallsPerTimeWindow());
assertEquals(SECONDS, rateMeter.getTimeUnit());
assertEquals(2, rateMeter.getNumTimeUnits());
assertEquals(2 * 1000, rateMeter.getTimeUnitIntervalInMilliseconds());
// Do as many calls as allowed within rateMeter.getTimeUnitIntervalInMilliseconds().
doMaxIsAllowedChecks(true,
rateMeter.getAllowedCallsPerTimeWindow(),
rateMeter);
// The next 3 calls are blocked because we've exceeded the 3calls/2s limit.
doMaxIsAllowedChecks(false,
rateMeter.getAllowedCallsPerTimeWindow(),
rateMeter);
// Let all of the rate meter's cached call timestamps become stale by waiting for
// 2001 ms, then we can call getversion another 'allowedCallsPerTimeUnit' times.
rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds());
// All the stale call timestamps are gone and the call count is back to zero.
assertEquals(0, rateMeter.getCallsCount());
doMaxIsAllowedChecks(true,
rateMeter.getAllowedCallsPerTimeWindow(),
rateMeter);
// We've exceeded the call/second limit.
assertFalse(rateMeter.checkAndIncrement());
// Let all of the call timestamps go stale again by waiting for 2001 ms.
rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds());
// Call twice, resting 0.5s after each call.
for (int i = 0; i < 2; i++) {
assertTrue(rateMeter.checkAndIncrement());
rest(500);
}
// Call the 3rd time, then let one of the rate meter's timestamps go stale.
assertTrue(rateMeter.checkAndIncrement());
rest(1001);
// The call count was decremented by one because one timestamp went stale.
assertEquals(2, rateMeter.getCallsCount());
assertTrue(rateMeter.checkAndIncrement());
assertEquals(rateMeter.getAllowedCallsPerTimeWindow(), rateMeter.getCallsCount());
// We've exceeded the call limit again.
assertFalse(rateMeter.checkAndIncrement());
}
private void doMaxIsAllowedChecks(boolean expectedIsAllowed,
int expectedCallsCount,
GrpcCallRateMeter rateMeter) {
for (int i = 1; i <= rateMeter.getAllowedCallsPerTimeWindow(); i++) {
assertEquals(expectedIsAllowed, rateMeter.checkAndIncrement());
}
assertEquals(expectedCallsCount, rateMeter.getCallsCount());
}
@AfterClass
public static void teardown() {
if (configFile != null)
configFile.deleteOnExit();
}
private void rest(long milliseconds) {
try {
TimeUnit.MILLISECONDS.sleep(milliseconds);
} catch (InterruptedException ignored) {
}
}
private CallRateMeteringInterceptor buildInterceptor() {
//noinspection OptionalAssignedToNull
if (versionServiceInterceptor == null) {
versionServiceInterceptor = getCustomRateMeteringInterceptor(
configFile.getParentFile(),
GrpcVersionService.class);
}
assertTrue(versionServiceInterceptor.isPresent());
return (CallRateMeteringInterceptor) versionServiceInterceptor.get();
}
}

View File

@ -38,7 +38,10 @@ import bisq.core.dao.governance.voteresult.MissingDataRequestService;
import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
import bisq.core.user.Cookie;
import bisq.core.user.CookieKey;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.common.app.DevEnv;
import bisq.common.app.Log;
@ -65,6 +68,8 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.geometry.BoundingBox;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
@ -102,6 +107,7 @@ public class BisqApp extends Application implements UncaughtExceptionHandler {
private boolean popupOpened;
private Scene scene;
private boolean shutDownRequested;
private MainView mainView;
public BisqApp() {
shutDownHandler = this::stop;
@ -126,7 +132,7 @@ public class BisqApp extends Application implements UncaughtExceptionHandler {
public void startApplication(Runnable onApplicationStartedHandler) {
try {
MainView mainView = loadMainView(injector);
mainView = loadMainView(injector);
mainView.setOnApplicationStartedHandler(onApplicationStartedHandler);
scene = createAndConfigScene(mainView, injector);
setupStage(scene);
@ -256,10 +262,47 @@ public class BisqApp extends Application implements UncaughtExceptionHandler {
stage.setMinHeight(MIN_WINDOW_HEIGHT);
stage.getIcons().add(ImageUtil.getApplicationIconImage());
User user = injector.getInstance(User.class);
layoutStageFromPersistedData(stage, user);
addStageLayoutListeners(stage, user);
// make the UI visible
stage.show();
}
private void layoutStageFromPersistedData(Stage stage, User user) {
Cookie cookie = user.getCookie();
cookie.getAsOptionalDouble(CookieKey.STAGE_X).flatMap(x ->
cookie.getAsOptionalDouble(CookieKey.STAGE_Y).flatMap(y ->
cookie.getAsOptionalDouble(CookieKey.STAGE_W).flatMap(w ->
cookie.getAsOptionalDouble(CookieKey.STAGE_H).map(h -> new BoundingBox(x, y, w, h)))))
.ifPresent(stageBoundingBox -> {
stage.setX(stageBoundingBox.getMinX());
stage.setY(stageBoundingBox.getMinY());
stage.setWidth(stageBoundingBox.getWidth());
stage.setHeight(stageBoundingBox.getHeight());
});
}
private void addStageLayoutListeners(Stage stage, User user) {
stage.widthProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_W, (double) newValue);
user.requestPersistence();
});
stage.heightProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_H, (double) newValue);
user.requestPersistence();
});
stage.xProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_X, (double) newValue);
user.requestPersistence();
});
stage.yProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_Y, (double) newValue);
user.requestPersistence();
});
}
private MainView loadMainView(Injector injector) {
CachingViewLoader viewLoader = injector.getInstance(CachingViewLoader.class);
return (MainView) viewLoader.load(MainView.class);

View File

@ -23,9 +23,13 @@ import bisq.desktop.common.view.View;
import bisq.desktop.common.view.ViewFactory;
import bisq.desktop.common.view.ViewLoader;
import bisq.common.util.Utilities;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.base.Joiner;
import javafx.fxml.FXMLLoader;
import java.net.URL;
@ -36,8 +40,11 @@ import java.util.ResourceBundle;
import java.lang.annotation.Annotation;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@Singleton
public class FxmlViewLoader implements ViewLoader {
@ -107,7 +114,17 @@ public class FxmlViewLoader implements ViewLoader {
"does not implement [%s] as expected.", controller.getClass(), fxmlUrl, View.class);
return (View) controller;
} catch (IOException ex) {
throw new ViewfxException(ex, "Failed to load view from FXML file at [%s]", fxmlUrl);
Throwable cause = ex.getCause();
if (cause != null) {
cause.printStackTrace();
log.error(cause.toString());
// We want to show stackTrace in error popup
String stackTrace = Utilities.toTruncatedString(Joiner.on("\n").join(cause.getStackTrace()), 800, false);
throw new ViewfxException(cause, "%s at loading view class\nStack trace:\n%s",
cause.getClass().getSimpleName(), stackTrace);
} else {
throw new ViewfxException(ex, "Failed to load view from FXML file at [%s]", fxmlUrl);
}
}
}
@ -122,8 +139,7 @@ public class FxmlViewLoader implements ViewLoader {
}
try {
return annotationType.getDeclaredMethod(attributeName).getDefaultValue();
}
catch (Exception ex) {
} catch (Exception ex) {
return null;
}
}

View File

@ -21,11 +21,12 @@ import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
@Singleton
public class CachingViewLoader implements ViewLoader {
private final HashMap<Object, View> cache = new HashMap<>();
private final Map<Class<? extends View>, View> cache = new HashMap<>();
private final ViewLoader viewLoader;
@Inject

View File

@ -19,6 +19,7 @@ package bisq.desktop.components;
import bisq.desktop.components.controlsfx.control.PopOver;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.scene.Node;
@ -30,23 +31,18 @@ import javafx.beans.property.StringProperty;
import lombok.Getter;
import static bisq.desktop.util.FormBuilder.getIcon;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
public class InfoInputTextField extends AnchorPane {
private final StringProperty text = new SimpleStringProperty();
@Getter
private final InputTextField inputTextField;
@Getter
private final Label infoIcon;
@Getter
private final Label warningIcon;
@Getter
private final Label privacyIcon;
private Label currentIcon;
private PopOverWrapper popoverWrapper = new PopOverWrapper();
private final Label icon;
private final PopOverWrapper popoverWrapper = new PopOverWrapper();
@Nullable
private Node node;
public InfoInputTextField() {
this(0);
@ -56,79 +52,67 @@ public class InfoInputTextField extends AnchorPane {
super();
inputTextField = new InputTextField(inputLineExtension);
infoIcon = getIcon(AwesomeIcon.INFO_SIGN);
infoIcon.setLayoutY(3);
infoIcon.getStyleClass().addAll("icon", "info");
warningIcon = getIcon(AwesomeIcon.WARNING_SIGN);
warningIcon.setLayoutY(3);
warningIcon.getStyleClass().addAll("icon", "warning");
privacyIcon = getIcon(AwesomeIcon.EYE_CLOSE);
privacyIcon.setLayoutY(3);
privacyIcon.getStyleClass().addAll("icon", "info");
AnchorPane.setLeftAnchor(infoIcon, 7.0);
AnchorPane.setLeftAnchor(warningIcon, 7.0);
AnchorPane.setLeftAnchor(privacyIcon, 7.0);
AnchorPane.setRightAnchor(inputTextField, 0.0);
AnchorPane.setLeftAnchor(inputTextField, 0.0);
hideIcons();
icon = new Label();
icon.setLayoutY(3);
AnchorPane.setLeftAnchor(icon, 7.0);
icon.setOnMouseEntered(e -> {
if (node != null) {
popoverWrapper.showPopOver(() -> checkNotNull(createPopOver()));
}
});
icon.setOnMouseExited(e -> {
if (node != null) {
popoverWrapper.hidePopOver();
}
});
getChildren().addAll(inputTextField, infoIcon, warningIcon, privacyIcon);
hideIcon();
getChildren().addAll(inputTextField, icon);
}
private void hideIcons() {
infoIcon.setManaged(false);
infoIcon.setVisible(false);
warningIcon.setManaged(false);
warningIcon.setVisible(false);
privacyIcon.setManaged(false);
privacyIcon.setVisible(false);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public
///////////////////////////////////////////////////////////////////////////////////////////
public void setContentForInfoPopOver(Node node) {
currentIcon = infoIcon;
hideIcons();
setActionHandlers(node);
setContentForPopOver(node, AwesomeIcon.INFO_SIGN);
}
public void setContentForWarningPopOver(Node node) {
currentIcon = warningIcon;
hideIcons();
setActionHandlers(node);
setContentForPopOver(node, AwesomeIcon.WARNING_SIGN, "warning");
}
public void setContentForPrivacyPopOver(Node node) {
currentIcon = privacyIcon;
hideIcons();
setActionHandlers(node);
setContentForPopOver(node, AwesomeIcon.EYE_CLOSE);
}
public void hideInfoContent() {
currentIcon = null;
hideIcons();
public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon) {
setContentForPopOver(node, awesomeIcon, null);
}
public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon, @Nullable String style) {
this.node = node;
AwesomeDude.setIcon(icon, awesomeIcon);
icon.getStyleClass().addAll("icon", style == null ? "info" : style);
icon.setManaged(true);
icon.setVisible(true);
}
public void hideIcon() {
icon.setManaged(false);
icon.setVisible(false);
}
public void setIconsRightAligned() {
AnchorPane.clearConstraints(infoIcon);
AnchorPane.clearConstraints(warningIcon);
AnchorPane.clearConstraints(privacyIcon);
AnchorPane.clearConstraints(icon);
AnchorPane.clearConstraints(inputTextField);
AnchorPane.setRightAnchor(infoIcon, 7.0);
AnchorPane.setRightAnchor(warningIcon, 7.0);
AnchorPane.setRightAnchor(privacyIcon, 7.0);
AnchorPane.setRightAnchor(icon, 7.0);
AnchorPane.setLeftAnchor(inputTextField, 0.0);
AnchorPane.setRightAnchor(inputTextField, 0.0);
}
@ -146,7 +130,7 @@ public class InfoInputTextField extends AnchorPane {
return text.get();
}
public final StringProperty textProperty() {
public StringProperty textProperty() {
return text;
}
@ -155,28 +139,18 @@ public class InfoInputTextField extends AnchorPane {
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void setActionHandlers(Node node) {
if (node != null) {
currentIcon.setManaged(true);
currentIcon.setVisible(true);
// As we don't use binding here we need to recreate it on mouse over to reflect the current state
currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node)));
currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver());
private PopOver createPopOver() {
if (node == null) {
return null;
}
}
private PopOver createPopOver(Node node) {
node.getStyleClass().add("default-text");
PopOver popover = new PopOver(node);
if (currentIcon.getScene() != null) {
if (icon.getScene() != null) {
popover.setDetachable(false);
popover.setArrowLocation(PopOver.ArrowLocation.LEFT_TOP);
popover.setArrowIndent(5);
popover.show(currentIcon, -17);
popover.show(icon, -17);
}
return popover;
}

View File

@ -43,6 +43,8 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeparatedPhaseBars extends VBox {
// Last day for creating github compensation request issue, as decided by general consensus
private static final double LAST_COMP_REQ_GH_ISSUE = (double) 18 / 25;
private double labelMinWidth = 150;
private double breakMinWidth = 20;
private int totalDuration;
@ -68,11 +70,14 @@ public class SeparatedPhaseBars extends VBox {
item.setTitleLabel(titleLabel);
titlesBars.getChildren().addAll(titleLabel);
ProgressBar progressBar = new JFXProgressBar();
JFXProgressBar progressBar = new JFXProgressBar();
progressBar.setMinHeight(9);
progressBar.setMaxHeight(9);
progressBar.progressProperty().bind(item.progressProperty);
progressBar.setOpacity(item.isShowBlocks() ? 1 : 0.25);
if (item.phase.name().startsWith("PROPOSAL")) {
progressBar.setSecondaryProgress(LAST_COMP_REQ_GH_ISSUE);
}
progressBars.getChildren().add(progressBar);
item.setProgressBar(progressBar);
});
@ -141,6 +146,9 @@ public class SeparatedPhaseBars extends VBox {
private Label titleLabel;
@Setter
private ProgressBar progressBar;
@Setter
private int indicatorBlock;
private ProgressBar indicatorBar;
public SeparatedPhaseBarsItem(DaoPhase.Phase phase, boolean showBlocks) {
this.phase = phase;
@ -160,5 +168,6 @@ public class SeparatedPhaseBars extends VBox {
lastBlockProperty.set(lastBlock);
this.duration = duration;
}
}
}

View File

@ -364,8 +364,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
.show());
bisqSetup.setDisplayLocalhostHandler(key -> {
if (!DevEnv.isDevMode()) {
Popup popup = new Popup().backgroundInfo(Res.get("popup.bitcoinLocalhostNode.msg") +
Res.get("popup.bitcoinLocalhostNode.additionalRequirements"))
Popup popup = new Popup().backgroundInfo(Res.get("popup.bitcoinLocalhostNode.msg"))
.dontShowAgainId(key);
popup.setDisplayOrderPriority(5);
popupQueue.add(popup);
@ -416,6 +415,11 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
.show();
});
bisqSetup.setDaoRequiresRestartHandler(() -> new Popup().warning("popup.warn.daoRequiresRestart")
.useShutDownButton()
.hideCloseButton()
.show());
corruptedStorageFileHandler.getFiles().ifPresent(files -> new Popup()
.warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir))
.useShutDownButton()

View File

@ -0,0 +1,233 @@
/*
* 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.desktop.main;
import bisq.desktop.util.validation.AltcoinValidator;
import bisq.desktop.util.validation.FiatPriceValidator;
import bisq.desktop.util.validation.MonetaryValidator;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.validation.InputValidator;
import bisq.common.util.MathUtils;
import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;
import javax.annotation.Nullable;
import static bisq.desktop.main.shared.ChatView.log;
import static com.google.common.base.Preconditions.checkNotNull;
@Singleton
public class PriceUtil {
private final PriceFeedService priceFeedService;
private final TradeStatisticsManager tradeStatisticsManager;
private final Preferences preferences;
@Nullable
private Price bsq30DayAveragePrice;
@Inject
public PriceUtil(PriceFeedService priceFeedService,
TradeStatisticsManager tradeStatisticsManager,
Preferences preferences) {
this.priceFeedService = priceFeedService;
this.tradeStatisticsManager = tradeStatisticsManager;
this.preferences = preferences;
}
public static MonetaryValidator getPriceValidator(boolean isFiatCurrency) {
return isFiatCurrency ?
new FiatPriceValidator() :
new AltcoinValidator();
}
public static InputValidator.ValidationResult isTriggerPriceValid(String triggerPriceAsString,
Price price,
boolean isSellOffer,
boolean isFiatCurrency) {
if (triggerPriceAsString == null || triggerPriceAsString.isEmpty()) {
return new InputValidator.ValidationResult(true);
}
InputValidator.ValidationResult result = getPriceValidator(isFiatCurrency).validate(triggerPriceAsString);
if (!result.isValid) {
return result;
}
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, price.getCurrencyCode());
long priceAsLong = price.getValue();
String priceAsString = FormattingUtils.formatPrice(price);
if ((isSellOffer && isFiatCurrency) || (!isSellOffer && !isFiatCurrency)) {
if (triggerPriceAsLong >= priceAsLong) {
return new InputValidator.ValidationResult(false,
Res.get("createOffer.triggerPrice.invalid.tooHigh", priceAsString));
} else {
return new InputValidator.ValidationResult(true);
}
} else {
if (triggerPriceAsLong <= priceAsLong) {
return new InputValidator.ValidationResult(false,
Res.get("createOffer.triggerPrice.invalid.tooLow", priceAsString));
} else {
return new InputValidator.ValidationResult(true);
}
}
}
public void recalculateBsq30DayAveragePrice() {
bsq30DayAveragePrice = null;
bsq30DayAveragePrice = getBsq30DayAveragePrice();
}
public Price getBsq30DayAveragePrice() {
if (bsq30DayAveragePrice == null) {
bsq30DayAveragePrice = AveragePriceUtil.getAveragePriceTuple(preferences,
tradeStatisticsManager, 30).second;
}
return bsq30DayAveragePrice;
}
public boolean hasMarketPrice(Offer offer) {
String currencyCode = offer.getCurrencyCode();
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
Price price = offer.getPrice();
return price != null && marketPrice != null && marketPrice.isRecentExternalPriceAvailable();
}
public Optional<Double> getMarketBasedPrice(Offer offer,
OfferPayload.Direction direction) {
if (offer.isUseMarketBasedPrice()) {
return Optional.of(offer.getMarketPriceMargin());
}
if (!hasMarketPrice(offer)) {
if (offer.getCurrencyCode().equals("BSQ")) {
Price bsq30DayAveragePrice = getBsq30DayAveragePrice();
if (bsq30DayAveragePrice.isPositive()) {
double scaled = MathUtils.scaleDownByPowerOf10(bsq30DayAveragePrice.getValue(), 8);
return calculatePercentage(offer, scaled, direction);
} else {
return Optional.empty();
}
} else {
log.trace("We don't have a market price. " +
"That case could only happen if you don't have a price feed.");
return Optional.empty();
}
}
String currencyCode = offer.getCurrencyCode();
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
double marketPriceAsDouble = checkNotNull(marketPrice).getPrice();
return calculatePercentage(offer, marketPriceAsDouble, direction);
}
public Optional<Double> calculatePercentage(Offer offer,
double marketPrice,
OfferPayload.Direction direction) {
// If the offer did not use % price we calculate % from current market price
String currencyCode = offer.getCurrencyCode();
Price price = offer.getPrice();
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT :
Fiat.SMALLEST_UNIT_EXPONENT;
long priceAsLong = checkNotNull(price).getValue();
double scaled = MathUtils.scaleDownByPowerOf10(priceAsLong, precision);
double value;
if (direction == OfferPayload.Direction.SELL) {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (marketPrice == 0) {
return Optional.empty();
}
value = 1 - scaled / marketPrice;
} else {
if (marketPrice == 1) {
return Optional.empty();
}
value = scaled / marketPrice - 1;
}
} else {
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
if (marketPrice == 1) {
return Optional.empty();
}
value = scaled / marketPrice - 1;
} else {
if (marketPrice == 0) {
return Optional.empty();
}
value = 1 - scaled / marketPrice;
}
}
return Optional.of(value);
}
public static long getMarketPriceAsLong(String inputValue, String currencyCode) {
if (inputValue == null || inputValue.isEmpty() || currencyCode == null) {
return 0;
}
try {
int precision = getMarketPricePrecision(currencyCode);
String stringValue = reformatMarketPrice(inputValue, currencyCode);
return ParsingUtils.parsePriceStringToLong(currencyCode, stringValue, precision);
} catch (Throwable t) {
return 0;
}
}
public static String reformatMarketPrice(String inputValue, String currencyCode) {
if (inputValue == null || inputValue.isEmpty() || currencyCode == null) {
return "";
}
double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue);
int precision = getMarketPricePrecision(currencyCode);
return FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision);
}
public static String formatMarketPrice(long price, String currencyCode) {
int marketPricePrecision = getMarketPricePrecision(currencyCode);
double scaled = MathUtils.scaleDownByPowerOf10(price, marketPricePrecision);
return FormattingUtils.formatMarketPrice(scaled, marketPricePrecision);
}
public static int getMarketPricePrecision(String currencyCode) {
return CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT;
}
}

View File

@ -21,6 +21,7 @@ import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.InfoInputTextField;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.PriceUtil;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.WebCamWindow;
import bisq.desktop.util.FormBuilder;
@ -33,7 +34,6 @@ import bisq.desktop.util.validation.PercentageNumberValidator;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Altcoin;
import bisq.core.notifications.MobileMessage;
import bisq.core.notifications.MobileNotificationService;
import bisq.core.notifications.alerts.DisputeMsgEvents;
@ -693,6 +693,7 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
currencyComboBox.getSelectionModel().select(optionalTradeCurrency.get());
onSelectedTradeCurrency();
priceAlertHighInputTextField.setText(PriceUtil.formatMarketPrice(priceAlertFilter.getHigh(), currencyCode));
priceAlertHighInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getHigh() / 10000d, currencyCode));
priceAlertLowInputTextField.setText(FormattingUtils.formatMarketPrice(priceAlertFilter.getLow() / 10000d, currencyCode));
} else {
@ -742,37 +743,13 @@ public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
}
private long getPriceAsLong(InputTextField inputTextField) {
try {
String inputValue = inputTextField.getText();
if (inputValue != null && !inputValue.isEmpty() && selectedPriceAlertTradeCurrency != null) {
double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue);
String currencyCode = selectedPriceAlertTradeCurrency;
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT : 2;
// We want to use the converted value not the inout value as we apply the converted value at focus out.
// E.g. if input is 5555.5555 it will be rounded to 5555.55 and we use that as the value for comparing
// low and high price...
String stringValue = FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision);
return ParsingUtils.parsePriceStringToLong(currencyCode, stringValue, precision);
} else {
return 0;
}
} catch (Throwable ignore) {
return 0;
}
return PriceUtil.getMarketPriceAsLong(inputTextField.getText(), selectedPriceAlertTradeCurrency);
}
private void applyPriceFormatting(InputTextField inputTextField) {
try {
String inputValue = inputTextField.getText();
if (inputValue != null && !inputValue.isEmpty() && selectedPriceAlertTradeCurrency != null) {
double priceAsDouble = ParsingUtils.parseNumberStringToDouble(inputValue);
String currencyCode = selectedPriceAlertTradeCurrency;
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT : 2;
String stringValue = FormattingUtils.formatRoundedDoubleWithPrecision(priceAsDouble, precision);
inputTextField.setText(stringValue);
}
String reformattedPrice = PriceUtil.reformatMarketPrice(inputTextField.getText(), selectedPriceAlertTradeCurrency);
inputTextField.setText(reformattedPrice);
} catch (Throwable ignore) {
updatePriceAlertFields();
}

View File

@ -150,13 +150,6 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
marketPriceBox.second.getStyleClass().add("dao-kpi-subtext");
avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow,
Res.get("dao.factsAndFigures.dashboard.avgPrice90")).second;
avgPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1,
Res.get("dao.factsAndFigures.dashboard.avgPrice30"), -15).second;
AnchorPane.setRightAnchor(avgPrice30TextField.getIconLabel(), 10d);
avgUSDPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow,
Res.get("dao.factsAndFigures.dashboard.avgUSDPrice90")).second;
@ -164,6 +157,13 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
Res.get("dao.factsAndFigures.dashboard.avgUSDPrice30"), -15).second;
AnchorPane.setRightAnchor(avgUSDPrice30TextField.getIconLabel(), 10d);
avgPrice90TextField = addTopLabelReadOnlyTextField(root, ++gridRow,
Res.get("dao.factsAndFigures.dashboard.avgPrice90")).second;
avgPrice30TextField = addTopLabelTextFieldWithIcon(root, gridRow, 1,
Res.get("dao.factsAndFigures.dashboard.avgPrice30"), -15).second;
AnchorPane.setRightAnchor(avgPrice30TextField.getIconLabel(), 10d);
marketCapTextField = addTopLabelReadOnlyTextField(root, ++gridRow,
Res.get("dao.factsAndFigures.dashboard.marketCap")).second;
@ -171,7 +171,6 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
Res.get("dao.factsAndFigures.dashboard.availableAmount")).second;
}
@Override
protected void activate() {
daoFacade.addBsqStateListener(this);

View File

@ -61,8 +61,6 @@ import javafx.collections.ListChangeListener;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -73,6 +71,8 @@ import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.text.DecimalFormat;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
@ -630,10 +630,10 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
.toLocalDate()
.with(ADJUSTERS.get(MONTH)));
Stream<Issuance> bsqByCompensation = daoStateService.getIssuanceSet(IssuanceType.COMPENSATION).stream()
Stream<Issuance> bsqByCompensation = daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream()
.sorted(Comparator.comparing(Issuance::getChainHeight));
Stream<Issuance> bsqByReimbursement = daoStateService.getIssuanceSet(IssuanceType.REIMBURSEMENT).stream()
Stream<Issuance> bsqByReimbursement = daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT).stream()
.sorted(Comparator.comparing(Issuance::getChainHeight));
Map<LocalDate, List<Issuance>> bsqAddedByVote = Stream.concat(bsqByCompensation, bsqByReimbursement)

View File

@ -247,7 +247,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();
int txVsize = signedTx.getVsize();
@ -305,7 +305,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
try {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
Coin miningFee = signedTx.getFee();

View File

@ -42,6 +42,7 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.config.Config;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
@ -66,9 +67,9 @@ import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import org.fxmisc.easybind.EasyBind;
@ -198,17 +199,15 @@ public class DepositView extends ActivatableView<VBox, Void> {
addressTextField.setManaged(false);
amountTextField.setManaged(false);
generateNewAddressButton = addButton(gridPane, ++gridRow, Res.get("funds.deposit.generateAddress"), -20);
GridPane.setColumnIndex(generateNewAddressButton, 0);
GridPane.setHalignment(generateNewAddressButton, HPos.LEFT);
generateNewAddressSegwitCheckbox = addCheckBox(gridPane, gridRow,
Res.get("funds.deposit.generateAddressSegwit"), 0);
Tuple3<Button, CheckBox, HBox> buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow,
Res.get("funds.deposit.generateAddress"),
Res.get("funds.deposit.generateAddressSegwit"),
15);
buttonCheckBoxHBox.third.setSpacing(25);
generateNewAddressButton = buttonCheckBoxHBox.first;
generateNewAddressSegwitCheckbox = buttonCheckBoxHBox.second;
generateNewAddressSegwitCheckbox.setAllowIndeterminate(false);
generateNewAddressSegwitCheckbox.setSelected(true);
GridPane.setColumnIndex(generateNewAddressSegwitCheckbox, 0);
GridPane.setHalignment(generateNewAddressSegwitCheckbox, HPos.LEFT);
GridPane.setMargin(generateNewAddressSegwitCheckbox, new Insets(15, 0, 0, 250));
generateNewAddressButton.setOnAction(event -> {
boolean segwit = generateNewAddressSegwitCheckbox.isSelected();

View File

@ -179,7 +179,7 @@ public class LockedView extends ActivatableView<VBox, Void> {
exportButton.setOnAction(event -> {
ObservableList<TableColumn<LockedListItem, ?>> tableColumns = tableView.getColumns();
int reportColumns = tableColumns.size();
CSVEntryConverter<LockedListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<LockedListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View File

@ -179,7 +179,7 @@ public class ReservedView extends ActivatableView<VBox, Void> {
exportButton.setOnAction(event -> {
ObservableList<TableColumn<ReservedListItem, ?>> tableColumns = tableView.getColumns();
int reportColumns = tableColumns.size();
CSVEntryConverter<ReservedListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<ReservedListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View File

@ -212,7 +212,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
exportButton.setOnAction(event -> {
final ObservableList<TableColumn<TransactionsListItem, ?>> tableColumns = tableView.getColumns();
final int reportColumns = tableColumns.size() - 1; // CSV report excludes the last column (an icon)
CSVEntryConverter<TransactionsListItem> headerConverter = transactionsListItem -> {
CSVEntryConverter<TransactionsListItem> headerConverter = item -> {
String[] columns = new String[reportColumns];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();

View File

@ -269,8 +269,8 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
feeToggleGroupListener = (observable, oldValue, newValue) -> {
feeExcluded = newValue == feeExcludedRadioButton;
amountLabel.setText(feeExcluded ?
Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()) :
Res.get("funds.withdrawal.senderAmount", Res.getBaseCurrencyCode()));
Res.get("funds.withdrawal.receiverAmount") :
Res.get("funds.withdrawal.senderAmount"));
};
}

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