mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
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:
commit
0c83a9b0cd
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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());
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -67,6 +67,7 @@ public class WalletTest extends MethodTest {
|
||||
|
||||
btcWalletTest.testInitialBtcBalances(testInfo);
|
||||
btcWalletTest.testFundAlicesBtcWallet(testInfo);
|
||||
btcWalletTest.testAliceSendBTCToBob(testInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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, ' ');
|
||||
}
|
||||
|
@ -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
|
||||
|
59
cli/src/main/java/bisq/cli/TransactionFormat.java
Normal file
59
cli/src/main/java/bisq/cli/TransactionFormat.java
Normal 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());
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
160
core/src/main/java/bisq/core/api/model/TxInfo.java
Normal file
160
core/src/main/java/bisq/core/api/model/TxInfo.java
Normal 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" +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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"));
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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() {
|
||||
|
@ -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");
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
|
@ -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
|
||||
|
@ -43,7 +43,7 @@ enum JsonTxOutputType {
|
||||
INVALID_OUTPUT("Invalid");
|
||||
|
||||
@Getter
|
||||
private String displayString;
|
||||
private final String displayString;
|
||||
|
||||
JsonTxOutputType(String displayString) {
|
||||
this.displayString = displayString;
|
||||
|
@ -40,7 +40,7 @@ enum JsonTxType {
|
||||
IRREGULAR("Irregular");
|
||||
|
||||
@Getter
|
||||
private String displayString;
|
||||
private final String displayString;
|
||||
|
||||
JsonTxType(String displayString) {
|
||||
this.displayString = displayString;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
163
core/src/main/java/bisq/core/offer/TriggerPriceService.java
Normal file
163
core/src/main/java/bisq/core/offer/TriggerPriceService.java
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
63
core/src/main/java/bisq/core/user/Cookie.java
Normal file
63
core/src/main/java/bisq/core/user/Cookie.java
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
26
core/src/main/java/bisq/core/user/CookieKey.java
Normal file
26
core/src/main/java/bisq/core/user/CookieKey.java
Normal 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
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) */);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
+ "}";
|
||||
}
|
||||
}
|
@ -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() +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
233
desktop/src/main/java/bisq/desktop/main/PriceUtil.java
Normal file
233
desktop/src/main/java/bisq/desktop/main/PriceUtil.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user