Merge branch 'master' of github.com:bisq-network/bisq into release/v1.5.2

# Conflicts:
#	core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java
#	core/src/main/java/bisq/core/trade/closed/CleanupMailboxMessages.java
This commit is contained in:
Christoph Atteneder 2020-12-28 14:57:08 +01:00
commit 850e031158
No known key found for this signature in database
GPG Key ID: CD5DC1C529CDFD3B
62 changed files with 2407 additions and 570 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,129 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method;
import io.grpc.StatusRuntimeException;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import bisq.daemon.grpc.GrpcVersionService;
import bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CallRateMeteringInterceptorTest extends MethodTest {
private static final GetVersionTest getVersionTest = new GetVersionTest();
@BeforeAll
public static void setUp() {
File callRateMeteringConfigFile = buildInterceptorConfigFile();
startSupportingApps(callRateMeteringConfigFile,
false,
false,
bitcoind, alicedaemon);
}
@BeforeEach
public void sleep200Milliseconds() {
sleep(200);
}
@Test
@Order(1)
public void testGetVersionCall1IsAllowed() {
getVersionTest.testGetVersion();
}
@Test
@Order(2)
public void testGetVersionCall2ShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
exception.getMessage());
}
@Test
@Order(3)
public void testGetVersionCall3ShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
exception.getMessage());
}
@Test
@Order(4)
public void testGetVersionCall4IsAllowed() {
sleep(1100); // Let the server's rate meter reset the call count.
getVersionTest.testGetVersion();
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
public static File buildInterceptorConfigFile() {
GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"getVersion",
1,
SECONDS);
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"shouldNotBreakAnything",
1000,
DAYS);
// Only GrpcVersionService is @VisibleForTesting, so we hardcode the class names.
builder.addCallRateMeter("GrpcOffersService",
"createOffer",
5,
MINUTES);
builder.addCallRateMeter("GrpcOffersService",
"takeOffer",
10,
DAYS);
builder.addCallRateMeter("GrpcTradesService",
"withdrawFunds",
3,
HOURS);
return builder.build();
}
}

View File

