mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-24 07:07:43 +01:00
Merge pull request #4966 from ghubstan/10-callrate-interceptor
Prevent excessive api calls
This commit is contained in:
commit
7d7f1b09e7
41 changed files with 2026 additions and 323 deletions
|
@ -154,7 +154,8 @@ public class Scaffold {
|
||||||
try {
|
try {
|
||||||
log.info("Shutting down executor service ...");
|
log.info("Shutting down executor service ...");
|
||||||
executor.shutdownNow();
|
executor.shutdownNow();
|
||||||
executor.awaitTermination(config.supportingApps.size() * 2000, MILLISECONDS);
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
executor.awaitTermination(config.supportingApps.size() * 2000L, MILLISECONDS);
|
||||||
|
|
||||||
SetupTask[] orderedTasks = new SetupTask[]{
|
SetupTask[] orderedTasks = new SetupTask[]{
|
||||||
bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask};
|
bobNodeTask, aliceNodeTask, arbNodeTask, seedNodeTask, bitcoindTask};
|
||||||
|
@ -218,20 +219,25 @@ public class Scaffold {
|
||||||
if (copyBitcoinRegtestDir.run().getExitStatus() != 0)
|
if (copyBitcoinRegtestDir.run().getExitStatus() != 0)
|
||||||
throw new IllegalStateException("Could not install bitcoin regtest dir");
|
throw new IllegalStateException("Could not install bitcoin regtest dir");
|
||||||
|
|
||||||
|
String aliceDataDir = daoSetupDir + "/" + alicedaemon.appName;
|
||||||
BashCommand copyAliceDataDir = new BashCommand(
|
BashCommand copyAliceDataDir = new BashCommand(
|
||||||
"cp -rf " + daoSetupDir + "/" + alicedaemon.appName
|
"cp -rf " + aliceDataDir + " " + config.rootAppDataDir);
|
||||||
+ " " + config.rootAppDataDir);
|
|
||||||
if (copyAliceDataDir.run().getExitStatus() != 0)
|
if (copyAliceDataDir.run().getExitStatus() != 0)
|
||||||
throw new IllegalStateException("Could not install alice data dir");
|
throw new IllegalStateException("Could not install alice data dir");
|
||||||
|
|
||||||
|
String bobDataDir = daoSetupDir + "/" + bobdaemon.appName;
|
||||||
BashCommand copyBobDataDir = new BashCommand(
|
BashCommand copyBobDataDir = new BashCommand(
|
||||||
"cp -rf " + daoSetupDir + "/" + bobdaemon.appName
|
"cp -rf " + bobDataDir + " " + config.rootAppDataDir);
|
||||||
+ " " + config.rootAppDataDir);
|
|
||||||
if (copyBobDataDir.run().getExitStatus() != 0)
|
if (copyBobDataDir.run().getExitStatus() != 0)
|
||||||
throw new IllegalStateException("Could not install bob data dir");
|
throw new IllegalStateException("Could not install bob data dir");
|
||||||
|
|
||||||
log.info("Installed dao-setup files into {}", buildDataDir);
|
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
|
// 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
|
// 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
|
// 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() {
|
private void installShutdownHook() {
|
||||||
// Background apps can be left running until the jvm is manually shutdown,
|
// Background apps can be left running until the jvm is manually shutdown,
|
||||||
// so we add a shutdown hook for that use case.
|
// so we add a shutdown hook for that use case.
|
||||||
|
|
|
@ -71,6 +71,7 @@ public class ApiTestConfig {
|
||||||
static final String SKIP_TESTS = "skipTests";
|
static final String SKIP_TESTS = "skipTests";
|
||||||
static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests";
|
static final String SHUTDOWN_AFTER_TESTS = "shutdownAfterTests";
|
||||||
static final String SUPPORTING_APPS = "supportingApps";
|
static final String SUPPORTING_APPS = "supportingApps";
|
||||||
|
static final String CALL_RATE_METERING_CONFIG_PATH = "callRateMeteringConfigPath";
|
||||||
static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging";
|
static final String ENABLE_BISQ_DEBUGGING = "enableBisqDebugging";
|
||||||
|
|
||||||
// Default values for certain options
|
// Default values for certain options
|
||||||
|
@ -102,6 +103,7 @@ public class ApiTestConfig {
|
||||||
public final boolean skipTests;
|
public final boolean skipTests;
|
||||||
public final boolean shutdownAfterTests;
|
public final boolean shutdownAfterTests;
|
||||||
public final List<String> supportingApps;
|
public final List<String> supportingApps;
|
||||||
|
public final String callRateMeteringConfigPath;
|
||||||
public final boolean enableBisqDebugging;
|
public final boolean enableBisqDebugging;
|
||||||
|
|
||||||
// Immutable system configurations set in the constructor.
|
// Immutable system configurations set in the constructor.
|
||||||
|
@ -228,6 +230,12 @@ public class ApiTestConfig {
|
||||||
.ofType(String.class)
|
.ofType(String.class)
|
||||||
.defaultsTo("bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon");
|
.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 =
|
ArgumentAcceptingOptionSpec<Boolean> enableBisqDebuggingOpt =
|
||||||
parser.accepts(ENABLE_BISQ_DEBUGGING,
|
parser.accepts(ENABLE_BISQ_DEBUGGING,
|
||||||
"Start Bisq apps with remote debug options")
|
"Start Bisq apps with remote debug options")
|
||||||
|
@ -289,6 +297,7 @@ public class ApiTestConfig {
|
||||||
this.skipTests = options.valueOf(skipTestsOpt);
|
this.skipTests = options.valueOf(skipTestsOpt);
|
||||||
this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt);
|
this.shutdownAfterTests = options.valueOf(shutdownAfterTestsOpt);
|
||||||
this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(","));
|
this.supportingApps = asList(options.valueOf(supportingAppsOpt).split(","));
|
||||||
|
this.callRateMeteringConfigPath = options.valueOf(callRateMeteringConfigPathOpt);
|
||||||
this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt);
|
this.enableBisqDebugging = options.valueOf(enableBisqDebuggingOpt);
|
||||||
|
|
||||||
// Assign values to special-case static fields.
|
// Assign values to special-case static fields.
|
||||||
|
|
|
@ -19,6 +19,7 @@ package bisq.apitest;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -72,6 +73,16 @@ public class ApiTestCase {
|
||||||
// gRPC service stubs are used by method & scenario tests, but not e2e tests.
|
// gRPC service stubs are used by method & scenario tests, but not e2e tests.
|
||||||
private static final Map<BisqAppConfig, GrpcStubs> grpcStubsCache = new HashMap<>();
|
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)
|
public static void setUpScaffold(Enum<?>... supportingApps)
|
||||||
throws InterruptedException, ExecutionException, IOException {
|
throws InterruptedException, ExecutionException, IOException {
|
||||||
scaffold = new Scaffold(stream(supportingApps).map(Enum::name)
|
scaffold = new Scaffold(stream(supportingApps).map(Enum::name)
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.apitest.method;
|
||||||
|
|
||||||
|
import io.grpc.StatusRuntimeException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Disabled;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
|
||||||
|
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
|
||||||
|
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
|
||||||
|
import static java.util.concurrent.TimeUnit.DAYS;
|
||||||
|
import static java.util.concurrent.TimeUnit.HOURS;
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.daemon.grpc.GrpcVersionService;
|
||||||
|
import bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig;
|
||||||
|
|
||||||
|
@Disabled
|
||||||
|
@Slf4j
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
public class CallRateMeteringInterceptorTest extends MethodTest {
|
||||||
|
|
||||||
|
private static final GetVersionTest getVersionTest = new GetVersionTest();
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void setUp() {
|
||||||
|
File callRateMeteringConfigFile = buildInterceptorConfigFile();
|
||||||
|
startSupportingApps(callRateMeteringConfigFile,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
bitcoind, alicedaemon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void sleep200Milliseconds() {
|
||||||
|
sleep(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
public void testGetVersionCall1IsAllowed() {
|
||||||
|
getVersionTest.testGetVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
public void testGetVersionCall2ShouldThrowException() {
|
||||||
|
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
|
||||||
|
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
|
||||||
|
exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
public void testGetVersionCall3ShouldThrowException() {
|
||||||
|
Throwable exception = assertThrows(StatusRuntimeException.class, getVersionTest::testGetVersion);
|
||||||
|
assertEquals("PERMISSION_DENIED: the maximum allowed number of getversion calls (1/second) has been exceeded",
|
||||||
|
exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(4)
|
||||||
|
public void testGetVersionCall4IsAllowed() {
|
||||||
|
sleep(1100); // Let the server's rate meter reset the call count.
|
||||||
|
getVersionTest.testGetVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
public static void tearDown() {
|
||||||
|
tearDownScaffold();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File buildInterceptorConfigFile() {
|
||||||
|
GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
|
||||||
|
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
|
||||||
|
"getVersion",
|
||||||
|
1,
|
||||||
|
SECONDS);
|
||||||
|
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
|
||||||
|
"shouldNotBreakAnything",
|
||||||
|
1000,
|
||||||
|
DAYS);
|
||||||
|
// Only GrpcVersionService is @VisibleForTesting, so we hardcode the class names.
|
||||||
|
builder.addCallRateMeter("GrpcOffersService",
|
||||||
|
"createOffer",
|
||||||
|
5,
|
||||||
|
MINUTES);
|
||||||
|
builder.addCallRateMeter("GrpcOffersService",
|
||||||
|
"takeOffer",
|
||||||
|
10,
|
||||||
|
DAYS);
|
||||||
|
builder.addCallRateMeter("GrpcTradesService",
|
||||||
|
"withdrawFunds",
|
||||||
|
3,
|
||||||
|
HOURS);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ import bisq.proto.grpc.GetPaymentAccountFormRequest;
|
||||||
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
||||||
import bisq.proto.grpc.GetPaymentMethodsRequest;
|
import bisq.proto.grpc.GetPaymentMethodsRequest;
|
||||||
import bisq.proto.grpc.GetTradeRequest;
|
import bisq.proto.grpc.GetTradeRequest;
|
||||||
|
import bisq.proto.grpc.GetTransactionRequest;
|
||||||
import bisq.proto.grpc.GetTxFeeRateRequest;
|
import bisq.proto.grpc.GetTxFeeRateRequest;
|
||||||
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
|
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
|
||||||
import bisq.proto.grpc.KeepFundsRequest;
|
import bisq.proto.grpc.KeepFundsRequest;
|
||||||
|
@ -48,10 +49,12 @@ import bisq.proto.grpc.OfferInfo;
|
||||||
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
||||||
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
||||||
import bisq.proto.grpc.SendBsqRequest;
|
import bisq.proto.grpc.SendBsqRequest;
|
||||||
|
import bisq.proto.grpc.SendBtcRequest;
|
||||||
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
|
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
|
||||||
import bisq.proto.grpc.SetWalletPasswordRequest;
|
import bisq.proto.grpc.SetWalletPasswordRequest;
|
||||||
import bisq.proto.grpc.TakeOfferRequest;
|
import bisq.proto.grpc.TakeOfferRequest;
|
||||||
import bisq.proto.grpc.TradeInfo;
|
import bisq.proto.grpc.TradeInfo;
|
||||||
|
import bisq.proto.grpc.TxInfo;
|
||||||
import bisq.proto.grpc.UnlockWalletRequest;
|
import bisq.proto.grpc.UnlockWalletRequest;
|
||||||
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
||||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||||
|
@ -64,6 +67,7 @@ import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
|
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 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,
|
public static void startSupportingApps(boolean registerDisputeAgents,
|
||||||
boolean generateBtcBlock,
|
boolean generateBtcBlock,
|
||||||
Enum<?>... supportingApps) {
|
Enum<?>... supportingApps) {
|
||||||
try {
|
try {
|
||||||
// To run Bisq apps in debug mode, use the other setUpScaffold method:
|
setUpScaffold(new String[]{
|
||||||
// setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,arbdaemon,alicedaemon,bobdaemon",
|
"--supportingApps", toNameList.apply(supportingApps),
|
||||||
// "--enableBisqDebugging", "true"});
|
"--enableBisqDebugging", "false"
|
||||||
setUpScaffold(supportingApps);
|
});
|
||||||
|
doPostStartup(registerDisputeAgents, generateBtcBlock, supportingApps);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
fail(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void doPostStartup(boolean registerDisputeAgents,
|
||||||
|
boolean generateBtcBlock,
|
||||||
|
Enum<?>... supportingApps) {
|
||||||
if (registerDisputeAgents) {
|
if (registerDisputeAgents) {
|
||||||
registerDisputeAgents(arbdaemon);
|
registerDisputeAgents(arbdaemon);
|
||||||
}
|
}
|
||||||
|
@ -122,9 +154,6 @@ public class MethodTest extends ApiTestCase {
|
||||||
// show 10 BTC balance, and allow time for daemons parse the new block.
|
// show 10 BTC balance, and allow time for daemons parse the new block.
|
||||||
if (generateBtcBlock)
|
if (generateBtcBlock)
|
||||||
genBtcBlocksThenWait(1, 1500);
|
genBtcBlocksThenWait(1, 1500);
|
||||||
} catch (Exception ex) {
|
|
||||||
fail(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience methods for building gRPC request objects
|
// Convenience methods for building gRPC request objects
|
||||||
|
@ -160,8 +189,26 @@ public class MethodTest extends ApiTestCase {
|
||||||
return GetUnusedBsqAddressRequest.newBuilder().build();
|
return GetUnusedBsqAddressRequest.newBuilder().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final SendBsqRequest createSendBsqRequest(String address, String amount) {
|
protected final SendBsqRequest createSendBsqRequest(String address,
|
||||||
return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build();
|
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() {
|
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
|
||||||
|
@ -208,10 +255,13 @@ public class MethodTest extends ApiTestCase {
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, String address) {
|
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId,
|
||||||
|
String address,
|
||||||
|
String memo) {
|
||||||
return WithdrawFundsRequest.newBuilder()
|
return WithdrawFundsRequest.newBuilder()
|
||||||
.setTradeId(tradeId)
|
.setTradeId(tradeId)
|
||||||
.setAddress(address)
|
.setAddress(address)
|
||||||
|
.setMemo(memo)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,9 +297,36 @@ public class MethodTest extends ApiTestCase {
|
||||||
return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress();
|
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
|
//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) {
|
protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) {
|
||||||
|
@ -354,8 +431,11 @@ public class MethodTest extends ApiTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||||
protected final void withdrawFunds(BisqAppConfig bisqAppConfig, String tradeId, String address) {
|
protected final void withdrawFunds(BisqAppConfig bisqAppConfig,
|
||||||
var req = createWithdrawFundsRequest(tradeId, address);
|
String tradeId,
|
||||||
|
String address,
|
||||||
|
String memo) {
|
||||||
|
var req = createWithdrawFundsRequest(tradeId, address, memo);
|
||||||
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
|
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,6 +459,11 @@ public class MethodTest extends ApiTestCase {
|
||||||
grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo());
|
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.
|
// Static conveniences for test methods and test case fixture setups.
|
||||||
|
|
||||||
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {
|
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {
|
||||||
|
|
|
@ -64,9 +64,11 @@ public class AbstractTradeTest extends AbstractOfferTest {
|
||||||
TestInfo testInfo,
|
TestInfo testInfo,
|
||||||
String description,
|
String description,
|
||||||
TradeInfo trade) {
|
TradeInfo trade) {
|
||||||
log.info(String.format("%s %s%n%s",
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug(String.format("%s %s%n%s",
|
||||||
testName(testInfo),
|
testName(testInfo),
|
||||||
description.toUpperCase(),
|
description.toUpperCase(),
|
||||||
format(trade)));
|
format(trade)));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
|
||||||
// Maker and Taker fees are in BTC.
|
// Maker and Taker fees are in BTC.
|
||||||
private static final String TRADE_FEE_CURRENCY_CODE = "btc";
|
private static final String TRADE_FEE_CURRENCY_CODE = "btc";
|
||||||
|
|
||||||
|
private static final String WITHDRAWAL_TX_MEMO = "Bob's trade withdrawal";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(1)
|
@Order(1)
|
||||||
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
|
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);
|
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
|
||||||
|
|
||||||
String toAddress = bitcoinCli.getNewBtcAddress();
|
String toAddress = bitcoinCli.getNewBtcAddress();
|
||||||
withdrawFunds(bobdaemon, tradeId, toAddress);
|
withdrawFunds(bobdaemon, tradeId, toAddress, WITHDRAWAL_TX_MEMO);
|
||||||
|
|
||||||
genBtcBlocksThenWait(1, 2250);
|
genBtcBlocksThenWait(1, 2250);
|
||||||
|
|
||||||
|
@ -158,7 +160,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
|
||||||
verifyExpectedProtocolStatus(trade);
|
verifyExpectedProtocolStatus(trade);
|
||||||
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
|
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
|
||||||
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
|
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
|
||||||
log.info("{} Bob's current available balance: {} BTC",
|
log.debug("{} Bob's current available balance: {} BTC",
|
||||||
testName(testInfo),
|
testName(testInfo),
|
||||||
formatSatoshis(currentBalance.getAvailableBalance()));
|
formatSatoshis(currentBalance.getAvailableBalance()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ public class BsqWalletTest extends MethodTest {
|
||||||
@Order(3)
|
@Order(3)
|
||||||
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
|
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
|
||||||
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
|
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
|
||||||
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT);
|
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT, "100");
|
||||||
sleep(2000);
|
sleep(2000);
|
||||||
|
|
||||||
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
|
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package bisq.apitest.method.wallet;
|
package bisq.apitest.method.wallet;
|
||||||
|
|
||||||
import bisq.proto.grpc.BtcBalanceInfo;
|
import bisq.proto.grpc.BtcBalanceInfo;
|
||||||
|
import bisq.proto.grpc.TxInfo;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@ import static bisq.cli.TableFormat.formatAddressBalanceTbl;
|
||||||
import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl;
|
import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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;
|
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,6 +34,8 @@ import bisq.apitest.method.MethodTest;
|
||||||
@TestMethodOrder(OrderAnnotation.class)
|
@TestMethodOrder(OrderAnnotation.class)
|
||||||
public class BtcWalletTest extends MethodTest {
|
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
|
// All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets
|
||||||
// are initialized with 10 BTC during the scaffolding setup.
|
// are initialized with 10 BTC during the scaffolding setup.
|
||||||
private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
|
private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
|
||||||
|
@ -92,6 +97,50 @@ public class BtcWalletTest extends MethodTest {
|
||||||
formatBtcBalanceInfoTbl(btcBalanceInfo));
|
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
|
@AfterAll
|
||||||
public static void tearDown() {
|
public static void tearDown() {
|
||||||
tearDownScaffold();
|
tearDownScaffold();
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package bisq.apitest.scenario;
|
package bisq.apitest.scenario;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterAll;
|
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.alicedaemon;
|
||||||
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
|
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
|
||||||
import static bisq.apitest.config.BisqAppConfig.seednode;
|
import static bisq.apitest.config.BisqAppConfig.seednode;
|
||||||
|
import static bisq.apitest.method.CallRateMeteringInterceptorTest.buildInterceptorConfigFile;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.apitest.method.CallRateMeteringInterceptorTest;
|
||||||
import bisq.apitest.method.GetVersionTest;
|
import bisq.apitest.method.GetVersionTest;
|
||||||
import bisq.apitest.method.MethodTest;
|
import bisq.apitest.method.MethodTest;
|
||||||
import bisq.apitest.method.RegisterDisputeAgentsTest;
|
import bisq.apitest.method.RegisterDisputeAgentsTest;
|
||||||
|
@ -46,7 +50,11 @@ public class StartupTest extends MethodTest {
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
public static void setUp() {
|
public static void setUp() {
|
||||||
try {
|
try {
|
||||||
setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon);
|
File callRateMeteringConfigFile = buildInterceptorConfigFile();
|
||||||
|
startSupportingApps(callRateMeteringConfigFile,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
bitcoind, seednode, arbdaemon, alicedaemon);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
fail(ex);
|
fail(ex);
|
||||||
}
|
}
|
||||||
|
@ -54,13 +62,27 @@ public class StartupTest extends MethodTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(1)
|
@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() {
|
public void testGetVersion() {
|
||||||
GetVersionTest test = new GetVersionTest();
|
GetVersionTest test = new GetVersionTest();
|
||||||
test.testGetVersion();
|
test.testGetVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(2)
|
@Order(3)
|
||||||
public void testRegisterDisputeAgents() {
|
public void testRegisterDisputeAgents() {
|
||||||
RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest();
|
RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest();
|
||||||
test.testRegisterArbitratorShouldThrowException();
|
test.testRegisterArbitratorShouldThrowException();
|
||||||
|
|
|
@ -67,6 +67,7 @@ public class WalletTest extends MethodTest {
|
||||||
|
|
||||||
btcWalletTest.testInitialBtcBalances(testInfo);
|
btcWalletTest.testInitialBtcBalances(testInfo);
|
||||||
btcWalletTest.testFundAlicesBtcWallet(testInfo);
|
btcWalletTest.testFundAlicesBtcWallet(testInfo);
|
||||||
|
btcWalletTest.testAliceSendBTCToBob(testInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -592,6 +592,12 @@ configure(project(':daemon')) {
|
||||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||||
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
|
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
|
||||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
|
testCompile "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
|
||||||
|
testCompile "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
|
||||||
|
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
testRuntime("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ import bisq.proto.grpc.GetPaymentAccountFormRequest;
|
||||||
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
||||||
import bisq.proto.grpc.GetPaymentMethodsRequest;
|
import bisq.proto.grpc.GetPaymentMethodsRequest;
|
||||||
import bisq.proto.grpc.GetTradeRequest;
|
import bisq.proto.grpc.GetTradeRequest;
|
||||||
|
import bisq.proto.grpc.GetTransactionRequest;
|
||||||
import bisq.proto.grpc.GetTxFeeRateRequest;
|
import bisq.proto.grpc.GetTxFeeRateRequest;
|
||||||
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
|
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
|
||||||
import bisq.proto.grpc.GetVersionRequest;
|
import bisq.proto.grpc.GetVersionRequest;
|
||||||
|
@ -40,9 +41,11 @@ import bisq.proto.grpc.OfferInfo;
|
||||||
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
||||||
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
||||||
import bisq.proto.grpc.SendBsqRequest;
|
import bisq.proto.grpc.SendBsqRequest;
|
||||||
|
import bisq.proto.grpc.SendBtcRequest;
|
||||||
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
|
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
|
||||||
import bisq.proto.grpc.SetWalletPasswordRequest;
|
import bisq.proto.grpc.SetWalletPasswordRequest;
|
||||||
import bisq.proto.grpc.TakeOfferRequest;
|
import bisq.proto.grpc.TakeOfferRequest;
|
||||||
|
import bisq.proto.grpc.TxInfo;
|
||||||
import bisq.proto.grpc.UnlockWalletRequest;
|
import bisq.proto.grpc.UnlockWalletRequest;
|
||||||
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
||||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||||
|
@ -110,9 +113,11 @@ public class CliMain {
|
||||||
getfundingaddresses,
|
getfundingaddresses,
|
||||||
getunusedbsqaddress,
|
getunusedbsqaddress,
|
||||||
sendbsq,
|
sendbsq,
|
||||||
|
sendbtc,
|
||||||
gettxfeerate,
|
gettxfeerate,
|
||||||
settxfeerate,
|
settxfeerate,
|
||||||
unsettxfeerate,
|
unsettxfeerate,
|
||||||
|
gettransaction,
|
||||||
lockwallet,
|
lockwallet,
|
||||||
unlockwallet,
|
unlockwallet,
|
||||||
removewalletpassword,
|
removewalletpassword,
|
||||||
|
@ -259,19 +264,56 @@ public class CliMain {
|
||||||
throw new IllegalArgumentException("no bsq amount specified");
|
throw new IllegalArgumentException("no bsq amount specified");
|
||||||
|
|
||||||
var amount = nonOptionArgs.get(2);
|
var amount = nonOptionArgs.get(2);
|
||||||
|
verifyStringIsValidDecimal(amount);
|
||||||
|
|
||||||
try {
|
var txFeeRate = nonOptionArgs.size() == 4 ? nonOptionArgs.get(3) : "";
|
||||||
Double.parseDouble(amount);
|
if (!txFeeRate.isEmpty())
|
||||||
} catch (NumberFormatException e) {
|
verifyStringIsValidLong(txFeeRate);
|
||||||
throw new IllegalArgumentException(format("'%s' is not a number", amount));
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = SendBsqRequest.newBuilder()
|
var request = SendBsqRequest.newBuilder()
|
||||||
.setAddress(address)
|
.setAddress(address)
|
||||||
.setAmount(amount)
|
.setAmount(amount)
|
||||||
|
.setTxFeeRate(txFeeRate)
|
||||||
.build();
|
.build();
|
||||||
walletsService.sendBsq(request);
|
var reply = walletsService.sendBsq(request);
|
||||||
out.printf("%s BSQ sent to %s%n", amount, address);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
case gettxfeerate: {
|
case gettxfeerate: {
|
||||||
|
@ -284,13 +326,7 @@ public class CliMain {
|
||||||
if (nonOptionArgs.size() < 2)
|
if (nonOptionArgs.size() < 2)
|
||||||
throw new IllegalArgumentException("no tx fee rate specified");
|
throw new IllegalArgumentException("no tx fee rate specified");
|
||||||
|
|
||||||
long txFeeRate;
|
var txFeeRate = toLong(nonOptionArgs.get(2));
|
||||||
try {
|
|
||||||
txFeeRate = Long.parseLong(nonOptionArgs.get(2));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = SetTxFeeRatePreferenceRequest.newBuilder()
|
var request = SetTxFeeRatePreferenceRequest.newBuilder()
|
||||||
.setTxFeeRatePreference(txFeeRate)
|
.setTxFeeRatePreference(txFeeRate)
|
||||||
.build();
|
.build();
|
||||||
|
@ -304,6 +340,18 @@ public class CliMain {
|
||||||
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
|
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
|
||||||
return;
|
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: {
|
case createoffer: {
|
||||||
if (nonOptionArgs.size() < 9)
|
if (nonOptionArgs.size() < 9)
|
||||||
throw new IllegalArgumentException("incorrect parameter count,"
|
throw new IllegalArgumentException("incorrect parameter count,"
|
||||||
|
@ -413,7 +461,7 @@ public class CliMain {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case gettrade: {
|
case gettrade: {
|
||||||
// TODO make short-id a valid argument
|
// TODO make short-id a valid argument?
|
||||||
if (nonOptionArgs.size() < 2)
|
if (nonOptionArgs.size() < 2)
|
||||||
throw new IllegalArgumentException("incorrect parameter count, "
|
throw new IllegalArgumentException("incorrect parameter count, "
|
||||||
+ " expecting trade id [,showcontract = true|false]");
|
+ " expecting trade id [,showcontract = true|false]");
|
||||||
|
@ -472,16 +520,21 @@ public class CliMain {
|
||||||
case withdrawfunds: {
|
case withdrawfunds: {
|
||||||
if (nonOptionArgs.size() < 3)
|
if (nonOptionArgs.size() < 3)
|
||||||
throw new IllegalArgumentException("incorrect parameter count, "
|
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 tradeId = nonOptionArgs.get(1);
|
||||||
var address = nonOptionArgs.get(2);
|
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()
|
var request = WithdrawFundsRequest.newBuilder()
|
||||||
.setTradeId(tradeId)
|
.setTradeId(tradeId)
|
||||||
.setAddress(address)
|
.setAddress(address)
|
||||||
|
.setMemo(memo)
|
||||||
.build();
|
.build();
|
||||||
tradesService.withdrawFunds(request);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
case getpaymentmethods: {
|
case getpaymentmethods: {
|
||||||
|
@ -560,12 +613,7 @@ public class CliMain {
|
||||||
if (nonOptionArgs.size() < 3)
|
if (nonOptionArgs.size() < 3)
|
||||||
throw new IllegalArgumentException("no unlock timeout specified");
|
throw new IllegalArgumentException("no unlock timeout specified");
|
||||||
|
|
||||||
long timeout;
|
var timeout = toLong(nonOptionArgs.get(2));
|
||||||
try {
|
|
||||||
timeout = Long.parseLong(nonOptionArgs.get(2));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
|
|
||||||
}
|
|
||||||
var request = UnlockWalletRequest.newBuilder()
|
var request = UnlockWalletRequest.newBuilder()
|
||||||
.setPassword(nonOptionArgs.get(1))
|
.setPassword(nonOptionArgs.get(1))
|
||||||
.setTimeout(timeout).build();
|
.setTimeout(timeout).build();
|
||||||
|
@ -627,6 +675,30 @@ public class CliMain {
|
||||||
return Method.valueOf(methodName.toLowerCase());
|
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,
|
private static File saveFileToDisk(String prefix,
|
||||||
@SuppressWarnings("SameParameterValue") String suffix,
|
@SuppressWarnings("SameParameterValue") String suffix,
|
||||||
String text) {
|
String text) {
|
||||||
|
@ -663,10 +735,12 @@ public class CliMain {
|
||||||
stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance");
|
stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance");
|
||||||
stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses");
|
stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses");
|
||||||
stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address");
|
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, "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, "settxfeerate", "satoshis (per byte)", "Set custom tx fee rate in sats/byte");
|
||||||
stream.format(rowFormat, "unsettxfeerate", "", "Unset custom tx fee rate");
|
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, "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, "", "amount (btc), min amount, use mkt based price, \\", "");
|
||||||
stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), security deposit (%) \\", "");
|
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, "confirmpaymentstarted", "trade id", "Confirm payment started");
|
||||||
stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received");
|
stream.format(rowFormat, "confirmpaymentreceived", "trade id", "Confirm payment received");
|
||||||
stream.format(rowFormat, "keepfunds", "trade id", "Keep received funds in Bisq wallet");
|
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, "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, "getpaymentacctform", "payment method id", "Get a new payment account form");
|
||||||
stream.format(rowFormat, "createpaymentacct", "path to payment account form", "Create a new payment account");
|
stream.format(rowFormat, "createpaymentacct", "path to payment account form", "Create a new payment account");
|
||||||
|
|
|
@ -59,6 +59,16 @@ class ColumnHeaderConstants {
|
||||||
static final String COL_HEADER_TRADE_SHORT_ID = "ID";
|
static final String COL_HEADER_TRADE_SHORT_ID = "ID";
|
||||||
static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)";
|
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_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_VOLUME = padEnd("%-3s(min - max)", 15, ' ');
|
||||||
static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');
|
static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,18 +66,19 @@ public class TradeFormat {
|
||||||
? String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode, baseCurrencyCode)
|
? String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode, baseCurrencyCode)
|
||||||
: String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode);
|
: String.format(headersFormat, counterCurrencyCode, baseCurrencyCode, baseCurrencyCode);
|
||||||
|
|
||||||
String colDataFormat = "%-" + shortIdColWidth + "s" // left justify
|
|
||||||
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left justify
|
String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify
|
||||||
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // right justify
|
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left
|
||||||
+ "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // right justify
|
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify
|
||||||
+ "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // right justify
|
+ "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify
|
||||||
+ takerFeeHeader.get() // right justify
|
+ "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify
|
||||||
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // left justify
|
+ takerFeeHeader.get() // rt justify
|
||||||
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // left justify
|
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify
|
||||||
+ " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // left justify
|
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify
|
||||||
+ " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // left justify
|
+ " %-" + COL_HEADER_TRADE_FIAT_SENT.length() + "s" // lt justify
|
||||||
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // left justify
|
+ " %-" + COL_HEADER_TRADE_FIAT_RECEIVED.length() + "s" // lt justify
|
||||||
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // left justify
|
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
|
||||||
|
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // lt justify
|
||||||
|
|
||||||
return headerLine +
|
return headerLine +
|
||||||
(isTaker
|
(isTaker
|
||||||
|
|
59
cli/src/main/java/bisq/cli/TransactionFormat.java
Normal file
59
cli/src/main/java/bisq/cli/TransactionFormat.java
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.cli;
|
||||||
|
|
||||||
|
import bisq.proto.grpc.TxInfo;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import static bisq.cli.ColumnHeaderConstants.*;
|
||||||
|
import static bisq.cli.CurrencyFormat.formatSatoshis;
|
||||||
|
import static com.google.common.base.Strings.padEnd;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public class TransactionFormat {
|
||||||
|
|
||||||
|
public static String format(TxInfo txInfo) {
|
||||||
|
String headerLine = padEnd(COL_HEADER_TX_ID, txInfo.getTxId().length(), ' ') + COL_HEADER_DELIMITER
|
||||||
|
+ COL_HEADER_TX_IS_CONFIRMED + COL_HEADER_DELIMITER
|
||||||
|
+ COL_HEADER_TX_INPUT_SUM + COL_HEADER_DELIMITER
|
||||||
|
+ COL_HEADER_TX_OUTPUT_SUM + COL_HEADER_DELIMITER
|
||||||
|
+ COL_HEADER_TX_FEE + COL_HEADER_DELIMITER
|
||||||
|
+ COL_HEADER_TX_SIZE + COL_HEADER_DELIMITER
|
||||||
|
+ (txInfo.getMemo().isEmpty() ? "" : COL_HEADER_TX_MEMO + COL_HEADER_DELIMITER)
|
||||||
|
+ "\n";
|
||||||
|
|
||||||
|
String colDataFormat = "%-" + txInfo.getTxId().length() + "s"
|
||||||
|
+ " %" + COL_HEADER_TX_IS_CONFIRMED.length() + "s"
|
||||||
|
+ " %" + COL_HEADER_TX_INPUT_SUM.length() + "s"
|
||||||
|
+ " %" + COL_HEADER_TX_OUTPUT_SUM.length() + "s"
|
||||||
|
+ " %" + COL_HEADER_TX_FEE.length() + "s"
|
||||||
|
+ " %" + COL_HEADER_TX_SIZE.length() + "s"
|
||||||
|
+ " %s";
|
||||||
|
|
||||||
|
return headerLine
|
||||||
|
+ String.format(colDataFormat,
|
||||||
|
txInfo.getTxId(),
|
||||||
|
txInfo.getIsPending() ? "NO" : "YES", // pending=true means not confirmed
|
||||||
|
formatSatoshis(txInfo.getInputSum()),
|
||||||
|
formatSatoshis(txInfo.getOutputSum()),
|
||||||
|
formatSatoshis(txInfo.getFee()),
|
||||||
|
txInfo.getSize(),
|
||||||
|
txInfo.getMemo().isEmpty() ? "" : txInfo.getMemo());
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,22 +31,25 @@ import bisq.core.trade.statistics.TradeStatistics3;
|
||||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||||
|
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
|
import bisq.common.config.Config;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides high level interface to functionality of core Bisq features.
|
* Provides high level interface to functionality of core Bisq features.
|
||||||
* E.g. useful for different APIs to access data of different domains of Bisq.
|
* E.g. useful for different APIs to access data of different domains of Bisq.
|
||||||
|
@ -55,6 +58,8 @@ import javax.annotation.Nullable;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CoreApi {
|
public class CoreApi {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final Config config;
|
||||||
private final CoreDisputeAgentsService coreDisputeAgentsService;
|
private final CoreDisputeAgentsService coreDisputeAgentsService;
|
||||||
private final CoreOffersService coreOffersService;
|
private final CoreOffersService coreOffersService;
|
||||||
private final CorePaymentAccountsService paymentAccountsService;
|
private final CorePaymentAccountsService paymentAccountsService;
|
||||||
|
@ -64,13 +69,15 @@ public class CoreApi {
|
||||||
private final TradeStatisticsManager tradeStatisticsManager;
|
private final TradeStatisticsManager tradeStatisticsManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CoreApi(CoreDisputeAgentsService coreDisputeAgentsService,
|
public CoreApi(Config config,
|
||||||
|
CoreDisputeAgentsService coreDisputeAgentsService,
|
||||||
CoreOffersService coreOffersService,
|
CoreOffersService coreOffersService,
|
||||||
CorePaymentAccountsService paymentAccountsService,
|
CorePaymentAccountsService paymentAccountsService,
|
||||||
CorePriceService corePriceService,
|
CorePriceService corePriceService,
|
||||||
CoreTradesService coreTradesService,
|
CoreTradesService coreTradesService,
|
||||||
CoreWalletsService walletsService,
|
CoreWalletsService walletsService,
|
||||||
TradeStatisticsManager tradeStatisticsManager) {
|
TradeStatisticsManager tradeStatisticsManager) {
|
||||||
|
this.config = config;
|
||||||
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
||||||
this.coreOffersService = coreOffersService;
|
this.coreOffersService = coreOffersService;
|
||||||
this.paymentAccountsService = paymentAccountsService;
|
this.paymentAccountsService = paymentAccountsService;
|
||||||
|
@ -210,7 +217,7 @@ public class CoreApi {
|
||||||
coreTradesService.keepFunds(tradeId);
|
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);
|
coreTradesService.withdrawFunds(tradeId, address, memo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,8 +253,19 @@ public class CoreApi {
|
||||||
return walletsService.getUnusedBsqAddress();
|
return walletsService.getUnusedBsqAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendBsq(String address, String amount, TxBroadcaster.Callback callback) {
|
public void sendBsq(String address,
|
||||||
walletsService.sendBsq(address, amount, callback);
|
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) {
|
public void getTxFeeRate(ResultHandler resultHandler) {
|
||||||
|
@ -267,6 +285,10 @@ public class CoreApi {
|
||||||
return walletsService.getMostRecentTxFeeRateInfo();
|
return walletsService.getMostRecentTxFeeRateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Transaction getTransaction(String txId) {
|
||||||
|
return walletsService.getTransaction(txId);
|
||||||
|
}
|
||||||
|
|
||||||
public void setWalletPassword(String password, String newPassword) {
|
public void setWalletPassword(String password, String newPassword) {
|
||||||
walletsService.setWalletPassword(password, newPassword);
|
walletsService.setWalletPassword(password, newPassword);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,6 @@ import java.util.function.Consumer;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
|
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
|
||||||
import static java.lang.String.format;
|
import static java.lang.String.format;
|
||||||
|
|
||||||
|
@ -85,6 +83,8 @@ class CoreTradesService {
|
||||||
String paymentAccountId,
|
String paymentAccountId,
|
||||||
String takerFeeCurrencyCode,
|
String takerFeeCurrencyCode,
|
||||||
Consumer<Trade> resultHandler) {
|
Consumer<Trade> resultHandler) {
|
||||||
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
|
|
||||||
offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode);
|
offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode);
|
||||||
|
|
||||||
|
@ -149,6 +149,9 @@ class CoreTradesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
void keepFunds(String tradeId) {
|
void keepFunds(String tradeId) {
|
||||||
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
|
|
||||||
verifyTradeIsNotClosed(tradeId);
|
verifyTradeIsNotClosed(tradeId);
|
||||||
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
||||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||||
|
@ -156,8 +159,10 @@ class CoreTradesService {
|
||||||
tradeManager.onTradeCompleted(trade);
|
tradeManager.onTradeCompleted(trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) {
|
void withdrawFunds(String tradeId, String toAddress, String memo) {
|
||||||
// An encrypted wallet must be unlocked for this operation.
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
|
|
||||||
verifyTradeIsNotClosed(tradeId);
|
verifyTradeIsNotClosed(tradeId);
|
||||||
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
||||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||||
|
@ -172,21 +177,21 @@ class CoreTradesService {
|
||||||
var receiverAmount = amount.subtract(fee);
|
var receiverAmount = amount.subtract(fee);
|
||||||
|
|
||||||
log.info(format("Withdrawing funds received from trade %s:"
|
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,
|
tradeId,
|
||||||
fromAddressEntry.getAddressString(),
|
fromAddressEntry.getAddressString(),
|
||||||
toAddress,
|
toAddress,
|
||||||
amount.toFriendlyString(),
|
amount.toFriendlyString(),
|
||||||
fee.toFriendlyString(),
|
fee.toFriendlyString(),
|
||||||
receiverAmount.toFriendlyString()));
|
receiverAmount.toFriendlyString(),
|
||||||
|
memo));
|
||||||
tradeManager.onWithdrawRequest(
|
tradeManager.onWithdrawRequest(
|
||||||
toAddress,
|
toAddress,
|
||||||
amount,
|
amount,
|
||||||
fee,
|
fee,
|
||||||
coreWalletsService.getKey(),
|
coreWalletsService.getKey(),
|
||||||
trade,
|
trade,
|
||||||
memo,
|
memo.isEmpty() ? null : memo,
|
||||||
() -> {
|
() -> {
|
||||||
},
|
},
|
||||||
(errorMessage, throwable) -> {
|
(errorMessage, throwable) -> {
|
||||||
|
@ -196,10 +201,14 @@ class CoreTradesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
String getTradeRole(String tradeId) {
|
String getTradeRole(String tradeId) {
|
||||||
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
return tradeUtil.getRole(getTrade(tradeId));
|
return tradeUtil.getRole(getTrade(tradeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
Trade getTrade(String tradeId) {
|
Trade getTrade(String tradeId) {
|
||||||
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
return getOpenTrade(tradeId).orElseGet(() ->
|
return getOpenTrade(tradeId).orElseGet(() ->
|
||||||
getClosedTrade(tradeId).orElseThrow(() ->
|
getClosedTrade(tradeId).orElseThrow(() ->
|
||||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId))
|
new IllegalArgumentException(format("trade with id '%s' not found", tradeId))
|
||||||
|
|
|
@ -23,7 +23,9 @@ import bisq.core.api.model.BsqBalanceInfo;
|
||||||
import bisq.core.api.model.BtcBalanceInfo;
|
import bisq.core.api.model.BtcBalanceInfo;
|
||||||
import bisq.core.api.model.TxFeeRateInfo;
|
import bisq.core.api.model.TxFeeRateInfo;
|
||||||
import bisq.core.btc.Balances;
|
import bisq.core.btc.Balances;
|
||||||
|
import bisq.core.btc.exceptions.AddressEntryException;
|
||||||
import bisq.core.btc.exceptions.BsqChangeBelowDustException;
|
import bisq.core.btc.exceptions.BsqChangeBelowDustException;
|
||||||
|
import bisq.core.btc.exceptions.InsufficientFundsException;
|
||||||
import bisq.core.btc.exceptions.TransactionVerificationException;
|
import bisq.core.btc.exceptions.TransactionVerificationException;
|
||||||
import bisq.core.btc.exceptions.WalletException;
|
import bisq.core.btc.exceptions.WalletException;
|
||||||
import bisq.core.btc.model.AddressEntry;
|
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.btc.wallet.WalletsManager;
|
||||||
import bisq.core.provider.fee.FeeService;
|
import bisq.core.provider.fee.FeeService;
|
||||||
import bisq.core.user.Preferences;
|
import bisq.core.user.Preferences;
|
||||||
|
import bisq.core.util.FormattingUtils;
|
||||||
import bisq.core.util.coin.BsqFormatter;
|
import bisq.core.util.coin.BsqFormatter;
|
||||||
|
import bisq.core.util.coin.CoinFormatter;
|
||||||
|
|
||||||
import bisq.common.Timer;
|
import bisq.common.Timer;
|
||||||
import bisq.common.UserThread;
|
import bisq.common.UserThread;
|
||||||
|
@ -46,10 +50,12 @@ import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
import org.bitcoinj.core.InsufficientMoneyException;
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.bitcoinj.core.TransactionConfidence;
|
import org.bitcoinj.core.TransactionConfidence;
|
||||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
|
@ -64,6 +70,7 @@ import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -85,6 +92,7 @@ class CoreWalletsService {
|
||||||
private final BsqTransferService bsqTransferService;
|
private final BsqTransferService bsqTransferService;
|
||||||
private final BsqFormatter bsqFormatter;
|
private final BsqFormatter bsqFormatter;
|
||||||
private final BtcWalletService btcWalletService;
|
private final BtcWalletService btcWalletService;
|
||||||
|
private final CoinFormatter btcFormatter;
|
||||||
private final FeeService feeService;
|
private final FeeService feeService;
|
||||||
private final Preferences preferences;
|
private final Preferences preferences;
|
||||||
|
|
||||||
|
@ -103,6 +111,7 @@ class CoreWalletsService {
|
||||||
BsqTransferService bsqTransferService,
|
BsqTransferService bsqTransferService,
|
||||||
BsqFormatter bsqFormatter,
|
BsqFormatter bsqFormatter,
|
||||||
BtcWalletService btcWalletService,
|
BtcWalletService btcWalletService,
|
||||||
|
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
|
||||||
FeeService feeService,
|
FeeService feeService,
|
||||||
Preferences preferences) {
|
Preferences preferences) {
|
||||||
this.balances = balances;
|
this.balances = balances;
|
||||||
|
@ -111,6 +120,7 @@ class CoreWalletsService {
|
||||||
this.bsqTransferService = bsqTransferService;
|
this.bsqTransferService = bsqTransferService;
|
||||||
this.bsqFormatter = bsqFormatter;
|
this.bsqFormatter = bsqFormatter;
|
||||||
this.btcWalletService = btcWalletService;
|
this.btcWalletService = btcWalletService;
|
||||||
|
this.btcFormatter = btcFormatter;
|
||||||
this.feeService = feeService;
|
this.feeService = feeService;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
}
|
}
|
||||||
|
@ -189,13 +199,27 @@ class CoreWalletsService {
|
||||||
|
|
||||||
void sendBsq(String address,
|
void sendBsq(String address,
|
||||||
String amount,
|
String amount,
|
||||||
|
String txFeeRate,
|
||||||
TxBroadcaster.Callback callback) {
|
TxBroadcaster.Callback callback) {
|
||||||
|
verifyWalletsAreAvailable();
|
||||||
|
verifyEncryptedWalletIsUnlocked();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
LegacyAddress legacyAddress = getValidBsqLegacyAddress(address);
|
LegacyAddress legacyAddress = getValidBsqLegacyAddress(address);
|
||||||
Coin receiverAmount = getValidBsqTransferAmount(amount);
|
Coin receiverAmount = getValidTransferAmount(amount, bsqFormatter);
|
||||||
BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount);
|
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);
|
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
|
| BsqChangeBelowDustException
|
||||||
| TransactionVerificationException
|
| TransactionVerificationException
|
||||||
| WalletException ex) {
|
| 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) {
|
void getTxFeeRate(ResultHandler resultHandler) {
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings({"unchecked", "Convert2MethodRef"})
|
@SuppressWarnings({"unchecked", "Convert2MethodRef"})
|
||||||
|
@ -252,6 +331,26 @@ class CoreWalletsService {
|
||||||
feeService.getLastRequest());
|
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) {
|
int getNumConfirmationsForMostRecentTransaction(String addressString) {
|
||||||
Address address = getAddressEntry(addressString).getAddress();
|
Address address = getAddressEntry(addressString).getAddress();
|
||||||
TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address);
|
TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address);
|
||||||
|
@ -342,13 +441,13 @@ class CoreWalletsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throws a RuntimeException if wallets are not available (encrypted or not).
|
// Throws a RuntimeException if wallets are not available (encrypted or not).
|
||||||
private void verifyWalletsAreAvailable() {
|
void verifyWalletsAreAvailable() {
|
||||||
if (!walletsManager.areWalletsAvailable())
|
if (!walletsManager.areWalletsAvailable())
|
||||||
throw new IllegalStateException("wallet is not yet available");
|
throw new IllegalStateException("wallet is not yet available");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throws a RuntimeException if wallets are not available or not encrypted.
|
// Throws a RuntimeException if wallets are not available or not encrypted.
|
||||||
private void verifyWalletIsAvailableAndEncrypted() {
|
void verifyWalletIsAvailableAndEncrypted() {
|
||||||
if (!walletsManager.areWalletsAvailable())
|
if (!walletsManager.areWalletsAvailable())
|
||||||
throw new IllegalStateException("wallet is not yet available");
|
throw new IllegalStateException("wallet is not yet available");
|
||||||
|
|
||||||
|
@ -357,7 +456,7 @@ class CoreWalletsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throws a RuntimeException if wallets are encrypted and locked.
|
// Throws a RuntimeException if wallets are encrypted and locked.
|
||||||
private void verifyEncryptedWalletIsUnlocked() {
|
void verifyEncryptedWalletIsUnlocked() {
|
||||||
if (walletsManager.areWalletsEncrypted() && tempAesKey == null)
|
if (walletsManager.areWalletsEncrypted() && tempAesKey == null)
|
||||||
throw new IllegalStateException("wallet is locked");
|
throw new IllegalStateException("wallet is locked");
|
||||||
}
|
}
|
||||||
|
@ -423,15 +522,22 @@ class CoreWalletsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a Coin for the amount string, or a RuntimeException if invalid.
|
// Returns a Coin for the transfer amount string, or a RuntimeException if invalid.
|
||||||
private Coin getValidBsqTransferAmount(String amount) {
|
private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) {
|
||||||
Coin amountAsCoin = parseToCoin(amount, bsqFormatter);
|
Coin amountAsCoin = parseToCoin(amount, coinFormatter);
|
||||||
if (amountAsCoin.isLessThan(getMinNonDustOutput()))
|
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;
|
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() {
|
private KeyCrypterScrypt getKeyCrypterScrypt() {
|
||||||
KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt();
|
KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt();
|
||||||
if (keyCrypterScrypt == null)
|
if (keyCrypterScrypt == null)
|
||||||
|
|
160
core/src/main/java/bisq/core/api/model/TxInfo.java
Normal file
160
core/src/main/java/bisq/core/api/model/TxInfo.java
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.core.api.model;
|
||||||
|
|
||||||
|
import bisq.common.Payload;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@EqualsAndHashCode
|
||||||
|
@Getter
|
||||||
|
public class TxInfo implements Payload {
|
||||||
|
|
||||||
|
// The client cannot see an instance of an org.bitcoinj.core.Transaction. We use the
|
||||||
|
// lighter weight TxInfo proto wrapper instead, containing just enough fields to
|
||||||
|
// view some transaction details. A block explorer or bitcoin-core client can be
|
||||||
|
// used to see more detail.
|
||||||
|
|
||||||
|
private final String txId;
|
||||||
|
private final long inputSum;
|
||||||
|
private final long outputSum;
|
||||||
|
private final long fee;
|
||||||
|
private final int size;
|
||||||
|
private final boolean isPending;
|
||||||
|
private final String memo;
|
||||||
|
|
||||||
|
public TxInfo(TxInfo.TxInfoBuilder builder) {
|
||||||
|
this.txId = builder.txId;
|
||||||
|
this.inputSum = builder.inputSum;
|
||||||
|
this.outputSum = builder.outputSum;
|
||||||
|
this.fee = builder.fee;
|
||||||
|
this.size = builder.size;
|
||||||
|
this.isPending = builder.isPending;
|
||||||
|
this.memo = builder.memo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TxInfo toTxInfo(Transaction transaction) {
|
||||||
|
if (transaction == null)
|
||||||
|
throw new IllegalStateException("server created a null transaction");
|
||||||
|
|
||||||
|
return new TxInfo.TxInfoBuilder()
|
||||||
|
.withTxId(transaction.getTxId().toString())
|
||||||
|
.withInputSum(transaction.getInputSum().value)
|
||||||
|
.withOutputSum(transaction.getOutputSum().value)
|
||||||
|
.withFee(transaction.getFee().value)
|
||||||
|
.withSize(transaction.getMessageSize())
|
||||||
|
.withIsPending(transaction.isPending())
|
||||||
|
.withMemo(transaction.getMemo())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PROTO BUFFER
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public bisq.proto.grpc.TxInfo toProtoMessage() {
|
||||||
|
return bisq.proto.grpc.TxInfo.newBuilder()
|
||||||
|
.setTxId(txId)
|
||||||
|
.setInputSum(inputSum)
|
||||||
|
.setOutputSum(outputSum)
|
||||||
|
.setFee(fee)
|
||||||
|
.setSize(size)
|
||||||
|
.setIsPending(isPending)
|
||||||
|
.setMemo(memo == null ? "" : memo)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) {
|
||||||
|
return new TxInfo.TxInfoBuilder()
|
||||||
|
.withTxId(proto.getTxId())
|
||||||
|
.withInputSum(proto.getInputSum())
|
||||||
|
.withOutputSum(proto.getOutputSum())
|
||||||
|
.withFee(proto.getFee())
|
||||||
|
.withSize(proto.getSize())
|
||||||
|
.withIsPending(proto.getIsPending())
|
||||||
|
.withMemo(proto.getMemo())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TxInfoBuilder {
|
||||||
|
private String txId;
|
||||||
|
private long inputSum;
|
||||||
|
private long outputSum;
|
||||||
|
private long fee;
|
||||||
|
private int size;
|
||||||
|
private boolean isPending;
|
||||||
|
private String memo;
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withTxId(String txId) {
|
||||||
|
this.txId = txId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withInputSum(long inputSum) {
|
||||||
|
this.inputSum = inputSum;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withOutputSum(long outputSum) {
|
||||||
|
this.outputSum = outputSum;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withFee(long fee) {
|
||||||
|
this.fee = fee;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withSize(int size) {
|
||||||
|
this.size = size;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withIsPending(boolean isPending) {
|
||||||
|
this.isPending = isPending;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo.TxInfoBuilder withMemo(String memo) {
|
||||||
|
this.memo = memo;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TxInfo build() {
|
||||||
|
return new TxInfo(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "TxInfo{" + "\n" +
|
||||||
|
" txId='" + txId + '\'' + "\n" +
|
||||||
|
", inputSum=" + inputSum + "\n" +
|
||||||
|
", outputSum=" + outputSum + "\n" +
|
||||||
|
", fee=" + fee + "\n" +
|
||||||
|
", size=" + size + "\n" +
|
||||||
|
", isPending=" + isPending + "\n" +
|
||||||
|
", memo='" + memo + '\'' + "\n" +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,14 +33,15 @@ public class BsqTransferService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public BsqTransferModel getBsqTransferModel(LegacyAddress address,
|
public BsqTransferModel getBsqTransferModel(LegacyAddress address,
|
||||||
Coin receiverAmount)
|
Coin receiverAmount,
|
||||||
|
Coin txFeePerVbyte)
|
||||||
throws TransactionVerificationException,
|
throws TransactionVerificationException,
|
||||||
WalletException,
|
WalletException,
|
||||||
BsqChangeBelowDustException,
|
BsqChangeBelowDustException,
|
||||||
InsufficientMoneyException {
|
InsufficientMoneyException {
|
||||||
|
|
||||||
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount);
|
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount);
|
||||||
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
|
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, txFeePerVbyte);
|
||||||
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
|
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
|
||||||
|
|
||||||
return new BsqTransferModel(address,
|
return new BsqTransferModel(address,
|
||||||
|
|
|
@ -440,8 +440,7 @@ public class BtcWalletService extends WalletService {
|
||||||
// Add fee input to prepared BSQ send tx
|
// Add fee input to prepared BSQ send tx
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx) throws
|
||||||
public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean isSendTx) throws
|
|
||||||
TransactionVerificationException, WalletException, InsufficientMoneyException {
|
TransactionVerificationException, WalletException, InsufficientMoneyException {
|
||||||
// preparedBsqTx has following structure:
|
// preparedBsqTx has following structure:
|
||||||
// inputs [1-n] BSQ inputs
|
// inputs [1-n] BSQ inputs
|
||||||
|
@ -455,13 +454,26 @@ public class BtcWalletService extends WalletService {
|
||||||
// outputs [0-1] BSQ change output
|
// outputs [0-1] BSQ change output
|
||||||
// outputs [0-1] BTC change output
|
// outputs [0-1] BTC change output
|
||||||
// mining fee: BTC mining fee
|
// 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,
|
public Transaction completePreparedBsqTx(Transaction preparedBsqTx,
|
||||||
boolean useCustomTxFee,
|
|
||||||
@Nullable byte[] opReturnData) throws
|
@Nullable byte[] opReturnData) throws
|
||||||
TransactionVerificationException, WalletException, InsufficientMoneyException {
|
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:
|
// preparedBsqTx has following structure:
|
||||||
// inputs [1-n] BSQ inputs
|
// inputs [1-n] BSQ inputs
|
||||||
|
@ -488,8 +500,6 @@ public class BtcWalletService extends WalletService {
|
||||||
int sigSizePerInput = 106;
|
int sigSizePerInput = 106;
|
||||||
// typical size for a tx with 2 inputs
|
// typical size for a tx with 2 inputs
|
||||||
int txVsizeWithUnsignedInputs = 203;
|
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
|
// In case there are no change outputs we force a change by adding min dust to the BTC input
|
||||||
Coin forcedChangeValue = Coin.ZERO;
|
Coin forcedChangeValue = Coin.ZERO;
|
||||||
|
|
||||||
|
@ -968,7 +978,7 @@ public class BtcWalletService extends WalletService {
|
||||||
}
|
}
|
||||||
if (sendResult != null) {
|
if (sendResult != null) {
|
||||||
log.info("Broadcasting double spending transaction. " + sendResult.tx);
|
log.info("Broadcasting double spending transaction. " + sendResult.tx);
|
||||||
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
|
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Transaction result) {
|
public void onSuccess(Transaction result) {
|
||||||
log.info("Double spending transaction published. " + result);
|
log.info("Double spending transaction published. " + result);
|
||||||
|
@ -1048,6 +1058,14 @@ public class BtcWalletService extends WalletService {
|
||||||
public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses,
|
public Transaction getFeeEstimationTransactionForMultipleAddresses(Set<String> fromAddresses,
|
||||||
Coin amount)
|
Coin amount)
|
||||||
throws AddressFormatException, AddressEntryException, InsufficientFundsException {
|
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()
|
Set<AddressEntry> addressEntries = fromAddresses.stream()
|
||||||
.map(address -> {
|
.map(address -> {
|
||||||
Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
|
Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
|
||||||
|
@ -1070,7 +1088,6 @@ public class BtcWalletService extends WalletService {
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
int txVsize = 0;
|
int txVsize = 0;
|
||||||
Transaction tx;
|
Transaction tx;
|
||||||
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
|
|
||||||
do {
|
do {
|
||||||
counter++;
|
counter++;
|
||||||
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
|
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
|
||||||
|
@ -1097,7 +1114,11 @@ public class BtcWalletService extends WalletService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean feeEstimationNotSatisfied(int counter, Transaction tx) {
|
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 &&
|
return counter < 10 &&
|
||||||
(tx.getFee().value < targetFee ||
|
(tx.getFee().value < targetFee ||
|
||||||
tx.getFee().value - targetFee > 1000);
|
tx.getFee().value - targetFee > 1000);
|
||||||
|
@ -1213,7 +1234,7 @@ public class BtcWalletService extends WalletService {
|
||||||
Coin fee,
|
Coin fee,
|
||||||
@Nullable String changeAddress,
|
@Nullable String changeAddress,
|
||||||
@Nullable KeyParameter aesKey) throws
|
@Nullable KeyParameter aesKey) throws
|
||||||
AddressFormatException, AddressEntryException, InsufficientMoneyException {
|
AddressFormatException, AddressEntryException {
|
||||||
Transaction tx = new Transaction(params);
|
Transaction tx = new Transaction(params);
|
||||||
final Coin netValue = amount.subtract(fee);
|
final Coin netValue = amount.subtract(fee);
|
||||||
checkArgument(Restrictions.isAboveDust(netValue),
|
checkArgument(Restrictions.isAboveDust(netValue),
|
||||||
|
@ -1246,12 +1267,12 @@ public class BtcWalletService extends WalletService {
|
||||||
|
|
||||||
sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries),
|
sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesFromAddressEntries(addressEntries),
|
||||||
preferences.getIgnoreDustThreshold());
|
preferences.getIgnoreDustThreshold());
|
||||||
Optional<AddressEntry> addressEntryOptional = Optional.<AddressEntry>empty();
|
Optional<AddressEntry> addressEntryOptional = Optional.empty();
|
||||||
AddressEntry changeAddressAddressEntry = null;
|
|
||||||
if (changeAddress != null)
|
if (changeAddress != null)
|
||||||
addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE);
|
addressEntryOptional = findAddressEntry(changeAddress, AddressEntry.Context.AVAILABLE);
|
||||||
|
|
||||||
changeAddressAddressEntry = addressEntryOptional.orElseGet(() -> getFreshAddressEntry());
|
AddressEntry changeAddressAddressEntry = addressEntryOptional.orElseGet(this::getFreshAddressEntry);
|
||||||
checkNotNull(changeAddressAddressEntry, "change address must not be null");
|
checkNotNull(changeAddressAddressEntry, "change address must not be null");
|
||||||
sendRequest.changeAddress = changeAddressAddressEntry.getAddress();
|
sendRequest.changeAddress = changeAddressAddressEntry.getAddress();
|
||||||
return sendRequest;
|
return sendRequest;
|
||||||
|
|
|
@ -103,7 +103,7 @@ public class LockupTxService {
|
||||||
throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException {
|
throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException {
|
||||||
byte[] opReturnData = BondConsensus.getLockupOpReturnData(lockTime, lockupReason, hash);
|
byte[] opReturnData = BondConsensus.getLockupOpReturnData(lockTime, lockupReason, hash);
|
||||||
Transaction preparedTx = bsqWalletService.getPreparedLockupTx(lockupAmount);
|
Transaction preparedTx = bsqWalletService.getPreparedLockupTx(lockupAmount);
|
||||||
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, true, opReturnData);
|
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, opReturnData);
|
||||||
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
|
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
|
||||||
log.info("Lockup tx: " + transaction);
|
log.info("Lockup tx: " + transaction);
|
||||||
return transaction;
|
return transaction;
|
||||||
|
|
|
@ -103,7 +103,7 @@ public class UnlockTxService {
|
||||||
checkArgument(optionalLockupTxOutput.isPresent(), "lockupTxOutput must be present");
|
checkArgument(optionalLockupTxOutput.isPresent(), "lockupTxOutput must be present");
|
||||||
TxOutput lockupTxOutput = optionalLockupTxOutput.get();
|
TxOutput lockupTxOutput = optionalLockupTxOutput.get();
|
||||||
Transaction preparedTx = bsqWalletService.getPreparedUnlockTx(lockupTxOutput);
|
Transaction preparedTx = bsqWalletService.getPreparedUnlockTx(lockupTxOutput);
|
||||||
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, true, null);
|
Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedTx, null);
|
||||||
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
|
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
|
||||||
log.info("Unlock tx: " + transaction);
|
log.info("Unlock tx: " + transaction);
|
||||||
return transaction;
|
return transaction;
|
||||||
|
|
|
@ -6,8 +6,6 @@ import bisq.proto.grpc.DisputeAgentsGrpc;
|
||||||
import bisq.proto.grpc.RegisterDisputeAgentReply;
|
import bisq.proto.grpc.RegisterDisputeAgentReply;
|
||||||
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -18,10 +16,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
|
class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcDisputeAgentsService(CoreApi coreApi) {
|
public GrpcDisputeAgentsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -32,14 +32,8 @@ class GrpcDisputeAgentsService extends DisputeAgentsGrpc.DisputeAgentsImplBase {
|
||||||
var reply = RegisterDisputeAgentReply.newBuilder().build();
|
var reply = RegisterDisputeAgentReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
} catch (IllegalStateException cause) {
|
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.daemon.grpc;
|
||||||
|
|
||||||
|
import io.grpc.Status;
|
||||||
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static io.grpc.Status.INVALID_ARGUMENT;
|
||||||
|
import static io.grpc.Status.UNKNOWN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The singleton instance of this class handles any expected core api Throwable by
|
||||||
|
* wrapping its message in a gRPC StatusRuntimeException and sending it to the client.
|
||||||
|
* An unexpected Throwable's message will be replaced with an 'unexpected' error message.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
@Slf4j
|
||||||
|
class GrpcExceptionHandler {
|
||||||
|
|
||||||
|
private final Predicate<Throwable> isExpectedException = (t) ->
|
||||||
|
t instanceof IllegalStateException || t instanceof IllegalArgumentException;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public GrpcExceptionHandler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleException(Throwable t, StreamObserver<?> responseObserver) {
|
||||||
|
// Log the core api error (this is last chance to do that), wrap it in a new
|
||||||
|
// gRPC StatusRuntimeException, then send it to the client in the gRPC response.
|
||||||
|
log.error("", t);
|
||||||
|
var grpcStatusRuntimeException = wrapException(t);
|
||||||
|
responseObserver.onError(grpcStatusRuntimeException);
|
||||||
|
throw grpcStatusRuntimeException;
|
||||||
|
}
|
||||||
|
|
||||||
|
private StatusRuntimeException wrapException(Throwable t) {
|
||||||
|
// We want to be careful about what kinds of exception messages we send to the
|
||||||
|
// client. Expected core exceptions should be wrapped in an IllegalStateException
|
||||||
|
// or IllegalArgumentException, with a consistently styled and worded error
|
||||||
|
// message. But only a small number of the expected error types are currently
|
||||||
|
// handled this way; there is much work to do to handle the variety of errors
|
||||||
|
// that can occur in the api. In the meantime, we take care to not pass full,
|
||||||
|
// unexpected error messages to the client. If the exception type is unexpected,
|
||||||
|
// we omit details from the gRPC exception sent to the client.
|
||||||
|
if (isExpectedException.test(t)) {
|
||||||
|
if (t.getCause() != null)
|
||||||
|
return new StatusRuntimeException(mapGrpcErrorStatus(t.getCause(), t.getCause().getMessage()));
|
||||||
|
else
|
||||||
|
return new StatusRuntimeException(mapGrpcErrorStatus(t, t.getMessage()));
|
||||||
|
} else {
|
||||||
|
return new StatusRuntimeException(mapGrpcErrorStatus(t, "unexpected error on server"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Status mapGrpcErrorStatus(Throwable t, String description) {
|
||||||
|
// We default to the UNKNOWN status, except were the mapping of a core api
|
||||||
|
// exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy
|
||||||
|
// to support RESTful clients, we will need to have more specific mappings
|
||||||
|
// to support correct HTTP 1.1. status codes.
|
||||||
|
//noinspection SwitchStatementWithTooFewBranches
|
||||||
|
switch (t.getClass().getSimpleName()) {
|
||||||
|
// We go ahead and use a switch statement instead of if, in anticipation
|
||||||
|
// of more, specific exception mappings.
|
||||||
|
case "IllegalArgumentException":
|
||||||
|
return INVALID_ARGUMENT.withDescription(description);
|
||||||
|
default:
|
||||||
|
return UNKNOWN.withDescription(description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,16 +16,18 @@ import java.util.stream.Collectors;
|
||||||
class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
|
class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcGetTradeStatisticsService(CoreApi coreApi) {
|
public GrpcGetTradeStatisticsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void getTradeStatistics(GetTradeStatisticsRequest req,
|
public void getTradeStatistics(GetTradeStatisticsRequest req,
|
||||||
StreamObserver<GetTradeStatisticsReply> responseObserver) {
|
StreamObserver<GetTradeStatisticsReply> responseObserver) {
|
||||||
|
try {
|
||||||
var tradeStatistics = coreApi.getTradeStatistics().stream()
|
var tradeStatistics = coreApi.getTradeStatistics().stream()
|
||||||
.map(TradeStatistics3::toProtoTradeStatistics3)
|
.map(TradeStatistics3::toProtoTradeStatistics3)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
@ -33,5 +35,8 @@ class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStati
|
||||||
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
|
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,6 @@ import bisq.proto.grpc.GetOffersReply;
|
||||||
import bisq.proto.grpc.GetOffersRequest;
|
import bisq.proto.grpc.GetOffersRequest;
|
||||||
import bisq.proto.grpc.OffersGrpc;
|
import bisq.proto.grpc.OffersGrpc;
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -48,10 +46,12 @@ import static bisq.core.api.model.OfferInfo.toOfferInfo;
|
||||||
class GrpcOffersService extends OffersGrpc.OffersImplBase {
|
class GrpcOffersService extends OffersGrpc.OffersImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcOffersService(CoreApi coreApi) {
|
public GrpcOffersService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,16 +64,15 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void getOffers(GetOffersRequest req,
|
public void getOffers(GetOffersRequest req,
|
||||||
StreamObserver<GetOffersReply> responseObserver) {
|
StreamObserver<GetOffersReply> responseObserver) {
|
||||||
|
try {
|
||||||
List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
|
List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
|
||||||
.stream().map(OfferInfo::toOfferInfo)
|
.stream().map(OfferInfo::toOfferInfo)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
@ -84,6 +83,9 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -111,10 +113,8 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,10 +126,8 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
|
||||||
var reply = CancelOfferReply.newBuilder().build();
|
var reply = CancelOfferReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,6 @@ import bisq.proto.grpc.GetPaymentMethodsReply;
|
||||||
import bisq.proto.grpc.GetPaymentMethodsRequest;
|
import bisq.proto.grpc.GetPaymentMethodsRequest;
|
||||||
import bisq.proto.grpc.PaymentAccountsGrpc;
|
import bisq.proto.grpc.PaymentAccountsGrpc;
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -43,10 +41,12 @@ import java.util.stream.Collectors;
|
||||||
class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImplBase {
|
class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcPaymentAccountsService(CoreApi coreApi) {
|
public GrpcPaymentAccountsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -59,14 +59,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
} catch (IllegalStateException cause) {
|
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,14 +75,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
|
||||||
.addAllPaymentAccounts(paymentAccounts).build();
|
.addAllPaymentAccounts(paymentAccounts).build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
} catch (IllegalStateException cause) {
|
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,14 +91,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
|
||||||
.addAllPaymentMethods(paymentMethods).build();
|
.addAllPaymentMethods(paymentMethods).build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
} catch (IllegalStateException cause) {
|
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,14 +106,8 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
} catch (IllegalStateException cause) {
|
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,6 @@ import bisq.proto.grpc.MarketPriceReply;
|
||||||
import bisq.proto.grpc.MarketPriceRequest;
|
import bisq.proto.grpc.MarketPriceRequest;
|
||||||
import bisq.proto.grpc.PriceGrpc;
|
import bisq.proto.grpc.PriceGrpc;
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -35,10 +33,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
class GrpcPriceService extends PriceGrpc.PriceImplBase {
|
class GrpcPriceService extends PriceGrpc.PriceImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcPriceService(CoreApi coreApi) {
|
public GrpcPriceService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -49,10 +49,8 @@ class GrpcPriceService extends PriceGrpc.PriceImplBase {
|
||||||
var reply = MarketPriceReply.newBuilder().setPrice(price).build();
|
var reply = MarketPriceReply.newBuilder().setPrice(price).build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package bisq.daemon.grpc;
|
package bisq.daemon.grpc;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
|
|
||||||
import io.grpc.Server;
|
import io.grpc.Server;
|
||||||
|
@ -30,6 +31,12 @@ import java.io.UncheckedIOException;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static io.grpc.ServerInterceptors.interceptForward;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.daemon.grpc.interceptor.PasswordAuthInterceptor;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class GrpcServer {
|
public class GrpcServer {
|
||||||
|
@ -48,13 +55,14 @@ public class GrpcServer {
|
||||||
GrpcTradesService tradesService,
|
GrpcTradesService tradesService,
|
||||||
GrpcWalletsService walletsService) {
|
GrpcWalletsService walletsService) {
|
||||||
this.server = ServerBuilder.forPort(config.apiPort)
|
this.server = ServerBuilder.forPort(config.apiPort)
|
||||||
|
.executor(UserThread.getExecutor())
|
||||||
.addService(disputeAgentsService)
|
.addService(disputeAgentsService)
|
||||||
.addService(offersService)
|
.addService(offersService)
|
||||||
.addService(paymentAccountsService)
|
.addService(paymentAccountsService)
|
||||||
.addService(priceService)
|
.addService(priceService)
|
||||||
.addService(tradeStatisticsService)
|
.addService(tradeStatisticsService)
|
||||||
.addService(tradesService)
|
.addService(tradesService)
|
||||||
.addService(versionService)
|
.addService(interceptForward(versionService, versionService.interceptors()))
|
||||||
.addService(walletsService)
|
.addService(walletsService)
|
||||||
.intercept(passwordAuthInterceptor)
|
.intercept(passwordAuthInterceptor)
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -35,8 +35,6 @@ import bisq.proto.grpc.TradesGrpc;
|
||||||
import bisq.proto.grpc.WithdrawFundsReply;
|
import bisq.proto.grpc.WithdrawFundsReply;
|
||||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
@ -49,10 +47,12 @@ import static bisq.core.api.model.TradeInfo.toTradeInfo;
|
||||||
class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcTradesService(CoreApi coreApi) {
|
public GrpcTradesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,10 +66,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,10 +86,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,10 +99,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
var reply = ConfirmPaymentStartedReply.newBuilder().build();
|
var reply = ConfirmPaymentStartedReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,10 +112,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
var reply = ConfirmPaymentReceivedReply.newBuilder().build();
|
var reply = ConfirmPaymentReceivedReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,10 +125,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
var reply = KeepFundsReply.newBuilder().build();
|
var reply = KeepFundsReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,15 +134,12 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
|
||||||
public void withdrawFunds(WithdrawFundsRequest req,
|
public void withdrawFunds(WithdrawFundsRequest req,
|
||||||
StreamObserver<WithdrawFundsReply> responseObserver) {
|
StreamObserver<WithdrawFundsReply> responseObserver) {
|
||||||
try {
|
try {
|
||||||
//TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI)
|
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), req.getMemo());
|
||||||
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null);
|
|
||||||
var reply = WithdrawFundsReply.newBuilder().build();
|
var reply = WithdrawFundsReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException | IllegalArgumentException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
package bisq.daemon.grpc;
|
||||||
|
|
||||||
import bisq.core.api.CoreApi;
|
import bisq.core.api.CoreApi;
|
||||||
|
@ -6,23 +23,64 @@ import bisq.proto.grpc.GetVersionGrpc;
|
||||||
import bisq.proto.grpc.GetVersionReply;
|
import bisq.proto.grpc.GetVersionReply;
|
||||||
import bisq.proto.grpc.GetVersionRequest;
|
import bisq.proto.grpc.GetVersionRequest;
|
||||||
|
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
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 CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcVersionService(CoreApi coreApi) {
|
public GrpcVersionService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void getVersion(GetVersionRequest req, StreamObserver<GetVersionReply> responseObserver) {
|
public void getVersion(GetVersionRequest req, StreamObserver<GetVersionReply> responseObserver) {
|
||||||
|
try {
|
||||||
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
|
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final ServerInterceptor[] interceptors() {
|
||||||
|
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
|
||||||
|
return rateMeteringInterceptor.map(serverInterceptor ->
|
||||||
|
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Optional<ServerInterceptor> rateMeteringInterceptor() {
|
||||||
|
@SuppressWarnings("unused") // Defined as a usage example.
|
||||||
|
CallRateMeteringInterceptor defaultCallRateMeteringInterceptor =
|
||||||
|
new CallRateMeteringInterceptor(new HashMap<>() {{
|
||||||
|
put("getVersion", new GrpcCallRateMeter(100, SECONDS));
|
||||||
|
}});
|
||||||
|
|
||||||
|
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
||||||
|
.or(Optional::empty /* Optional.of(defaultCallRateMeteringInterceptor) */);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ import bisq.proto.grpc.GetBalancesReply;
|
||||||
import bisq.proto.grpc.GetBalancesRequest;
|
import bisq.proto.grpc.GetBalancesRequest;
|
||||||
import bisq.proto.grpc.GetFundingAddressesReply;
|
import bisq.proto.grpc.GetFundingAddressesReply;
|
||||||
import bisq.proto.grpc.GetFundingAddressesRequest;
|
import bisq.proto.grpc.GetFundingAddressesRequest;
|
||||||
|
import bisq.proto.grpc.GetTransactionReply;
|
||||||
|
import bisq.proto.grpc.GetTransactionRequest;
|
||||||
import bisq.proto.grpc.GetTxFeeRateReply;
|
import bisq.proto.grpc.GetTxFeeRateReply;
|
||||||
import bisq.proto.grpc.GetTxFeeRateRequest;
|
import bisq.proto.grpc.GetTxFeeRateRequest;
|
||||||
import bisq.proto.grpc.GetUnusedBsqAddressReply;
|
import bisq.proto.grpc.GetUnusedBsqAddressReply;
|
||||||
|
@ -39,6 +41,8 @@ import bisq.proto.grpc.RemoveWalletPasswordReply;
|
||||||
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
||||||
import bisq.proto.grpc.SendBsqReply;
|
import bisq.proto.grpc.SendBsqReply;
|
||||||
import bisq.proto.grpc.SendBsqRequest;
|
import bisq.proto.grpc.SendBsqRequest;
|
||||||
|
import bisq.proto.grpc.SendBtcReply;
|
||||||
|
import bisq.proto.grpc.SendBtcRequest;
|
||||||
import bisq.proto.grpc.SetTxFeeRatePreferenceReply;
|
import bisq.proto.grpc.SetTxFeeRatePreferenceReply;
|
||||||
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
|
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
|
||||||
import bisq.proto.grpc.SetWalletPasswordReply;
|
import bisq.proto.grpc.SetWalletPasswordReply;
|
||||||
|
@ -49,27 +53,33 @@ import bisq.proto.grpc.UnsetTxFeeRatePreferenceReply;
|
||||||
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
||||||
import bisq.proto.grpc.WalletsGrpc;
|
import bisq.proto.grpc.WalletsGrpc;
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import static bisq.core.api.model.TxInfo.toTxInfo;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
|
|
||||||
private final CoreApi coreApi;
|
private final CoreApi coreApi;
|
||||||
|
private final GrpcExceptionHandler exceptionHandler;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public GrpcWalletsService(CoreApi coreApi) {
|
public GrpcWalletsService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) {
|
||||||
this.coreApi = coreApi;
|
this.coreApi = coreApi;
|
||||||
|
this.exceptionHandler = exceptionHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -81,10 +91,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,10 +105,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
.setAddressBalanceInfo(balanceInfo.toProtoMessage()).build();
|
.setAddressBalanceInfo(balanceInfo.toProtoMessage()).build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,10 +123,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,10 +138,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +147,10 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
public void sendBsq(SendBsqRequest req,
|
public void sendBsq(SendBsqRequest req,
|
||||||
StreamObserver<SendBsqReply> responseObserver) {
|
StreamObserver<SendBsqReply> responseObserver) {
|
||||||
try {
|
try {
|
||||||
coreApi.sendBsq(req.getAddress(), req.getAmount(), new TxBroadcaster.Callback() {
|
coreApi.sendBsq(req.getAddress(),
|
||||||
|
req.getAmount(),
|
||||||
|
req.getTxFeeRate(),
|
||||||
|
new TxBroadcaster.Callback() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Transaction tx) {
|
public void onSuccess(Transaction tx) {
|
||||||
log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes",
|
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.getOutputSum(),
|
||||||
tx.getFee(),
|
tx.getFee(),
|
||||||
tx.getMessageSize());
|
tx.getMessageSize());
|
||||||
var reply = SendBsqReply.newBuilder().build();
|
var reply = SendBsqReply.newBuilder()
|
||||||
|
.setTxInfo(toTxInfo(tx).toProtoMessage())
|
||||||
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
}
|
}
|
||||||
|
@ -163,10 +170,46 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
}
|
||||||
throw ex;
|
}
|
||||||
|
|
||||||
|
@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.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,10 +242,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,10 +259,23 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
});
|
});
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
}
|
||||||
throw ex;
|
}
|
||||||
|
|
||||||
|
@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();
|
var reply = SetWalletPasswordReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,10 +300,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
var reply = RemoveWalletPasswordReply.newBuilder().build();
|
var reply = RemoveWalletPasswordReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,10 +313,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
var reply = LockWalletReply.newBuilder().build();
|
var reply = LockWalletReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,10 +326,8 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
|
||||||
var reply = UnlockWalletReply.newBuilder().build();
|
var reply = UnlockWalletReply.newBuilder().build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (IllegalStateException cause) {
|
} catch (Throwable cause) {
|
||||||
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
|
exceptionHandler.handleException(cause, responseObserver);
|
||||||
responseObserver.onError(ex);
|
|
||||||
throw ex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.daemon.grpc.interceptor;
|
||||||
|
|
||||||
|
import io.grpc.Metadata;
|
||||||
|
import io.grpc.ServerCall;
|
||||||
|
import io.grpc.ServerCallHandler;
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
import io.grpc.StatusRuntimeException;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static io.grpc.Status.PERMISSION_DENIED;
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public final class CallRateMeteringInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
|
// Maps the gRPC server method names to rate meters. This allows one interceptor
|
||||||
|
// instance to handle rate metering for any or all the methods in a Grpc*Service.
|
||||||
|
protected final Map<String, GrpcCallRateMeter> serviceCallRateMeters;
|
||||||
|
|
||||||
|
public CallRateMeteringInterceptor(Map<String, GrpcCallRateMeter> serviceCallRateMeters) {
|
||||||
|
this.serviceCallRateMeters = serviceCallRateMeters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
|
||||||
|
Metadata headers,
|
||||||
|
ServerCallHandler<ReqT, RespT> serverCallHandler) {
|
||||||
|
Optional<Map.Entry<String, GrpcCallRateMeter>> rateMeterKV = getRateMeterKV(serverCall);
|
||||||
|
rateMeterKV.ifPresentOrElse(
|
||||||
|
(kv) -> checkRateMeterAndMaybeCloseCall(kv, serverCall),
|
||||||
|
() -> handleMissingRateMeterConfiguration(serverCall));
|
||||||
|
|
||||||
|
// We leave it to the gRPC framework to clean up if the server call was closed
|
||||||
|
// above. But we still have to invoke startCall here because the method must
|
||||||
|
// return a ServerCall.Listener<RequestT>.
|
||||||
|
return serverCallHandler.startCall(serverCall, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkRateMeterAndMaybeCloseCall(Map.Entry<String, GrpcCallRateMeter> rateMeterKV,
|
||||||
|
ServerCall<?, ?> serverCall) {
|
||||||
|
String methodName = rateMeterKV.getKey();
|
||||||
|
GrpcCallRateMeter rateMeter = rateMeterKV.getValue();
|
||||||
|
|
||||||
|
if (!rateMeter.checkAndIncrement())
|
||||||
|
handlePermissionDeniedWarningAndCloseCall(methodName, rateMeter, serverCall);
|
||||||
|
else
|
||||||
|
log.info(rateMeter.getCallsCountProgress(methodName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMissingRateMeterConfiguration(ServerCall<?, ?> serverCall)
|
||||||
|
throws StatusRuntimeException {
|
||||||
|
log.debug("The gRPC service's call rate metering interceptor does not"
|
||||||
|
+ " meter the {} method.",
|
||||||
|
getRateMeterKey(serverCall));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePermissionDeniedWarningAndCloseCall(String methodName,
|
||||||
|
GrpcCallRateMeter rateMeter,
|
||||||
|
ServerCall<?, ?> serverCall)
|
||||||
|
throws StatusRuntimeException {
|
||||||
|
String msg = getDefaultRateExceededError(methodName, rateMeter);
|
||||||
|
log.warn(StringUtils.capitalize(msg) + ".");
|
||||||
|
serverCall.close(PERMISSION_DENIED.withDescription(msg), new Metadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDefaultRateExceededError(String methodName,
|
||||||
|
GrpcCallRateMeter rateMeter) {
|
||||||
|
// The derived method name may not be an exact match to CLI's method name.
|
||||||
|
String timeUnitName = StringUtils.chop(rateMeter.getTimeUnit().name().toLowerCase());
|
||||||
|
return format("the maximum allowed number of %s calls (%d/%s) has been exceeded",
|
||||||
|
methodName.toLowerCase(),
|
||||||
|
rateMeter.getAllowedCallsPerTimeWindow(),
|
||||||
|
timeUnitName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Map.Entry<String, GrpcCallRateMeter>> getRateMeterKV(ServerCall<?, ?> serverCall) {
|
||||||
|
String rateMeterKey = getRateMeterKey(serverCall);
|
||||||
|
return serviceCallRateMeters.entrySet().stream()
|
||||||
|
.filter((e) -> e.getKey().equals(rateMeterKey)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getRateMeterKey(ServerCall<?, ?> serverCall) {
|
||||||
|
// Get the rate meter map key from the full rpc service name. The key name
|
||||||
|
// is hard coded in the Grpc*Service interceptors() method.
|
||||||
|
String fullServiceName = serverCall.getMethodDescriptor().getServiceName();
|
||||||
|
return StringUtils.uncapitalize(Objects.requireNonNull(fullServiceName)
|
||||||
|
.substring("io.bisq.protobuffer.".length()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String rateMetersString =
|
||||||
|
serviceCallRateMeters.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(Object::toString)
|
||||||
|
.collect(joining("\n\t\t"));
|
||||||
|
return "CallRateMeteringInterceptor {" + "\n\t" +
|
||||||
|
"serviceCallRateMeters {" + "\n\t\t" +
|
||||||
|
rateMetersString + "\n\t" + "}" + "\n"
|
||||||
|
+ "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package bisq.daemon.grpc.interceptor;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static java.lang.System.currentTimeMillis;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class GrpcCallRateMeter {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private final int allowedCallsPerTimeWindow;
|
||||||
|
@Getter
|
||||||
|
private final TimeUnit timeUnit;
|
||||||
|
@Getter
|
||||||
|
private final int numTimeUnits;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private transient final long timeUnitIntervalInMilliseconds;
|
||||||
|
|
||||||
|
private transient final ArrayDeque<Long> callTimestamps;
|
||||||
|
|
||||||
|
public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit) {
|
||||||
|
this(allowedCallsPerTimeWindow, timeUnit, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GrpcCallRateMeter(int allowedCallsPerTimeWindow, TimeUnit timeUnit, int numTimeUnits) {
|
||||||
|
this.allowedCallsPerTimeWindow = allowedCallsPerTimeWindow;
|
||||||
|
this.timeUnit = timeUnit;
|
||||||
|
this.numTimeUnits = numTimeUnits;
|
||||||
|
this.timeUnitIntervalInMilliseconds = timeUnit.toMillis(1) * numTimeUnits;
|
||||||
|
this.callTimestamps = new ArrayDeque<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkAndIncrement() {
|
||||||
|
if (getCallsCount() < allowedCallsPerTimeWindow) {
|
||||||
|
incrementCallsCount();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCallsCount() {
|
||||||
|
removeStaleCallTimestamps();
|
||||||
|
return callTimestamps.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCallsCountProgress(String calledMethodName) {
|
||||||
|
String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase());
|
||||||
|
return format("%s has been called %d time%s in the last %s, rate limit is %d/%s",
|
||||||
|
calledMethodName,
|
||||||
|
callTimestamps.size(),
|
||||||
|
callTimestamps.size() == 1 ? "" : "s",
|
||||||
|
shortTimeUnitName,
|
||||||
|
allowedCallsPerTimeWindow,
|
||||||
|
shortTimeUnitName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementCallsCount() {
|
||||||
|
callTimestamps.add(currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeStaleCallTimestamps() {
|
||||||
|
while (!callTimestamps.isEmpty() && isStale.test(callTimestamps.peek())) {
|
||||||
|
callTimestamps.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Predicate<Long> isStale = (t) -> {
|
||||||
|
long stale = currentTimeMillis() - this.getTimeUnitIntervalInMilliseconds();
|
||||||
|
// Is the given timestamp before the current time minus 1 timeUnit in millis?
|
||||||
|
return t < stale;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GrpcCallRateMeter{" +
|
||||||
|
"allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow +
|
||||||
|
", timeUnit=" + timeUnit.name() +
|
||||||
|
", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds +
|
||||||
|
", callsCount=" + callTimestamps.size() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,287 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.daemon.grpc.interceptor;
|
||||||
|
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import static bisq.common.file.FileUtil.deleteFileIfExists;
|
||||||
|
import static bisq.common.file.FileUtil.renameFile;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static java.lang.System.getProperty;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
import static java.nio.file.Files.readAllBytes;
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@Slf4j
|
||||||
|
public class GrpcServiceRateMeteringConfig {
|
||||||
|
|
||||||
|
public static final String RATE_METERS_CONFIG_FILENAME = "ratemeters.json";
|
||||||
|
|
||||||
|
private static final String KEY_GRPC_SERVICE_CLASS_NAME = "grpcServiceClassName";
|
||||||
|
private static final String KEY_METHOD_RATE_METERS = "methodRateMeters";
|
||||||
|
private static final String KEY_ALLOWED_CALL_PER_TIME_WINDOW = "allowedCallsPerTimeWindow";
|
||||||
|
private static final String KEY_TIME_UNIT = "timeUnit";
|
||||||
|
private static final String KEY_NUM_TIME_UNITS = "numTimeUnits";
|
||||||
|
|
||||||
|
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
|
||||||
|
private final List<Map<String, GrpcCallRateMeter>> methodRateMeters;
|
||||||
|
private final String grpcServiceClassName;
|
||||||
|
|
||||||
|
public GrpcServiceRateMeteringConfig(String grpcServiceClassName) {
|
||||||
|
this(grpcServiceClassName, new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GrpcServiceRateMeteringConfig(String grpcServiceClassName,
|
||||||
|
List<Map<String, GrpcCallRateMeter>> methodRateMeters) {
|
||||||
|
this.grpcServiceClassName = grpcServiceClassName;
|
||||||
|
this.methodRateMeters = methodRateMeters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
|
||||||
|
int maxCalls,
|
||||||
|
TimeUnit timeUnit) {
|
||||||
|
return addMethodCallRateMeter(methodName, maxCalls, timeUnit, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GrpcServiceRateMeteringConfig addMethodCallRateMeter(String methodName,
|
||||||
|
int maxCalls,
|
||||||
|
TimeUnit timeUnit,
|
||||||
|
int numTimeUnits) {
|
||||||
|
methodRateMeters.add(new LinkedHashMap<>() {{
|
||||||
|
put(methodName, new GrpcCallRateMeter(maxCalls, timeUnit, numTimeUnits));
|
||||||
|
}});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfigForGrpcService(Class<?> clazz) {
|
||||||
|
return isConfigForGrpcService(clazz.getSimpleName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isConfigForGrpcService(String grpcServiceClassSimpleName) {
|
||||||
|
return this.grpcServiceClassName.equals(grpcServiceClassSimpleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GrpcServiceRateMeteringConfig{" + "\n" +
|
||||||
|
" grpcServiceClassName='" + grpcServiceClassName + '\'' + "\n" +
|
||||||
|
", methodRateMeters=" + methodRateMeters + "\n" +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<ServerInterceptor> getCustomRateMeteringInterceptor(File installationDir,
|
||||||
|
Class<?> grpcServiceClass) {
|
||||||
|
File configFile = new File(installationDir, RATE_METERS_CONFIG_FILENAME);
|
||||||
|
return configFile.exists()
|
||||||
|
? toServerInterceptor(configFile, grpcServiceClass)
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Optional<ServerInterceptor> toServerInterceptor(File configFile, Class<?> grpcServiceClass) {
|
||||||
|
// From a global rate metering config file, create a specific gRPC service
|
||||||
|
// interceptor configuration in the form of an interceptor constructor argument,
|
||||||
|
// a map<method-name, rate-meter>.
|
||||||
|
// Transforming json into the List<Map<String, GrpcCallRateMeter>> is a bit
|
||||||
|
// convoluted due to Gson's loss of generic type information during deserialization.
|
||||||
|
Optional<GrpcServiceRateMeteringConfig> grpcServiceConfig = getAllDeserializedConfigs(configFile)
|
||||||
|
.stream().filter(x -> x.isConfigForGrpcService(grpcServiceClass)).findFirst();
|
||||||
|
if (grpcServiceConfig.isPresent()) {
|
||||||
|
Map<String, GrpcCallRateMeter> serviceCallRateMeters = new HashMap<>();
|
||||||
|
for (Map<String, GrpcCallRateMeter> methodToRateMeterMap : grpcServiceConfig.get().methodRateMeters) {
|
||||||
|
Map.Entry<String, GrpcCallRateMeter> entry = methodToRateMeterMap.entrySet().stream().findFirst().orElseThrow(()
|
||||||
|
-> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map."));
|
||||||
|
serviceCallRateMeters.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
return Optional.of(new CallRateMeteringInterceptor(serviceCallRateMeters));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static List<Map<String, GrpcCallRateMeter>> getMethodRateMetersMap(Map<String, Object> gsonMap) {
|
||||||
|
List<Map<String, GrpcCallRateMeter>> rateMeters = new ArrayList<>();
|
||||||
|
// Each gsonMap is a Map<String, Object> with a single entry:
|
||||||
|
// {getVersion={allowedCallsPerTimeUnit=8.0, timeUnit=SECONDS, callsCount=0.0, isRunning=false}}
|
||||||
|
// Convert it to a multiple entry Map<String, GrpcCallRateMeter>, where the key
|
||||||
|
// is a method name.
|
||||||
|
for (Map<String, Object> singleEntryRateMeterMap : (List<Map<String, Object>>) gsonMap.get(KEY_METHOD_RATE_METERS)) {
|
||||||
|
log.debug("Gson's single entry {} {}<String, Object> = {}",
|
||||||
|
gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME),
|
||||||
|
singleEntryRateMeterMap.getClass().getSimpleName(),
|
||||||
|
singleEntryRateMeterMap);
|
||||||
|
Map.Entry<String, Object> entry = singleEntryRateMeterMap.entrySet().stream().findFirst().orElseThrow(()
|
||||||
|
-> new IllegalStateException("Gson deserialized a method rate meter configuration into an empty map."));
|
||||||
|
String methodName = entry.getKey();
|
||||||
|
GrpcCallRateMeter rateMeter = getGrpcCallRateMeter(entry);
|
||||||
|
rateMeters.add(new LinkedHashMap<>() {{
|
||||||
|
put(methodName, rateMeter);
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
return rateMeters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
public static List<GrpcServiceRateMeteringConfig> deserialize(File configFile) {
|
||||||
|
verifyConfigFile(configFile);
|
||||||
|
List<GrpcServiceRateMeteringConfig> serviceMethodConfigurations = new ArrayList<>();
|
||||||
|
// Gson cannot deserialize a json string to List<GrpcServiceRateMeteringConfig>
|
||||||
|
// so easily for us, so we do it here before returning the list of configurations.
|
||||||
|
List rawConfigList = gson.fromJson(toJson(configFile), ArrayList.class);
|
||||||
|
// Gson gave us a list of maps with keys grpcServiceClassName, methodRateMeters:
|
||||||
|
// String grpcServiceClassName
|
||||||
|
// List<Map> methodRateMeters
|
||||||
|
for (Object rawConfig : rawConfigList) {
|
||||||
|
Map<String, Object> gsonMap = (Map<String, Object>) rawConfig;
|
||||||
|
String grpcServiceClassName = (String) gsonMap.get(KEY_GRPC_SERVICE_CLASS_NAME);
|
||||||
|
List<Map<String, GrpcCallRateMeter>> rateMeters = getMethodRateMetersMap(gsonMap);
|
||||||
|
serviceMethodConfigurations.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName, rateMeters));
|
||||||
|
}
|
||||||
|
return serviceMethodConfigurations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static GrpcCallRateMeter getGrpcCallRateMeter(Map.Entry<String, Object> gsonEntry) {
|
||||||
|
Map<String, Object> valueMap = (Map<String, Object>) gsonEntry.getValue();
|
||||||
|
int allowedCallsPerTimeWindow = ((Number) valueMap.get(KEY_ALLOWED_CALL_PER_TIME_WINDOW)).intValue();
|
||||||
|
TimeUnit timeUnit = TimeUnit.valueOf((String) valueMap.get(KEY_TIME_UNIT));
|
||||||
|
int numTimeUnits = ((Number) valueMap.get(KEY_NUM_TIME_UNITS)).intValue();
|
||||||
|
return new GrpcCallRateMeter(allowedCallsPerTimeWindow, timeUnit, numTimeUnits);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyConfigFile(File configFile) {
|
||||||
|
if (configFile == null)
|
||||||
|
throw new IllegalStateException("Cannot read null json config file.");
|
||||||
|
|
||||||
|
if (!configFile.exists())
|
||||||
|
throw new IllegalStateException(format("cannot find json config file %s", configFile.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toJson(File configFile) {
|
||||||
|
try {
|
||||||
|
return new String(readAllBytes(Paths.get(configFile.getAbsolutePath())));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(format("Cannot read json string from file %s.",
|
||||||
|
configFile.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<GrpcServiceRateMeteringConfig> allDeserializedConfigs;
|
||||||
|
|
||||||
|
private static List<GrpcServiceRateMeteringConfig> getAllDeserializedConfigs(File configFile) {
|
||||||
|
// We deserialize once, not for each gRPC service wanting an interceptor.
|
||||||
|
if (allDeserializedConfigs == null)
|
||||||
|
allDeserializedConfigs = deserialize(configFile);
|
||||||
|
|
||||||
|
return allDeserializedConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static class Builder {
|
||||||
|
private final List<GrpcServiceRateMeteringConfig> rateMeterConfigs = new ArrayList<>();
|
||||||
|
|
||||||
|
public void addCallRateMeter(String grpcServiceClassName,
|
||||||
|
String methodName,
|
||||||
|
int maxCalls,
|
||||||
|
TimeUnit timeUnit) {
|
||||||
|
addCallRateMeter(grpcServiceClassName,
|
||||||
|
methodName,
|
||||||
|
maxCalls,
|
||||||
|
timeUnit,
|
||||||
|
1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallRateMeter(String grpcServiceClassName,
|
||||||
|
String methodName,
|
||||||
|
int maxCalls,
|
||||||
|
TimeUnit timeUnit,
|
||||||
|
int numTimeUnits) {
|
||||||
|
log.info("Adding call rate metering definition {}.{} ({}/{}ms).",
|
||||||
|
grpcServiceClassName,
|
||||||
|
methodName,
|
||||||
|
maxCalls,
|
||||||
|
timeUnit.toMillis(1) * numTimeUnits);
|
||||||
|
rateMeterConfigs.stream().filter(c -> c.isConfigForGrpcService(grpcServiceClassName))
|
||||||
|
.findFirst().ifPresentOrElse(
|
||||||
|
(config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits),
|
||||||
|
() -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName)
|
||||||
|
.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public File build() {
|
||||||
|
File tmpFile = serializeRateMeterDefinitions();
|
||||||
|
File configFile = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toFile();
|
||||||
|
try {
|
||||||
|
deleteFileIfExists(configFile);
|
||||||
|
renameFile(tmpFile, configFile);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(format("Could not create config file %s.",
|
||||||
|
configFile.getAbsolutePath()), ex);
|
||||||
|
}
|
||||||
|
return configFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File serializeRateMeterDefinitions() {
|
||||||
|
String json = gson.toJson(rateMeterConfigs);
|
||||||
|
File file = createTmpFile();
|
||||||
|
try (OutputStreamWriter outputStreamWriter =
|
||||||
|
new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) {
|
||||||
|
outputStreamWriter.write(json);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
throw new IllegalStateException(format("Cannot write file for json string %s.", json), ex);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File createTmpFile() {
|
||||||
|
File file;
|
||||||
|
try {
|
||||||
|
file = File.createTempFile("ratemeters_",
|
||||||
|
".tmp",
|
||||||
|
Paths.get(getProperty("java.io.tmpdir")).toFile());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException("Cannot create tmp ratemeters json file.", ex);
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
* 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;
|
import bisq.common.config.Config;
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ import static java.lang.String.format;
|
||||||
*
|
*
|
||||||
* @see bisq.common.config.Config#apiPassword
|
* @see bisq.common.config.Config#apiPassword
|
||||||
*/
|
*/
|
||||||
class PasswordAuthInterceptor implements ServerInterceptor {
|
public class PasswordAuthInterceptor implements ServerInterceptor {
|
||||||
|
|
||||||
private static final String PASSWORD_KEY = "password";
|
private static final String PASSWORD_KEY = "password";
|
||||||
|
|
||||||
|
@ -50,7 +50,8 @@ class PasswordAuthInterceptor implements ServerInterceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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) {
|
ServerCallHandler<ReqT, RespT> serverCallHandler) {
|
||||||
var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER));
|
var actualPasswordValue = headers.get(Key.of(PASSWORD_KEY, ASCII_STRING_MARSHALLER));
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Bisq.
|
||||||
|
*
|
||||||
|
* Bisq is free software: you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or (at
|
||||||
|
* your option) any later version.
|
||||||
|
*
|
||||||
|
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||||
|
* License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bisq.daemon.grpc.interceptor;
|
||||||
|
|
||||||
|
import io.grpc.ServerInterceptor;
|
||||||
|
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
|
||||||
|
import static java.lang.System.getProperty;
|
||||||
|
import static java.util.concurrent.TimeUnit.DAYS;
|
||||||
|
import static java.util.concurrent.TimeUnit.HOURS;
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import bisq.daemon.grpc.GrpcVersionService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class GrpcServiceRateMeteringConfigTest {
|
||||||
|
|
||||||
|
private static final GrpcServiceRateMeteringConfig.Builder builder = new GrpcServiceRateMeteringConfig.Builder();
|
||||||
|
private static File configFile;
|
||||||
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
|
private static Optional<ServerInterceptor> versionServiceInterceptor;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setup() {
|
||||||
|
// This is the tested rate meter, it allows 3 calls every 2 seconds.
|
||||||
|
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
|
||||||
|
"getVersion",
|
||||||
|
3,
|
||||||
|
SECONDS,
|
||||||
|
2);
|
||||||
|
builder.addCallRateMeter(GrpcVersionService.class.getSimpleName(),
|
||||||
|
"badMethodNameDoesNotBreakAnything",
|
||||||
|
100,
|
||||||
|
DAYS);
|
||||||
|
// The other Grpc*Service classes are not @VisibleForTesting, so we hardcode
|
||||||
|
// the simple class name.
|
||||||
|
builder.addCallRateMeter("GrpcOffersService",
|
||||||
|
"createOffer",
|
||||||
|
5,
|
||||||
|
MINUTES);
|
||||||
|
builder.addCallRateMeter("GrpcOffersService",
|
||||||
|
"takeOffer",
|
||||||
|
10,
|
||||||
|
DAYS);
|
||||||
|
builder.addCallRateMeter("GrpcWalletsService",
|
||||||
|
"sendBtc",
|
||||||
|
3,
|
||||||
|
HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void buildConfigFile() {
|
||||||
|
if (configFile == null)
|
||||||
|
configFile = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConfigFileBuild() {
|
||||||
|
assertNotNull(configFile);
|
||||||
|
assertTrue(configFile.exists());
|
||||||
|
assertTrue(configFile.length() > 0);
|
||||||
|
String expectedConfigFilePath = Paths.get(getProperty("java.io.tmpdir"), "ratemeters.json").toString();
|
||||||
|
assertEquals(expectedConfigFilePath, configFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetVersionCallRateMeter() {
|
||||||
|
// Check the interceptor has 2 rate meters, for getVersion and badMethodNameDoesNotBreakAnything.
|
||||||
|
CallRateMeteringInterceptor versionServiceInterceptor = buildInterceptor();
|
||||||
|
assertEquals(2, versionServiceInterceptor.serviceCallRateMeters.size());
|
||||||
|
|
||||||
|
// Check the rate meter config.
|
||||||
|
GrpcCallRateMeter rateMeter = versionServiceInterceptor.serviceCallRateMeters.get("getVersion");
|
||||||
|
assertEquals(3, rateMeter.getAllowedCallsPerTimeWindow());
|
||||||
|
assertEquals(SECONDS, rateMeter.getTimeUnit());
|
||||||
|
assertEquals(2, rateMeter.getNumTimeUnits());
|
||||||
|
assertEquals(2 * 1000, rateMeter.getTimeUnitIntervalInMilliseconds());
|
||||||
|
|
||||||
|
// Do as many calls as allowed within rateMeter.getTimeUnitIntervalInMilliseconds().
|
||||||
|
doMaxIsAllowedChecks(true,
|
||||||
|
rateMeter.getAllowedCallsPerTimeWindow(),
|
||||||
|
rateMeter);
|
||||||
|
|
||||||
|
// The next 3 calls are blocked because we've exceeded the 3calls/2s limit.
|
||||||
|
doMaxIsAllowedChecks(false,
|
||||||
|
rateMeter.getAllowedCallsPerTimeWindow(),
|
||||||
|
rateMeter);
|
||||||
|
|
||||||
|
// Let all of the rate meter's cached call timestamps become stale by waiting for
|
||||||
|
// 2001 ms, then we can call getversion another 'allowedCallsPerTimeUnit' times.
|
||||||
|
rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds());
|
||||||
|
// All the stale call timestamps are gone and the call count is back to zero.
|
||||||
|
assertEquals(0, rateMeter.getCallsCount());
|
||||||
|
|
||||||
|
doMaxIsAllowedChecks(true,
|
||||||
|
rateMeter.getAllowedCallsPerTimeWindow(),
|
||||||
|
rateMeter);
|
||||||
|
// We've exceeded the call/second limit.
|
||||||
|
assertFalse(rateMeter.checkAndIncrement());
|
||||||
|
|
||||||
|
// Let all of the call timestamps go stale again by waiting for 2001 ms.
|
||||||
|
rest(1 + rateMeter.getTimeUnitIntervalInMilliseconds());
|
||||||
|
|
||||||
|
// Call twice, resting 0.5s after each call.
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
assertTrue(rateMeter.checkAndIncrement());
|
||||||
|
rest(500);
|
||||||
|
}
|
||||||
|
// Call the 3rd time, then let one of the rate meter's timestamps go stale.
|
||||||
|
assertTrue(rateMeter.checkAndIncrement());
|
||||||
|
rest(1001);
|
||||||
|
|
||||||
|
// The call count was decremented by one because one timestamp went stale.
|
||||||
|
assertEquals(2, rateMeter.getCallsCount());
|
||||||
|
assertTrue(rateMeter.checkAndIncrement());
|
||||||
|
assertEquals(rateMeter.getAllowedCallsPerTimeWindow(), rateMeter.getCallsCount());
|
||||||
|
|
||||||
|
// We've exceeded the call limit again.
|
||||||
|
assertFalse(rateMeter.checkAndIncrement());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doMaxIsAllowedChecks(boolean expectedIsAllowed,
|
||||||
|
int expectedCallsCount,
|
||||||
|
GrpcCallRateMeter rateMeter) {
|
||||||
|
for (int i = 1; i <= rateMeter.getAllowedCallsPerTimeWindow(); i++) {
|
||||||
|
assertEquals(expectedIsAllowed, rateMeter.checkAndIncrement());
|
||||||
|
}
|
||||||
|
assertEquals(expectedCallsCount, rateMeter.getCallsCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void teardown() {
|
||||||
|
if (configFile != null)
|
||||||
|
configFile.deleteOnExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rest(long milliseconds) {
|
||||||
|
try {
|
||||||
|
TimeUnit.MILLISECONDS.sleep(milliseconds);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallRateMeteringInterceptor buildInterceptor() {
|
||||||
|
//noinspection OptionalAssignedToNull
|
||||||
|
if (versionServiceInterceptor == null) {
|
||||||
|
versionServiceInterceptor = getCustomRateMeteringInterceptor(
|
||||||
|
configFile.getParentFile(),
|
||||||
|
GrpcVersionService.class);
|
||||||
|
}
|
||||||
|
assertTrue(versionServiceInterceptor.isPresent());
|
||||||
|
return (CallRateMeteringInterceptor) versionServiceInterceptor.get();
|
||||||
|
}
|
||||||
|
}
|
|
@ -247,7 +247,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
|
||||||
Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
|
Coin receiverAmount = ParsingUtils.parseToCoin(amountInputTextField.getText(), bsqFormatter);
|
||||||
try {
|
try {
|
||||||
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount);
|
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(receiversAddressString, receiverAmount);
|
||||||
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
|
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
|
||||||
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
|
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
|
||||||
Coin miningFee = signedTx.getFee();
|
Coin miningFee = signedTx.getFee();
|
||||||
int txVsize = signedTx.getVsize();
|
int txVsize = signedTx.getVsize();
|
||||||
|
@ -305,7 +305,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
|
||||||
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
|
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
|
||||||
try {
|
try {
|
||||||
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount);
|
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount);
|
||||||
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
|
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx);
|
||||||
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
|
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
|
||||||
Coin miningFee = signedTx.getFee();
|
Coin miningFee = signedTx.getFee();
|
||||||
|
|
||||||
|
|
|
@ -255,6 +255,7 @@ message KeepFundsReply {
|
||||||
message WithdrawFundsRequest {
|
message WithdrawFundsRequest {
|
||||||
string tradeId = 1;
|
string tradeId = 1;
|
||||||
string address = 2;
|
string address = 2;
|
||||||
|
string memo = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WithdrawFundsReply {
|
message WithdrawFundsReply {
|
||||||
|
@ -287,6 +288,27 @@ message TradeInfo {
|
||||||
string contractAsJson = 24;
|
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
|
// Wallets
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -300,11 +322,15 @@ service Wallets {
|
||||||
}
|
}
|
||||||
rpc SendBsq (SendBsqRequest) returns (SendBsqReply) {
|
rpc SendBsq (SendBsqRequest) returns (SendBsqReply) {
|
||||||
}
|
}
|
||||||
|
rpc SendBtc (SendBtcRequest) returns (SendBtcReply) {
|
||||||
|
}
|
||||||
rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) {
|
rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) {
|
||||||
}
|
}
|
||||||
rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) {
|
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) {
|
rpc GetFundingAddresses (GetFundingAddressesRequest) returns (GetFundingAddressesReply) {
|
||||||
}
|
}
|
||||||
|
@ -344,9 +370,22 @@ message GetUnusedBsqAddressReply {
|
||||||
message SendBsqRequest {
|
message SendBsqRequest {
|
||||||
string address = 1;
|
string address = 1;
|
||||||
string amount = 2;
|
string amount = 2;
|
||||||
|
string txFeeRate = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SendBsqReply {
|
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 {
|
message GetTxFeeRateRequest {
|
||||||
|
@ -371,6 +410,14 @@ message UnsetTxFeeRatePreferenceReply {
|
||||||
TxFeeRateInfo txFeeRateInfo = 1;
|
TxFeeRateInfo txFeeRateInfo = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetTransactionRequest {
|
||||||
|
string txId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetTransactionReply {
|
||||||
|
TxInfo txInfo = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetFundingAddressesRequest {
|
message GetFundingAddressesRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,13 +484,6 @@ message AddressBalanceInfo {
|
||||||
int64 numConfirmations = 3;
|
int64 numConfirmations = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TxFeeRateInfo {
|
|
||||||
bool useCustomTxFeeRate = 1;
|
|
||||||
uint64 customTxFeeRate = 2;
|
|
||||||
uint64 feeServiceRate = 3;
|
|
||||||
uint64 lastFeeServiceRequestTs = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Version
|
// Version
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Add table
Reference in a new issue