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