@ -39,6 +39,7 @@ import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.KeepFundsRequest;
@ -48,10 +49,12 @@ import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
@ -64,6 +67,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
@ -96,14 +100,42 @@ 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);
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);
}
@ -122,9 +154,6 @@ public class MethodTest extends ApiTestCase {
// show 10 BTC balance, and allow time for daemons parse the new block.
if (generateBtcBlock)
genBtcBlocksThenWait(1, 1500);
} catch (Exception ex) {
fail(ex);
}
}
// Convenience methods for building gRPC request objects
@ -160,8 +189,26 @@ public class MethodTest extends ApiTestCase {
return GetUnusedBsqAddressRequest.newBuilder().build();
}
protected final SendBsqRequest createSendBsqRequest(String address, String amount) {
return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build();
protected final SendBsqRequest createSendBsqRequest(String address,
String amount,
String txFeeRate) {
return SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
}
protected final SendBtcRequest createSendBtcRequest(String address,
String amount,
String txFeeRate,
String memo) {
return SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
@ -208,10 +255,13 @@ public class MethodTest extends ApiTestCase {
.build();
}
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, String address) {
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId,
String address,
String memo) {
return WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
}
@ -247,9 +297,36 @@ public class MethodTest extends ApiTestCase {
return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress();
}
protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, String amount) {
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount) {
return sendBsq(bisqAppConfig, address, amount, "");
}
protected final TxInfo sendBsq(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount));
return grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address,
amount,
txFeeRate))
.getTxInfo();
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig, String address, String amount) {
return sendBtc(bisqAppConfig, address, amount, "", "");
}
protected final TxInfo sendBtc(BisqAppConfig bisqAppConfig,
String address,
String amount,
String txFeeRate,
String memo) {
//noinspection ResultOfMethodCallIgnored
return grpcStubs(bisqAppConfig).walletsService.sendBtc(
createSendBtcRequest(address, amount, txFeeRate, memo))
.getTxInfo();
}
protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) {
@ -354,8 +431,11 @@ public class MethodTest extends ApiTestCase {
}
@SuppressWarnings("ResultOfMethodCallIgnored")
protected final void withdrawFunds(BisqAppConfig bisqAppConfig, String tradeId, String address) {
var req = createWithdrawFundsRequest(tradeId, address);
protected final void withdrawFunds(BisqAppConfig bisqAppConfig,
String tradeId,
String address,
String memo) {
var req = createWithdrawFundsRequest(tradeId, address, memo);
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
}
@ -379,6 +459,11 @@ public class MethodTest extends ApiTestCase {
grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxInfo getTransaction(BisqAppConfig bisqAppConfig, String txId) {
var req = GetTransactionRequest.newBuilder().setTxId(txId).build();
return grpcStubs(bisqAppConfig).walletsService.getTransaction(req).getTxInfo();
}
// Static conveniences for test methods and test case fixture setups.
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -592,6 +592,12 @@ configure(project(':daemon')) {
compileOnly "org.projectlombok:lombok:$lombokVersion"
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,59 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.TxInfo;
import com.google.common.annotations.VisibleForTesting;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static com.google.common.base.Strings.padEnd;
@VisibleForTesting
public class TransactionFormat {
public static String format(TxInfo txInfo) {
String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER
+ COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER
+ COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER
+ COL_HEADER_TX_FEE + COL_HEADER_DELIMITER
+ COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER
+ (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER)
+ "\n";
String colDataFormat = "%-" + txInfo.getTxId().length() + "s"
+ " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s"
+ " %" + COL_HEADER_TX_INPUT_SUM.length() + "s"
+ " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s"
+ " %" + COL_HEADER_TX_FEE.length() + "s"
+ " %" + COL_HEADER_TX_SIZE.length() + "s"
+ " %s";
return headerLine
+ String.format(colDataFormat,
txInfo.getTxId(),
txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed
formatSatoshis(txInfo.getInputSum()),
formatSatoshis(txInfo.getOutputSum()),
formatSatoshis(txInfo.getFee()),
txInfo.getSize(),
txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo());
}
}

View File

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

View File

@ -74,6 +74,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -139,9 +140,14 @@ public class AccountAgeWitnessService {
private final FilterManager filterManager;
@Getter
private final AccountAgeWitnessUtils accountAgeWitnessUtils;
@Getter
private final Map<P2PDataStorage.ByteArray, AccountAgeWitness> accountAgeWitnessMap = new HashMap<>();
// The accountAgeWitnessMap is very large (70k items) and access is a bit expensive. We usually only access less
// than 100 items, those who have offers online. So we use a cache for a fast lookup and only if
// not found there we use the accountAgeWitnessMap and put then the new item into our cache.
private final Map<P2PDataStorage.ByteArray, AccountAgeWitness> accountAgeWitnessCache = new ConcurrentHashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -235,9 +241,18 @@ 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) {
return findTradePeerWitness(trade)
@ -286,12 +301,21 @@ public class AccountAgeWitnessService {
private Optional<AccountAgeWitness> getWitnessByHash(byte[] hash) {
P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash);
final boolean containsKey = accountAgeWitnessMap.containsKey(hashAsByteArray);
if (!containsKey)
log.debug("hash not found in accountAgeWitnessMap");
// First we look up in our fast lookup cache
if (accountAgeWitnessCache.containsKey(hashAsByteArray)) {
return Optional.of(accountAgeWitnessCache.get(hashAsByteArray));
}
return accountAgeWitnessMap.containsKey(hashAsByteArray) ?
Optional.of(accountAgeWitnessMap.get(hashAsByteArray)) : Optional.empty();
if (accountAgeWitnessMap.containsKey(hashAsByteArray)) {
AccountAgeWitness accountAgeWitness = accountAgeWitnessMap.get(hashAsByteArray);
// We add it to our fast lookup cache
accountAgeWitnessCache.put(hashAsByteArray, accountAgeWitness);
return Optional.of(accountAgeWitness);
}
return Optional.empty();
}
private Optional<AccountAgeWitness> getWitnessByHashAsHex(String hashAsHex) {
@ -658,16 +682,20 @@ public class AccountAgeWitnessService {
}
public String arbitratorSignOrphanWitness(AccountAgeWitness accountAgeWitness,
ECKey key,
ECKey ecKey,
long time) {
// Find AccountAgeWitness as signedwitness
var signedWitness = signedWitnessService.getSignedWitnessMap().values().stream()
.filter(sw -> Arrays.equals(sw.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
// TODO Is not found signedWitness considered an error case?
// Previous code version was throwing an exception in case no signedWitness was found...
// signAndPublishAccountAgeWitness returns an empty string in success case and error otherwise
return signedWitnessService.getSignedWitnessSet(accountAgeWitness).stream()
.findAny()
.orElse(null);
checkNotNull(signedWitness);
return signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, key,
signedWitness.getWitnessOwnerPubKey(), time);
.map(SignedWitness::getWitnessOwnerPubKey)
.map(witnessOwnerPubKey ->
signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, ecKey,
witnessOwnerPubKey, time)
)
.orElse("No signedWitness found");
}
public String arbitratorSignOrphanPubKey(ECKey key,

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,160 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api.model;
import bisq.common.Payload;
import org.bitcoinj.core.Transaction;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode
@Getter
public class TxInfo implements Payload {
// The client cannot see an instance of an org.bitcoinj.core.Transaction. We use the
// lighter weight TxInfo proto wrapper instead, containing just enough fields to
// view some transaction details. A block explorer or bitcoin-core client can be
// used to see more detail.
private final String txId;
private final long inputSum;
private final long outputSum;
private final long fee;
private final int size;
private final boolean isPending;
private final String memo;
public TxInfo(TxInfo.TxInfoBuilder builder) {
this.txId = builder.txId;
this.inputSum = builder.inputSum;
this.outputSum = builder.outputSum;
this.fee = builder.fee;
this.size = builder.size;
this.isPending = builder.isPending;
this.memo = builder.memo;
}
public static TxInfo toTxInfo(Transaction transaction) {
if (transaction == null)
throw new IllegalStateException("server created a null transaction");
return new TxInfo.TxInfoBuilder()
.withTxId(transaction.getTxId().toString())
.withInputSum(transaction.getInputSum().value)
.withOutputSum(transaction.getOutputSum().value)
.withFee(transaction.getFee().value)
.withSize(transaction.getMessageSize())
.withIsPending(transaction.isPending())
.withMemo(transaction.getMemo())
.build();
}
//////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
//////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.TxInfo toProtoMessage() {
return bisq.proto.grpc.TxInfo.newBuilder()
.setTxId(txId)
.setInputSum(inputSum)
.setOutputSum(outputSum)
.setFee(fee)
.setSize(size)
.setIsPending(isPending)
.setMemo(memo == null ? "" : memo)
.build();
}
@SuppressWarnings("unused")
public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) {
return new TxInfo.TxInfoBuilder()
.withTxId(proto.getTxId())
.withInputSum(proto.getInputSum())
.withOutputSum(proto.getOutputSum())
.withFee(proto.getFee())
.withSize(proto.getSize())
.withIsPending(proto.getIsPending())
.withMemo(proto.getMemo())
.build();
}
public static class TxInfoBuilder {
private String txId;
private long inputSum;
private long outputSum;
private long fee;
private int size;
private boolean isPending;
private String memo;
public TxInfo.TxInfoBuilder withTxId(String txId) {
this.txId = txId;
return this;
}
public TxInfo.TxInfoBuilder withInputSum(long inputSum) {
this.inputSum = inputSum;
return this;
}
public TxInfo.TxInfoBuilder withOutputSum(long outputSum) {
this.outputSum = outputSum;
return this;
}
public TxInfo.TxInfoBuilder withFee(long fee) {
this.fee = fee;
return this;
}
public TxInfo.TxInfoBuilder withSize(int size) {
this.size = size;
return this;
}
public TxInfo.TxInfoBuilder withIsPending(boolean isPending) {
this.isPending = isPending;
return this;
}
public TxInfo.TxInfoBuilder withMemo(String memo) {
this.memo = memo;
return this;
}
public TxInfo build() {
return new TxInfo(this);
}
}
@Override
public String toString() {
return "TxInfo{" + "\n" +
" txId='" + txId + '\'' + "\n" +
", inputSum=" + inputSum + "\n" +
", outputSum=" + outputSum + "\n" +
", fee=" + fee + "\n" +
", size=" + size + "\n" +
", isPending=" + isPending + "\n" +
", memo='" + memo + '\'' + "\n" +
'}';
}
}

View File

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

View File

@ -182,6 +182,9 @@ public class BisqSetup {
private Runnable qubesOSInfoHandler;
@Setter
@Nullable
private Runnable daoRequiresRestartHandler;
@Setter
@Nullable
private Consumer<String> downGradePreventionHandler;
@Getter
@ -443,7 +446,8 @@ public class BisqSetup {
daoWarnMessageHandler,
filterWarningHandler,
voteResultExceptionHandler,
revolutAccountsUpdateHandler);
revolutAccountsUpdateHandler,
daoRequiresRestartHandler);
if (walletsSetup.downloadPercentageProperty().get() == 1) {
checkForLockedUpFunds();

View File

@ -25,6 +25,7 @@ import bisq.core.btc.Balances;
import bisq.core.dao.DaoSetup;
import bisq.core.dao.governance.voteresult.VoteResultException;
import bisq.core.dao.governance.voteresult.VoteResultService;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.filter.FilterManager;
import bisq.core.notifications.MobileNotificationService;
import bisq.core.notifications.alerts.DisputeMsgEvents;
@ -104,6 +105,7 @@ public class DomainInitialisation {
private final PriceAlert priceAlert;
private final MarketAlerts marketAlerts;
private final User user;
private final DaoStateSnapshotService daoStateSnapshotService;
@Inject
public DomainInitialisation(ClockWatcher clockWatcher,
@ -138,7 +140,8 @@ public class DomainInitialisation {
DisputeMsgEvents disputeMsgEvents,
PriceAlert priceAlert,
MarketAlerts marketAlerts,
User user) {
User user,
DaoStateSnapshotService daoStateSnapshotService) {
this.clockWatcher = clockWatcher;
this.tradeLimits = tradeLimits;
this.arbitrationManager = arbitrationManager;
@ -172,6 +175,7 @@ public class DomainInitialisation {
this.priceAlert = priceAlert;
this.marketAlerts = marketAlerts;
this.user = user;
this.daoStateSnapshotService = daoStateSnapshotService;
}
public void initDomainServices(Consumer<String> rejectedTxErrorMessageHandler,
@ -180,7 +184,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 +227,8 @@ public class DomainInitialisation {
if (daoWarnMessageHandler != null)
daoWarnMessageHandler.accept(warningMessage);
});
daoStateSnapshotService.setDaoRequiresRestartHandler(daoRequiresRestartHandler);
}
tradeStatisticsManager.onAllServicesInitialized();

View File

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

View File

@ -440,8 +440,7 @@ public class BtcWalletService extends WalletService {
// Add fee input to prepared BSQ send tx
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean isSendTx) throws
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
// preparedBsqTx has following structure:
// inputs [1-n] BSQ inputs
@ -455,13 +454,26 @@ public class BtcWalletService extends WalletService {
// outputs [0-1] BSQ change output
// outputs [0-1] BTC change output
// mining fee: BTC mining fee
return completePreparedBsqTx(preparedBsqTx, isSendTx, null);
Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte();
return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte);
}
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, Coin txFeePerVbyte) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
return completePreparedBsqTx(preparedBsqTx, null, txFeePerVbyte);
}
public Transaction completePreparedBsqTx(Transaction preparedBsqTx,
boolean useCustomTxFee,
@Nullable byte[] opReturnData) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
Coin txFeePerVbyte = getTxFeeForWithdrawalPerVbyte();
return completePreparedBsqTx(preparedBsqTx, opReturnData, txFeePerVbyte);
}
public Transaction completePreparedBsqTx(Transaction preparedBsqTx,
@Nullable byte[] opReturnData,
Coin txFeePerVbyte) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
// preparedBsqTx has following structure:
// inputs [1-n] BSQ inputs
@ -488,8 +500,6 @@ public class BtcWalletService extends WalletService {
int sigSizePerInput = 106;
// typical size for a tx with 2 inputs
int txVsizeWithUnsignedInputs = 203;
// If useCustomTxFee we allow overriding the estimated fee from preferences
Coin txFeePerVbyte = useCustomTxFee ? getTxFeeForWithdrawalPerVbyte() : feeService.getTxFeePerVbyte();
// In case there are no change outputs we force a change by adding min dust to the BTC input
Coin forcedChangeValue = Coin.ZERO;
@ -968,7 +978,7 @@ public class BtcWalletService extends WalletService {
}
if (sendResult != null) {
log.info("Broadcasting double spending transaction. " + sendResult.tx);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(Transaction result) {
log.info("Double spending transaction published. " + result);
@ -1048,6 +1058,14 @@ public class BtcWalletService extends WalletService {
public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses,
Coin amount)
throws AddressFormatException, AddressEntryException, InsufficientFundsException {
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
return getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amount, txFeeForWithdrawalPerVbyte);
}
public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses,
Coin amount,
Coin txFeeForWithdrawalPerVbyte)
throws AddressFormatException, AddressEntryException, InsufficientFundsException {
Set<AddressEntry> addressEntries = fromAddresses.stream()
.map(address -> {
Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
@ -1070,7 +1088,6 @@ public class BtcWalletService extends WalletService {
int counter = 0;
int txVsize = 0;
Transaction tx;
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
@ -1097,7 +1114,11 @@ public class BtcWalletService extends WalletService {
}
private boolean feeEstimationNotSatisfied(int counter, Transaction tx) {
long targetFee = getTxFeeForWithdrawalPerVbyte().multiply(tx.getVsize()).value;
return feeEstimationNotSatisfied(counter, tx, getTxFeeForWithdrawalPerVbyte());
}
private boolean feeEstimationNotSatisfied(int counter, Transaction tx, Coin txFeeForWithdrawalPerVbyte) {
long targetFee = txFeeForWithdrawalPerVbyte.multiply(tx.getVsize()).value;
return counter < 10 &&
(tx.getFee().value < targetFee ||
tx.getFee().value - targetFee > 1000);
@ -1213,7 +1234,7 @@ public class BtcWalletService extends WalletService {
Coin fee,
@Nullable String changeAddress,
@Nullable KeyParameter aesKey) throws
AddressFormatException, AddressEntryException, InsufficientMoneyException {
AddressFormatException, AddressEntryException {
Transaction tx = new Transaction(params);
final Coin netValue = amount.subtract(fee);
checkArgument(Restrictions.isAboveDust(netValue),
@ -1246,12 +1267,12 @@ public class BtcWalletService extends WalletService {
sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries),
preferences.getIgnoreDustThreshold());
Optional<AddressEntry> addressEntryOptional = Optional.<AddressEntry>empty();
AddressEntry changeAddressAddressEntry = null;
Optional<AddressEntry> addressEntryOptional = Optional.empty();
if (changeAddress != null)
addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE);
changeAddressAddressEntry = addressEntryOptional.orElseGet(() -> getFreshAddressEntry());
AddressEntry changeAddressAddressEntry = addressEntryOptional.orElseGet(this::getFreshAddressEntry);
checkNotNull(changeAddressAddressEntry, "change address must not be null");
sendRequest.changeAddress = changeAddressAddressEntry.getAddress();
return sendRequest;

View File

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

View File

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

View File

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

View File

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

View File

@ -192,7 +192,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 +454,20 @@ 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
# 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 +524,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
@ -1398,17 +1402,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 +2704,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 +2854,18 @@ popup.info.shutDownWithOpenOffers=Bisq is being shut down, but there are open of
popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\n\
Please make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes].
popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version.
popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue.
popup.privateNotification.headline=Important private notification!
popup.securityRecommendation.headline=Important security recommendation
popup.securityRecommendation.msg=We would like to remind you to consider using password protection for your wallet if you have not already enabled that.\n\nIt is also highly recommended to write down the wallet seed words. Those seed words are like a master password for recovering your Bitcoin wallet.\nAt the \"Wallet Seed\" section you find more information.\n\nAdditionally you should backup the complete application data folder at the \"Backup\" section.
popup.bitcoinLocalhostNode.msg=Bisq detected a locally running Bitcoin Core node (at localhost).\n\
Please make sure that this node is fully synced before you start Bisq and that it is not running in pruned mode.
popup.bitcoinLocalhostNode.additionalRequirements=\n\nFor a well configured node, the requirements are for the node to have pruning disabled and bloom filters enabled.
popup.bitcoinLocalhostNode.msg=Bisq detected a Bitcoin Core node running on this machine (at localhost).\n\n\
Please ensure:\n\
- the node is fully synced before starting Bisq\n\
- pruning is disabled ('prune=0' in bitcoin.conf)\n\
- bloom filters are enabled ('peerbloomfilters=1' in bitcoin.conf)
popup.shutDownInProgress.headline=Shut down in progress
popup.shutDownInProgress.msg=Shutting down application can take a few seconds.\nPlease don't interrupt this process.

View File

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

View File

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

View File

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

View File

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

View File

@ -16,16 +16,18 @@ 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());
@ -33,5 +35,8 @@ class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStati
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,20 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
@ -6,23 +23,64 @@ import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionReply;
import bisq.proto.grpc.GetVersionRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.util.concurrent.TimeUnit.SECONDS;
import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor;
import bisq.daemon.grpc.interceptor.GrpcCallRateMeter;
@VisibleForTesting
@Slf4j
public class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
private final CoreApi coreApi;
private final GrpcExceptionHandler exceptionHandler;
@Inject
public GrpcVersionService(CoreApi coreApi) {
public GrpcVersionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
this.coreApi = coreApi;
this.exceptionHandler = exceptionHandler;
}
@Override
public void getVersion(GetVersionRequest req, StreamObserver<GetVersionReply> responseObserver) {
try {
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
}
final Optional<ServerInterceptor> rateMeteringInterceptor() {
@SuppressWarnings("unused") // Defined as a usage example.
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
new CallRateMeteringInterceptor(new HashMap<>() {{
put("getVersion", new GrpcCallRateMeter(100, SECONDS));
}});
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(Optional::empty /* Optional.of(defaultCallRateMeteringInterceptor) */);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,287 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc.interceptor;
import io.grpc.ServerInterceptor;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.common.annotations.VisibleForTesting;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static bisq.common.file.FileUtil.deleteFileIfExists;
import static bisq.common.file.FileUtil.renameFile;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.readAllBytes;
@VisibleForTesting
@Slf4j
public class GrpcServiceRateMeteringConfig {
public static final String RATE_METERS_CONFIG_FILENAME = "ratemeters.json";
private static final String KEY_GRPC_SERVICE_CLASS_NAME = "grpcServiceClassName";
private static final String KEY_METHOD_RATE_METERS = "methodRateMeters";
private static final String KEY_ALLOWED_CALL_PER_TIME_WINDOW = "allowedCallsPerTimeWindow";
private static final String KEY_TIME_UNIT = "timeUnit";
private static final String KEY_NUM_TIME_UNITS = "numTimeUnits";
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final List<Map<String, GrpcCallRateMeter>> methodRateMeters;
private final String grpcServiceClassName;
public GrpcServiceRateMeteringConfig(String grpcServiceClassName) {
this(grpcServiceClassName, new ArrayList<>());
}
public GrpcServiceRateMeteringConfig(String grpcServiceClassName,
List<Map<String, GrpcCallRateMeter>> methodRateMeters) {
this.grpcServiceClassName = grpcServiceClassName;
this.methodRateMeters = methodRateMeters;
}
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
int maxCalls,
TimeUnit timeUnit) {
return addMethodCallRateMeter(methodName, maxCalls, timeUnit, 1);
}
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
int maxCalls,
TimeUnit timeUnit,
int numTimeUnits) {
methodRateMeters.add(new LinkedHashMap<>() {{
put(methodName, new GrpcCallRateMeter(maxCalls, timeUnit, numTimeUnits));
}});
return this;
}
public boolean isConfigForGrpcService(Class<?> clazz) {
return isConfigForGrpcService(clazz.getSimpleName());
}
public boolean isConfigForGrpcService(String grpcServiceClassSimpleName) {
return this.grpcServiceClassName.equals(grpcServiceClassSimpleName);
}
@Override
public String toString() {
return "GrpcServiceRateMeteringConfig{" + "\n" +
" grpcServiceClassName='" + grpcServiceClassName + '\'' + "\n" +
", methodRateMeters=" + methodRateMeters + "\n" +
'}';
}
public static Optional<ServerInterceptor> getCustomRateMeteringInterceptor(File installationDir,
Class<?> grpcServiceClass) {
File configFile = new File(installationDir, RATE_METERS_CONFIG_FILENAME);
return configFile.exists()
? toServerInterceptor(configFile, grpcServiceClass)
: Optional.empty();
}
public static Optional<ServerInterceptor> toServerInterceptor(File configFile, Class<?> grpcServiceClass) {
// From a global rate metering config file, create a specific gRPC service
// interceptor configuration in the form of an interceptor constructor argument,
// a map<method-name, rate-meter>.
// Transforming json into the List<Map<String, GrpcCallRateMeter>> is a bit
// convoluted due to Gson's loss of generic type information during deserialization.
Optional<GrpcServiceRateMeteringConfig> grpcServiceConfig = getAllDeserializedConfigs(configFile)
.stream().filter(x -> x.isConfigForGrpcService(grpcServiceClass)).findFirst();
if (grpcServiceConfig.isPresent()) {
Map<String, GrpcCallRateMeter> serviceCallRateMeters = new HashMap<>();
for (Map<String, GrpcCallRateMeter> methodToRateMeterMap : grpcServiceConfig.get().methodRateMeters) {
Map.Entry<String, GrpcCallRateMeter> entry = methodToRateMeterMap.entrySet().stream().findFirst().orElseThrow(()
-> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map."));
serviceCallRateMeters.put(entry.getKey(), entry.getValue());
}
return Optional.of(new CallRateMeteringInterceptor(serviceCallRateMeters));
} else {
return Optional.empty();
}
}
@SuppressWarnings("unchecked")
private static List<Map<String, GrpcCallRateMeter>> getMethodRateMetersMap(Map<String, Object> gsonMap) {
List<Map<String, GrpcCallRateMeter>> rateMeters = new ArrayList<>();
// Each gsonMap is a Map<String, Object> with a single entry:
// {getVersion={allowedCallsPerTimeUnit=8.0, timeUnit=SECONDS, callsCount=0.0, isRunning=false}}
// Convert it to a multiple entry Map<String, GrpcCallRateMeter>, where the key
// is a method name.
for (Map<String, Object> singleEntryRateMeterMap : (List<Map<String, Object>>) gsonMap.get(KEY_METHOD_RATE_METERS)) {
log.debug("Gson's single entry {} {}<String, Object> = {}",
gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME),
singleEntryRateMeterMap.getClass().getSimpleName(),
singleEntryRateMeterMap);
Map.Entry<String, Object> entry = singleEntryRateMeterMap.entrySet().stream().findFirst().orElseThrow(()
-> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map."));
String methodName = entry.getKey();
GrpcCallRateMeter rateMeter = getGrpcCallRateMeter(entry);
rateMeters.add(new LinkedHashMap<>() {{
put(methodName, rateMeter);
}});
}
return rateMeters;
}
@SuppressWarnings({"rawtypes", "unchecked"})
public static List<GrpcServiceRateMeteringConfig> deserialize(File configFile) {
verifyConfigFile(configFile);
List<GrpcServiceRateMeteringConfig> serviceMethodConfigurations = new ArrayList<>();
// Gson cannot deserialize a json string to List<GrpcServiceRateMeteringConfig>
// so easily for us, so we do it here before returning the list of configurations.
List rawConfigList = gson.fromJson(toJson(configFile), ArrayList.class);
// Gson gave us a list of maps with keys grpcServiceClassName, methodRateMeters:
// String grpcServiceClassName
// List<Map> methodRateMeters
for (Object rawConfig : rawConfigList) {
Map<String, Object> gsonMap = (Map<String, Object>) rawConfig;
String grpcServiceClassName = (String) gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME);
List<Map<String, GrpcCallRateMeter>> rateMeters = getMethodRateMetersMap(gsonMap);
serviceMethodConfigurations.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName, rateMeters));
}
return serviceMethodConfigurations;
}
@SuppressWarnings("unchecked")
private static GrpcCallRateMeter getGrpcCallRateMeter(Map.Entry<String, Object> gsonEntry) {
Map<String, Object> valueMap = (Map<String, Object>) gsonEntry.getValue();
int allowedCallsPerTimeWindow = ((Number) valueMap.get(KEY_ALLOWED_CALL_PER_TIME_WINDOW)).intValue();
TimeUnit timeUnit = TimeUnit.valueOf((String) valueMap.get(KEY_TIME_UNIT));
int numTimeUnits = ((Number) valueMap.get(KEY_NUM_TIME_UNITS)).intValue();
return new GrpcCallRateMeter(allowedCallsPerTimeWindow, timeUnit, numTimeUnits);
}
private static void verifyConfigFile(File configFile) {
if (configFile == null)
throw new IllegalStateException("Cannot read null json config file.");
if (!configFile.exists())
throw new IllegalStateException(format("cannot find json config file %s", configFile.getAbsolutePath()));
}
private static String toJson(File configFile) {
try {
return new String(readAllBytes(Paths.get(configFile.getAbsolutePath())));
} catch (IOException ex) {
throw new IllegalStateException(format("Cannot read json string from file %s.",
configFile.getAbsolutePath()));
}
}
private static List<GrpcServiceRateMeteringConfig> allDeserializedConfigs;
private static List<GrpcServiceRateMeteringConfig> getAllDeserializedConfigs(File configFile) {
// We deserialize once, not for each gRPC service wanting an interceptor.
if (allDeserializedConfigs == null)
allDeserializedConfigs = deserialize(configFile);
return allDeserializedConfigs;
}
@VisibleForTesting
public static class Builder {
private final List<GrpcServiceRateMeteringConfig> rateMeterConfigs = new ArrayList<>();
public void addCallRateMeter(String grpcServiceClassName,
String methodName,
int maxCalls,
TimeUnit timeUnit) {
addCallRateMeter(grpcServiceClassName,
methodName,
maxCalls,
timeUnit,
1);
}
public void addCallRateMeter(String grpcServiceClassName,
String methodName,
int maxCalls,
TimeUnit timeUnit,
int numTimeUnits) {
log.info("Adding call rate metering definition {}.{} ({}/{}ms).",
grpcServiceClassName,
methodName,
maxCalls,
timeUnit.toMillis(1) * numTimeUnits);
rateMeterConfigs.stream().filter(c -> c.isConfigForGrpcService(grpcServiceClassName))
.findFirst().ifPresentOrElse(
(config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits),
() -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName)
.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits)));
}
public File build() {
File tmpFile = serializeRateMeterDefinitions();
File configFile = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toFile();
try {
deleteFileIfExists(configFile);
renameFile(tmpFile, configFile);
} catch (IOException ex) {
throw new IllegalStateException(format("Could not create config file %s.",
configFile.getAbsolutePath()), ex);
}
return configFile;
}
private File serializeRateMeterDefinitions() {
String json = gson.toJson(rateMeterConfigs);
File file = createTmpFile();
try (OutputStreamWriter outputStreamWriter =
new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) {
outputStreamWriter.write(json);
} catch (Exception ex) {
throw new IllegalStateException(format("Cannot write file for json string %s.", json), ex);
}
return file;
}
private File createTmpFile() {
File file;
try {
file = File.createTempFile("ratemeters_",
".tmp",
Paths.get(getProperty("java.io.tmpdir")).toFile());
} catch (IOException ex) {
throw new IllegalStateException("Cannot create tmp ratemeters json file.", ex);
}
return file;
}
}
}

View File

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

View File

@ -0,0 +1,190 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.daemon.grpc.interceptor;
import io.grpc.ServerInterceptor;
import java.nio.file.Paths;
import java.io.File;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static java.lang.System.getProperty;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import bisq.daemon.grpc.GrpcVersionService;
@Slf4j
public class GrpcServiceRateMeteringConfigTest {
private static final GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
private static File configFile;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Optional<ServerInterceptor> versionServiceInterceptor;
@BeforeClass
public static void setup() {
// This is the tested rate meter, it allows 3 calls every 2 seconds.
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"getVersion",
3,
SECONDS,
2);
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
"badMethodNameDoesNotBreakAnything",
100,
DAYS);
// The other Grpc*Service classes are not @VisibleForTesting, so we hardcode
// the simple class name.
builder.addCallRateMeter("GrpcOffersService",
"createOffer",
5,
MINUTES);
builder.addCallRateMeter("GrpcOffersService",
"takeOffer",
10,
DAYS);
builder.addCallRateMeter("GrpcWalletsService",
"sendBtc",
3,
HOURS);
}
@Before
public void buildConfigFile() {
if (configFile == null)
configFile = builder.build();
}
@Test
public void testConfigFileBuild() {
assertNotNull(configFile);
assertTrue(configFile.exists());
assertTrue(configFile.length() > 0);
String expectedConfigFilePath = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toString();
assertEquals(expectedConfigFilePath, configFile.getAbsolutePath());
}
@Test
public void testGetVersionCallRateMeter() {
// Check the interceptor has 2 rate meters, for getVersion and badMethodNameDoesNotBreakAnything.
CallRateMeteringInterceptor versionServiceInterceptor = buildInterceptor();
assertEquals(2, versionServiceInterceptor.serviceCallRateMeters.size());
// Check the rate meter config.
GrpcCallRateMeter rateMeter = versionServiceInterceptor.serviceCallRateMeters.get("getVersion");
assertEquals(3, rateMeter.getAllowedCallsPerTimeWindow());
assertEquals(SECONDS, rateMeter.getTimeUnit());
assertEquals(2, rateMeter.getNumTimeUnits());
assertEquals(2 * 1000, rateMeter.getTimeUnitIntervalInMilliseconds());
// Do as many calls as allowed within rateMeter.getTimeUnitIntervalInMilliseconds().
doMaxIsAllowedChecks(true,
rateMeter.getAllowedCallsPerTimeWindow(),
rateMeter);
// The next 3 calls are blocked because we've exceeded the 3calls/2s limit.
doMaxIsAllowedChecks(false,
rateMeter.getAllowedCallsPerTimeWindow(),
rateMeter);
// Let all of the rate meter's cached call timestamps become stale by waiting for
// 2001 ms, then we can call getversion another 'allowedCallsPerTimeUnit' times.
rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds());
// All the stale call timestamps are gone and the call count is back to zero.
assertEquals(0, rateMeter.getCallsCount());
doMaxIsAllowedChecks(true,
rateMeter.getAllowedCallsPerTimeWindow(),
rateMeter);
// We've exceeded the call/second limit.
assertFalse(rateMeter.checkAndIncrement());
// Let all of the call timestamps go stale again by waiting for 2001 ms.
rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds());
// Call twice, resting 0.5s after each call.
for (int i = 0; i < 2; i++) {
assertTrue(rateMeter.checkAndIncrement());
rest(500);
}
// Call the 3rd time, then let one of the rate meter's timestamps go stale.
assertTrue(rateMeter.checkAndIncrement());
rest(1001);
// The call count was decremented by one because one timestamp went stale.
assertEquals(2, rateMeter.getCallsCount());
assertTrue(rateMeter.checkAndIncrement());
assertEquals(rateMeter.getAllowedCallsPerTimeWindow(), rateMeter.getCallsCount());
// We've exceeded the call limit again.
assertFalse(rateMeter.checkAndIncrement());
}
private void doMaxIsAllowedChecks(boolean expectedIsAllowed,
int expectedCallsCount,
GrpcCallRateMeter rateMeter) {
for (int i = 1; i <= rateMeter.getAllowedCallsPerTimeWindow(); i++) {
assertEquals(expectedIsAllowed, rateMeter.checkAndIncrement());
}
assertEquals(expectedCallsCount, rateMeter.getCallsCount());
}
@AfterClass
public static void teardown() {
if (configFile != null)
configFile.deleteOnExit();
}
private void rest(long milliseconds) {
try {
TimeUnit.MILLISECONDS.sleep(milliseconds);
} catch (InterruptedException ignored) {
}
}
private CallRateMeteringInterceptor buildInterceptor() {
//noinspection OptionalAssignedToNull
if (versionServiceInterceptor == null) {
versionServiceInterceptor = getCustomRateMeteringInterceptor(
configFile.getParentFile(),
GrpcVersionService.class);
}
assertTrue(versionServiceInterceptor.isPresent());
return (CallRateMeteringInterceptor) versionServiceInterceptor.get();
}
}

View File

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

View File

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

View File

@ -0,0 +1,82 @@
/*
* 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.offer;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Volume;
import bisq.core.offer.OfferUtil;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.app.DevEnv;
import org.bitcoinj.core.Coin;
import java.util.Optional;
public class FeeUtil {
public static String getTradeFeeWithFiatEquivalent(OfferUtil offerUtil,
Coin tradeFee,
boolean isCurrencyForMakerFeeBtc,
CoinFormatter formatter) {
if (!isCurrencyForMakerFeeBtc && !DevEnv.isDaoActivated()) {
return "";
}
Optional<Volume> optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(tradeFee,
isCurrencyForMakerFeeBtc,
formatter);
return DisplayUtils.getFeeWithFiatAmount(tradeFee, optionalBtcFeeInFiat, formatter);
}
public static String getTradeFeeWithFiatEquivalentAndPercentage(OfferUtil offerUtil,
Coin tradeFee,
Coin tradeAmount,
boolean isCurrencyForMakerFeeBtc,
CoinFormatter formatter,
Coin minTradeFee) {
if (isCurrencyForMakerFeeBtc) {
String feeAsBtc = formatter.formatCoinWithCode(tradeFee);
String percentage;
if (!tradeFee.isGreaterThan(minTradeFee)) {
percentage = Res.get("guiUtil.requiredMinimum")
.replace("(", "")
.replace(")", "");
} else {
percentage = GUIUtil.getPercentage(tradeFee, tradeAmount) +
" " + Res.get("guiUtil.ofTradeAmount");
}
return offerUtil.getFeeInUserFiatCurrency(tradeFee,
isCurrencyForMakerFeeBtc,
formatter)
.map(DisplayUtils::formatAverageVolumeWithCode)
.map(feeInFiat -> Res.get("feeOptionWindow.btcFeeWithFiatAndPercentage", feeAsBtc, feeInFiat, percentage))
.orElseGet(() -> Res.get("feeOptionWindow.btcFeeWithPercentage", feeAsBtc, percentage));
} else {
// For BSQ we use the fiat equivalent only. Calculating the % value would be more effort.
// We could calculate the BTC value if the BSQ fee and use that...
return FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
tradeFee,
false,
formatter);
}
}
}

View File

@ -35,8 +35,6 @@ import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView;
import bisq.desktop.main.dao.DaoView;
import bisq.desktop.main.dao.wallet.BsqWalletView;
import bisq.desktop.main.dao.wallet.receive.BsqReceiveView;
import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.withdrawal.WithdrawalView;
import bisq.desktop.main.overlays.notifications.Notification;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
@ -328,14 +326,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
// we use model.placeOfferCompleted to not react on close which was triggered by a successful placeOffer
if (model.getDataModel().getBalance().get().isPositive() && !model.placeOfferCompleted.get()) {
model.getDataModel().swapTradeToSavings();
String key = "CreateOfferCancelAndFunded";
if (preferences.showAgain(key)) {
new Popup().information(Res.get("createOffer.alreadyFunded"))
.actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal")
.onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class))
.dontShowAgainId(key)
.show();
}
}
}
@ -1239,14 +1229,17 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
fundingHBox.getChildren().add(cancelButton2);
cancelButton2.setOnAction(e -> {
if (model.getDataModel().getIsBtcWalletFunded().get()) {
new Popup().warning(Res.get("createOffer.warnCancelOffer"))
String key = "CreateOfferCancelAndFunded";
if (model.getDataModel().getIsBtcWalletFunded().get() &&
preferences.showAgain(key)) {
new Popup().backgroundInfo(Res.get("createOffer.warnCancelOffer"))
.closeButtonText(Res.get("shared.no"))
.actionButtonText(Res.get("shared.yesCancel"))
.onAction(() -> {
close();
model.getDataModel().swapTradeToSavings();
})
.dontShowAgainId(key)
.show();
} else {
close();

View File

@ -87,11 +87,13 @@ import javafx.beans.value.ChangeListener;
import javafx.util.Callback;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static javafx.beans.binding.Bindings.createStringBinding;
@Slf4j
public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> extends ActivatableWithDataModel<M> {
private final BtcValidator btcValidator;
private final BsqValidator bsqValidator;
@ -489,55 +491,28 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
private void applyMakerFee() {
Coin makerFeeAsCoin = dataModel.getMakerFee();
if (makerFeeAsCoin != null) {
isTradeFeeVisible.setValue(true);
tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin));
Coin makerFeeInBtc = dataModel.getMakerFeeInBtc();
Optional<Volume> optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc,
true,
bsqFormatter);
String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount);
} else {
tradeFeeInBtcWithFiat.set(btcFormatter.formatCoinWithCode(makerFeeAsCoin));
}
Coin makerFeeInBsq = dataModel.getMakerFeeInBsq();
Optional<Volume> optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq,
false,
bsqFormatter);
String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq,
optionalBsqFeeInFiat,
bsqFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount);
} else {
// Before DAO is enabled we show fee as fiat and % in second line
String feeInFiatAsString;
if (optionalBtcFeeInFiat != null && optionalBtcFeeInFiat.isPresent()) {
feeInFiatAsString = DisplayUtils.formatVolumeWithCode(optionalBtcFeeInFiat.get());
} else {
feeInFiatAsString = Res.get("shared.na");
}
double amountAsDouble = (double) dataModel.getAmount().get().value;
double makerFeeInBtcAsDouble = (double) makerFeeInBtc.value;
double percent = makerFeeInBtcAsDouble / amountAsDouble;
tradeFeeInBsqWithFiat.set(Res.get("createOffer.tradeFee.fiatAndPercent",
feeInFiatAsString,
FormattingUtils.formatToPercentWithSymbol(percent)));
}
}
tradeFeeCurrencyCode.set(dataModel.isCurrencyForMakerFeeBtc() ? Res.getBaseCurrencyCode() : "BSQ");
tradeFeeDescription.set(DevEnv.isDaoActivated() ? Res.get("createOffer.tradeFee.descriptionBSQEnabled") :
Res.get("createOffer.tradeFee.descriptionBTCOnly"));
Coin makerFeeAsCoin = dataModel.getMakerFee();
if (makerFeeAsCoin == null) {
return;
}
isTradeFeeVisible.setValue(true);
tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin));
tradeFeeInBtcWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getMakerFeeInBtc(),
true,
btcFormatter));
tradeFeeInBsqWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getMakerFeeInBsq(),
false,
bsqFormatter));
}
private void updateMarketPriceAvailable() {
marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get());
marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1);
@ -985,15 +960,23 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit());
}
public String getTradeFee() {
//TODO use last bisq market price to estimate BSQ val
final Coin makerFeeAsCoin = dataModel.getMakerFee();
final String makerFee = getFormatterForMakerFee().formatCoinWithCode(makerFeeAsCoin);
if (dataModel.isCurrencyForMakerFeeBtc())
return makerFee + GUIUtil.getPercentageOfTradeAmount(makerFeeAsCoin, dataModel.getAmount().get(),
if (dataModel.isCurrencyForMakerFeeBtc()) {
return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getMakerFeeInBtc(),
dataModel.getAmount().get(),
true,
btcFormatter,
FeeService.getMinMakerFee(dataModel.isCurrencyForMakerFeeBtc()));
else
return makerFee + " (" + Res.get("shared.tradingFeeInBsqInfo", btcFormatter.formatCoinWithCode(makerFeeAsCoin)) + ")";
} else {
// For BSQ we use the fiat equivalent only. Calculating the % value would require to
// calculate the BTC value of the BSQ fee and use that...
return FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getMakerFeeInBsq(),
false,
bsqFormatter);
}
}
public String getMakerFeePercentage() {
@ -1025,10 +1008,13 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
public String getTxFee() {
Coin txFeeAsCoin = dataModel.getTxFee();
return btcFormatter.formatCoinWithCode(txFeeAsCoin) +
GUIUtil.getPercentageOfTradeAmount(txFeeAsCoin, dataModel.getAmount().get(), Coin.ZERO);
return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getTxFee(),
dataModel.getAmount().get(),
true,
btcFormatter,
Coin.ZERO
);
}
public String getTxFeePercentage() {

View File

@ -87,7 +87,7 @@ class TakeOfferDataModel extends OfferDataModel {
private final User user;
private final FeeService feeService;
private final FilterManager filterManager;
private final Preferences preferences;
final Preferences preferences;
private final TxFeeEstimationService txFeeEstimationService;
private final PriceFeedService priceFeedService;
private final AccountAgeWitnessService accountAgeWitnessService;

View File

@ -396,21 +396,12 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
this.closeHandler = closeHandler;
}
// called form parent as the view does not get notified when the tab is closed
// Called from parent as the view does not get notified when the tab is closed
public void onClose() {
Coin balance = model.dataModel.getBalance().get();
if (balance != null && balance.isPositive() && !model.takeOfferCompleted.get() && !DevEnv.isDevMode()) {
model.dataModel.swapTradeToSavings();
new Popup().information(Res.get("takeOffer.alreadyFunded.movedFunds"))
.actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal")
.onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class))
.show();
}
// TODO need other implementation as it is displayed also if there are old funds in the wallet
/*
if (model.dataModel.getIsWalletFunded().get())
new Popup<>().warning("You have already funds paid in.\nIn the <Funds/Open for withdrawal> section you can withdraw those funds.").show();*/
}
public void onTabSelected(boolean isSelected) {
@ -1034,14 +1025,17 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
fundingHBox.getChildren().add(cancelButton2);
cancelButton2.setOnAction(e -> {
if (model.dataModel.getIsBtcWalletFunded().get()) {
new Popup().warning(Res.get("takeOffer.alreadyFunded.askCancel"))
String key = "CreateOfferCancelAndFunded";
if (model.dataModel.getIsBtcWalletFunded().get() &&
model.dataModel.preferences.showAgain(key)) {
new Popup().backgroundInfo(Res.get("takeOffer.alreadyFunded.askCancel"))
.closeButtonText(Res.get("shared.no"))
.actionButtonText(Res.get("shared.yesCancel"))
.onAction(() -> {
model.dataModel.swapTradeToSavings();
close();
close(false);
})
.dontShowAgainId(key)
.show();
} else {
close(false);

View File

@ -23,6 +23,7 @@ import bisq.desktop.common.model.ViewModel;
import bisq.desktop.main.MainView;
import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.deposit.DepositView;
import bisq.desktop.main.offer.FeeUtil;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
@ -33,7 +34,6 @@ import bisq.core.btc.wallet.Restrictions;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferRestrictions;
@ -76,8 +76,6 @@ import javafx.collections.ObservableList;
import javafx.util.Callback;
import java.util.Optional;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@ -279,50 +277,23 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
private void applyTakerFee() {
Coin takerFeeAsCoin = dataModel.getTakerFee();
if (takerFeeAsCoin != null) {
isTradeFeeVisible.setValue(true);
tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin));
Coin makerFeeInBtc = dataModel.getTakerFeeInBtc();
Optional<Volume> optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc,
true,
bsqFormatter);
String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount);
} else {
tradeFeeInBtcWithFiat.set(btcFormatter.formatCoinWithCode(takerFeeAsCoin));
}
Coin makerFeeInBsq = dataModel.getTakerFeeInBsq();
Optional<Volume> optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq,
false,
bsqFormatter);
String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, optionalBsqFeeInFiat, bsqFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount);
} else {
// Before DAO is enabled we show fee as fiat and % in second line
String feeInFiatAsString;
if (optionalBtcFeeInFiat != null && optionalBtcFeeInFiat.isPresent()) {
feeInFiatAsString = DisplayUtils.formatVolumeWithCode(optionalBtcFeeInFiat.get());
} else {
feeInFiatAsString = Res.get("shared.na");
}
double amountAsLong = (double) dataModel.getAmount().get().value;
double makerFeeInBtcAsLong = (double) makerFeeInBtc.value;
double percent = makerFeeInBtcAsLong / amountAsLong;
tradeFeeInBsqWithFiat.set(Res.get("createOffer.tradeFee.fiatAndPercent",
feeInFiatAsString,
FormattingUtils.formatToPercentWithSymbol(percent)));
}
}
tradeFeeDescription.set(DevEnv.isDaoActivated() ? Res.get("createOffer.tradeFee.descriptionBSQEnabled") :
Res.get("createOffer.tradeFee.descriptionBTCOnly"));
Coin takerFeeAsCoin = dataModel.getTakerFee();
if (takerFeeAsCoin == null) {
return;
}
isTradeFeeVisible.setValue(true);
tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin));
tradeFeeInBtcWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getTakerFeeInBtc(),
true,
btcFormatter));
tradeFeeInBsqWithFiat.set(FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getTakerFeeInBsq(),
false,
bsqFormatter));
}
@ -706,14 +677,21 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
public String getTradeFee() {
//TODO use last bisq market price to estimate BSQ val
final Coin takerFeeAsCoin = dataModel.getTakerFee();
final String takerFee = getFormatterForTakerFee().formatCoinWithCode(takerFeeAsCoin);
if (dataModel.isCurrencyForTakerFeeBtc())
return takerFee + GUIUtil.getPercentageOfTradeAmount(takerFeeAsCoin, dataModel.getAmount().get(),
FeeService.getMinTakerFee(dataModel.isCurrencyForTakerFeeBtc()));
else
return takerFee + " (" + Res.get("shared.tradingFeeInBsqInfo", btcFormatter.formatCoinWithCode(takerFeeAsCoin)) + ")";
if (dataModel.isCurrencyForTakerFeeBtc()) {
return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getTakerFeeInBtc(),
dataModel.getAmount().get(),
true,
btcFormatter,
FeeService.getMinMakerFee(dataModel.isCurrencyForTakerFeeBtc()));
} else {
// For BSQ we use the fiat equivalent only. Calculating the % value would require to
// calculate the BTC value of the BSQ fee and use that...
return FeeUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getTakerFeeInBsq(),
false,
bsqFormatter);
}
}
public String getTakerFeePercentage() {
@ -733,11 +711,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
public String getTxFee() {
Coin txFeeAsCoin = dataModel.getTotalTxFee();
return btcFormatter.formatCoinWithCode(txFeeAsCoin) +
GUIUtil.getPercentageOfTradeAmount(txFeeAsCoin, dataModel.getAmount().get(),
Coin.ZERO);
return FeeUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getTotalTxFee(),
dataModel.getAmount().get(),
true,
btcFormatter,
Coin.ZERO
);
}
public String getTxFeePercentage() {

View File

@ -126,7 +126,7 @@ public class DisplayUtils {
return formatVolume(volume, FIAT_VOLUME_FORMAT, true);
}
static String formatAverageVolumeWithCode(Volume volume) {
public static String formatAverageVolumeWithCode(Volume volume) {
return formatVolume(volume, FIAT_VOLUME_FORMAT.minDecimals(2), true);
}
@ -257,16 +257,16 @@ public class DisplayUtils {
public static String getFeeWithFiatAmount(Coin makerFeeAsCoin,
Optional<Volume> optionalFeeInFiat,
CoinFormatter formatter) {
String fee = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na");
String feeInFiatAsString;
String feeInBtc = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na");
if (optionalFeeInFiat != null && optionalFeeInFiat.isPresent()) {
feeInFiatAsString = formatAverageVolumeWithCode(optionalFeeInFiat.get());
String feeInFiat = formatAverageVolumeWithCode(optionalFeeInFiat.get());
return Res.get("feeOptionWindow.fee", feeInBtc, feeInFiat);
} else {
feeInFiatAsString = Res.get("shared.na");
return feeInBtc;
}
return Res.get("feeOptionWindow.fee", fee, feeInFiatAsString);
}
/**
* Converts to a coin with max. 4 decimal places. Last place gets rounded.
* <p>0.01234 -> 0.0123

View File

@ -255,6 +255,7 @@ message KeepFundsReply {
message WithdrawFundsRequest {
string tradeId = 1;
string address = 2;
string memo = 3;
}
message WithdrawFundsReply {
@ -287,6 +288,27 @@ message TradeInfo {
string contractAsJson = 24;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Transactions
///////////////////////////////////////////////////////////////////////////////////////////
message TxFeeRateInfo {
bool useCustomTxFeeRate = 1;
uint64 customTxFeeRate = 2;
uint64 feeServiceRate = 3;
uint64 lastFeeServiceRequestTs = 4;
}
message TxInfo {
string txId = 1;
uint64 inputSum = 2;
uint64 outputSum = 3;
uint64 fee = 4;
int32 size = 5;
bool isPending = 6;
string memo = 7;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Wallets
///////////////////////////////////////////////////////////////////////////////////////////
@ -300,11 +322,15 @@ service Wallets {
}
rpc SendBsq (SendBsqRequest) returns (SendBsqReply) {
}
rpc SendBtc (SendBtcRequest) returns (SendBtcReply) {
}
rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) {
}
rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) {
}
rpc unsetTxFeeRatePreference (UnsetTxFeeRatePreferenceRequest) returns (UnsetTxFeeRatePreferenceReply) {
rpc UnsetTxFeeRatePreference (UnsetTxFeeRatePreferenceRequest) returns (UnsetTxFeeRatePreferenceReply) {
}
rpc GetTransaction (GetTransactionRequest) returns (GetTransactionReply) {
}
rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) {
}
@ -344,9 +370,22 @@ message GetUnusedBsqAddressReply {
message SendBsqRequest {
string address = 1;
string amount = 2;
string txFeeRate = 3;
}
message SendBsqReply {
TxInfo txInfo = 1;
}
message SendBtcRequest {
string address = 1;
string amount = 2;
string txFeeRate = 3;
string memo = 4;
}
message SendBtcReply {
TxInfo txInfo = 1;
}
message GetTxFeeRateRequest {
@ -371,6 +410,14 @@ message UnsetTxFeeRatePreferenceReply {
TxFeeRateInfo txFeeRateInfo = 1;
}
message GetTransactionRequest {
string txId = 1;
}
message GetTransactionReply {
TxInfo txInfo = 1;
}
message GetFundingAddressesRequest {
}
@ -437,13 +484,6 @@ message AddressBalanceInfo {
int64 numConfirmations = 3;
}
message TxFeeRateInfo {
bool useCustomTxFeeRate = 1;
uint64 customTxFeeRate = 2;
uint64 feeServiceRate = 3;
uint64 lastFeeServiceRequestTs = 4;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Version
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -20,6 +20,7 @@ package bisq.seednode;
import bisq.core.app.TorSetup;
import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.app.misc.ModuleForAppWithP2p;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.P2PServiceListener;
@ -30,6 +31,7 @@ import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.app.DevEnv;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
@ -98,6 +100,11 @@ public class SeedNodeMain extends ExecutableForAppWithP2p {
super.applyInjector();
seedNode.setInjector(injector);
if (DevEnv.isDaoActivated()) {
injector.getInstance(DaoStateSnapshotService.class).setDaoRequiresRestartHandler(() -> gracefulShutDown(() -> {
}));
}
}
@Override