mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
Merge remote-tracking branch 'bisq-network/release/v1.5.0' into upgrade-javafax-14
This commit is contained in:
commit
72a719dcc9
@ -14,7 +14,7 @@
|
||||
#
|
||||
# This script must be run from the root of the project, e.g.:
|
||||
#
|
||||
# ./cli/test.sh
|
||||
# bats apitest/scripts/mainnet-test.sh
|
||||
|
||||
@test "test unsupported method error" {
|
||||
run ./bisq-cli --password=xyz bogus
|
||||
|
@ -70,10 +70,8 @@ abstract class AbstractLinuxProcess implements LinuxProcess {
|
||||
|
||||
@Override
|
||||
public void logExceptions(List<Throwable> exceptions, org.slf4j.Logger log) {
|
||||
StringBuilder errorBuilder = new StringBuilder();
|
||||
for (Throwable t : exceptions) {
|
||||
log.error("", t);
|
||||
errorBuilder.append(t.getMessage()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,8 @@ import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
@ -105,11 +107,22 @@ public class ApiTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
protected void sleep(long ms) {
|
||||
protected static void genBtcBlocksThenWait(int numBlocks, long wait) {
|
||||
bitcoinCli.generateBlocks(numBlocks);
|
||||
sleep(wait);
|
||||
}
|
||||
|
||||
protected static void sleep(long ms) {
|
||||
try {
|
||||
MILLISECONDS.sleep(ms);
|
||||
} catch (InterruptedException ignored) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
|
||||
protected final String testName(TestInfo testInfo) {
|
||||
return testInfo.getTestMethod().isPresent()
|
||||
? testInfo.getTestMethod().get().getName()
|
||||
: "unknown test name";
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
@ -40,7 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class CreatePaymentAccountTest extends MethodTest {
|
||||
|
@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
@ -35,7 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class GetBalanceTest extends MethodTest {
|
||||
|
@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
@ -33,7 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class GetVersionTest extends MethodTest {
|
||||
|
@ -17,32 +17,46 @@
|
||||
|
||||
package bisq.apitest.method;
|
||||
|
||||
import bisq.proto.grpc.CancelOfferRequest;
|
||||
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
|
||||
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
|
||||
import bisq.proto.grpc.CreatePaymentAccountRequest;
|
||||
import bisq.proto.grpc.GetBalanceRequest;
|
||||
import bisq.proto.grpc.GetFundingAddressesRequest;
|
||||
import bisq.proto.grpc.GetOfferRequest;
|
||||
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
||||
import bisq.proto.grpc.GetTradeRequest;
|
||||
import bisq.proto.grpc.KeepFundsRequest;
|
||||
import bisq.proto.grpc.LockWalletRequest;
|
||||
import bisq.proto.grpc.MarketPriceRequest;
|
||||
import bisq.proto.grpc.OfferInfo;
|
||||
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
||||
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
||||
import bisq.proto.grpc.SetWalletPasswordRequest;
|
||||
import bisq.proto.grpc.TakeOfferRequest;
|
||||
import bisq.proto.grpc.TradeInfo;
|
||||
import bisq.proto.grpc.UnlockWalletRequest;
|
||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.arbdaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
|
||||
import static bisq.common.app.DevEnv.DEV_PRIVILEGE_PRIV_KEY;
|
||||
import static bisq.core.payment.payload.PaymentMethod.PERFECT_MONEY;
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.ApiTestCase;
|
||||
import bisq.apitest.config.BisqAppConfig;
|
||||
import bisq.cli.GrpcStubs;
|
||||
|
||||
public class MethodTest extends ApiTestCase {
|
||||
|
||||
@ -50,6 +64,43 @@ public class MethodTest extends ApiTestCase {
|
||||
protected static final String MEDIATOR = "mediator";
|
||||
protected static final String REFUND_AGENT = "refundagent";
|
||||
|
||||
protected static GrpcStubs aliceStubs;
|
||||
protected static GrpcStubs bobStubs;
|
||||
|
||||
protected static PaymentAccount alicesDummyAcct;
|
||||
protected static PaymentAccount bobsDummyAcct;
|
||||
|
||||
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);
|
||||
} catch (Exception ex) {
|
||||
fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for building gRPC request objects
|
||||
|
||||
protected final GetBalanceRequest createBalanceRequest() {
|
||||
@ -88,6 +139,39 @@ public class MethodTest extends ApiTestCase {
|
||||
return GetOfferRequest.newBuilder().setId(offerId).build();
|
||||
}
|
||||
|
||||
protected final CancelOfferRequest createCancelOfferRequest(String offerId) {
|
||||
return CancelOfferRequest.newBuilder().setId(offerId).build();
|
||||
}
|
||||
|
||||
protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) {
|
||||
return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build();
|
||||
}
|
||||
|
||||
protected final GetTradeRequest createGetTradeRequest(String tradeId) {
|
||||
return GetTradeRequest.newBuilder().setTradeId(tradeId).build();
|
||||
}
|
||||
|
||||
protected final ConfirmPaymentStartedRequest createConfirmPaymentStartedRequest(String tradeId) {
|
||||
return ConfirmPaymentStartedRequest.newBuilder().setTradeId(tradeId).build();
|
||||
}
|
||||
|
||||
protected final ConfirmPaymentReceivedRequest createConfirmPaymentReceivedRequest(String tradeId) {
|
||||
return ConfirmPaymentReceivedRequest.newBuilder().setTradeId(tradeId).build();
|
||||
}
|
||||
|
||||
protected final KeepFundsRequest createKeepFundsRequest(String tradeId) {
|
||||
return KeepFundsRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected final WithdrawFundsRequest createWithdrawFundsRequest(String tradeId, String address) {
|
||||
return WithdrawFundsRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setAddress(address)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Convenience methods for calling frequently used & thoroughly tested gRPC services.
|
||||
|
||||
protected final long getBalance(BisqAppConfig bisqAppConfig) {
|
||||
@ -127,7 +211,7 @@ public class MethodTest extends ApiTestCase {
|
||||
.build();
|
||||
}
|
||||
|
||||
protected final PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
|
||||
protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
|
||||
var req = GetPaymentAccountsRequest.newBuilder().build();
|
||||
var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService;
|
||||
PaymentAccount paymentAccount = paymentAccountsService.getPaymentAccounts(req)
|
||||
@ -149,6 +233,40 @@ public class MethodTest extends ApiTestCase {
|
||||
return grpcStubs(bisqAppConfig).offersService.getOffer(req).getOffer();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
protected final void cancelOffer(BisqAppConfig bisqAppConfig, String offerId) {
|
||||
var req = createCancelOfferRequest(offerId);
|
||||
grpcStubs(bisqAppConfig).offersService.cancelOffer(req);
|
||||
}
|
||||
|
||||
protected final TradeInfo getTrade(BisqAppConfig bisqAppConfig, String tradeId) {
|
||||
var req = createGetTradeRequest(tradeId);
|
||||
return grpcStubs(bisqAppConfig).tradesService.getTrade(req).getTrade();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
protected final void confirmPaymentStarted(BisqAppConfig bisqAppConfig, String tradeId) {
|
||||
var req = createConfirmPaymentStartedRequest(tradeId);
|
||||
grpcStubs(bisqAppConfig).tradesService.confirmPaymentStarted(req);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
protected final void confirmPaymentReceived(BisqAppConfig bisqAppConfig, String tradeId) {
|
||||
var req = createConfirmPaymentReceivedRequest(tradeId);
|
||||
grpcStubs(bisqAppConfig).tradesService.confirmPaymentReceived(req);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
protected final void keepFunds(BisqAppConfig bisqAppConfig, String tradeId) {
|
||||
var req = createKeepFundsRequest(tradeId);
|
||||
grpcStubs(bisqAppConfig).tradesService.keepFunds(req);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
protected final void withdrawFunds(BisqAppConfig bisqAppConfig, String tradeId, String address) {
|
||||
var req = createWithdrawFundsRequest(tradeId, address);
|
||||
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
|
||||
}
|
||||
// Static conveniences for test methods and test case fixture setups.
|
||||
|
||||
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {
|
||||
|
@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
@ -40,6 +41,7 @@ import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class RegisterDisputeAgentsTest extends MethodTest {
|
||||
@ -56,8 +58,7 @@ public class RegisterDisputeAgentsTest extends MethodTest {
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testRegisterArbitratorShouldThrowException() {
|
||||
var req =
|
||||
createRegisterDisputeAgentRequest(ARBITRATOR);
|
||||
var req = createRegisterDisputeAgentRequest(ARBITRATOR);
|
||||
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
|
||||
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
|
||||
assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI",
|
||||
@ -67,8 +68,7 @@ public class RegisterDisputeAgentsTest extends MethodTest {
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testInvalidDisputeAgentTypeArgShouldThrowException() {
|
||||
var req =
|
||||
createRegisterDisputeAgentRequest("badagent");
|
||||
var req = createRegisterDisputeAgentRequest("badagent");
|
||||
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
|
||||
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req));
|
||||
assertEquals("INVALID_ARGUMENT: unknown dispute agent type 'badagent'",
|
||||
@ -90,16 +90,14 @@ public class RegisterDisputeAgentsTest extends MethodTest {
|
||||
@Test
|
||||
@Order(4)
|
||||
public void testRegisterMediator() {
|
||||
var req =
|
||||
createRegisterDisputeAgentRequest(MEDIATOR);
|
||||
var req = createRegisterDisputeAgentRequest(MEDIATOR);
|
||||
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
public void testRegisterRefundAgent() {
|
||||
var req =
|
||||
createRegisterDisputeAgentRequest(REFUND_AGENT);
|
||||
var req = createRegisterDisputeAgentRequest(REFUND_AGENT);
|
||||
grpcStubs(arbdaemon).disputeAgentsService.registerDisputeAgent(req);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
@ -18,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
public class WalletProtectionTest extends MethodTest {
|
||||
|
@ -19,9 +19,12 @@ package bisq.apitest.method.offer;
|
||||
|
||||
import bisq.core.monetary.Altcoin;
|
||||
|
||||
import bisq.proto.grpc.CreateOfferRequest;
|
||||
import bisq.proto.grpc.GetOffersRequest;
|
||||
import bisq.proto.grpc.OfferInfo;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@ -36,14 +39,16 @@ import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
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.bobdaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.seednode;
|
||||
import static bisq.common.util.MathUtils.roundDouble;
|
||||
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
|
||||
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
|
||||
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
|
||||
import static java.lang.String.format;
|
||||
import static java.math.RoundingMode.HALF_UP;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
@ -51,49 +56,79 @@ import static org.junit.jupiter.api.Assertions.fail;
|
||||
import bisq.apitest.method.MethodTest;
|
||||
import bisq.cli.GrpcStubs;
|
||||
|
||||
|
||||
@Slf4j
|
||||
abstract class AbstractCreateOfferTest extends MethodTest {
|
||||
|
||||
protected static GrpcStubs aliceStubs;
|
||||
public abstract class AbstractOfferTest extends MethodTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
startSupportingApps();
|
||||
startSupportingApps(true,
|
||||
true,
|
||||
bitcoind,
|
||||
seednode,
|
||||
arbdaemon,
|
||||
alicedaemon,
|
||||
bobdaemon);
|
||||
}
|
||||
|
||||
static void startSupportingApps() {
|
||||
try {
|
||||
// setUpScaffold(new String[]{"--supportingApps", "bitcoind,seednode,alicedaemon", "--enableBisqDebugging", "true"});
|
||||
setUpScaffold(bitcoind, seednode, alicedaemon);
|
||||
aliceStubs = grpcStubs(alicedaemon);
|
||||
protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount,
|
||||
String direction,
|
||||
String currencyCode,
|
||||
long amount) {
|
||||
return createMarketBasedPricedOffer(aliceStubs, paymentAccount, direction, currencyCode, amount);
|
||||
}
|
||||
|
||||
// Generate 1 regtest block for alice's wallet to show 10 BTC balance,
|
||||
// and give alicedaemon time to parse the new block.
|
||||
bitcoinCli.generateBlocks(1);
|
||||
MILLISECONDS.sleep(1500);
|
||||
} catch (Exception ex) {
|
||||
fail(ex);
|
||||
}
|
||||
protected final OfferInfo createBobOffer(PaymentAccount paymentAccount,
|
||||
String direction,
|
||||
String currencyCode,
|
||||
long amount) {
|
||||
return createMarketBasedPricedOffer(bobStubs, paymentAccount, direction, currencyCode, amount);
|
||||
}
|
||||
|
||||
protected final OfferInfo createMarketBasedPricedOffer(GrpcStubs grpcStubs,
|
||||
PaymentAccount paymentAccount,
|
||||
String direction,
|
||||
String currencyCode,
|
||||
long amount) {
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setDirection(direction)
|
||||
.setCurrencyCode(currencyCode)
|
||||
.setAmount(amount)
|
||||
.setMinAmount(amount)
|
||||
.setUseMarketBasedPrice(true)
|
||||
.setMarketPriceMargin(0.00)
|
||||
.setPrice("0")
|
||||
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
|
||||
.build();
|
||||
return grpcStubs.offersService.createOffer(req).getOffer();
|
||||
}
|
||||
|
||||
protected final OfferInfo getOffer(String offerId) {
|
||||
return aliceStubs.offersService.getOffer(createGetOfferRequest(offerId)).getOffer();
|
||||
}
|
||||
|
||||
protected final OfferInfo getMostRecentOffer(String direction, String currencyCode) {
|
||||
List<OfferInfo> offerInfoList = getOffersSortedByDate(direction, currencyCode);
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
protected final void cancelOffer(GrpcStubs grpcStubs, String offerId) {
|
||||
grpcStubs.offersService.cancelOffer(createCancelOfferRequest(offerId));
|
||||
}
|
||||
|
||||
protected final OfferInfo getMostRecentOffer(GrpcStubs grpcStubs, String direction, String currencyCode) {
|
||||
List<OfferInfo> offerInfoList = getOffersSortedByDate(grpcStubs, direction, currencyCode);
|
||||
if (offerInfoList.isEmpty())
|
||||
fail(format("No %s offers found for currency %s", direction, currencyCode));
|
||||
|
||||
return offerInfoList.get(offerInfoList.size() - 1);
|
||||
}
|
||||
|
||||
protected final List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
|
||||
protected final int getOpenOffersCount(GrpcStubs grpcStubs, String direction, String currencyCode) {
|
||||
return getOffersSortedByDate(grpcStubs, direction, currencyCode).size();
|
||||
}
|
||||
|
||||
protected final List<OfferInfo> getOffersSortedByDate(GrpcStubs grpcStubs, String direction, String currencyCode) {
|
||||
var req = GetOffersRequest.newBuilder()
|
||||
.setDirection(direction)
|
||||
.setCurrencyCode(currencyCode).build();
|
||||
var reply = aliceStubs.offersService.getOffers(req);
|
||||
var reply = grpcStubs.offersService.getOffers(req);
|
||||
return sortOffersByDate(reply.getOffersList());
|
||||
}
|
||||
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.method.offer;
|
||||
|
||||
import bisq.core.btc.wallet.Restrictions;
|
||||
|
||||
import bisq.proto.grpc.CreateOfferRequest;
|
||||
import bisq.proto.grpc.OfferInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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 org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class CancelOfferTest extends AbstractOfferTest {
|
||||
|
||||
private static final int MAX_OFFERS = 3;
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testCancelOffer() {
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("buy")
|
||||
.setCurrencyCode("cad")
|
||||
.setAmount(10000000)
|
||||
.setMinAmount(10000000)
|
||||
.setUseMarketBasedPrice(true)
|
||||
.setMarketPriceMargin(0.00)
|
||||
.setPrice("0")
|
||||
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
|
||||
.build();
|
||||
|
||||
// Create some offers.
|
||||
for (int i = 1; i <= MAX_OFFERS; i++) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
aliceStubs.offersService.createOffer(req);
|
||||
// Wait for Alice's AddToOfferBook task.
|
||||
// Wait times vary; my logs show >= 2 second delay.
|
||||
sleep(2500);
|
||||
}
|
||||
|
||||
List<OfferInfo> offers = getOffersSortedByDate(aliceStubs, "buy", "cad");
|
||||
assertEquals(MAX_OFFERS, offers.size());
|
||||
|
||||
// Cancel the offers, checking the open offer count after each offer removal.
|
||||
for (int i = 1; i <= MAX_OFFERS; i++) {
|
||||
cancelOffer(aliceStubs, offers.remove(0).getId());
|
||||
assertEquals(MAX_OFFERS - i, getOpenOffersCount(aliceStubs, "buy", "cad"));
|
||||
}
|
||||
|
||||
sleep(1000); // wait for offer removal
|
||||
|
||||
offers = getOffersSortedByDate(aliceStubs, "buy", "cad");
|
||||
assertEquals(0, offers.size());
|
||||
}
|
||||
}
|
@ -23,26 +23,26 @@ import bisq.proto.grpc.CreateOfferRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.config.BisqAppConfig.alicedaemon;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("buy")
|
||||
.setCurrencyCode("aud")
|
||||
.setAmount(10000000)
|
||||
@ -61,7 +61,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("AUD", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -73,7 +73,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("AUD", newOffer.getCounterCurrencyCode());
|
||||
}
|
||||
@ -81,9 +81,8 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testCreateUSDBTCBuyOfferUsingFixedPrice100001234() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("buy")
|
||||
.setCurrencyCode("usd")
|
||||
.setAmount(10000000)
|
||||
@ -102,7 +101,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("USD", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -114,7 +113,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("USD", newOffer.getCounterCurrencyCode());
|
||||
}
|
||||
@ -122,9 +121,8 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testCreateEURBTCSellOfferUsingFixedPrice95001234() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("sell")
|
||||
.setCurrencyCode("eur")
|
||||
.setAmount(10000000)
|
||||
@ -143,7 +141,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("EUR", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -155,7 +153,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractCreateOfferTest {
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("EUR", newOffer.getCounterCurrencyCode());
|
||||
}
|
||||
|
@ -26,12 +26,12 @@ import java.text.DecimalFormat;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.config.BisqAppConfig.alicedaemon;
|
||||
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
|
||||
import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
|
||||
import static java.lang.Math.abs;
|
||||
@ -41,9 +41,10 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static protobuf.OfferPayload.Direction.BUY;
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTest {
|
||||
public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
|
||||
|
||||
private static final DecimalFormat PCT_FORMAT = new DecimalFormat("##0.00");
|
||||
private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50%
|
||||
@ -52,10 +53,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
double priceMarginPctInput = 5.00;
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("buy")
|
||||
.setCurrencyCode("usd")
|
||||
.setAmount(10000000)
|
||||
@ -73,7 +73,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("USD", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -84,7 +84,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("USD", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -94,10 +94,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testCreateNZDBTCBuyOfferMinus2PctPriceMargin() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
double priceMarginPctInput = -2.00;
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("buy")
|
||||
.setCurrencyCode("nzd")
|
||||
.setAmount(10000000)
|
||||
@ -115,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("NZD", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -126,7 +125,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("NZD", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -136,10 +135,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
double priceMarginPctInput = -1.5;
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("sell")
|
||||
.setCurrencyCode("gbp")
|
||||
.setAmount(10000000)
|
||||
@ -158,7 +156,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("GBP", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -169,7 +167,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("GBP", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -179,10 +177,9 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
@Test
|
||||
@Order(4)
|
||||
public void testCreateBRLBTCSellOffer6Point55PctPriceMargin() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
double priceMarginPctInput = 6.55;
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("sell")
|
||||
.setCurrencyCode("brl")
|
||||
.setAmount(10000000)
|
||||
@ -201,7 +198,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("BRL", newOffer.getCounterCurrencyCode());
|
||||
|
||||
@ -212,7 +209,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractCreateOfferTe
|
||||
assertEquals(10000000, newOffer.getAmount());
|
||||
assertEquals(10000000, newOffer.getMinAmount());
|
||||
assertEquals(1500000, newOffer.getBuyerSecurityDeposit());
|
||||
assertEquals(paymentAccount.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
|
||||
assertEquals("BTC", newOffer.getBaseCurrencyCode());
|
||||
assertEquals("BRL", newOffer.getCounterCurrencyCode());
|
||||
|
||||
|
@ -25,25 +25,25 @@ import io.grpc.StatusRuntimeException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.config.BisqAppConfig.alicedaemon;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class ValidateCreateOfferTest extends AbstractCreateOfferTest {
|
||||
public class ValidateCreateOfferTest extends AbstractOfferTest {
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testAmtTooLargeShouldThrowException() {
|
||||
var paymentAccount = getDefaultPerfectDummyPaymentAccount(alicedaemon);
|
||||
var req = CreateOfferRequest.newBuilder()
|
||||
.setPaymentAccountId(paymentAccount.getId())
|
||||
.setPaymentAccountId(alicesDummyAcct.getId())
|
||||
.setDirection("buy")
|
||||
.setCurrencyCode("usd")
|
||||
.setAmount(100000000000L)
|
||||
|
@ -0,0 +1,60 @@
|
||||
package bisq.apitest.method.trade;
|
||||
|
||||
import bisq.proto.grpc.TradeInfo;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
|
||||
import static bisq.cli.TradeFormat.format;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.offer.AbstractOfferTest;
|
||||
|
||||
public class AbstractTradeTest extends AbstractOfferTest {
|
||||
|
||||
public static final ExpectedProtocolStatus EXPECTED_PROTOCOL_STATUS = new ExpectedProtocolStatus();
|
||||
|
||||
// A Trade ID cache for use in @Test sequences.
|
||||
protected static String tradeId;
|
||||
|
||||
@BeforeAll
|
||||
public static void initStaticFixtures() {
|
||||
EXPECTED_PROTOCOL_STATUS.init();
|
||||
}
|
||||
|
||||
protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId) {
|
||||
return bobStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected final TradeInfo takeBobsOffer(String offerId, String paymentAccountId) {
|
||||
return aliceStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade();
|
||||
}
|
||||
|
||||
protected final void verifyExpectedProtocolStatus(TradeInfo trade) {
|
||||
assertNotNull(trade);
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.state.name(), trade.getState());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.phase.name(), trade.getPhase());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositPublished, trade.getIsDepositPublished());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isDepositConfirmed, trade.getIsDepositConfirmed());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isFiatSent, trade.getIsFiatSent());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isFiatReceived, trade.getIsFiatReceived());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isPayoutPublished, trade.getIsPayoutPublished());
|
||||
assertEquals(EXPECTED_PROTOCOL_STATUS.isWithdrawn, trade.getIsWithdrawn());
|
||||
}
|
||||
|
||||
protected final void logTrade(Logger log,
|
||||
TestInfo testInfo,
|
||||
String description,
|
||||
TradeInfo trade) {
|
||||
log.info(String.format("%s %s%n%s",
|
||||
testName(testInfo),
|
||||
description.toUpperCase(),
|
||||
format(trade)));
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package bisq.apitest.method.trade;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
/**
|
||||
* A test fixture encapsulating expected trade protocol status.
|
||||
* Status flags should be cleared via init() before starting a new trade protocol.
|
||||
*/
|
||||
public class ExpectedProtocolStatus {
|
||||
Trade.State state;
|
||||
Trade.Phase phase;
|
||||
boolean isDepositPublished;
|
||||
boolean isDepositConfirmed;
|
||||
boolean isFiatSent;
|
||||
boolean isFiatReceived;
|
||||
boolean isPayoutPublished;
|
||||
boolean isWithdrawn;
|
||||
|
||||
public ExpectedProtocolStatus setState(Trade.State state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setPhase(Trade.Phase phase) {
|
||||
this.phase = phase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setDepositPublished(boolean depositPublished) {
|
||||
isDepositPublished = depositPublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setDepositConfirmed(boolean depositConfirmed) {
|
||||
isDepositConfirmed = depositConfirmed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setFiatSent(boolean fiatSent) {
|
||||
isFiatSent = fiatSent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setFiatReceived(boolean fiatReceived) {
|
||||
isFiatReceived = fiatReceived;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setPayoutPublished(boolean payoutPublished) {
|
||||
isPayoutPublished = payoutPublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedProtocolStatus setWithdrawn(boolean withdrawn) {
|
||||
isWithdrawn = withdrawn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void init() {
|
||||
state = null;
|
||||
phase = null;
|
||||
isDepositPublished = false;
|
||||
isDepositConfirmed = false;
|
||||
isFiatSent = false;
|
||||
isFiatReceived = false;
|
||||
isPayoutPublished = false;
|
||||
isWithdrawn = false;
|
||||
}
|
||||
}
|
@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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.trade;
|
||||
|
||||
import io.grpc.StatusRuntimeException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
|
||||
import static bisq.cli.CurrencyFormat.formatSatoshis;
|
||||
import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED;
|
||||
import static bisq.core.trade.Trade.Phase.DEPOSIT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.Phase.FIAT_SENT;
|
||||
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.State.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static protobuf.Offer.State.OFFER_FEE_PAID;
|
||||
import static protobuf.OpenOffer.State.AVAILABLE;
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class TakeBuyBTCOfferTest extends AbstractTradeTest {
|
||||
|
||||
// Alice is buyer, Bob is seller.
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
|
||||
try {
|
||||
var alicesOffer = createAliceOffer(alicesDummyAcct,
|
||||
"buy",
|
||||
"usd",
|
||||
12500000);
|
||||
var offerId = alicesOffer.getId();
|
||||
|
||||
// Wait for Alice's AddToOfferBook task.
|
||||
// Wait times vary; my logs show >= 2 second delay.
|
||||
sleep(3000);
|
||||
assertEquals(1, getOpenOffersCount(aliceStubs, "buy", "usd"));
|
||||
|
||||
var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId());
|
||||
assertNotNull(trade);
|
||||
assertEquals(offerId, trade.getTradeId());
|
||||
// Cache the trade id for the other tests.
|
||||
tradeId = trade.getTradeId();
|
||||
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
assertEquals(0, getOpenOffersCount(aliceStubs, "buy", "usd"));
|
||||
|
||||
trade = getTrade(bobdaemon, trade.getTradeId());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_PUBLISHED_DEPOSIT_TX)
|
||||
.setPhase(DEPOSIT_PUBLISHED)
|
||||
.setDepositPublished(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
|
||||
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
trade = getTrade(bobdaemon, trade.getTradeId());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
|
||||
.setPhase(DEPOSIT_CONFIRMED)
|
||||
.setDepositConfirmed(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade);
|
||||
} catch (StatusRuntimeException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testAlicesConfirmPaymentStarted(final TestInfo testInfo) {
|
||||
try {
|
||||
var trade = getTrade(alicedaemon, tradeId);
|
||||
confirmPaymentStarted(alicedaemon, trade.getTradeId());
|
||||
sleep(3000);
|
||||
|
||||
trade = getTrade(alicedaemon, tradeId);
|
||||
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG)
|
||||
.setPhase(FIAT_SENT)
|
||||
.setFiatSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade);
|
||||
} catch (StatusRuntimeException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testBobsConfirmPaymentReceived(final TestInfo testInfo) {
|
||||
var trade = getTrade(bobdaemon, tradeId);
|
||||
confirmPaymentReceived(bobdaemon, trade.getTradeId());
|
||||
sleep(3000);
|
||||
|
||||
trade = getTrade(bobdaemon, tradeId);
|
||||
// Note: offer.state == available
|
||||
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
.setPayoutPublished(true)
|
||||
.setFiatReceived(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Bob's view after confirming fiat payment received", trade);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
public void testAlicesKeepFunds(final TestInfo testInfo) {
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
|
||||
var trade = getTrade(alicedaemon, tradeId);
|
||||
logTrade(log, testInfo, "Alice's view before keeping funds", trade);
|
||||
|
||||
keepFunds(alicedaemon, tradeId);
|
||||
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
|
||||
trade = getTrade(alicedaemon, tradeId);
|
||||
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Alice's view after keeping funds", trade);
|
||||
log.info("{} Alice's current available balance: {} BTC",
|
||||
testName(testInfo),
|
||||
formatSatoshis(getBalance(alicedaemon)));
|
||||
}
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.trade;
|
||||
|
||||
import io.grpc.StatusRuntimeException;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.bobdaemon;
|
||||
import static bisq.cli.CurrencyFormat.formatSatoshis;
|
||||
import static bisq.core.trade.Trade.Phase.*;
|
||||
import static bisq.core.trade.Trade.State.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static protobuf.Offer.State.OFFER_FEE_PAID;
|
||||
import static protobuf.OpenOffer.State.AVAILABLE;
|
||||
|
||||
@Disabled
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class TakeSellBTCOfferTest extends AbstractTradeTest {
|
||||
|
||||
// Alice is seller, Bob is buyer.
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
|
||||
try {
|
||||
var alicesOffer = createAliceOffer(alicesDummyAcct,
|
||||
"sell",
|
||||
"usd",
|
||||
12500000);
|
||||
var offerId = alicesOffer.getId();
|
||||
|
||||
// Wait for Alice's AddToOfferBook task.
|
||||
// Wait times vary; my logs show >= 2 second delay, but taking sell offers
|
||||
// seems to require more time to prepare.
|
||||
sleep(3000);
|
||||
assertEquals(1, getOpenOffersCount(bobStubs, "sell", "usd"));
|
||||
|
||||
var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId());
|
||||
assertNotNull(trade);
|
||||
assertEquals(offerId, trade.getTradeId());
|
||||
// Cache the trade id for the other tests.
|
||||
tradeId = trade.getTradeId();
|
||||
|
||||
genBtcBlocksThenWait(1, 4000);
|
||||
assertEquals(0, getOpenOffersCount(bobStubs, "sell", "usd"));
|
||||
|
||||
trade = getTrade(bobdaemon, trade.getTradeId());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
|
||||
.setPhase(DEPOSIT_PUBLISHED)
|
||||
.setDepositPublished(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
|
||||
logTrade(log, testInfo, "Bob's view after taking offer and sending deposit", trade);
|
||||
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
trade = getTrade(bobdaemon, trade.getTradeId());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN)
|
||||
.setPhase(DEPOSIT_CONFIRMED)
|
||||
.setDepositConfirmed(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Bob's view after deposit is confirmed", trade);
|
||||
} catch (StatusRuntimeException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testBobsConfirmPaymentStarted(final TestInfo testInfo) {
|
||||
try {
|
||||
var trade = getTrade(bobdaemon, tradeId);
|
||||
confirmPaymentStarted(bobdaemon, trade.getTradeId());
|
||||
sleep(3000);
|
||||
|
||||
trade = getTrade(bobdaemon, tradeId);
|
||||
// Note: offer.state == available
|
||||
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG)
|
||||
.setPhase(FIAT_SENT)
|
||||
.setFiatSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade);
|
||||
} catch (StatusRuntimeException e) {
|
||||
fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testAlicesConfirmPaymentReceived(final TestInfo testInfo) {
|
||||
var trade = getTrade(alicedaemon, tradeId);
|
||||
confirmPaymentReceived(alicedaemon, trade.getTradeId());
|
||||
sleep(3000);
|
||||
|
||||
trade = getTrade(alicedaemon, tradeId);
|
||||
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
.setPayoutPublished(true)
|
||||
.setFiatReceived(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Alice's view after confirming fiat payment received", trade);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
public void testBobsBtcWithdrawalToExternalAddress(final TestInfo testInfo) {
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
|
||||
var trade = getTrade(bobdaemon, tradeId);
|
||||
logTrade(log, testInfo, "Bob's view before withdrawing funds to external wallet", trade);
|
||||
|
||||
String toAddress = bitcoinCli.getNewBtcAddress();
|
||||
withdrawFunds(bobdaemon, tradeId, toAddress);
|
||||
|
||||
genBtcBlocksThenWait(1, 2250);
|
||||
|
||||
trade = getTrade(bobdaemon, tradeId);
|
||||
EXPECTED_PROTOCOL_STATUS.setState(WITHDRAW_COMPLETED)
|
||||
.setPhase(WITHDRAWN)
|
||||
.setWithdrawn(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
|
||||
log.info("{} Bob's current available balance: {} BTC",
|
||||
testName(testInfo),
|
||||
formatSatoshis(getBalance(bobdaemon)));
|
||||
}
|
||||
}
|
@ -33,9 +33,13 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.MethodTest;
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class FundWalletScenarioTest extends ScenarioTest {
|
||||
public class FundWalletScenarioTest extends MethodTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
|
72
apitest/src/test/java/bisq/apitest/scenario/OfferTest.java
Normal file
72
apitest/src/test/java/bisq/apitest/scenario/OfferTest.java
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.scenario;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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 bisq.apitest.method.offer.AbstractOfferTest;
|
||||
import bisq.apitest.method.offer.CancelOfferTest;
|
||||
import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest;
|
||||
import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest;
|
||||
import bisq.apitest.method.offer.ValidateCreateOfferTest;
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class OfferTest extends AbstractOfferTest {
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testAmtTooLargeShouldThrowException() {
|
||||
ValidateCreateOfferTest test = new ValidateCreateOfferTest();
|
||||
test.testAmtTooLargeShouldThrowException();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testCancelOffer() {
|
||||
CancelOfferTest test = new CancelOfferTest();
|
||||
test.testCancelOffer();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testCreateOfferUsingFixedPrice() {
|
||||
CreateOfferUsingFixedPriceTest test = new CreateOfferUsingFixedPriceTest();
|
||||
test.testCreateAUDBTCBuyOfferUsingFixedPrice16000();
|
||||
test.testCreateUSDBTCBuyOfferUsingFixedPrice100001234();
|
||||
test.testCreateEURBTCSellOfferUsingFixedPrice95001234();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
public void testCreateOfferUsingMarketPriceMargin() {
|
||||
CreateOfferUsingMarketPriceMarginTest test = new CreateOfferUsingMarketPriceMarginTest();
|
||||
test.testCreateUSDBTCBuyOffer5PctPriceMargin();
|
||||
test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin();
|
||||
test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin();
|
||||
test.testCreateBRLBTCSellOffer6Point55PctPriceMargin();
|
||||
}
|
||||
}
|
85
apitest/src/test/java/bisq/apitest/scenario/StartupTest.java
Normal file
85
apitest/src/test/java/bisq/apitest/scenario/StartupTest.java
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.scenario;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
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 bisq.apitest.config.BisqAppConfig.arbdaemon;
|
||||
import static bisq.apitest.config.BisqAppConfig.seednode;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.CreatePaymentAccountTest;
|
||||
import bisq.apitest.method.GetVersionTest;
|
||||
import bisq.apitest.method.MethodTest;
|
||||
import bisq.apitest.method.RegisterDisputeAgentsTest;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class StartupTest extends MethodTest {
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
try {
|
||||
setUpScaffold(bitcoind, seednode, arbdaemon, alicedaemon);
|
||||
} catch (Exception ex) {
|
||||
fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testGetVersion() {
|
||||
GetVersionTest test = new GetVersionTest();
|
||||
test.testGetVersion();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testRegisterDisputeAgents() {
|
||||
RegisterDisputeAgentsTest test = new RegisterDisputeAgentsTest();
|
||||
test.testRegisterArbitratorShouldThrowException();
|
||||
test.testInvalidDisputeAgentTypeArgShouldThrowException();
|
||||
test.testInvalidRegistrationKeyArgShouldThrowException();
|
||||
test.testRegisterMediator();
|
||||
test.testRegisterRefundAgent();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
public void testCreatePaymentAccount() {
|
||||
CreatePaymentAccountTest test = new CreatePaymentAccountTest();
|
||||
test.testCreatePerfectMoneyUSDPaymentAccount();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
tearDownScaffold();
|
||||
}
|
||||
}
|
64
apitest/src/test/java/bisq/apitest/scenario/TradeTest.java
Normal file
64
apitest/src/test/java/bisq/apitest/scenario/TradeTest.java
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.scenario;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.trade.AbstractTradeTest;
|
||||
import bisq.apitest.method.trade.TakeBuyBTCOfferTest;
|
||||
import bisq.apitest.method.trade.TakeSellBTCOfferTest;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class TradeTest extends AbstractTradeTest {
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
EXPECTED_PROTOCOL_STATUS.init();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testTakeBuyBTCOffer(final TestInfo testInfo) {
|
||||
TakeBuyBTCOfferTest test = new TakeBuyBTCOfferTest();
|
||||
test.testTakeAlicesBuyOffer(testInfo);
|
||||
test.testAlicesConfirmPaymentStarted(testInfo);
|
||||
test.testBobsConfirmPaymentReceived(testInfo);
|
||||
test.testAlicesKeepFunds(testInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testTakeSellBTCOffer(final TestInfo testInfo) {
|
||||
TakeSellBTCOfferTest test = new TakeSellBTCOfferTest();
|
||||
test.testTakeAlicesSellOffer(testInfo);
|
||||
test.testBobsConfirmPaymentStarted(testInfo);
|
||||
test.testAlicesConfirmPaymentReceived(testInfo);
|
||||
test.testBobsBtcWithdrawalToExternalAddress(testInfo);
|
||||
}
|
||||
}
|
99
apitest/src/test/java/bisq/apitest/scenario/WalletTest.java
Normal file
99
apitest/src/test/java/bisq/apitest/scenario/WalletTest.java
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.scenario;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
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 bisq.apitest.config.BisqAppConfig.seednode;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.MethodTest;
|
||||
import bisq.apitest.method.WalletProtectionTest;
|
||||
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
public class WalletTest extends MethodTest {
|
||||
|
||||
// All tests depend on the DAO / regtest environment, and Alice's wallet is
|
||||
// initialized with 10 BTC during the scaffolding setup.
|
||||
|
||||
@BeforeAll
|
||||
public static void setUp() {
|
||||
try {
|
||||
setUpScaffold(bitcoind, seednode, alicedaemon);
|
||||
genBtcBlocksThenWait(1, 1500);
|
||||
} catch (Exception ex) {
|
||||
fail(ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testFundWallet() {
|
||||
// The regtest Bisq wallet was initialized with 10 BTC.
|
||||
long balance = getBalance(alicedaemon);
|
||||
assertEquals(1000000000, balance);
|
||||
|
||||
String unusedAddress = getUnusedBtcAddress(alicedaemon);
|
||||
bitcoinCli.sendToAddress(unusedAddress, "2.5");
|
||||
|
||||
bitcoinCli.generateBlocks(1);
|
||||
sleep(1500);
|
||||
|
||||
balance = getBalance(alicedaemon);
|
||||
assertEquals(1250000000L, balance); // new balance is 12.5 btc
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
public void testWalletProtection() {
|
||||
// Batching all wallet tests in this test case reduces scaffold setup
|
||||
// time. Here, we create a method WalletProtectionTest instance and run each
|
||||
// test in declared order.
|
||||
|
||||
WalletProtectionTest walletProtectionTest = new WalletProtectionTest();
|
||||
|
||||
walletProtectionTest.testSetWalletPassword();
|
||||
walletProtectionTest.testGetBalanceOnEncryptedWalletShouldThrowException();
|
||||
walletProtectionTest.testUnlockWalletFor4Seconds();
|
||||
walletProtectionTest.testGetBalanceAfterUnlockTimeExpiryShouldThrowException();
|
||||
walletProtectionTest.testLockWalletBeforeUnlockTimeoutExpiry();
|
||||
walletProtectionTest.testLockWalletWhenWalletAlreadyLockedShouldThrowException();
|
||||
walletProtectionTest.testUnlockWalletTimeoutOverride();
|
||||
walletProtectionTest.testSetNewWalletPassword();
|
||||
walletProtectionTest.testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException();
|
||||
walletProtectionTest.testRemoveNewWalletPassword();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void tearDown() {
|
||||
tearDownScaffold();
|
||||
}
|
||||
}
|
57
build.gradle
57
build.gradle
@ -33,7 +33,7 @@ configure(subprojects) {
|
||||
|
||||
ext { // in alphabetical order
|
||||
bcVersion = '1.63'
|
||||
bitcoinjVersion = 'a733034'
|
||||
bitcoinjVersion = '7752cb7'
|
||||
btcdCli4jVersion = '27b94333'
|
||||
codecVersion = '1.13'
|
||||
easybindVersion = '1.0.3'
|
||||
@ -60,7 +60,7 @@ configure(subprojects) {
|
||||
joptVersion = '5.0.4'
|
||||
jsonsimpleVersion = '1.1.1'
|
||||
junitVersion = '4.12'
|
||||
jupiterVersion = '5.3.2'
|
||||
jupiterVersion = '5.7.0'
|
||||
kotlinVersion = '1.3.41'
|
||||
knowmXchangeVersion = '4.4.2'
|
||||
langVersion = '3.11'
|
||||
@ -104,6 +104,7 @@ configure([project(':cli'),
|
||||
project(':seednode'),
|
||||
project(':statsnode'),
|
||||
project(':pricenode'),
|
||||
project(':inventory'),
|
||||
project(':apitest')]) {
|
||||
|
||||
apply plugin: 'application'
|
||||
@ -276,10 +277,10 @@ configure(project(':common')) {
|
||||
configure(project(':p2p')) {
|
||||
dependencies {
|
||||
compile project(':common')
|
||||
compile("com.github.cd2357.netlayer:tor.native:$netlayerVersion") {
|
||||
compile("com.github.bisq-network.netlayer:tor.native:$netlayerVersion") {
|
||||
exclude(module: 'slf4j-api')
|
||||
}
|
||||
compile("com.github.cd2357.netlayer:tor.external:$netlayerVersion") {
|
||||
compile("com.github.bisq-network.netlayer:tor.external:$netlayerVersion") {
|
||||
exclude(module: 'slf4j-api')
|
||||
}
|
||||
implementation("org.apache.httpcomponents:httpclient:$httpclientVersion") {
|
||||
@ -391,7 +392,7 @@ configure(project(':desktop')) {
|
||||
apply from: '../gradle/witness/gradle-witness.gradle'
|
||||
apply from: 'package/package.gradle'
|
||||
|
||||
version = '1.4.2-SNAPSHOT'
|
||||
version = '1.5.0-SNAPSHOT'
|
||||
|
||||
jar.manifest.attributes(
|
||||
"Implementation-Title": project.name,
|
||||
@ -604,6 +605,21 @@ configure(project(':daemon')) {
|
||||
}
|
||||
}
|
||||
|
||||
configure(project(':inventory')) {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
|
||||
mainClassName = 'bisq.inventory.InventoryMonitorMain'
|
||||
|
||||
dependencies {
|
||||
compile project(':core')
|
||||
compile "com.google.guava:guava:$guavaVersion"
|
||||
compile "com.sparkjava:spark-core:$sparkVersion"
|
||||
|
||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
}
|
||||
}
|
||||
|
||||
configure(project(':apitest')) {
|
||||
mainClassName = 'bisq.apitest.ApiTestMain'
|
||||
|
||||
@ -637,8 +653,31 @@ configure(project(':apitest')) {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
outputs.upToDateWhen { false } // Don't use previously cached test outputs.
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed"
|
||||
showStackTraces = true // Show full stack traces in the console.
|
||||
exceptionFormat = "full"
|
||||
// Show passed & failed tests, and anything printed to stderr by the tests in the console.
|
||||
// Do not show skipped tests in the console; they are shown in the html report.
|
||||
events "passed", "failed", "standardError"
|
||||
}
|
||||
|
||||
afterSuite { desc, result ->
|
||||
if (!desc.parent) {
|
||||
println("${result.resultType} " +
|
||||
"[${result.testCount} tests, " +
|
||||
"${result.successfulTestCount} passed, " +
|
||||
"${result.failedTestCount} failed, " +
|
||||
"${result.skippedTestCount} skipped] html report contains skipped test info")
|
||||
|
||||
// Show report link if all tests passed in case you want to see more detail, stdout, skipped, etc.
|
||||
if(result.resultType == TestResult.ResultType.SUCCESS) {
|
||||
DirectoryReport htmlReport = getReports().getHtml()
|
||||
String reportUrl = new org.gradle.internal.logging.ConsoleRenderer()
|
||||
.asClickableFileUrl(htmlReport.getEntryPoint())
|
||||
println("REPORT " + reportUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -668,12 +707,12 @@ configure(project(':apitest')) {
|
||||
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
|
||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
|
||||
testCompile "org.junit.jupiter:junit-jupiter-api:5.6.2"
|
||||
testCompile "org.junit.jupiter:junit-jupiter-params:5.6.2"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:$jupiterVersion"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-params:$jupiterVersion"
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion")
|
||||
testAnnotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||
testCompileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||
testRuntime "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
|
||||
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.6.2")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,9 @@
|
||||
|
||||
package bisq.cli;
|
||||
|
||||
import bisq.proto.grpc.CancelOfferRequest;
|
||||
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
|
||||
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
|
||||
import bisq.proto.grpc.CreateOfferRequest;
|
||||
import bisq.proto.grpc.CreatePaymentAccountRequest;
|
||||
import bisq.proto.grpc.GetAddressBalanceRequest;
|
||||
@ -25,12 +28,16 @@ import bisq.proto.grpc.GetFundingAddressesRequest;
|
||||
import bisq.proto.grpc.GetOfferRequest;
|
||||
import bisq.proto.grpc.GetOffersRequest;
|
||||
import bisq.proto.grpc.GetPaymentAccountsRequest;
|
||||
import bisq.proto.grpc.GetTradeRequest;
|
||||
import bisq.proto.grpc.GetVersionRequest;
|
||||
import bisq.proto.grpc.KeepFundsRequest;
|
||||
import bisq.proto.grpc.LockWalletRequest;
|
||||
import bisq.proto.grpc.RegisterDisputeAgentRequest;
|
||||
import bisq.proto.grpc.RemoveWalletPasswordRequest;
|
||||
import bisq.proto.grpc.SetWalletPasswordRequest;
|
||||
import bisq.proto.grpc.TakeOfferRequest;
|
||||
import bisq.proto.grpc.UnlockWalletRequest;
|
||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||
|
||||
import io.grpc.StatusRuntimeException;
|
||||
|
||||
@ -68,8 +75,15 @@ public class CliMain {
|
||||
|
||||
private enum Method {
|
||||
createoffer,
|
||||
canceloffer,
|
||||
getoffer,
|
||||
getoffers,
|
||||
takeoffer,
|
||||
gettrade,
|
||||
confirmpaymentstarted,
|
||||
confirmpaymentreceived,
|
||||
keepfunds,
|
||||
withdrawfunds,
|
||||
createpaymentacct,
|
||||
getpaymentaccts,
|
||||
getversion,
|
||||
@ -154,9 +168,10 @@ public class CliMain {
|
||||
|
||||
GrpcStubs grpcStubs = new GrpcStubs(host, port, password);
|
||||
var disputeAgentsService = grpcStubs.disputeAgentsService;
|
||||
var versionService = grpcStubs.versionService;
|
||||
var offersService = grpcStubs.offersService;
|
||||
var paymentAccountsService = grpcStubs.paymentAccountsService;
|
||||
var tradesService = grpcStubs.tradesService;
|
||||
var versionService = grpcStubs.versionService;
|
||||
var walletsService = grpcStubs.walletsService;
|
||||
|
||||
try {
|
||||
@ -225,6 +240,18 @@ public class CliMain {
|
||||
out.println(formatOfferTable(singletonList(reply.getOffer()), currencyCode));
|
||||
return;
|
||||
}
|
||||
case canceloffer: {
|
||||
if (nonOptionArgs.size() < 2)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting offer id");
|
||||
|
||||
var offerId = nonOptionArgs.get(1);
|
||||
var request = CancelOfferRequest.newBuilder()
|
||||
.setId(offerId)
|
||||
.build();
|
||||
offersService.cancelOffer(request);
|
||||
out.println("offer canceled and removed from offer book");
|
||||
return;
|
||||
}
|
||||
case getoffer: {
|
||||
if (nonOptionArgs.size() < 2)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting offer id");
|
||||
@ -254,6 +281,89 @@ public class CliMain {
|
||||
out.println(formatOfferTable(reply.getOffersList(), currencyCode));
|
||||
return;
|
||||
}
|
||||
case takeoffer: {
|
||||
if (nonOptionArgs.size() < 3)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting offer id, payment acct id");
|
||||
|
||||
var offerId = nonOptionArgs.get(1);
|
||||
var paymentAccountId = nonOptionArgs.get(2);
|
||||
var request = TakeOfferRequest.newBuilder()
|
||||
.setOfferId(offerId)
|
||||
.setPaymentAccountId(paymentAccountId)
|
||||
.build();
|
||||
var reply = tradesService.takeOffer(request);
|
||||
out.printf("trade '%s' successfully taken", reply.getTrade().getShortId());
|
||||
return;
|
||||
}
|
||||
case gettrade: {
|
||||
if (nonOptionArgs.size() < 2)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting trade id, [,showcontract = true|false]");
|
||||
|
||||
var tradeId = nonOptionArgs.get(1);
|
||||
var showContract = false;
|
||||
if (nonOptionArgs.size() == 3)
|
||||
showContract = Boolean.getBoolean(nonOptionArgs.get(2));
|
||||
|
||||
var request = GetTradeRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.build();
|
||||
var reply = tradesService.getTrade(request);
|
||||
if (showContract)
|
||||
out.println(reply.getTrade().getContractAsJson());
|
||||
else
|
||||
out.println(TradeFormat.format(reply.getTrade()));
|
||||
return;
|
||||
}
|
||||
case confirmpaymentstarted: {
|
||||
if (nonOptionArgs.size() < 2)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting trade id");
|
||||
|
||||
var tradeId = nonOptionArgs.get(1);
|
||||
var request = ConfirmPaymentStartedRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.build();
|
||||
tradesService.confirmPaymentStarted(request);
|
||||
out.printf("trade '%s' payment started message sent", tradeId);
|
||||
return;
|
||||
}
|
||||
case confirmpaymentreceived: {
|
||||
if (nonOptionArgs.size() < 2)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting trade id");
|
||||
|
||||
var tradeId = nonOptionArgs.get(1);
|
||||
var request = ConfirmPaymentReceivedRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.build();
|
||||
tradesService.confirmPaymentReceived(request);
|
||||
out.printf("trade '%s' payment received message sent", tradeId);
|
||||
return;
|
||||
}
|
||||
case keepfunds: {
|
||||
if (nonOptionArgs.size() < 2)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting trade id");
|
||||
|
||||
var tradeId = nonOptionArgs.get(1);
|
||||
var request = KeepFundsRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.build();
|
||||
tradesService.keepFunds(request);
|
||||
out.printf("funds from trade '%s' saved in bisq wallet", tradeId);
|
||||
return;
|
||||
}
|
||||
case withdrawfunds: {
|
||||
if (nonOptionArgs.size() < 3)
|
||||
throw new IllegalArgumentException("incorrect parameter count, expecting trade id, bitcoin wallet address");
|
||||
|
||||
var tradeId = nonOptionArgs.get(1);
|
||||
var address = nonOptionArgs.get(2);
|
||||
var request = WithdrawFundsRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setAddress(address)
|
||||
.build();
|
||||
tradesService.withdrawFunds(request);
|
||||
out.printf("funds from trade '%s' sent to btc address '%s'", tradeId, address);
|
||||
return;
|
||||
}
|
||||
case createpaymentacct: {
|
||||
if (nonOptionArgs.size() < 5)
|
||||
throw new IllegalArgumentException(
|
||||
@ -379,8 +489,15 @@ public class CliMain {
|
||||
stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", "");
|
||||
stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", "");
|
||||
stream.format(rowFormat, "", "security deposit (%)", "");
|
||||
stream.format(rowFormat, "canceloffer", "offer id", "Cancel offer with id");
|
||||
stream.format(rowFormat, "getoffer", "offer id", "Get current offer with id");
|
||||
stream.format(rowFormat, "getoffers", "buy | sell, currency code", "Get current offers");
|
||||
stream.format(rowFormat, "takeoffer", "offer id", "Take offer with id");
|
||||
stream.format(rowFormat, "gettrade", "trade id [,showcontract]", "Get trade summary or full contract");
|
||||
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, "createpaymentacct", "account name, account number, currency code", "Create PerfectMoney dummy account");
|
||||
stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts");
|
||||
stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet");
|
||||
|
55
cli/src/main/java/bisq/cli/ColumnHeaderConstants.java
Normal file
55
cli/src/main/java/bisq/cli/ColumnHeaderConstants.java
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 static com.google.common.base.Strings.padEnd;
|
||||
import static com.google.common.base.Strings.padStart;
|
||||
|
||||
class ColumnHeaderConstants {
|
||||
|
||||
// For inserting 2 spaces between column headers.
|
||||
static final String COL_HEADER_DELIMITER = " ";
|
||||
|
||||
// Table column header format specs, right padded with two spaces. In some cases
|
||||
// such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the
|
||||
// expected max data string length is accounted for. In others, the column header length
|
||||
// are expected to be greater than any column value length.
|
||||
static final String COL_HEADER_ADDRESS = padEnd("Address", 34, ' ');
|
||||
static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' ');
|
||||
static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' ');
|
||||
static final String COL_HEADER_CONFIRMATIONS = "Confirmations";
|
||||
static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' ');
|
||||
static final String COL_HEADER_CURRENCY = "Currency";
|
||||
static final String COL_HEADER_DIRECTION = "Buy/Sell";
|
||||
static final String COL_HEADER_NAME = "Name";
|
||||
static final String COL_HEADER_PAYMENT_METHOD = "Payment Method";
|
||||
static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC";
|
||||
static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' ');
|
||||
static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
|
||||
static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published";
|
||||
static final String COL_HEADER_TRADE_FIAT_SENT = "Fiat Sent";
|
||||
static final String COL_HEADER_TRADE_FIAT_RECEIVED = "Fiat Received";
|
||||
static final String COL_HEADER_TRADE_PAYOUT_PUBLISHED = "Payout Published";
|
||||
static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn";
|
||||
static final String COL_HEADER_TRADE_ROLE = "My Role";
|
||||
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_VOLUME = padEnd("%-3s(min - max)", 15, ' ');
|
||||
static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');
|
||||
}
|
@ -17,6 +17,8 @@
|
||||
|
||||
package bisq.cli;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
@ -27,15 +29,17 @@ import java.util.Locale;
|
||||
|
||||
import static java.lang.String.format;
|
||||
|
||||
class CurrencyFormat {
|
||||
@VisibleForTesting
|
||||
public class CurrencyFormat {
|
||||
|
||||
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
|
||||
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000);
|
||||
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000");
|
||||
|
||||
@VisibleForTesting
|
||||
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
|
||||
static String formatSatoshis(long sats) {
|
||||
public static String formatSatoshis(long sats) {
|
||||
return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import bisq.proto.grpc.GetVersionGrpc;
|
||||
import bisq.proto.grpc.OffersGrpc;
|
||||
import bisq.proto.grpc.PaymentAccountsGrpc;
|
||||
import bisq.proto.grpc.PriceGrpc;
|
||||
import bisq.proto.grpc.TradesGrpc;
|
||||
import bisq.proto.grpc.WalletsGrpc;
|
||||
|
||||
import io.grpc.CallCredentials;
|
||||
@ -36,6 +37,7 @@ public class GrpcStubs {
|
||||
public final OffersGrpc.OffersBlockingStub offersService;
|
||||
public final PaymentAccountsGrpc.PaymentAccountsBlockingStub paymentAccountsService;
|
||||
public final PriceGrpc.PriceBlockingStub priceService;
|
||||
public final TradesGrpc.TradesBlockingStub tradesService;
|
||||
public final WalletsGrpc.WalletsBlockingStub walletsService;
|
||||
|
||||
public GrpcStubs(String apiHost, int apiPort, String apiPassword) {
|
||||
@ -55,6 +57,7 @@ public class GrpcStubs {
|
||||
this.offersService = OffersGrpc.newBlockingStub(channel).withCallCredentials(credentials);
|
||||
this.paymentAccountsService = PaymentAccountsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
|
||||
this.priceService = PriceGrpc.newBlockingStub(channel).withCallCredentials(credentials);
|
||||
this.tradesService = TradesGrpc.newBlockingStub(channel).withCallCredentials(credentials);
|
||||
this.walletsService = WalletsGrpc.newBlockingStub(channel).withCallCredentials(credentials);
|
||||
}
|
||||
}
|
||||
|
@ -29,12 +29,12 @@ import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static bisq.cli.ColumnHeaderConstants.*;
|
||||
import static bisq.cli.CurrencyFormat.formatAmountRange;
|
||||
import static bisq.cli.CurrencyFormat.formatOfferPrice;
|
||||
import static bisq.cli.CurrencyFormat.formatSatoshis;
|
||||
import static bisq.cli.CurrencyFormat.formatVolumeRange;
|
||||
import static com.google.common.base.Strings.padEnd;
|
||||
import static com.google.common.base.Strings.padStart;
|
||||
import static java.lang.String.format;
|
||||
import static java.util.Collections.max;
|
||||
import static java.util.Comparator.comparing;
|
||||
@ -42,28 +42,8 @@ import static java.util.TimeZone.getTimeZone;
|
||||
|
||||
class TableFormat {
|
||||
|
||||
private static final TimeZone TZ_UTC = getTimeZone("UTC");
|
||||
private static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
|
||||
// For inserting 2 spaces between column headers.
|
||||
private static final String COL_HEADER_DELIMITER = " ";
|
||||
|
||||
// Table column header format specs, right padded with two spaces. In some cases
|
||||
// such as COL_HEADER_CREATION_DATE, COL_HEADER_VOLUME and COL_HEADER_UUID, the
|
||||
// expected max data string length is accounted for. In others, the column header length
|
||||
// are expected to be greater than any column value length.
|
||||
private static final String COL_HEADER_ADDRESS = padEnd("Address", 34, ' ');
|
||||
private static final String COL_HEADER_AMOUNT = padEnd("BTC(min - max)", 24, ' ');
|
||||
private static final String COL_HEADER_BALANCE = padStart("Balance", 12, ' ');
|
||||
private static final String COL_HEADER_CONFIRMATIONS = "Confirmations";
|
||||
private static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' ');
|
||||
private static final String COL_HEADER_CURRENCY = "Currency";
|
||||
private static final String COL_HEADER_DIRECTION = "Buy/Sell"; // TODO "Take Offer to
|
||||
private static final String COL_HEADER_NAME = "Name";
|
||||
private static final String COL_HEADER_PAYMENT_METHOD = "Payment Method";
|
||||
private static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC";
|
||||
private static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' ');
|
||||
private static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');
|
||||
static final TimeZone TZ_UTC = getTimeZone("UTC");
|
||||
static final SimpleDateFormat DATE_FORMAT_ISO_8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
|
||||
static String formatAddressBalanceTbl(List<AddressBalanceInfo> addressBalanceInfo) {
|
||||
String headerLine = (COL_HEADER_ADDRESS + COL_HEADER_DELIMITER
|
||||
@ -83,7 +63,7 @@ class TableFormat {
|
||||
|
||||
static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
|
||||
|
||||
// Some column values might be longer than header, so we need to calculated them.
|
||||
// Some column values might be longer than header, so we need to calculate them.
|
||||
int paymentMethodColWidth = getLengthOfLongestColumn(
|
||||
COL_HEADER_PAYMENT_METHOD.length(),
|
||||
offerInfo.stream()
|
||||
@ -120,7 +100,7 @@ class TableFormat {
|
||||
}
|
||||
|
||||
static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
|
||||
// Some column values might be longer than header, so we need to calculated them.
|
||||
// Some column values might be longer than header, so we need to calculate them.
|
||||
int nameColWidth = getLengthOfLongestColumn(
|
||||
COL_HEADER_NAME.length(),
|
||||
paymentAccounts.stream().map(PaymentAccount::getAccountName)
|
||||
|
118
cli/src/main/java/bisq/cli/TradeFormat.java
Normal file
118
cli/src/main/java/bisq/cli/TradeFormat.java
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.TradeInfo;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static bisq.cli.ColumnHeaderConstants.*;
|
||||
import static bisq.cli.CurrencyFormat.formatOfferPrice;
|
||||
import static bisq.cli.CurrencyFormat.formatSatoshis;
|
||||
import static com.google.common.base.Strings.padEnd;
|
||||
|
||||
@VisibleForTesting
|
||||
public class TradeFormat {
|
||||
|
||||
@VisibleForTesting
|
||||
public static String format(TradeInfo tradeInfo) {
|
||||
// Some column values might be longer than header, so we need to calculate them.
|
||||
int shortIdColWidth = Math.max(COL_HEADER_TRADE_SHORT_ID.length(), tradeInfo.getShortId().length());
|
||||
int roleColWidth = Math.max(COL_HEADER_TRADE_ROLE.length(), tradeInfo.getRole().length());
|
||||
|
||||
// We only show taker fee under its header when user is the taker.
|
||||
boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker");
|
||||
Supplier<String> takerFeeHeaderFormat = () -> isTaker ?
|
||||
padEnd(COL_HEADER_TRADE_TAKER_FEE, 12, ' ') + COL_HEADER_DELIMITER
|
||||
: "";
|
||||
Supplier<String> takerFeeHeader = () -> isTaker ?
|
||||
"%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 1) + "s"
|
||||
: "";
|
||||
|
||||
String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER
|
||||
+ padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> currencyCode
|
||||
+ padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER
|
||||
+ padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER
|
||||
+ takerFeeHeaderFormat.get()
|
||||
+ COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_FIAT_SENT + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_FIAT_RECEIVED + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER
|
||||
+ "%n";
|
||||
|
||||
String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode();
|
||||
String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode();
|
||||
String headerLine = isTaker
|
||||
? 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
|
||||
|
||||
return headerLine +
|
||||
(isTaker
|
||||
? formatTradeForTaker(colDataFormat, tradeInfo)
|
||||
: formatTradeForMaker(colDataFormat, tradeInfo));
|
||||
}
|
||||
|
||||
private static String formatTradeForMaker(String format, TradeInfo tradeInfo) {
|
||||
return String.format(format,
|
||||
tradeInfo.getShortId(),
|
||||
tradeInfo.getRole(),
|
||||
formatOfferPrice(tradeInfo.getTradePrice()),
|
||||
formatSatoshis(tradeInfo.getTradeAmountAsLong()),
|
||||
formatSatoshis(tradeInfo.getTxFeeAsLong()),
|
||||
tradeInfo.getIsDepositPublished() ? "YES" : "NO",
|
||||
tradeInfo.getIsDepositConfirmed() ? "YES" : "NO",
|
||||
tradeInfo.getIsFiatSent() ? "YES" : "NO",
|
||||
tradeInfo.getIsFiatReceived() ? "YES" : "NO",
|
||||
tradeInfo.getIsPayoutPublished() ? "YES" : "NO",
|
||||
tradeInfo.getIsWithdrawn() ? "YES" : "NO");
|
||||
}
|
||||
|
||||
private static String formatTradeForTaker(String format, TradeInfo tradeInfo) {
|
||||
return String.format(format,
|
||||
tradeInfo.getShortId(),
|
||||
tradeInfo.getRole(),
|
||||
formatOfferPrice(tradeInfo.getTradePrice()),
|
||||
formatSatoshis(tradeInfo.getTradeAmountAsLong()),
|
||||
formatSatoshis(tradeInfo.getTxFeeAsLong()),
|
||||
formatSatoshis(tradeInfo.getTakerFeeAsLong()),
|
||||
tradeInfo.getIsDepositPublished() ? "YES" : "NO",
|
||||
tradeInfo.getIsDepositConfirmed() ? "YES" : "NO",
|
||||
tradeInfo.getIsFiatSent() ? "YES" : "NO",
|
||||
tradeInfo.getIsFiatReceived() ? "YES" : "NO",
|
||||
tradeInfo.getIsPayoutPublished() ? "YES" : "NO",
|
||||
tradeInfo.getIsWithdrawn() ? "YES" : "NO");
|
||||
}
|
||||
}
|
@ -30,14 +30,14 @@ public class Version {
|
||||
// VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update
|
||||
// Therefore all sub versions start again with 1
|
||||
// We use semantic versioning with major, minor and patch
|
||||
public static final String VERSION = "1.4.2";
|
||||
public static final String VERSION = "1.5.0";
|
||||
|
||||
/**
|
||||
* Holds a list of the tagged resource files for optimizing the getData requests.
|
||||
* This must not contain each version but only those where we add new version-tagged resource files for
|
||||
* historical data stores.
|
||||
*/
|
||||
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0");
|
||||
public static final List<String> HISTORICAL_RESOURCE_FILE_VERSION_TAGS = Arrays.asList("1.4.0", "1.5.0");
|
||||
|
||||
public static int getMajorVersion(String version) {
|
||||
return getSubVersion(version, 0);
|
||||
@ -92,10 +92,13 @@ public class Version {
|
||||
|
||||
// The version no. of the current protocol. The offer holds that version.
|
||||
// A taker will check the version of the offers to see if his version is compatible.
|
||||
// Offers created with the old version will become invalid and have to be canceled.
|
||||
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
|
||||
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
|
||||
// the Bisq app.
|
||||
// VERSION = 0.5.0 -> TRADE_PROTOCOL_VERSION = 1
|
||||
// Version 1.2.2 -> TRADE_PROTOCOL_VERSION = 2
|
||||
public static final int TRADE_PROTOCOL_VERSION = 2;
|
||||
// Version 1.5.0 -> TRADE_PROTOCOL_VERSION = 3
|
||||
public static final int TRADE_PROTOCOL_VERSION = 3;
|
||||
private static int p2pMessageVersion;
|
||||
|
||||
public static final String BSQ_TX_VERSION = "1";
|
||||
|
@ -72,7 +72,7 @@ public enum BaseCurrencyNetwork {
|
||||
return "BTC_REGTEST".equals(name());
|
||||
}
|
||||
|
||||
public long getDefaultMinFeePerByte() {
|
||||
public long getDefaultMinFeePerVbyte() {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,9 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -79,43 +81,59 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final Map<String, PersistenceManager<?>> ALL_PERSISTENCE_MANAGERS = new HashMap<>();
|
||||
public static boolean FLUSH_ALL_DATA_TO_DISK_CALLED = false;
|
||||
|
||||
// We don't know from which thread we are called so we map back to user thread
|
||||
|
||||
// We require being called only once from the global shutdown routine. As the shutdown routine has a timeout
|
||||
// and error condition where we call the method as well beside the standard path and it could be that those
|
||||
// alternative code paths call our method after it was called already, so it is a valid but rare case.
|
||||
// We add a guard to prevent repeated calls.
|
||||
public static void flushAllDataToDisk(ResultHandler completeHandler) {
|
||||
log.info("Start flushAllDataToDisk at shutdown");
|
||||
AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size());
|
||||
// We don't know from which thread we are called so we map to user thread
|
||||
UserThread.execute(() -> {
|
||||
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
|
||||
log.warn("We got flushAllDataToDisk called again. This can happen in some rare cases. We ignore the repeated call.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (openInstances.get() == 0) {
|
||||
log.info("flushAllDataToDisk completed");
|
||||
UserThread.execute(completeHandler::handleResult);
|
||||
}
|
||||
FLUSH_ALL_DATA_TO_DISK_CALLED = true;
|
||||
|
||||
new HashSet<>(ALL_PERSISTENCE_MANAGERS.values()).forEach(persistenceManager -> {
|
||||
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
|
||||
// a requestPersistence call after an important state update. Those are usually rather small data stores.
|
||||
// Otherwise we only persist if requestPersistence was called since the last persist call.
|
||||
if (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested) {
|
||||
// We don't know from which thread we are called so we map back to user thread when calling persistNow
|
||||
UserThread.execute(() -> {
|
||||
log.info("Start flushAllDataToDisk at shutdown");
|
||||
AtomicInteger openInstances = new AtomicInteger(ALL_PERSISTENCE_MANAGERS.size());
|
||||
|
||||
if (openInstances.get() == 0) {
|
||||
log.info("No PersistenceManager instances have been created yet.");
|
||||
completeHandler.handleResult();
|
||||
}
|
||||
|
||||
new HashSet<>(ALL_PERSISTENCE_MANAGERS.values()).forEach(persistenceManager -> {
|
||||
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
|
||||
// a requestPersistence call after an important state update. Those are usually rather small data stores.
|
||||
// Otherwise we only persist if requestPersistence was called since the last persist call.
|
||||
if (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested) {
|
||||
// We always get our completeHandler called even if exceptions happen. In case a file write fails
|
||||
// we still call our shutdown and count down routine as the completeHandler is triggered in any case.
|
||||
|
||||
// We get our result handler called from the write thread so we map back to user thread.
|
||||
persistenceManager.persistNow(() ->
|
||||
onWriteCompleted(completeHandler, openInstances, persistenceManager));
|
||||
});
|
||||
} else {
|
||||
onWriteCompleted(completeHandler, openInstances, persistenceManager);
|
||||
}
|
||||
UserThread.execute(() -> onWriteCompleted(completeHandler, openInstances, persistenceManager)));
|
||||
} else {
|
||||
onWriteCompleted(completeHandler, openInstances, persistenceManager);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// We get called always from user thread here.
|
||||
private static void onWriteCompleted(ResultHandler completeHandler,
|
||||
AtomicInteger openInstances,
|
||||
PersistenceManager<?> persistenceManager) {
|
||||
persistenceManager.shutdown();
|
||||
if (openInstances.decrementAndGet() == 0) {
|
||||
log.info("flushAllDataToDisk completed");
|
||||
UserThread.execute(completeHandler::handleResult);
|
||||
completeHandler.handleResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -125,25 +143,25 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
|
||||
public enum Source {
|
||||
// For data stores we received from the network and which could be rebuilt. We store only for avoiding too much network traffic.
|
||||
NETWORK(1, TimeUnit.HOURS.toSeconds(1), false),
|
||||
NETWORK(1, TimeUnit.MINUTES.toMillis(5), false),
|
||||
|
||||
// For data stores which are created from private local data. This data could only be rebuilt from backup files.
|
||||
PRIVATE(10, TimeUnit.SECONDS.toSeconds(30), true),
|
||||
PRIVATE(10, 200, true),
|
||||
|
||||
// For data stores which are created from private local data. Loss of that data would not have any critical consequences.
|
||||
PRIVATE_LOW_PRIO(4, TimeUnit.HOURS.toSeconds(2), false);
|
||||
// For data stores which are created from private local data. Loss of that data would not have critical consequences.
|
||||
PRIVATE_LOW_PRIO(4, TimeUnit.MINUTES.toMillis(1), false);
|
||||
|
||||
|
||||
@Getter
|
||||
private final int numMaxBackupFiles;
|
||||
@Getter
|
||||
private final long delayInSec;
|
||||
private final long delay;
|
||||
@Getter
|
||||
private final boolean flushAtShutDown;
|
||||
|
||||
Source(int numMaxBackupFiles, long delayInSec, boolean flushAtShutDown) {
|
||||
Source(int numMaxBackupFiles, long delay, boolean flushAtShutDown) {
|
||||
this.numMaxBackupFiles = numMaxBackupFiles;
|
||||
this.delayInSec = delayInSec;
|
||||
this.delay = delay;
|
||||
this.flushAtShutDown = flushAtShutDown;
|
||||
}
|
||||
}
|
||||
@ -165,6 +183,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
@Nullable
|
||||
private Timer timer;
|
||||
private ExecutorService writeToDiskExecutor;
|
||||
public final AtomicBoolean initCalled = new AtomicBoolean(false);
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -189,6 +208,29 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
}
|
||||
|
||||
public void initialize(T persistable, String fileName, Source source) {
|
||||
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
|
||||
log.warn("We have started the shut down routine already. We ignore that initialize call.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ALL_PERSISTENCE_MANAGERS.containsKey(fileName)) {
|
||||
RuntimeException runtimeException = new RuntimeException("We must not create multiple " +
|
||||
"PersistenceManager instances for file " + fileName + ".");
|
||||
// We want to get logged from where we have been called so lets print the stack trace.
|
||||
runtimeException.printStackTrace();
|
||||
throw runtimeException;
|
||||
}
|
||||
|
||||
if (initCalled.get()) {
|
||||
RuntimeException runtimeException = new RuntimeException("We must not call initialize multiple times. " +
|
||||
"PersistenceManager for file: " + fileName + ".");
|
||||
// We want to get logged from where we have been called so lets print the stack trace.
|
||||
runtimeException.printStackTrace();
|
||||
throw runtimeException;
|
||||
}
|
||||
|
||||
initCalled.set(true);
|
||||
|
||||
this.persistable = persistable;
|
||||
this.fileName = fileName;
|
||||
this.source = source;
|
||||
@ -213,16 +255,54 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
// Reading file
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Read persisted file in a thread.
|
||||
*
|
||||
* @param resultHandler Consumer of persisted data once it was read from disk.
|
||||
* @param orElse Called if no file exists or reading of file failed.
|
||||
*/
|
||||
public void readPersisted(Consumer<T> resultHandler, Runnable orElse) {
|
||||
readPersisted(checkNotNull(fileName), resultHandler, orElse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read persisted file in a thread.
|
||||
* We map result handler calls to UserThread, so clients don't need to worry about threading
|
||||
*
|
||||
* @param fileName File name of our persisted data.
|
||||
* @param resultHandler Consumer of persisted data once it was read from disk.
|
||||
* @param orElse Called if no file exists or reading of file failed.
|
||||
*/
|
||||
public void readPersisted(String fileName, Consumer<T> resultHandler, Runnable orElse) {
|
||||
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
|
||||
log.warn("We have started the shut down routine already. We ignore that readPersisted call.");
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
T persisted = getPersisted(fileName);
|
||||
if (persisted != null) {
|
||||
UserThread.execute(() -> resultHandler.accept(persisted));
|
||||
} else {
|
||||
UserThread.execute(orElse);
|
||||
}
|
||||
}, "PersistenceManager-read-" + fileName).start();
|
||||
}
|
||||
|
||||
// API for synchronous reading of data. Not recommended to be used in application code.
|
||||
// Currently used by tests and monitor. Should be converted to the threaded API as well.
|
||||
@Nullable
|
||||
public T getPersisted() {
|
||||
return getPersisted(checkNotNull(fileName));
|
||||
}
|
||||
|
||||
//TODO use threading here instead in the clients
|
||||
// We get called at startup either by readAllPersisted or readFromResources. Both are wrapped in a thread so we
|
||||
// are not on the user thread.
|
||||
@Nullable
|
||||
public T getPersisted(String fileName) {
|
||||
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
|
||||
log.warn("We have started the shut down routine already. We ignore that getPersisted call.");
|
||||
return null;
|
||||
}
|
||||
|
||||
File storageFile = new File(dir, fileName);
|
||||
if (!storageFile.exists()) {
|
||||
return null;
|
||||
@ -259,6 +339,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void requestPersistence() {
|
||||
if (FLUSH_ALL_DATA_TO_DISK_CALLED) {
|
||||
log.warn("We have started the shut down routine already. We ignore that requestPersistence call.");
|
||||
return;
|
||||
}
|
||||
|
||||
persistenceRequested = true;
|
||||
|
||||
// We write to disk with a delay to avoid frequent write operations. Depending on the priority those delays
|
||||
@ -267,7 +352,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
timer = UserThread.runAfter(() -> {
|
||||
persistNow(null);
|
||||
UserThread.execute(() -> timer = null);
|
||||
}, source.delayInSec, TimeUnit.SECONDS);
|
||||
}, source.delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@ -369,7 +454,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
|
||||
",\n dir=" + dir +
|
||||
",\n storageFile=" + storageFile +
|
||||
",\n persistable=" + persistable +
|
||||
",\n priority=" + source +
|
||||
",\n source=" + source +
|
||||
",\n usedTempFilePath=" + usedTempFilePath +
|
||||
",\n persistenceRequested=" + persistenceRequested +
|
||||
"\n}";
|
||||
|
@ -17,12 +17,6 @@
|
||||
|
||||
package bisq.common.proto.persistable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PersistedDataHost {
|
||||
void readPersisted();
|
||||
|
||||
static void apply(List<PersistedDataHost> persistedDataHosts) {
|
||||
persistedDataHosts.forEach(PersistedDataHost::readPersisted);
|
||||
}
|
||||
void readPersisted(Runnable completeHandler);
|
||||
}
|
||||
|
@ -48,8 +48,8 @@ import sun.misc.Signal;
|
||||
public class CommonSetup {
|
||||
|
||||
public static void setup(Config config, GracefulShutDownHandler gracefulShutDownHandler) {
|
||||
AsciiLogo.showAsciiLogo();
|
||||
setupLog(config);
|
||||
AsciiLogo.showAsciiLogo();
|
||||
Version.setBaseCryptoNetworkId(config.baseCurrencyNetwork.ordinal());
|
||||
Version.printVersion();
|
||||
maybePrintPathOfCodeSource();
|
||||
@ -102,13 +102,15 @@ public class CommonSetup {
|
||||
|
||||
protected static void setupSigIntHandlers(GracefulShutDownHandler gracefulShutDownHandler) {
|
||||
Signal.handle(new Signal("INT"), signal -> {
|
||||
gracefulShutDownHandler.gracefulShutDown(() -> {
|
||||
});
|
||||
log.info("Received {}", signal);
|
||||
UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> {
|
||||
}));
|
||||
});
|
||||
|
||||
Signal.handle(new Signal("TERM"), signal -> {
|
||||
gracefulShutDownHandler.gracefulShutDown(() -> {
|
||||
});
|
||||
log.info("Received {}", signal);
|
||||
UserThread.execute(() -> gracefulShutDownHandler.gracefulShutDown(() -> {
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -32,9 +32,13 @@ public class Profiler {
|
||||
}
|
||||
|
||||
public static long getUsedMemoryInMB() {
|
||||
return getUsedMemoryInBytes() / 1024 / 1024;
|
||||
}
|
||||
|
||||
public static long getUsedMemoryInBytes() {
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long free = runtime.freeMemory() / 1024 / 1024;
|
||||
long total = runtime.totalMemory() / 1024 / 1024;
|
||||
long free = runtime.freeMemory();
|
||||
long total = runtime.totalMemory();
|
||||
return total - free;
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,8 @@ import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
@ -523,4 +525,11 @@ public class Utilities {
|
||||
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
|
||||
}
|
||||
|
||||
public static String readableFileSize(long size) {
|
||||
if (size <= 0) return "0";
|
||||
String[] units = new String[]{"B", "kB", "MB", "GB", "TB"};
|
||||
int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
|
||||
return new DecimalFormat("#,##0.###").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -85,7 +85,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
@Slf4j
|
||||
public class AccountAgeWitnessService {
|
||||
private static final Date RELEASE = Utilities.getUTCDate(2017, GregorianCalendar.NOVEMBER, 11);
|
||||
public static final Date FULL_ACTIVATION = Utilities.getUTCDate(2018, GregorianCalendar.FEBRUARY, 15);
|
||||
private static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.MARCH, 1).getTime();
|
||||
|
||||
public enum AccountAge {
|
||||
@ -105,6 +104,7 @@ public class AccountAgeWitnessService {
|
||||
|
||||
private String presentation;
|
||||
private String hash = "";
|
||||
private long daysUntilLimitLifted = 0;
|
||||
|
||||
SignState(String presentation) {
|
||||
this.presentation = presentation;
|
||||
@ -115,11 +115,16 @@ public class AccountAgeWitnessService {
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignState setDaysUntilLimitLifted(long days) {
|
||||
this.daysUntilLimitLifted = days;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getPresentation() {
|
||||
if (!hash.isEmpty()) { // Only showing in DEBUG mode
|
||||
return presentation + " " + hash;
|
||||
}
|
||||
return presentation;
|
||||
return String.format(presentation, daysUntilLimitLifted);
|
||||
}
|
||||
|
||||
}
|
||||
@ -208,10 +213,11 @@ public class AccountAgeWitnessService {
|
||||
user.getPaymentAccounts().stream()
|
||||
.filter(e -> !(e instanceof AssetAccount))
|
||||
.forEach(e -> {
|
||||
// We delay with a random interval of 20-60 sec to ensure to be better connected and don't stress the
|
||||
// P2P network with publishing all at once at startup time.
|
||||
// We delay with a random interval of 20-60 sec to ensure to be better connected and don't
|
||||
// stress the P2P network with publishing all at once at startup time.
|
||||
final int delayInSec = 20 + new Random().nextInt(40);
|
||||
UserThread.runAfter(() -> p2PService.addPersistableNetworkPayload(getMyWitness(e.getPaymentAccountPayload()), true), delayInSec);
|
||||
UserThread.runAfter(() -> p2PService.addPersistableNetworkPayload(getMyWitness(
|
||||
e.getPaymentAccountPayload()), true), delayInSec);
|
||||
});
|
||||
}
|
||||
|
||||
@ -238,7 +244,8 @@ public class AccountAgeWitnessService {
|
||||
}
|
||||
|
||||
byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) {
|
||||
return Utilities.concatenateByteArrays(paymentAccountPayload.getAgeWitnessInputData(), paymentAccountPayload.getSalt());
|
||||
return Utilities.concatenateByteArrays(paymentAccountPayload.getAgeWitnessInputData(),
|
||||
paymentAccountPayload.getSalt());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@ -281,7 +288,8 @@ public class AccountAgeWitnessService {
|
||||
if (!containsKey)
|
||||
log.debug("hash not found in accountAgeWitnessMap");
|
||||
|
||||
return accountAgeWitnessMap.containsKey(hashAsByteArray) ? Optional.of(accountAgeWitnessMap.get(hashAsByteArray)) : Optional.empty();
|
||||
return accountAgeWitnessMap.containsKey(hashAsByteArray) ?
|
||||
Optional.of(accountAgeWitnessMap.get(hashAsByteArray)) : Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<AccountAgeWitness> getWitnessByHashAsHex(String hashAsHex) {
|
||||
@ -359,65 +367,52 @@ public class AccountAgeWitnessService {
|
||||
}
|
||||
}
|
||||
|
||||
// Checks trade limit based on time since signing of AccountAgeWitness
|
||||
// Get trade limit based on a time schedule
|
||||
// Buying of BTC with a payment method that has chargeback risk will use a low trade limit schedule
|
||||
// All selling and all other fiat payment methods use the normal trade limit schedule
|
||||
// Non fiat always has max limit
|
||||
// Account types that can get signed will use time since signing, other methods use time since account age creation
|
||||
// when measuring account age
|
||||
private long getTradeLimit(Coin maxTradeLimit,
|
||||
String currencyCode,
|
||||
AccountAgeWitness accountAgeWitness,
|
||||
AccountAge accountAgeCategory,
|
||||
OfferPayload.Direction direction,
|
||||
PaymentMethod paymentMethod) {
|
||||
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
|
||||
double factor;
|
||||
boolean isRisky = PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode);
|
||||
if (!isRisky || direction == OfferPayload.Direction.SELL) {
|
||||
// Get age of witness rather than time since signing for non risky payment methods and for selling
|
||||
accountAgeCategory = getAccountAgeCategory(getAccountAge(accountAgeWitness, new Date()));
|
||||
}
|
||||
long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
|
||||
if (direction == OfferPayload.Direction.BUY && isRisky) {
|
||||
// Used only for bying of BTC with risky payment methods
|
||||
switch (accountAgeCategory) {
|
||||
case TWO_MONTHS_OR_MORE:
|
||||
factor = 1;
|
||||
break;
|
||||
case ONE_TO_TWO_MONTHS:
|
||||
factor = 0.5;
|
||||
break;
|
||||
case LESS_ONE_MONTH:
|
||||
case UNVERIFIED:
|
||||
default:
|
||||
factor = 0;
|
||||
}
|
||||
} else {
|
||||
// Used by non risky payment methods and for selling BTC with risky methods
|
||||
switch (accountAgeCategory) {
|
||||
case TWO_MONTHS_OR_MORE:
|
||||
factor = 1;
|
||||
break;
|
||||
case ONE_TO_TWO_MONTHS:
|
||||
factor = 0.5;
|
||||
break;
|
||||
case LESS_ONE_MONTH:
|
||||
case UNVERIFIED:
|
||||
factor = 0.25;
|
||||
break;
|
||||
default:
|
||||
factor = 0;
|
||||
}
|
||||
}
|
||||
if (factor > 0) {
|
||||
limit = MathUtils.roundDoubleToLong((double) maxTradeLimit.value * factor);
|
||||
}
|
||||
|
||||
log.debug("accountAgeCategory={}, limit={}, factor={}, accountAgeWitnessHash={}",
|
||||
accountAgeCategory,
|
||||
Coin.valueOf(limit).toFriendlyString(),
|
||||
factor,
|
||||
Utilities.bytesAsHexString(accountAgeWitness.getHash()));
|
||||
return limit;
|
||||
} else {
|
||||
if (CurrencyUtil.isCryptoCurrency(currencyCode) ||
|
||||
!PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode) ||
|
||||
direction == OfferPayload.Direction.SELL) {
|
||||
return maxTradeLimit.value;
|
||||
}
|
||||
|
||||
long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
|
||||
var factor = signedBuyFactor(accountAgeCategory);
|
||||
if (factor > 0) {
|
||||
limit = MathUtils.roundDoubleToLong((double) maxTradeLimit.value * factor);
|
||||
}
|
||||
|
||||
log.debug("limit={}, factor={}, accountAgeWitnessHash={}",
|
||||
Coin.valueOf(limit).toFriendlyString(),
|
||||
factor,
|
||||
Utilities.bytesAsHexString(accountAgeWitness.getHash()));
|
||||
return limit;
|
||||
}
|
||||
|
||||
private double signedBuyFactor(AccountAge accountAgeCategory) {
|
||||
switch (accountAgeCategory) {
|
||||
case TWO_MONTHS_OR_MORE:
|
||||
return 1;
|
||||
case ONE_TO_TWO_MONTHS:
|
||||
return 0.5;
|
||||
case LESS_ONE_MONTH:
|
||||
case UNVERIFIED:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private double normalFactor() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -444,7 +439,8 @@ public class AccountAgeWitnessService {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public AccountAgeWitness getMyWitness(PaymentAccountPayload paymentAccountPayload) {
|
||||
final Optional<AccountAgeWitness> accountAgeWitnessOptional = findWitness(paymentAccountPayload, keyRing.getPubKeyRing());
|
||||
final Optional<AccountAgeWitness> accountAgeWitnessOptional =
|
||||
findWitness(paymentAccountPayload, keyRing.getPubKeyRing());
|
||||
return accountAgeWitnessOptional.orElseGet(() -> getNewWitness(paymentAccountPayload, keyRing.getPubKeyRing()));
|
||||
}
|
||||
|
||||
@ -460,8 +456,7 @@ public class AccountAgeWitnessService {
|
||||
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
|
||||
}
|
||||
|
||||
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferPayload.Direction
|
||||
direction) {
|
||||
public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferPayload.Direction direction) {
|
||||
if (paymentAccount == null)
|
||||
return 0;
|
||||
|
||||
@ -492,7 +487,8 @@ public class AccountAgeWitnessService {
|
||||
byte[] nonce,
|
||||
byte[] signature,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
final Optional<AccountAgeWitness> accountAgeWitnessOptional = findWitness(peersPaymentAccountPayload, peersPubKeyRing);
|
||||
final Optional<AccountAgeWitness> accountAgeWitnessOptional =
|
||||
findWitness(peersPaymentAccountPayload, peersPubKeyRing);
|
||||
// If we don't find a stored witness data we create a new dummy object which makes is easier to reuse the
|
||||
// below validation methods. This peersWitness object is not used beside for validation. Some of the
|
||||
// validation calls are pointless in the case we create a new Witness ourselves but the verifyPeersTradeLimit
|
||||
@ -513,8 +509,10 @@ public class AccountAgeWitnessService {
|
||||
if (!verifyPeersCurrentDate(peersCurrentDate, errorMessageHandler))
|
||||
return false;
|
||||
|
||||
final byte[] peersAccountInputDataWithSalt = Utilities.concatenateByteArrays(peersPaymentAccountPayload.getAgeWitnessInputData(), peersPaymentAccountPayload.getSalt());
|
||||
byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(peersAccountInputDataWithSalt, peersPubKeyRing.getSignaturePubKeyBytes()));
|
||||
final byte[] peersAccountInputDataWithSalt = Utilities.concatenateByteArrays(
|
||||
peersPaymentAccountPayload.getAgeWitnessInputData(), peersPaymentAccountPayload.getSalt());
|
||||
byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(peersAccountInputDataWithSalt,
|
||||
peersPubKeyRing.getSignaturePubKeyBytes()));
|
||||
|
||||
// Check if the hash in the witness data matches the hash derived from the data provided by the peer
|
||||
final byte[] peersWitnessHash = peersWitness.getHash();
|
||||
@ -591,7 +589,8 @@ public class AccountAgeWitnessService {
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
checkNotNull(offer);
|
||||
final String currencyCode = offer.getCurrencyCode();
|
||||
final Coin defaultMaxTradeLimit = PaymentMethod.getPaymentMethodById(offer.getOfferPayload().getPaymentMethodId()).getMaxTradeLimitAsCoin(currencyCode);
|
||||
final Coin defaultMaxTradeLimit = PaymentMethod.getPaymentMethodById(
|
||||
offer.getOfferPayload().getPaymentMethodId()).getMaxTradeLimitAsCoin(currencyCode);
|
||||
long peersCurrentTradeLimit = defaultMaxTradeLimit.value;
|
||||
if (!hasTradeLimitException(peersWitness)) {
|
||||
final long accountSignAge = getWitnessSignAge(peersWitness, peersCurrentDate);
|
||||
@ -654,8 +653,8 @@ public class AccountAgeWitnessService {
|
||||
.findAny()
|
||||
.orElse(null);
|
||||
checkNotNull(signedWitness);
|
||||
return signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, key, signedWitness.getWitnessOwnerPubKey(),
|
||||
time);
|
||||
return signedWitnessService.signAndPublishAccountAgeWitness(accountAgeWitness, key,
|
||||
signedWitness.getWitnessOwnerPubKey(), time);
|
||||
}
|
||||
|
||||
public String arbitratorSignOrphanPubKey(ECKey key,
|
||||
@ -676,7 +675,8 @@ public class AccountAgeWitnessService {
|
||||
Coin tradeAmount = trade.getTradeAmount();
|
||||
checkNotNull(trade.getProcessModel().getTradingPeer().getPubKeyRing(), "Peer must have a keyring");
|
||||
PublicKey peersPubKey = trade.getProcessModel().getTradingPeer().getPubKeyRing().getSignaturePubKey();
|
||||
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}", trade.toString());
|
||||
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}",
|
||||
trade.toString());
|
||||
checkNotNull(tradeAmount, "Trade amount must not be null");
|
||||
checkNotNull(peersPubKey, "Peers pub key must not be null");
|
||||
|
||||
@ -717,7 +717,8 @@ public class AccountAgeWitnessService {
|
||||
filterManager.isPaymentMethodBanned(
|
||||
PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) ||
|
||||
filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getBuyerPaymentAccountPayload()) ||
|
||||
filterManager.arePeersPaymentAccountDataBanned(dispute.getContract().getSellerPaymentAccountPayload()) ||
|
||||
filterManager.arePeersPaymentAccountDataBanned(
|
||||
dispute.getContract().getSellerPaymentAccountPayload()) ||
|
||||
filterManager.isWitnessSignerPubKeyBanned(
|
||||
Utils.HEX.encode(dispute.getContract().getBuyerPubKeyRing().getSignaturePubKeyBytes())) ||
|
||||
filterManager.isWitnessSignerPubKeyBanned(
|
||||
@ -811,7 +812,8 @@ public class AccountAgeWitnessService {
|
||||
case ONE_TO_TWO_MONTHS:
|
||||
return SignState.PEER_SIGNER.addHash(hash);
|
||||
case LESS_ONE_MONTH:
|
||||
return SignState.PEER_INITIAL.addHash(hash);
|
||||
return SignState.PEER_INITIAL.addHash(hash)
|
||||
.setDaysUntilLimitLifted(30 - TimeUnit.MILLISECONDS.toDays(accountSignAge));
|
||||
case UNVERIFIED:
|
||||
default:
|
||||
return SignState.UNSIGNED.addHash(hash);
|
||||
|
@ -35,7 +35,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class AccountAgeWitnessStore extends PersistableNetworkPayloadStore<AccountAgeWitness> {
|
||||
|
||||
AccountAgeWitnessStore() {
|
||||
public AccountAgeWitnessStore() {
|
||||
}
|
||||
|
||||
|
||||
|
@ -133,7 +133,7 @@ public class PrivateNotificationManager {
|
||||
}
|
||||
|
||||
public void removePrivateNotification() {
|
||||
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
|
||||
p2PService.removeMailboxMsg(decryptedMessageWithPubKey);
|
||||
}
|
||||
|
||||
private boolean isKeyValid(String privKeyString) {
|
||||
|
@ -22,6 +22,7 @@ import bisq.core.monetary.Price;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.statistics.TradeStatistics3;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
|
||||
@ -51,6 +52,7 @@ public class CoreApi {
|
||||
private final CoreOffersService coreOffersService;
|
||||
private final CorePaymentAccountsService paymentAccountsService;
|
||||
private final CorePriceService corePriceService;
|
||||
private final CoreTradesService coreTradesService;
|
||||
private final CoreWalletsService walletsService;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
|
||||
@ -59,12 +61,14 @@ public class CoreApi {
|
||||
CoreOffersService coreOffersService,
|
||||
CorePaymentAccountsService paymentAccountsService,
|
||||
CorePriceService corePriceService,
|
||||
CoreTradesService coreTradesService,
|
||||
CoreWalletsService walletsService,
|
||||
TradeStatisticsManager tradeStatisticsManager) {
|
||||
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
||||
this.coreOffersService = coreOffersService;
|
||||
this.corePriceService = corePriceService;
|
||||
this.paymentAccountsService = paymentAccountsService;
|
||||
this.coreTradesService = coreTradesService;
|
||||
this.corePriceService = corePriceService;
|
||||
this.walletsService = walletsService;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
}
|
||||
@ -138,6 +142,10 @@ public class CoreApi {
|
||||
paymentAccount);
|
||||
}
|
||||
|
||||
public void cancelOffer(String id) {
|
||||
coreOffersService.cancelOffer(id);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PaymentAccounts
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -164,6 +172,43 @@ public class CoreApi {
|
||||
return corePriceService.getMarketPrice(currencyCode);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Trades
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void takeOffer(String offerId,
|
||||
String paymentAccountId,
|
||||
Consumer<Trade> resultHandler) {
|
||||
Offer offer = coreOffersService.getOffer(offerId);
|
||||
coreTradesService.takeOffer(offer,
|
||||
paymentAccountId,
|
||||
resultHandler);
|
||||
}
|
||||
|
||||
public void confirmPaymentStarted(String tradeId) {
|
||||
coreTradesService.confirmPaymentStarted(tradeId);
|
||||
}
|
||||
|
||||
public void confirmPaymentReceived(String tradeId) {
|
||||
coreTradesService.confirmPaymentReceived(tradeId);
|
||||
}
|
||||
|
||||
public void keepFunds(String tradeId) {
|
||||
coreTradesService.keepFunds(tradeId);
|
||||
}
|
||||
|
||||
public void withdrawFunds(String tradeId, String address) {
|
||||
coreTradesService.withdrawFunds(tradeId, address);
|
||||
}
|
||||
|
||||
public Trade getTrade(String tradeId) {
|
||||
return coreTradesService.getTrade(tradeId);
|
||||
}
|
||||
|
||||
public String getTradeRole(String tradeId) {
|
||||
return coreTradesService.getTradeRole(tradeId);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Wallets
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -160,6 +160,16 @@ class CoreOffersService {
|
||||
paymentAccount);
|
||||
}
|
||||
|
||||
void cancelOffer(String id) {
|
||||
Offer offer = getOffer(id);
|
||||
openOfferManager.removeOffer(offer,
|
||||
() -> {
|
||||
},
|
||||
errorMessage -> {
|
||||
throw new IllegalStateException(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
private void placeOffer(Offer offer,
|
||||
double buyerSecurityDeposit,
|
||||
boolean useSavingsWallet,
|
||||
|
247
core/src/main/java/bisq/core/api/CoreTradesService.java
Normal file
247
core/src/main/java/bisq/core/api/CoreTradesService.java
Normal file
@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import bisq.core.btc.model.AddressEntry;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.takeoffer.TakeOfferModel;
|
||||
import bisq.core.trade.Tradable;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.TradeUtil;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
import bisq.core.trade.protocol.BuyerProtocol;
|
||||
import bisq.core.trade.protocol.SellerProtocol;
|
||||
import bisq.core.user.User;
|
||||
import bisq.core.util.validation.BtcAddressValidator;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
|
||||
import static java.lang.String.format;
|
||||
|
||||
@Slf4j
|
||||
class CoreTradesService {
|
||||
|
||||
// Dependencies on core api services in this package must be kept to an absolute
|
||||
// minimum, but some trading functions require an unlocked wallet's key, so an
|
||||
// exception is made in this case.
|
||||
private final CoreWalletsService coreWalletsService;
|
||||
|
||||
private final BtcWalletService btcWalletService;
|
||||
private final ClosedTradableManager closedTradableManager;
|
||||
private final TakeOfferModel takeOfferModel;
|
||||
private final TradeManager tradeManager;
|
||||
private final TradeUtil tradeUtil;
|
||||
private final User user;
|
||||
|
||||
@Inject
|
||||
public CoreTradesService(CoreWalletsService coreWalletsService,
|
||||
BtcWalletService btcWalletService,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
TakeOfferModel takeOfferModel,
|
||||
TradeManager tradeManager,
|
||||
TradeUtil tradeUtil,
|
||||
User user) {
|
||||
this.coreWalletsService = coreWalletsService;
|
||||
this.btcWalletService = btcWalletService;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
this.takeOfferModel = takeOfferModel;
|
||||
this.tradeManager = tradeManager;
|
||||
this.tradeUtil = tradeUtil;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
void takeOffer(Offer offer,
|
||||
String paymentAccountId,
|
||||
Consumer<Trade> resultHandler) {
|
||||
var paymentAccount = user.getPaymentAccount(paymentAccountId);
|
||||
if (paymentAccount == null)
|
||||
throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId));
|
||||
|
||||
var useSavingsWallet = true;
|
||||
//noinspection ConstantConditions
|
||||
takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet);
|
||||
log.info("Initiating take {} offer, {}",
|
||||
offer.isBuyOffer() ? "buy" : "sell",
|
||||
takeOfferModel);
|
||||
//noinspection ConstantConditions
|
||||
tradeManager.onTakeOffer(offer.getAmount(),
|
||||
takeOfferModel.getTxFeeFromFeeService(),
|
||||
takeOfferModel.getTakerFee(),
|
||||
takeOfferModel.isCurrencyForTakerFeeBtc(),
|
||||
offer.getPrice().getValue(),
|
||||
takeOfferModel.getFundsNeededForTrade(),
|
||||
offer,
|
||||
paymentAccountId,
|
||||
useSavingsWallet,
|
||||
resultHandler::accept,
|
||||
errorMessage -> {
|
||||
log.error(errorMessage);
|
||||
throw new IllegalStateException(errorMessage);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void confirmPaymentStarted(String tradeId) {
|
||||
var trade = getTrade(tradeId);
|
||||
if (isFollowingBuyerProtocol(trade)) {
|
||||
var tradeProtocol = tradeManager.getTradeProtocol(trade);
|
||||
((BuyerProtocol) tradeProtocol).onPaymentStarted(
|
||||
() -> {
|
||||
},
|
||||
errorMessage -> {
|
||||
throw new IllegalStateException(errorMessage);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw new IllegalStateException("you are the seller and not sending payment");
|
||||
}
|
||||
}
|
||||
|
||||
void confirmPaymentReceived(String tradeId) {
|
||||
var trade = getTrade(tradeId);
|
||||
if (isFollowingBuyerProtocol(trade)) {
|
||||
throw new IllegalStateException("you are the buyer, and not receiving payment");
|
||||
} else {
|
||||
var tradeProtocol = tradeManager.getTradeProtocol(trade);
|
||||
((SellerProtocol) tradeProtocol).onPaymentReceived(
|
||||
() -> {
|
||||
},
|
||||
errorMessage -> {
|
||||
throw new IllegalStateException(errorMessage);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void keepFunds(String tradeId) {
|
||||
verifyTradeIsNotClosed(tradeId);
|
||||
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||
log.info("Keeping funds received from trade {}", tradeId);
|
||||
tradeManager.onTradeCompleted(trade);
|
||||
}
|
||||
|
||||
void withdrawFunds(String tradeId, String toAddress) {
|
||||
// An encrypted wallet must be unlocked for this operation.
|
||||
verifyTradeIsNotClosed(tradeId);
|
||||
var trade = getOpenTrade(tradeId).orElseThrow(() ->
|
||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||
|
||||
verifyIsValidBTCAddress(toAddress);
|
||||
|
||||
var fromAddressEntry = btcWalletService.getOrCreateAddressEntry(trade.getId(), TRADE_PAYOUT);
|
||||
verifyFundsNotWithdrawn(fromAddressEntry);
|
||||
|
||||
var amount = trade.getPayoutAmount();
|
||||
var fee = getEstimatedTxFee(fromAddressEntry.getAddressString(), toAddress, amount);
|
||||
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",
|
||||
tradeId,
|
||||
fromAddressEntry.getAddressString(),
|
||||
toAddress,
|
||||
amount.toFriendlyString(),
|
||||
fee.toFriendlyString(),
|
||||
receiverAmount.toFriendlyString()));
|
||||
|
||||
tradeManager.onWithdrawRequest(
|
||||
toAddress,
|
||||
amount,
|
||||
fee,
|
||||
coreWalletsService.getKey(),
|
||||
trade,
|
||||
() -> {
|
||||
},
|
||||
(errorMessage, throwable) -> {
|
||||
log.error(errorMessage, throwable);
|
||||
throw new IllegalStateException(errorMessage, throwable);
|
||||
});
|
||||
}
|
||||
|
||||
String getTradeRole(String tradeId) {
|
||||
return tradeUtil.getRole(getTrade(tradeId));
|
||||
}
|
||||
|
||||
Trade getTrade(String tradeId) {
|
||||
return getOpenTrade(tradeId).orElseGet(() ->
|
||||
getClosedTrade(tradeId).orElseThrow(() ->
|
||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId))
|
||||
));
|
||||
}
|
||||
|
||||
private Optional<Trade> getOpenTrade(String tradeId) {
|
||||
return tradeManager.getTradeById(tradeId);
|
||||
}
|
||||
|
||||
private Optional<Trade> getClosedTrade(String tradeId) {
|
||||
Optional<Tradable> tradable = closedTradableManager.getTradableById(tradeId);
|
||||
return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
|
||||
}
|
||||
|
||||
private boolean isFollowingBuyerProtocol(Trade trade) {
|
||||
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
|
||||
}
|
||||
|
||||
private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) {
|
||||
// TODO This and identical logic should be refactored into TradeUtil.
|
||||
try {
|
||||
return btcWalletService.getFeeEstimationTransaction(fromAddress,
|
||||
toAddress,
|
||||
amount,
|
||||
TRADE_PAYOUT).getFee();
|
||||
} catch (Exception ex) {
|
||||
log.error("", ex);
|
||||
throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// Throws a RuntimeException trade is already closed.
|
||||
private void verifyTradeIsNotClosed(String tradeId) {
|
||||
if (getClosedTrade(tradeId).isPresent())
|
||||
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
|
||||
}
|
||||
|
||||
// Throws a RuntimeException if address is not valid.
|
||||
private void verifyIsValidBTCAddress(String address) {
|
||||
try {
|
||||
new BtcAddressValidator().validate(address);
|
||||
} catch (Throwable t) {
|
||||
log.error("", t);
|
||||
throw new IllegalArgumentException(format("'%s' is not a valid btc address", address));
|
||||
}
|
||||
}
|
||||
|
||||
// Throws a RuntimeException if address has a zero balance.
|
||||
private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) {
|
||||
Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress());
|
||||
if (fromAddressBalance.isZero())
|
||||
throw new IllegalStateException(format("funds already withdrawn from address '%s'",
|
||||
fromAddressEntry.getAddressString()));
|
||||
}
|
||||
}
|
@ -23,6 +23,9 @@ import bisq.core.btc.model.AddressEntry;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.TransactionConfidence;
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
@ -37,8 +40,6 @@ import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -57,7 +58,7 @@ class CoreWalletsService {
|
||||
private final BtcWalletService btcWalletService;
|
||||
|
||||
@Nullable
|
||||
private TimerTask lockTask;
|
||||
private Timer lockTimer;
|
||||
|
||||
@Nullable
|
||||
private KeyParameter tempAesKey;
|
||||
@ -71,6 +72,12 @@ class CoreWalletsService {
|
||||
this.btcWalletService = btcWalletService;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
KeyParameter getKey() {
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
return tempAesKey;
|
||||
}
|
||||
|
||||
long getAvailableBalance() {
|
||||
verifyWalletsAreAvailable();
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
@ -184,29 +191,22 @@ class CoreWalletsService {
|
||||
if (!walletsManager.checkAESKey(tempAesKey))
|
||||
throw new IllegalStateException("incorrect password");
|
||||
|
||||
if (lockTask != null) {
|
||||
// The user is overriding a prior unlock timeout. Cancel the existing
|
||||
// lock TimerTask to prevent it from calling lockWallet() before or after the
|
||||
// new timer task does.
|
||||
lockTask.cancel();
|
||||
// Avoid the synchronized(lock) overhead of an unnecessary lockTask.cancel()
|
||||
// call the next time 'unlockwallet' is called.
|
||||
lockTask = null;
|
||||
if (lockTimer != null) {
|
||||
// The user has called unlockwallet again, before the prior unlockwallet
|
||||
// timeout has expired. He's overriding it with a new timeout value.
|
||||
// Remove the existing lock timer to prevent it from calling lockwallet
|
||||
// before or after the new one does.
|
||||
lockTimer.stop();
|
||||
lockTimer = null;
|
||||
}
|
||||
|
||||
lockTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (tempAesKey != null) {
|
||||
// Do not try to lock wallet after timeout if the user has already
|
||||
// done so via 'lockwallet'
|
||||
log.info("Locking wallet after {} second timeout expired.", timeout);
|
||||
tempAesKey = null;
|
||||
}
|
||||
lockTimer = UserThread.runAfter(() -> {
|
||||
if (tempAesKey != null) {
|
||||
// The unlockwallet timeout has expired; re-lock the wallet.
|
||||
log.info("Locking wallet after {} second timeout expired.", timeout);
|
||||
tempAesKey = null;
|
||||
}
|
||||
};
|
||||
Timer timer = new Timer("Lock Wallet Timer");
|
||||
timer.schedule(lockTask, SECONDS.toMillis(timeout));
|
||||
}, timeout, SECONDS);
|
||||
}
|
||||
|
||||
// Provided for automated wallet protection method testing, despite the
|
||||
|
@ -1,60 +0,0 @@
|
||||
package bisq.core.api;
|
||||
|
||||
import bisq.core.btc.Balances;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
import bisq.core.dao.state.DaoStateService;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Singleton
|
||||
@Slf4j
|
||||
class StatusCheck {
|
||||
|
||||
private final Config config;
|
||||
private final P2PService p2PService;
|
||||
private final DaoStateService daoStateService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final WalletsManager walletsManager;
|
||||
private final Balances balances;
|
||||
|
||||
@Inject
|
||||
public StatusCheck(Config config,
|
||||
P2PService p2PService,
|
||||
DaoStateService daoStateService,
|
||||
WalletsSetup walletsSetup,
|
||||
WalletsManager walletsManager,
|
||||
Balances balances) {
|
||||
this.config = config;
|
||||
this.p2PService = p2PService;
|
||||
this.daoStateService = daoStateService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.walletsManager = walletsManager;
|
||||
this.balances = balances;
|
||||
}
|
||||
|
||||
public void verifyCanTrade() {
|
||||
if (!p2PService.isBootstrapped())
|
||||
throw new IllegalStateException("p2p service is not yet bootstrapped");
|
||||
|
||||
if (!daoStateService.isParseBlockChainComplete())
|
||||
throw new IllegalStateException("dao block chain sync is not yet complete");
|
||||
|
||||
if (config.baseCurrencyNetwork.isMainnet()
|
||||
&& p2PService.getNumConnectedPeers().get() < walletsSetup.getMinBroadcastConnections())
|
||||
throw new IllegalStateException("not enough connected peers");
|
||||
|
||||
if (!walletsManager.areWalletsAvailable())
|
||||
throw new IllegalStateException("wallet is not yet available");
|
||||
|
||||
if (balances.getAvailableBalance().get() == null)
|
||||
throw new IllegalStateException("balance is not yet available");
|
||||
}
|
||||
}
|
@ -17,8 +17,12 @@
|
||||
|
||||
package bisq.core.api.model;
|
||||
|
||||
import bisq.core.offer.Offer;
|
||||
|
||||
import bisq.common.Payload;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
@ -28,6 +32,10 @@ import lombok.ToString;
|
||||
@Getter
|
||||
public class OfferInfo implements Payload {
|
||||
|
||||
// The client cannot see bisq.core.Offer or its fromProto method. We use the lighter
|
||||
// weight OfferInfo proto wrapper instead, containing just enough fields to view,
|
||||
// create and take offers.
|
||||
|
||||
private final String id;
|
||||
private final String direction;
|
||||
private final long price;
|
||||
@ -46,6 +54,7 @@ public class OfferInfo implements Payload {
|
||||
private final String baseCurrencyCode;
|
||||
private final String counterCurrencyCode;
|
||||
private final long date;
|
||||
private final String state;
|
||||
|
||||
public OfferInfo(OfferInfoBuilder builder) {
|
||||
this.id = builder.id;
|
||||
@ -64,6 +73,29 @@ public class OfferInfo implements Payload {
|
||||
this.baseCurrencyCode = builder.baseCurrencyCode;
|
||||
this.counterCurrencyCode = builder.counterCurrencyCode;
|
||||
this.date = builder.date;
|
||||
this.state = builder.state;
|
||||
}
|
||||
|
||||
public static OfferInfo toOfferInfo(Offer offer) {
|
||||
return new OfferInfo.OfferInfoBuilder()
|
||||
.withId(offer.getId())
|
||||
.withDirection(offer.getDirection().name())
|
||||
.withPrice(Objects.requireNonNull(offer.getPrice()).getValue())
|
||||
.withUseMarketBasedPrice(offer.isUseMarketBasedPrice())
|
||||
.withMarketPriceMargin(offer.getMarketPriceMargin())
|
||||
.withAmount(offer.getAmount().value)
|
||||
.withMinAmount(offer.getMinAmount().value)
|
||||
.withVolume(Objects.requireNonNull(offer.getVolume()).getValue())
|
||||
.withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue())
|
||||
.withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value)
|
||||
.withPaymentAccountId(offer.getMakerPaymentAccountId())
|
||||
.withPaymentMethodId(offer.getPaymentMethod().getId())
|
||||
.withPaymentMethodShortName(offer.getPaymentMethod().getShortName())
|
||||
.withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode())
|
||||
.withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode())
|
||||
.withDate(offer.getDate().getTime())
|
||||
.withState(offer.getState().name())
|
||||
.build();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -89,16 +121,13 @@ public class OfferInfo implements Payload {
|
||||
.setBaseCurrencyCode(baseCurrencyCode)
|
||||
.setCounterCurrencyCode(counterCurrencyCode)
|
||||
.setDate(date)
|
||||
.setState(state)
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "SameReturnValue"})
|
||||
public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) {
|
||||
/*
|
||||
TODO (will be needed by the createoffer method)
|
||||
return new OfferInfo(proto.getOfferPayload().getId(),
|
||||
proto.getOfferPayload().getDate());
|
||||
*/
|
||||
return null;
|
||||
return null; // TODO
|
||||
}
|
||||
|
||||
/*
|
||||
@ -124,9 +153,7 @@ public class OfferInfo implements Payload {
|
||||
private String baseCurrencyCode;
|
||||
private String counterCurrencyCode;
|
||||
private long date;
|
||||
|
||||
public OfferInfoBuilder() {
|
||||
}
|
||||
private String state;
|
||||
|
||||
public OfferInfoBuilder withId(String id) {
|
||||
this.id = id;
|
||||
@ -208,6 +235,11 @@ public class OfferInfo implements Payload {
|
||||
return this;
|
||||
}
|
||||
|
||||
public OfferInfoBuilder withState(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public OfferInfo build() {
|
||||
return new OfferInfo(this);
|
||||
}
|
||||
|
351
core/src/main/java/bisq/core/api/model/TradeInfo.java
Normal file
351
core/src/main/java/bisq/core/api/model/TradeInfo.java
Normal file
@ -0,0 +1,351 @@
|
||||
/*
|
||||
* 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.core.trade.Trade;
|
||||
|
||||
import bisq.common.Payload;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
import static bisq.core.api.model.OfferInfo.toOfferInfo;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@Getter
|
||||
public class TradeInfo implements Payload {
|
||||
|
||||
// The client cannot see bisq.core.trade.Trade or its fromProto method. We use the
|
||||
// lighter weight TradeInfo proto wrapper instead, containing just enough fields to
|
||||
// view and interact with trades.
|
||||
|
||||
private final OfferInfo offer;
|
||||
private final String tradeId;
|
||||
private final String shortId;
|
||||
private final long date;
|
||||
private final String role;
|
||||
private final boolean isCurrencyForTakerFeeBtc;
|
||||
private final long txFeeAsLong;
|
||||
private final long takerFeeAsLong;
|
||||
private final String takerFeeTxId;
|
||||
private final String depositTxId;
|
||||
private final String payoutTxId;
|
||||
private final long tradeAmountAsLong;
|
||||
private final long tradePrice;
|
||||
private final String tradingPeerNodeAddress;
|
||||
private final String state;
|
||||
private final String phase;
|
||||
private final String tradePeriodState;
|
||||
private final boolean isDepositPublished;
|
||||
private final boolean isDepositConfirmed;
|
||||
private final boolean isFiatSent;
|
||||
private final boolean isFiatReceived;
|
||||
private final boolean isPayoutPublished;
|
||||
private final boolean isWithdrawn;
|
||||
private final String contractAsJson;
|
||||
|
||||
public TradeInfo(TradeInfoBuilder builder) {
|
||||
this.offer = builder.offer;
|
||||
this.tradeId = builder.tradeId;
|
||||
this.shortId = builder.shortId;
|
||||
this.date = builder.date;
|
||||
this.role = builder.role;
|
||||
this.isCurrencyForTakerFeeBtc = builder.isCurrencyForTakerFeeBtc;
|
||||
this.txFeeAsLong = builder.txFeeAsLong;
|
||||
this.takerFeeAsLong = builder.takerFeeAsLong;
|
||||
this.takerFeeTxId = builder.takerFeeTxId;
|
||||
this.depositTxId = builder.depositTxId;
|
||||
this.payoutTxId = builder.payoutTxId;
|
||||
this.tradeAmountAsLong = builder.tradeAmountAsLong;
|
||||
this.tradePrice = builder.tradePrice;
|
||||
this.tradingPeerNodeAddress = builder.tradingPeerNodeAddress;
|
||||
this.state = builder.state;
|
||||
this.phase = builder.phase;
|
||||
this.tradePeriodState = builder.tradePeriodState;
|
||||
this.isDepositPublished = builder.isDepositPublished;
|
||||
this.isDepositConfirmed = builder.isDepositConfirmed;
|
||||
this.isFiatSent = builder.isFiatSent;
|
||||
this.isFiatReceived = builder.isFiatReceived;
|
||||
this.isPayoutPublished = builder.isPayoutPublished;
|
||||
this.isWithdrawn = builder.isWithdrawn;
|
||||
this.contractAsJson = builder.contractAsJson;
|
||||
}
|
||||
|
||||
public static TradeInfo toTradeInfo(Trade trade) {
|
||||
return toTradeInfo(trade, null);
|
||||
}
|
||||
|
||||
public static TradeInfo toTradeInfo(Trade trade, String role) {
|
||||
return new TradeInfo.TradeInfoBuilder()
|
||||
.withOffer(toOfferInfo(trade.getOffer()))
|
||||
.withTradeId(trade.getId())
|
||||
.withShortId(trade.getShortId())
|
||||
.withDate(trade.getDate().getTime())
|
||||
.withRole(role == null ? "" : role)
|
||||
.withIsCurrencyForTakerFeeBtc(trade.isCurrencyForTakerFeeBtc())
|
||||
.withTxFeeAsLong(trade.getTxFeeAsLong())
|
||||
.withTakerFeeAsLong(trade.getTakerFeeAsLong())
|
||||
.withTakerFeeAsLong(trade.getTakerFeeAsLong())
|
||||
.withTakerFeeTxId(trade.getTakerFeeTxId())
|
||||
.withDepositTxId(trade.getDepositTxId())
|
||||
.withPayoutTxId(trade.getPayoutTxId())
|
||||
.withTradeAmountAsLong(trade.getTradeAmountAsLong())
|
||||
.withTradePrice(trade.getTradePrice().getValue())
|
||||
.withTradingPeerNodeAddress(Objects.requireNonNull(
|
||||
trade.getTradingPeerNodeAddress()).getHostNameWithoutPostFix())
|
||||
.withState(trade.getState().name())
|
||||
.withPhase(trade.getPhase().name())
|
||||
.withTradePeriodState(trade.getTradePeriodState().name())
|
||||
.withIsDepositPublished(trade.isDepositPublished())
|
||||
.withIsDepositConfirmed(trade.isDepositConfirmed())
|
||||
.withIsFiatSent(trade.isFiatSent())
|
||||
.withIsFiatReceived(trade.isFiatReceived())
|
||||
.withIsPayoutPublished(trade.isPayoutPublished())
|
||||
.withIsWithdrawn(trade.isWithdrawn())
|
||||
.withContractAsJson(trade.getContractAsJson())
|
||||
.build();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public bisq.proto.grpc.TradeInfo toProtoMessage() {
|
||||
return bisq.proto.grpc.TradeInfo.newBuilder()
|
||||
.setOffer(offer.toProtoMessage())
|
||||
.setTradeId(tradeId)
|
||||
.setShortId(shortId)
|
||||
.setDate(date)
|
||||
.setRole(role)
|
||||
.setIsCurrencyForTakerFeeBtc(isCurrencyForTakerFeeBtc)
|
||||
.setTxFeeAsLong(txFeeAsLong)
|
||||
.setTakerFeeAsLong(takerFeeAsLong)
|
||||
.setTakerFeeTxId(takerFeeTxId == null ? "" : takerFeeTxId)
|
||||
.setDepositTxId(depositTxId == null ? "" : depositTxId)
|
||||
.setPayoutTxId(payoutTxId == null ? "" : payoutTxId)
|
||||
.setTradeAmountAsLong(tradeAmountAsLong)
|
||||
.setTradePrice(tradePrice)
|
||||
.setTradingPeerNodeAddress(tradingPeerNodeAddress)
|
||||
.setState(state)
|
||||
.setPhase(phase)
|
||||
.setTradePeriodState(tradePeriodState)
|
||||
.setIsDepositPublished(isDepositPublished)
|
||||
.setIsDepositConfirmed(isDepositConfirmed)
|
||||
.setIsFiatSent(isFiatSent)
|
||||
.setIsFiatReceived(isFiatReceived)
|
||||
.setIsPayoutPublished(isPayoutPublished)
|
||||
.setIsWithdrawn(isWithdrawn)
|
||||
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "SameReturnValue"})
|
||||
public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) {
|
||||
return null; // TODO
|
||||
}
|
||||
|
||||
/*
|
||||
* TradeInfoBuilder helps avoid bungling use of a large TradeInfo constructor
|
||||
* argument list. If consecutive argument values of the same type are not
|
||||
* ordered correctly, the compiler won't complain but the resulting bugs could
|
||||
* be hard to find and fix.
|
||||
*/
|
||||
public static class TradeInfoBuilder {
|
||||
private OfferInfo offer;
|
||||
private String tradeId;
|
||||
private String shortId;
|
||||
private long date;
|
||||
private String role;
|
||||
private boolean isCurrencyForTakerFeeBtc;
|
||||
private long txFeeAsLong;
|
||||
private long takerFeeAsLong;
|
||||
private String takerFeeTxId;
|
||||
private String depositTxId;
|
||||
private String payoutTxId;
|
||||
private long tradeAmountAsLong;
|
||||
private long tradePrice;
|
||||
private String tradingPeerNodeAddress;
|
||||
private String state;
|
||||
private String phase;
|
||||
private String tradePeriodState;
|
||||
private boolean isDepositPublished;
|
||||
private boolean isDepositConfirmed;
|
||||
private boolean isFiatSent;
|
||||
private boolean isFiatReceived;
|
||||
private boolean isPayoutPublished;
|
||||
private boolean isWithdrawn;
|
||||
private String contractAsJson;
|
||||
|
||||
public TradeInfoBuilder withOffer(OfferInfo offer) {
|
||||
this.offer = offer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTradeId(String tradeId) {
|
||||
this.tradeId = tradeId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withShortId(String shortId) {
|
||||
this.shortId = shortId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withDate(long date) {
|
||||
this.date = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withRole(String role) {
|
||||
this.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) {
|
||||
this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTxFeeAsLong(long txFeeAsLong) {
|
||||
this.txFeeAsLong = txFeeAsLong;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTakerFeeAsLong(long takerFeeAsLong) {
|
||||
this.takerFeeAsLong = takerFeeAsLong;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTakerFeeTxId(String takerFeeTxId) {
|
||||
this.takerFeeTxId = takerFeeTxId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withDepositTxId(String depositTxId) {
|
||||
this.depositTxId = depositTxId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withPayoutTxId(String payoutTxId) {
|
||||
this.payoutTxId = payoutTxId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTradeAmountAsLong(long tradeAmountAsLong) {
|
||||
this.tradeAmountAsLong = tradeAmountAsLong;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTradePrice(long tradePrice) {
|
||||
this.tradePrice = tradePrice;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTradePeriodState(String tradePeriodState) {
|
||||
this.tradePeriodState = tradePeriodState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withState(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withPhase(String phase) {
|
||||
this.phase = phase;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withTradingPeerNodeAddress(String tradingPeerNodeAddress) {
|
||||
this.tradingPeerNodeAddress = tradingPeerNodeAddress;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsDepositPublished(boolean isDepositPublished) {
|
||||
this.isDepositPublished = isDepositPublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsDepositConfirmed(boolean isDepositConfirmed) {
|
||||
this.isDepositConfirmed = isDepositConfirmed;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsFiatSent(boolean isFiatSent) {
|
||||
this.isFiatSent = isFiatSent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsFiatReceived(boolean isFiatReceived) {
|
||||
this.isFiatReceived = isFiatReceived;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsPayoutPublished(boolean isPayoutPublished) {
|
||||
this.isPayoutPublished = isPayoutPublished;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withIsWithdrawn(boolean isWithdrawn) {
|
||||
this.isWithdrawn = isWithdrawn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoBuilder withContractAsJson(String contractAsJson) {
|
||||
this.contractAsJson = contractAsJson;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfo build() {
|
||||
return new TradeInfo(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TradeInfo{" +
|
||||
" tradeId='" + tradeId + '\'' + "\n" +
|
||||
", shortId='" + shortId + '\'' + "\n" +
|
||||
", date='" + date + '\'' + "\n" +
|
||||
", role='" + role + '\'' + "\n" +
|
||||
", isCurrencyForTakerFeeBtc='" + isCurrencyForTakerFeeBtc + '\'' + "\n" +
|
||||
", txFeeAsLong='" + txFeeAsLong + '\'' + "\n" +
|
||||
", takerFeeAsLong='" + takerFeeAsLong + '\'' + "\n" +
|
||||
", takerFeeTxId='" + takerFeeTxId + '\'' + "\n" +
|
||||
", depositTxId='" + depositTxId + '\'' + "\n" +
|
||||
", payoutTxId='" + payoutTxId + '\'' + "\n" +
|
||||
", tradeAmountAsLong='" + tradeAmountAsLong + '\'' + "\n" +
|
||||
", tradePrice='" + tradePrice + '\'' + "\n" +
|
||||
", tradingPeerNodeAddress='" + tradingPeerNodeAddress + '\'' + "\n" +
|
||||
", state='" + state + '\'' + "\n" +
|
||||
", phase='" + phase + '\'' + "\n" +
|
||||
", tradePeriodState='" + tradePeriodState + '\'' + "\n" +
|
||||
", isDepositPublished=" + isDepositPublished + "\n" +
|
||||
", isDepositConfirmed=" + isDepositConfirmed + "\n" +
|
||||
", isFiatSent=" + isFiatSent + "\n" +
|
||||
", isFiatReceived=" + isFiatReceived + "\n" +
|
||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||
", isWithdrawn=" + isWithdrawn + "\n" +
|
||||
", offer=" + offer + "\n" +
|
||||
", contractAsJson=" + contractAsJson + "\n" +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -68,6 +68,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
|
||||
protected AppModule module;
|
||||
protected Config config;
|
||||
private boolean isShutdownInProgress;
|
||||
private boolean hasDowngraded;
|
||||
|
||||
public BisqExecutable(String fullName, String scriptName, String appName, String version) {
|
||||
this.fullName = fullName;
|
||||
@ -133,9 +134,17 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
|
||||
CommonSetup.setupUncaughtExceptionHandler(this);
|
||||
setupGuice();
|
||||
setupAvoidStandbyMode();
|
||||
readAllPersisted(this::startApplication);
|
||||
}
|
||||
|
||||
hasDowngraded = BisqSetup.hasDowngraded();
|
||||
if (hasDowngraded) {
|
||||
// If user tried to downgrade we do not read the persisted data to avoid data corruption
|
||||
// We call startApplication to enable UI to show popup. We prevent in BisqSetup to go further
|
||||
// in the process and require a shut down.
|
||||
startApplication();
|
||||
} else {
|
||||
readAllPersisted(this::startApplication);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// We continue with a series of synchronous execution tasks
|
||||
@ -168,15 +177,12 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
|
||||
}
|
||||
|
||||
AtomicInteger remaining = new AtomicInteger(hosts.size());
|
||||
hosts.forEach(e -> {
|
||||
new Thread(() -> {
|
||||
e.readPersisted();
|
||||
remaining.decrementAndGet();
|
||||
if (remaining.get() == 0) {
|
||||
hosts.forEach(host -> {
|
||||
host.readPersisted(() -> {
|
||||
if (remaining.decrementAndGet() == 0) {
|
||||
UserThread.execute(completeHandler);
|
||||
}
|
||||
|
||||
}, "BisqExecutable-read-" + e.getClass().getSimpleName()).start();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -239,11 +245,16 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
|
||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||
log.info("P2PService shutdown completed");
|
||||
module.close(injector);
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
log.info("Graceful shutdown completed. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
System.exit(EXIT_SUCCESS);
|
||||
});
|
||||
if (!hasDowngraded) {
|
||||
// If user tried to downgrade we do not write the persistable data to avoid data corruption
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
log.info("Graceful shutdown completed. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
});
|
||||
} else {
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
walletsSetup.shutDown();
|
||||
@ -253,20 +264,31 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
|
||||
// Wait max 20 sec.
|
||||
UserThread.runAfter(() -> {
|
||||
log.warn("Timeout triggered resultHandler");
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
log.info("Graceful shutdown resulted in a timeout. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
System.exit(EXIT_SUCCESS);
|
||||
});
|
||||
if (!hasDowngraded) {
|
||||
// If user tried to downgrade we do not write the persistable data to avoid data corruption
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
log.info("Graceful shutdown resulted in a timeout. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
});
|
||||
} else {
|
||||
UserThread.runAfter(() -> System.exit(EXIT_SUCCESS), 1);
|
||||
}
|
||||
|
||||
}, 20);
|
||||
} catch (Throwable t) {
|
||||
log.error("App shutdown failed with exception {}", t.toString());
|
||||
t.printStackTrace();
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
log.info("Graceful shutdown resulted in an error. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
System.exit(EXIT_FAILURE);
|
||||
});
|
||||
if (!hasDowngraded) {
|
||||
// If user tried to downgrade we do not write the persistable data to avoid data corruption
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
log.info("Graceful shutdown resulted in an error. Exiting now.");
|
||||
resultHandler.handleResult();
|
||||
UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1);
|
||||
});
|
||||
} else {
|
||||
UserThread.runAfter(() -> System.exit(EXIT_FAILURE), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ package bisq.core.app;
|
||||
import bisq.core.trade.TradeManager;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.file.CorruptedStorageFileHandler;
|
||||
import bisq.common.setup.GracefulShutDownHandler;
|
||||
|
||||
@ -94,6 +95,8 @@ public class BisqHeadlessApp implements HeadlessApp {
|
||||
bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
|
||||
bisqSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
|
||||
bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
|
||||
bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported",
|
||||
lastVersion, Version.VERSION));
|
||||
|
||||
corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files));
|
||||
tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("onTakeOfferRequestErrorMessageHandler"));
|
||||
|
@ -48,6 +48,7 @@ import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.app.Log;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.util.InvalidVersionException;
|
||||
import bisq.common.util.Utilities;
|
||||
@ -71,11 +72,15 @@ import javafx.collections.SetChangeListener;
|
||||
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiConsumer;
|
||||
@ -92,6 +97,7 @@ import javax.annotation.Nullable;
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class BisqSetup {
|
||||
private static final String VERSION_FILE_NAME = "version";
|
||||
|
||||
public interface BisqSetupListener {
|
||||
default void onInitP2pNetwork() {
|
||||
@ -172,6 +178,9 @@ public class BisqSetup {
|
||||
@Setter
|
||||
@Nullable
|
||||
private Runnable qubesOSInfoHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<String> downGradePreventionHandler;
|
||||
|
||||
@Getter
|
||||
final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false);
|
||||
@ -255,12 +264,17 @@ public class BisqSetup {
|
||||
}
|
||||
|
||||
public void start() {
|
||||
// If user tried to downgrade we require a shutdown
|
||||
if (hasDowngraded(downGradePreventionHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
persistBisqVersion();
|
||||
maybeReSyncSPVChain();
|
||||
maybeShowTac(this::step2);
|
||||
}
|
||||
|
||||
private void step2() {
|
||||
torSetup.cleanupTorFiles();
|
||||
readMapsFromResources(this::step3);
|
||||
checkForCorrectOSArchitecture();
|
||||
checkOSXVersion();
|
||||
@ -317,11 +331,9 @@ public class BisqSetup {
|
||||
}
|
||||
}
|
||||
|
||||
private void readMapsFromResources(Runnable nextStep) {
|
||||
SetupUtils.readFromResources(p2PService.getP2PDataStorage(), config).addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue)
|
||||
nextStep.run();
|
||||
});
|
||||
private void readMapsFromResources(Runnable completeHandler) {
|
||||
String postFix = "_" + config.baseCurrencyNetwork.name();
|
||||
p2PService.getP2PDataStorage().readFromResources(postFix, completeHandler);
|
||||
}
|
||||
|
||||
private void startP2pNetworkAndWallet(Runnable nextStep) {
|
||||
@ -390,7 +402,7 @@ public class BisqSetup {
|
||||
requestWalletPasswordHandler.accept(aesKey -> {
|
||||
walletsManager.setAesKey(aesKey);
|
||||
walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(),
|
||||
aesKey);
|
||||
aesKey);
|
||||
if (preferences.isResyncSpvRequested()) {
|
||||
if (showFirstPopupIfResyncSPVRequestedHandler != null)
|
||||
showFirstPopupIfResyncSPVRequestedHandler.run();
|
||||
@ -490,6 +502,68 @@ public class BisqSetup {
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getLastBisqVersion() {
|
||||
File versionFile = getVersionFile();
|
||||
if (!versionFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
try (Scanner scanner = new Scanner(versionFile)) {
|
||||
// We only expect 1 line
|
||||
if (scanner.hasNextLine()) {
|
||||
return scanner.nextLine();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static File getVersionFile() {
|
||||
return new File(Config.appDataDir(), VERSION_FILE_NAME);
|
||||
}
|
||||
|
||||
public static boolean hasDowngraded() {
|
||||
return hasDowngraded(getLastBisqVersion());
|
||||
}
|
||||
|
||||
public static boolean hasDowngraded(String lastVersion) {
|
||||
return lastVersion != null && Version.isNewVersion(lastVersion, Version.VERSION);
|
||||
}
|
||||
|
||||
public static boolean hasDowngraded(@Nullable Consumer<String> downGradePreventionHandler) {
|
||||
String lastVersion = getLastBisqVersion();
|
||||
boolean hasDowngraded = hasDowngraded(lastVersion);
|
||||
if (hasDowngraded) {
|
||||
log.error("Downgrade from version {} to version {} is not supported", lastVersion, Version.VERSION);
|
||||
if (downGradePreventionHandler != null) {
|
||||
downGradePreventionHandler.accept(lastVersion);
|
||||
}
|
||||
}
|
||||
return hasDowngraded;
|
||||
}
|
||||
|
||||
public static void persistBisqVersion() {
|
||||
File versionFile = getVersionFile();
|
||||
if (!versionFile.exists()) {
|
||||
try {
|
||||
if (!versionFile.createNewFile()) {
|
||||
log.error("Version file could not be created");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("Version file could not be created. {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
try (FileWriter fileWriter = new FileWriter(versionFile, false)) {
|
||||
fileWriter.write(Version.VERSION);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("Writing Version failed. {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForCorrectOSArchitecture() {
|
||||
if (!Utilities.isCorrectOSArchitecture() && wrongOSArchitectureHandler != null) {
|
||||
String osArchitecture = Utilities.getOSArchitecture();
|
||||
|
@ -45,6 +45,8 @@ import bisq.core.support.dispute.refund.RefundManager;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.support.traderchat.TraderChatManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
import bisq.core.trade.failed.FailedTradesManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.trade.txproof.xmr.XmrTxProofService;
|
||||
import bisq.core.user.User;
|
||||
@ -76,6 +78,8 @@ public class DomainInitialisation {
|
||||
private final RefundManager refundManager;
|
||||
private final TraderChatManager traderChatManager;
|
||||
private final TradeManager tradeManager;
|
||||
private final ClosedTradableManager closedTradableManager;
|
||||
private final FailedTradesManager failedTradesManager;
|
||||
private final XmrTxProofService xmrTxProofService;
|
||||
private final OpenOfferManager openOfferManager;
|
||||
private final Balances balances;
|
||||
@ -109,6 +113,8 @@ public class DomainInitialisation {
|
||||
RefundManager refundManager,
|
||||
TraderChatManager traderChatManager,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
FailedTradesManager failedTradesManager,
|
||||
XmrTxProofService xmrTxProofService,
|
||||
OpenOfferManager openOfferManager,
|
||||
Balances balances,
|
||||
@ -140,6 +146,8 @@ public class DomainInitialisation {
|
||||
this.refundManager = refundManager;
|
||||
this.traderChatManager = traderChatManager;
|
||||
this.tradeManager = tradeManager;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
this.failedTradesManager = failedTradesManager;
|
||||
this.xmrTxProofService = xmrTxProofService;
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.balances = balances;
|
||||
@ -183,6 +191,8 @@ public class DomainInitialisation {
|
||||
traderChatManager.onAllServicesInitialized();
|
||||
|
||||
tradeManager.onAllServicesInitialized();
|
||||
closedTradableManager.onAllServicesInitialized();
|
||||
failedTradesManager.onAllServicesInitialized();
|
||||
xmrTxProofService.onAllServicesInitialized();
|
||||
|
||||
openOfferManager.onAllServicesInitialized();
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package bisq.core.app;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.user.Preferences;
|
||||
@ -50,6 +51,7 @@ import javax.annotation.Nullable;
|
||||
public class P2PNetworkSetup {
|
||||
private final PriceFeedService priceFeedService;
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final Preferences preferences;
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
@ -73,10 +75,12 @@ public class P2PNetworkSetup {
|
||||
@Inject
|
||||
public P2PNetworkSetup(PriceFeedService priceFeedService,
|
||||
P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
Preferences preferences) {
|
||||
|
||||
this.priceFeedService = priceFeedService;
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
@ -86,18 +90,19 @@ public class P2PNetworkSetup {
|
||||
BooleanProperty hiddenServicePublished = new SimpleBooleanProperty();
|
||||
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
|
||||
|
||||
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||
(state, warning, numPeers, hiddenService, dataReceived) -> {
|
||||
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
|
||||
walletsSetup.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
|
||||
(state, warning, numP2pPeers, numBtcPeers, hiddenService, dataReceived) -> {
|
||||
String result;
|
||||
String daoFullNode = preferences.isDaoFullNode() ? Res.get("mainView.footer.daoFullNode") + " / " : "";
|
||||
int peers = (int) numPeers;
|
||||
if (warning != null && peers == 0) {
|
||||
int p2pPeers = (int) numP2pPeers;
|
||||
if (warning != null && p2pPeers == 0) {
|
||||
result = warning;
|
||||
} else {
|
||||
String p2pInfo = Res.get("mainView.footer.p2pInfo", numPeers);
|
||||
String p2pInfo = Res.get("mainView.footer.p2pInfo", numBtcPeers, numP2pPeers);
|
||||
if (dataReceived && hiddenService) {
|
||||
result = p2pInfo;
|
||||
} else if (peers == 0)
|
||||
} else if (p2pPeers == 0)
|
||||
result = state;
|
||||
else
|
||||
result = state + " / " + p2pInfo;
|
||||
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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.app;
|
||||
|
||||
import bisq.network.p2p.storage.P2PDataStorage;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.config.BaseCurrencyNetwork;
|
||||
import bisq.common.config.Config;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class SetupUtils {
|
||||
|
||||
public static BooleanProperty readFromResources(P2PDataStorage p2PDataStorage, Config config) {
|
||||
BooleanProperty result = new SimpleBooleanProperty();
|
||||
new Thread(() -> {
|
||||
// Used to load different files per base currency (EntryMap_BTC_MAINNET, EntryMap_LTC,...)
|
||||
final BaseCurrencyNetwork baseCurrencyNetwork = config.baseCurrencyNetwork;
|
||||
final String postFix = "_" + baseCurrencyNetwork.name();
|
||||
long ts = new Date().getTime();
|
||||
p2PDataStorage.readFromResources(postFix);
|
||||
log.info("readFromResources took {} ms", (new Date().getTime() - ts));
|
||||
UserThread.execute(() -> result.set(true));
|
||||
}, "BisqSetup-readFromResources").start();
|
||||
return result;
|
||||
}
|
||||
}
|
@ -46,15 +46,7 @@ public class TorSetup {
|
||||
this.torDir = checkDir(torDir);
|
||||
}
|
||||
|
||||
public void cleanupTorFiles() {
|
||||
cleanupTorFiles(null, null);
|
||||
}
|
||||
|
||||
// We get sometimes Tor startup problems which is related to some tor files in the tor directory. It happens
|
||||
// more often if the application got killed (not graceful shutdown).
|
||||
// Creating all tor files newly takes about 3-4 sec. longer and it does not benefit from cache files.
|
||||
// TODO: We should fix those startup problems in the netlayer library, once fixed there we can remove that call at the
|
||||
// Bisq startup again.
|
||||
// Should only be called if needed. Slows down Tor startup from about 5 sec. to 30 sec. if it gets deleted.
|
||||
public void cleanupTorFiles(@Nullable Runnable resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
File hiddenservice = new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString());
|
||||
try {
|
||||
|
@ -23,6 +23,7 @@ import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.WalletsManager;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
@ -64,6 +65,7 @@ public class WalletAppSetup {
|
||||
|
||||
private final WalletsManager walletsManager;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final FeeService feeService;
|
||||
private final Config config;
|
||||
private final Preferences preferences;
|
||||
|
||||
@ -81,17 +83,17 @@ public class WalletAppSetup {
|
||||
@Getter
|
||||
private final ObjectProperty<RejectedTxException> rejectedTxException = new SimpleObjectProperty<>();
|
||||
@Getter
|
||||
private int numBtcPeers = 0;
|
||||
@Getter
|
||||
private final BooleanProperty useTorForBTC = new SimpleBooleanProperty();
|
||||
|
||||
@Inject
|
||||
public WalletAppSetup(WalletsManager walletsManager,
|
||||
WalletsSetup walletsSetup,
|
||||
FeeService feeService,
|
||||
Config config,
|
||||
Preferences preferences) {
|
||||
this.walletsManager = walletsManager;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.feeService = feeService;
|
||||
this.config = config;
|
||||
this.preferences = preferences;
|
||||
this.useTorForBTC.set(preferences.getUseTorForBitcoinJ());
|
||||
@ -105,40 +107,36 @@ public class WalletAppSetup {
|
||||
Runnable downloadCompleteHandler,
|
||||
Runnable walletInitializedHandler) {
|
||||
log.info("Initialize WalletAppSetup with BitcoinJ version {} and hash of BitcoinJ commit {}",
|
||||
VersionMessage.BITCOINJ_VERSION, "a733034");
|
||||
VersionMessage.BITCOINJ_VERSION, "7752cb7");
|
||||
|
||||
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
|
||||
btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(),
|
||||
walletsSetup.numPeersProperty(),
|
||||
feeService.feeUpdateCounterProperty(),
|
||||
walletServiceException,
|
||||
(downloadPercentage, numPeers, exception) -> {
|
||||
(downloadPercentage, feeUpdate, exception) -> {
|
||||
String result;
|
||||
if (exception == null) {
|
||||
double percentage = (double) downloadPercentage;
|
||||
int peers = (int) numPeers;
|
||||
btcSyncProgress.set(percentage);
|
||||
if (percentage == 1) {
|
||||
result = Res.get("mainView.footer.btcInfo",
|
||||
peers,
|
||||
Res.get("mainView.footer.btcInfo.synchronizedWith"),
|
||||
getBtcNetworkAsString());
|
||||
getBtcNetworkAsString(),
|
||||
feeService.getFeeTextForDisplay());
|
||||
getBtcSplashSyncIconId().set("image-connection-synced");
|
||||
|
||||
downloadCompleteHandler.run();
|
||||
} else if (percentage > 0.0) {
|
||||
result = Res.get("mainView.footer.btcInfo",
|
||||
peers,
|
||||
Res.get("mainView.footer.btcInfo.synchronizingWith"),
|
||||
getBtcNetworkAsString() + ": " + FormattingUtils.formatToPercentWithSymbol(percentage));
|
||||
getBtcNetworkAsString() + ": " + FormattingUtils.formatToPercentWithSymbol(percentage), "");
|
||||
} else {
|
||||
result = Res.get("mainView.footer.btcInfo",
|
||||
peers,
|
||||
Res.get("mainView.footer.btcInfo.connectingTo"),
|
||||
getBtcNetworkAsString());
|
||||
getBtcNetworkAsString(), "");
|
||||
}
|
||||
} else {
|
||||
result = Res.get("mainView.footer.btcInfo",
|
||||
getNumBtcPeers(),
|
||||
Res.get("mainView.footer.btcInfo.connectionFailed"),
|
||||
getBtcNetworkAsString());
|
||||
log.error(exception.toString());
|
||||
@ -164,8 +162,6 @@ public class WalletAppSetup {
|
||||
|
||||
walletsSetup.initialize(null,
|
||||
() -> {
|
||||
numBtcPeers = walletsSetup.numPeersProperty().get();
|
||||
|
||||
// We only check one wallet as we apply encryption to all or none
|
||||
if (walletsManager.areWalletsEncrypted()) {
|
||||
walletPasswordHandler.run();
|
||||
@ -251,6 +247,7 @@ public class WalletAppSetup {
|
||||
String finalDetails = details;
|
||||
UserThread.runAfter(() -> {
|
||||
trade.setErrorMessage(newValue.getMessage());
|
||||
tradeManager.requestPersistence();
|
||||
if (rejectedTxErrorMessageHandler != null) {
|
||||
rejectedTxErrorMessageHandler.accept(Res.get("popup.warning.trade.txRejected",
|
||||
finalDetails, trade.getShortId(), txId));
|
||||
|
@ -19,8 +19,6 @@ package bisq.core.app.misc;
|
||||
|
||||
import bisq.core.account.sign.SignedWitnessService;
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.app.SetupUtils;
|
||||
import bisq.core.app.TorSetup;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
|
||||
@ -29,6 +27,8 @@ import bisq.network.p2p.P2PServiceListener;
|
||||
import bisq.network.p2p.network.CloseConnectionReason;
|
||||
import bisq.network.p2p.network.Connection;
|
||||
import bisq.network.p2p.network.ConnectionListener;
|
||||
import bisq.network.p2p.peers.PeerManager;
|
||||
import bisq.network.p2p.storage.P2PDataStorage;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.proto.persistable.PersistedDataHost;
|
||||
@ -48,38 +48,42 @@ public class AppSetupWithP2P extends AppSetup {
|
||||
protected final AccountAgeWitnessService accountAgeWitnessService;
|
||||
private final SignedWitnessService signedWitnessService;
|
||||
protected final FilterManager filterManager;
|
||||
private final TorSetup torSetup;
|
||||
protected BooleanProperty p2pNetWorkReady;
|
||||
private final P2PDataStorage p2PDataStorage;
|
||||
private final PeerManager peerManager;
|
||||
protected final TradeStatisticsManager tradeStatisticsManager;
|
||||
protected ArrayList<PersistedDataHost> persistedDataHosts;
|
||||
protected BooleanProperty p2pNetWorkReady;
|
||||
|
||||
@Inject
|
||||
public AppSetupWithP2P(P2PService p2PService,
|
||||
P2PDataStorage p2PDataStorage,
|
||||
PeerManager peerManager,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
SignedWitnessService signedWitnessService,
|
||||
FilterManager filterManager,
|
||||
TorSetup torSetup,
|
||||
Config config) {
|
||||
super(config);
|
||||
this.p2PService = p2PService;
|
||||
this.p2PDataStorage = p2PDataStorage;
|
||||
this.peerManager = peerManager;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.signedWitnessService = signedWitnessService;
|
||||
this.filterManager = filterManager;
|
||||
this.torSetup = torSetup;
|
||||
this.persistedDataHosts = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPersistedDataHosts() {
|
||||
torSetup.cleanupTorFiles();
|
||||
persistedDataHosts.add(p2PService);
|
||||
persistedDataHosts.add(p2PDataStorage);
|
||||
persistedDataHosts.add(peerManager);
|
||||
|
||||
// we apply at startup the reading of persisted data but don't want to get it triggered in the constructor
|
||||
persistedDataHosts.forEach(e -> {
|
||||
try {
|
||||
e.readPersisted();
|
||||
e.readPersisted(() -> {
|
||||
});
|
||||
} catch (Throwable e1) {
|
||||
log.error("readPersisted error", e1);
|
||||
}
|
||||
@ -88,10 +92,8 @@ public class AppSetupWithP2P extends AppSetup {
|
||||
|
||||
@Override
|
||||
protected void initBasicServices() {
|
||||
SetupUtils.readFromResources(p2PService.getP2PDataStorage(), config).addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue)
|
||||
startInitP2PNetwork();
|
||||
});
|
||||
String postFix = "_" + config.baseCurrencyNetwork.name();
|
||||
p2PDataStorage.readFromResources(postFix, this::startInitP2PNetwork);
|
||||
}
|
||||
|
||||
private void startInitP2PNetwork() {
|
||||
|
@ -19,7 +19,6 @@ package bisq.core.app.misc;
|
||||
|
||||
import bisq.core.account.sign.SignedWitnessService;
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.app.TorSetup;
|
||||
import bisq.core.dao.DaoSetup;
|
||||
import bisq.core.dao.governance.ballot.BallotListService;
|
||||
import bisq.core.dao.governance.blindvote.MyBlindVoteListService;
|
||||
@ -31,6 +30,8 @@ import bisq.core.filter.FilterManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.peers.PeerManager;
|
||||
import bisq.network.p2p.storage.P2PDataStorage;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
|
||||
@ -44,6 +45,8 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
|
||||
|
||||
@Inject
|
||||
public AppSetupWithP2PAndDAO(P2PService p2PService,
|
||||
P2PDataStorage p2PDataStorage,
|
||||
PeerManager peerManager,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
SignedWitnessService signedWitnessService,
|
||||
@ -55,14 +58,14 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
|
||||
MyProposalListService myProposalListService,
|
||||
MyReputationListService myReputationListService,
|
||||
MyProofOfBurnListService myProofOfBurnListService,
|
||||
TorSetup torSetup,
|
||||
Config config) {
|
||||
super(p2PService,
|
||||
p2PDataStorage,
|
||||
peerManager,
|
||||
tradeStatisticsManager,
|
||||
accountAgeWitnessService,
|
||||
signedWitnessService,
|
||||
filterManager,
|
||||
torSetup,
|
||||
config);
|
||||
|
||||
this.daoSetup = daoSetup;
|
||||
|
@ -95,7 +95,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
resultHandler.handleResult();
|
||||
log.info("Graceful shutdown completed. Exiting now.");
|
||||
System.exit(BisqExecutable.EXIT_SUCCESS);
|
||||
UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_SUCCESS), 1);
|
||||
});
|
||||
});
|
||||
injector.getInstance(WalletsSetup.class).shutDown();
|
||||
@ -107,7 +107,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
resultHandler.handleResult();
|
||||
log.info("Graceful shutdown caused a timeout. Exiting now.");
|
||||
System.exit(BisqExecutable.EXIT_SUCCESS);
|
||||
UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_SUCCESS), 1);
|
||||
});
|
||||
}, 5);
|
||||
} else {
|
||||
@ -122,7 +122,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
|
||||
PersistenceManager.flushAllDataToDisk(() -> {
|
||||
resultHandler.handleResult();
|
||||
log.info("Graceful shutdown resulted in an error. Exiting now.");
|
||||
System.exit(BisqExecutable.EXIT_FAILURE);
|
||||
UserThread.runAfter(() -> System.exit(BisqExecutable.EXIT_FAILURE), 1);
|
||||
});
|
||||
|
||||
}
|
||||
|
@ -41,9 +41,23 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
*/
|
||||
@Slf4j
|
||||
public class TxFeeEstimationService {
|
||||
public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 260;
|
||||
private static int DEPOSIT_TX_SIZE = 320;
|
||||
private static int PAYOUT_TX_SIZE = 380;
|
||||
|
||||
// Size/vsize of typical trade txs
|
||||
// Real txs size/vsize may vary in 1 or 2 bytes from the estimated values.
|
||||
// Values calculated with https://gist.github.com/oscarguindzberg/3d1349cb65d9fd9af9de0feaa3fd27ac
|
||||
// legacy fee tx with 1 input, maker/taker fee paid in btc size/vsize = 258
|
||||
// legacy deposit tx without change size/vsize = 381
|
||||
// legacy deposit tx with change size/vsize = 414
|
||||
// legacy payout tx size/vsize = 337
|
||||
// legacy delayed payout tx size/vsize = 302
|
||||
// segwit fee tx with 1 input, maker/taker fee paid in btc vsize = 173
|
||||
// segwit deposit tx without change vsize = 232
|
||||
// segwit deposit tx with change vsize = 263
|
||||
// segwit payout tx vsize = 169
|
||||
// segwit delayed payout tx vsize = 139
|
||||
public static int TYPICAL_TX_WITH_1_INPUT_VSIZE = 175;
|
||||
private static int DEPOSIT_TX_VSIZE = 233;
|
||||
|
||||
private static int BSQ_INPUT_INCREASE = 150;
|
||||
private static int MAX_ITERATIONS = 10;
|
||||
|
||||
@ -61,8 +75,8 @@ public class TxFeeEstimationService {
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSizeForTaker(Coin fundsNeededForTrade, Coin tradeFee) {
|
||||
return getEstimatedFeeAndTxSize(true,
|
||||
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsizeForTaker(Coin fundsNeededForTrade, Coin tradeFee) {
|
||||
return getEstimatedFeeAndTxVsize(true,
|
||||
fundsNeededForTrade,
|
||||
tradeFee,
|
||||
feeService,
|
||||
@ -70,9 +84,9 @@ public class TxFeeEstimationService {
|
||||
preferences);
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSizeForMaker(Coin reservedFundsForOffer,
|
||||
Coin tradeFee) {
|
||||
return getEstimatedFeeAndTxSize(false,
|
||||
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsizeForMaker(Coin reservedFundsForOffer,
|
||||
Coin tradeFee) {
|
||||
return getEstimatedFeeAndTxVsize(false,
|
||||
reservedFundsForOffer,
|
||||
tradeFee,
|
||||
feeService,
|
||||
@ -80,120 +94,120 @@ public class TxFeeEstimationService {
|
||||
preferences);
|
||||
}
|
||||
|
||||
private Tuple2<Coin, Integer> getEstimatedFeeAndTxSize(boolean isTaker,
|
||||
Coin amount,
|
||||
Coin tradeFee,
|
||||
FeeService feeService,
|
||||
BtcWalletService btcWalletService,
|
||||
Preferences preferences) {
|
||||
Coin txFeePerByte = feeService.getTxFeePerByte();
|
||||
// We start with min taker fee size of 260
|
||||
int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE;
|
||||
private Tuple2<Coin, Integer> getEstimatedFeeAndTxVsize(boolean isTaker,
|
||||
Coin amount,
|
||||
Coin tradeFee,
|
||||
FeeService feeService,
|
||||
BtcWalletService btcWalletService,
|
||||
Preferences preferences) {
|
||||
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
|
||||
// We start with min taker fee vsize of 175
|
||||
int estimatedTxVsize = TYPICAL_TX_WITH_1_INPUT_VSIZE;
|
||||
try {
|
||||
estimatedTxSize = getEstimatedTxSize(List.of(tradeFee, amount), estimatedTxSize, txFeePerByte, btcWalletService);
|
||||
estimatedTxVsize = getEstimatedTxVsize(List.of(tradeFee, amount), estimatedTxVsize, txFeePerVbyte, btcWalletService);
|
||||
} catch (InsufficientMoneyException e) {
|
||||
if (isTaker) {
|
||||
// if we cannot do the estimation we use the payout tx size
|
||||
estimatedTxSize = PAYOUT_TX_SIZE;
|
||||
// If we cannot do the estimation, we use the vsize o the largest of our txs which is the deposit tx.
|
||||
estimatedTxVsize = DEPOSIT_TX_VSIZE;
|
||||
}
|
||||
log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " +
|
||||
"if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize);
|
||||
"if the user pays from an external wallet. In that case we use an estimated tx vsize of {} vbytes.", estimatedTxVsize);
|
||||
}
|
||||
|
||||
if (!preferences.isPayFeeInBtc()) {
|
||||
// If we pay the fee in BSQ we have one input more which adds about 150 bytes
|
||||
// TODO: Clarify if there is always just one additional input or if there can be more.
|
||||
estimatedTxSize += BSQ_INPUT_INCREASE;
|
||||
estimatedTxVsize += BSQ_INPUT_INCREASE;
|
||||
}
|
||||
|
||||
Coin txFee;
|
||||
int size;
|
||||
int vsize;
|
||||
if (isTaker) {
|
||||
int averageSize = (estimatedTxSize + DEPOSIT_TX_SIZE) / 2; // deposit tx has about 320 bytes
|
||||
// We use at least the size of the payout tx to not underpay at payout.
|
||||
size = Math.max(PAYOUT_TX_SIZE, averageSize);
|
||||
txFee = txFeePerByte.multiply(size);
|
||||
log.info("Fee estimation resulted in a tx size of {} bytes.\n" +
|
||||
"We use an average between the taker fee tx and the deposit tx (320 bytes) which results in {} bytes.\n" +
|
||||
"The payout tx has 380 bytes, we use that as our min value. Size for fee calculation is {} bytes.\n" +
|
||||
"The tx fee of {} Sat", estimatedTxSize, averageSize, size, txFee.value);
|
||||
int averageVsize = (estimatedTxVsize + DEPOSIT_TX_VSIZE) / 2; // deposit tx has about 233 vbytes
|
||||
// We use at least the vsize of the deposit tx to not underpay it.
|
||||
vsize = Math.max(DEPOSIT_TX_VSIZE, averageVsize);
|
||||
txFee = txFeePerVbyte.multiply(vsize);
|
||||
log.info("Fee estimation resulted in a tx vsize of {} vbytes.\n" +
|
||||
"We use an average between the taker fee tx and the deposit tx (233 vbytes) which results in {} vbytes.\n" +
|
||||
"The deposit tx has 233 vbytes, we use that as our min value. Vsize for fee calculation is {} vbytes.\n" +
|
||||
"The tx fee of {} Sat", estimatedTxVsize, averageVsize, vsize, txFee.value);
|
||||
} else {
|
||||
size = estimatedTxSize;
|
||||
txFee = txFeePerByte.multiply(size);
|
||||
log.info("Fee estimation resulted in a tx size of {} bytes and a tx fee of {} Sat.", size, txFee.value);
|
||||
vsize = estimatedTxVsize;
|
||||
txFee = txFeePerVbyte.multiply(vsize);
|
||||
log.info("Fee estimation resulted in a tx vsize of {} vbytes and a tx fee of {} Sat.", vsize, txFee.value);
|
||||
}
|
||||
|
||||
return new Tuple2<>(txFee, size);
|
||||
return new Tuple2<>(txFee, vsize);
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getEstimatedFeeAndTxSize(Coin amount,
|
||||
FeeService feeService,
|
||||
BtcWalletService btcWalletService) {
|
||||
Coin txFeePerByte = feeService.getTxFeePerByte();
|
||||
// We start with min taker fee size of 260
|
||||
int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE;
|
||||
public Tuple2<Coin, Integer> getEstimatedFeeAndTxVsize(Coin amount,
|
||||
FeeService feeService,
|
||||
BtcWalletService btcWalletService) {
|
||||
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
|
||||
// We start with min taker fee vsize of 175
|
||||
int estimatedTxVsize = TYPICAL_TX_WITH_1_INPUT_VSIZE;
|
||||
try {
|
||||
estimatedTxSize = getEstimatedTxSize(List.of(amount), estimatedTxSize, txFeePerByte, btcWalletService);
|
||||
estimatedTxVsize = getEstimatedTxVsize(List.of(amount), estimatedTxVsize, txFeePerVbyte, btcWalletService);
|
||||
} catch (InsufficientMoneyException e) {
|
||||
log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " +
|
||||
"if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize);
|
||||
"if the user pays from an external wallet. In that case we use an estimated tx vsize of {} vbytes.", estimatedTxVsize);
|
||||
}
|
||||
|
||||
Coin txFee = txFeePerByte.multiply(estimatedTxSize);
|
||||
log.info("Fee estimation resulted in a tx size of {} bytes and a tx fee of {} Sat.", estimatedTxSize, txFee.value);
|
||||
Coin txFee = txFeePerVbyte.multiply(estimatedTxVsize);
|
||||
log.info("Fee estimation resulted in a tx vsize of {} vbytes and a tx fee of {} Sat.", estimatedTxVsize, txFee.value);
|
||||
|
||||
return new Tuple2<>(txFee, estimatedTxSize);
|
||||
return new Tuple2<>(txFee, estimatedTxVsize);
|
||||
}
|
||||
|
||||
// We start with the initialEstimatedTxSize for a tx with 1 input (260) bytes and get from BitcoinJ a tx back which
|
||||
// We start with the initialEstimatedTxVsize for a tx with 1 input (175) vbytes and get from BitcoinJ a tx back which
|
||||
// contains the required inputs to fund that tx (outputs + miner fee). The miner fee in that case is based on
|
||||
// the assumption that we only need 1 input. Once we receive back the real tx size from the tx BitcoinJ has created
|
||||
// with the required inputs we compare if the size is not more then 20% different to our assumed tx size. If we are inside
|
||||
// that tolerance we use that tx size for our fee estimation, if not (if there has been more then 1 inputs) we
|
||||
// apply the new fee based on the reported tx size and request again from BitcoinJ to fill that tx with the inputs
|
||||
// the assumption that we only need 1 input. Once we receive back the real tx vsize from the tx BitcoinJ has created
|
||||
// with the required inputs we compare if the vsize is not more then 20% different to our assumed tx vsize. If we are inside
|
||||
// that tolerance we use that tx vsize for our fee estimation, if not (if there has been more then 1 inputs) we
|
||||
// apply the new fee based on the reported tx vsize and request again from BitcoinJ to fill that tx with the inputs
|
||||
// to be sufficiently funded. The algorithm how BitcoinJ selects utxos is complex and contains several aspects
|
||||
// (minimize fee, don't create too many tiny utxos,...). We treat that algorithm as an unknown and it is not
|
||||
// guaranteed that there are more inputs required if we increase the fee (it could be that there is a better
|
||||
// selection of inputs chosen if we have increased the fee and therefore less inputs and smaller tx size). As the increased fee might
|
||||
// selection of inputs chosen if we have increased the fee and therefore less inputs and smaller tx vsize). As the increased fee might
|
||||
// change the number of inputs we need to repeat that process until we are inside of a certain tolerance. To avoid
|
||||
// potential endless loops we add a counter (we use 10, usually it takes just very few iterations).
|
||||
// Worst case would be that the last size we got reported is > 20% off to
|
||||
// the real tx size but as fee estimation is anyway a educated guess in the best case we don't worry too much.
|
||||
// Worst case would be that the last vsize we got reported is > 20% off to
|
||||
// the real tx vsize but as fee estimation is anyway a educated guess in the best case we don't worry too much.
|
||||
// If we have underpaid the tx might take longer to get confirmed.
|
||||
@VisibleForTesting
|
||||
static int getEstimatedTxSize(List<Coin> outputValues,
|
||||
int initialEstimatedTxSize,
|
||||
Coin txFeePerByte,
|
||||
BtcWalletService btcWalletService)
|
||||
static int getEstimatedTxVsize(List<Coin> outputValues,
|
||||
int initialEstimatedTxVsize,
|
||||
Coin txFeePerVbyte,
|
||||
BtcWalletService btcWalletService)
|
||||
throws InsufficientMoneyException {
|
||||
boolean isInTolerance;
|
||||
int estimatedTxSize = initialEstimatedTxSize;
|
||||
int realTxSize;
|
||||
int estimatedTxVsize = initialEstimatedTxVsize;
|
||||
int realTxVsize;
|
||||
int counter = 0;
|
||||
do {
|
||||
Coin txFee = txFeePerByte.multiply(estimatedTxSize);
|
||||
realTxSize = btcWalletService.getEstimatedFeeTxSize(outputValues, txFee);
|
||||
isInTolerance = isInTolerance(estimatedTxSize, realTxSize, 0.2);
|
||||
Coin txFee = txFeePerVbyte.multiply(estimatedTxVsize);
|
||||
realTxVsize = btcWalletService.getEstimatedFeeTxVsize(outputValues, txFee);
|
||||
isInTolerance = isInTolerance(estimatedTxVsize, realTxVsize, 0.2);
|
||||
if (!isInTolerance) {
|
||||
estimatedTxSize = realTxSize;
|
||||
estimatedTxVsize = realTxVsize;
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
while (!isInTolerance && counter < MAX_ITERATIONS);
|
||||
if (!isInTolerance) {
|
||||
log.warn("We could not find a tx which satisfies our tolerance requirement of 20%. " +
|
||||
"realTxSize={}, estimatedTxSize={}",
|
||||
realTxSize, estimatedTxSize);
|
||||
"realTxVsize={}, estimatedTxVsize={}",
|
||||
realTxVsize, estimatedTxVsize);
|
||||
}
|
||||
return estimatedTxSize;
|
||||
return estimatedTxVsize;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static boolean isInTolerance(int estimatedSize, int txSize, double tolerance) {
|
||||
checkArgument(estimatedSize > 0, "estimatedSize must be positive");
|
||||
checkArgument(txSize > 0, "txSize must be positive");
|
||||
static boolean isInTolerance(int estimatedVsize, int txVsize, double tolerance) {
|
||||
checkArgument(estimatedVsize > 0, "estimatedVsize must be positive");
|
||||
checkArgument(txVsize > 0, "txVsize must be positive");
|
||||
checkArgument(tolerance > 0, "tolerance must be positive");
|
||||
double deviation = Math.abs(1 - ((double) estimatedSize / (double) txSize));
|
||||
double deviation = Math.abs(1 - ((double) estimatedVsize / (double) txVsize));
|
||||
return deviation <= tolerance;
|
||||
}
|
||||
}
|
||||
|
@ -97,10 +97,6 @@ public final class AddressEntry implements PersistablePayload {
|
||||
Context context,
|
||||
@Nullable String offerId,
|
||||
boolean segwit) {
|
||||
if (segwit && (!Context.AVAILABLE.equals(context) || offerId != null)) {
|
||||
throw new IllegalArgumentException("Segwit addresses are only allowed for " +
|
||||
"AVAILABLE entries without an offerId");
|
||||
}
|
||||
this.keyPair = keyPair;
|
||||
this.context = context;
|
||||
this.offerId = offerId;
|
||||
|
@ -63,12 +63,13 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
AddressEntryList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
entrySet.clear();
|
||||
entrySet.addAll(persisted.entrySet);
|
||||
}
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
entrySet.clear();
|
||||
entrySet.addAll(persisted.entrySet);
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
}
|
||||
|
||||
|
||||
@ -110,12 +111,12 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
|
||||
Set<AddressEntry> toBeRemoved = new HashSet<>();
|
||||
entrySet.forEach(addressEntry -> {
|
||||
Script.ScriptType scriptType = addressEntry.isSegwit() ? Script.ScriptType.P2WPKH
|
||||
: Script.ScriptType.P2PKH;
|
||||
: Script.ScriptType.P2PKH;
|
||||
DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubKeyHash(
|
||||
addressEntry.getPubKeyHash(), scriptType);
|
||||
addressEntry.getPubKeyHash(), scriptType);
|
||||
if (keyFromPubHash != null) {
|
||||
Address addressFromKey = Address.fromKey(Config.baseCurrencyNetworkParameters(), keyFromPubHash,
|
||||
scriptType);
|
||||
scriptType);
|
||||
// We want to ensure key and address matches in case we have address in entry available already
|
||||
if (addressEntry.getAddress() == null || addressFromKey.equals(addressEntry.getAddress())) {
|
||||
addressEntry.setDeterministicKey(keyFromPubHash);
|
||||
@ -197,8 +198,8 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
|
||||
public void swapToAvailable(AddressEntry addressEntry) {
|
||||
boolean setChangedByRemove = entrySet.remove(addressEntry);
|
||||
boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(),
|
||||
AddressEntry.Context.AVAILABLE,
|
||||
addressEntry.isSegwit()));
|
||||
AddressEntry.Context.AVAILABLE,
|
||||
addressEntry.isSegwit()));
|
||||
if (setChangedByRemove || setChangedByAdd) {
|
||||
requestPersistence();
|
||||
}
|
||||
@ -234,7 +235,7 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
|
||||
.map(address -> Pair.of(address, (DeterministicKey) wallet.findKeyFromAddress(address)))
|
||||
.filter(pair -> pair.getRight() != null)
|
||||
.map(pair -> new AddressEntry(pair.getRight(), AddressEntry.Context.AVAILABLE,
|
||||
pair.getLeft() instanceof SegwitAddress))
|
||||
pair.getLeft() instanceof SegwitAddress))
|
||||
.forEach(this::addAddressEntry);
|
||||
}
|
||||
|
||||
|
@ -242,6 +242,7 @@ public class WalletsSetup {
|
||||
return message;
|
||||
});
|
||||
|
||||
chainHeight.set(chain.getBestChainHeight());
|
||||
chain.addNewBestBlockListener(block -> {
|
||||
connectedPeers.set(peerGroup.getConnectedPeers());
|
||||
chainHeight.set(block.getHeight());
|
||||
@ -522,6 +523,16 @@ public class WalletsSetup {
|
||||
return downloadPercentageProperty().get() == 1d;
|
||||
}
|
||||
|
||||
public boolean isChainHeightSyncedWithinTolerance() {
|
||||
int peersChainHeight = PeerGroup.getMostCommonChainHeight(connectedPeers.get());
|
||||
int bestChainHeight = walletConfig.chain().getBestChainHeight();
|
||||
if (peersChainHeight - bestChainHeight <= 3) {
|
||||
return true;
|
||||
}
|
||||
log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), peersChainHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
public Set<Address> getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) {
|
||||
return addressEntryList.getAddressEntriesAsListImmutable().stream()
|
||||
.filter(addressEntry -> addressEntry.getContext() == context)
|
||||
|
@ -774,12 +774,12 @@ public class BsqWalletService extends WalletService implements DaoStateListener
|
||||
// Addresses
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Address getChangeAddress() {
|
||||
private LegacyAddress getChangeAddress() {
|
||||
return getUnusedAddress();
|
||||
}
|
||||
|
||||
public Address getUnusedAddress() {
|
||||
return wallet.getIssuedReceiveAddresses().stream()
|
||||
public LegacyAddress getUnusedAddress() {
|
||||
return (LegacyAddress) wallet.getIssuedReceiveAddresses().stream()
|
||||
.filter(this::isAddressUnused)
|
||||
.findAny()
|
||||
.orElse(wallet.freshReceiveAddress());
|
||||
|
@ -28,13 +28,14 @@ import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.user.Preferences;
|
||||
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.SegwitAddress;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionConfidence;
|
||||
import org.bitcoinj.core.TransactionInput;
|
||||
@ -44,6 +45,7 @@ import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptPattern;
|
||||
import org.bitcoinj.wallet.SendRequest;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
|
||||
@ -218,8 +220,8 @@ public class BtcWalletService extends WalletService {
|
||||
// estimated size of input sig
|
||||
int sigSizePerInput = 106;
|
||||
// typical size for a tx with 3 inputs
|
||||
int txSizeWithUnsignedInputs = 300;
|
||||
Coin txFeePerByte = feeService.getTxFeePerByte();
|
||||
int txVsizeWithUnsignedInputs = 300;
|
||||
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
|
||||
|
||||
Address changeAddress = getFreshAddressEntry().getAddress();
|
||||
checkNotNull(changeAddress, "changeAddress must not be null");
|
||||
@ -228,7 +230,9 @@ public class BtcWalletService extends WalletService {
|
||||
preferences.getIgnoreDustThreshold());
|
||||
List<TransactionInput> preparedBsqTxInputs = preparedTx.getInputs();
|
||||
List<TransactionOutput> preparedBsqTxOutputs = preparedTx.getOutputs();
|
||||
int numInputs = preparedBsqTxInputs.size();
|
||||
Tuple2<Integer, Integer> numInputs = getNumInputs(preparedTx);
|
||||
int numLegacyInputs = numInputs.first;
|
||||
int numSegwitInputs = numInputs.second;
|
||||
Transaction resultTx = null;
|
||||
boolean isFeeOutsideTolerance;
|
||||
do {
|
||||
@ -249,7 +253,10 @@ public class BtcWalletService extends WalletService {
|
||||
// signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet)
|
||||
sendRequest.signInputs = false;
|
||||
|
||||
sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs);
|
||||
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
|
||||
sendRequest.feePerKb = Coin.ZERO;
|
||||
sendRequest.ensureMinRequiredFee = false;
|
||||
|
||||
@ -262,9 +269,14 @@ public class BtcWalletService extends WalletService {
|
||||
// add OP_RETURN output
|
||||
resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram()));
|
||||
|
||||
numInputs = resultTx.getInputs().size();
|
||||
txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length;
|
||||
long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value;
|
||||
numInputs = getNumInputs(resultTx);
|
||||
numLegacyInputs = numInputs.first;
|
||||
numSegwitInputs = numInputs.second;
|
||||
txVsizeWithUnsignedInputs = resultTx.getVsize();
|
||||
long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
|
||||
// calculated fee must be inside of a tolerance range with tx fee
|
||||
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
|
||||
}
|
||||
@ -328,8 +340,8 @@ public class BtcWalletService extends WalletService {
|
||||
// estimated size of input sig
|
||||
int sigSizePerInput = 106;
|
||||
// typical size for a tx with 3 inputs
|
||||
int txSizeWithUnsignedInputs = 300;
|
||||
Coin txFeePerByte = feeService.getTxFeePerByte();
|
||||
int txVsizeWithUnsignedInputs = 300;
|
||||
Coin txFeePerVbyte = feeService.getTxFeePerVbyte();
|
||||
|
||||
Address changeAddress = getFreshAddressEntry().getAddress();
|
||||
checkNotNull(changeAddress, "changeAddress must not be null");
|
||||
@ -338,7 +350,9 @@ public class BtcWalletService extends WalletService {
|
||||
preferences.getIgnoreDustThreshold());
|
||||
List<TransactionInput> preparedBsqTxInputs = preparedTx.getInputs();
|
||||
List<TransactionOutput> preparedBsqTxOutputs = preparedTx.getOutputs();
|
||||
int numInputs = preparedBsqTxInputs.size();
|
||||
Tuple2<Integer, Integer> numInputs = getNumInputs(preparedTx);
|
||||
int numLegacyInputs = numInputs.first;
|
||||
int numSegwitInputs = numInputs.second;
|
||||
Transaction resultTx = null;
|
||||
boolean isFeeOutsideTolerance;
|
||||
do {
|
||||
@ -359,7 +373,9 @@ public class BtcWalletService extends WalletService {
|
||||
// signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet)
|
||||
sendRequest.signInputs = false;
|
||||
|
||||
sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs);
|
||||
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
sendRequest.feePerKb = Coin.ZERO;
|
||||
sendRequest.ensureMinRequiredFee = false;
|
||||
|
||||
@ -372,9 +388,13 @@ public class BtcWalletService extends WalletService {
|
||||
// add OP_RETURN output
|
||||
resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram()));
|
||||
|
||||
numInputs = resultTx.getInputs().size();
|
||||
txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length;
|
||||
final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value;
|
||||
numInputs = getNumInputs(resultTx);
|
||||
numLegacyInputs = numInputs.first;
|
||||
numSegwitInputs = numInputs.second;
|
||||
txVsizeWithUnsignedInputs = resultTx.getVsize();
|
||||
final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
// calculated fee must be inside of a tolerance range with tx fee
|
||||
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
|
||||
}
|
||||
@ -466,9 +486,9 @@ public class BtcWalletService extends WalletService {
|
||||
// estimated size of input sig
|
||||
int sigSizePerInput = 106;
|
||||
// typical size for a tx with 2 inputs
|
||||
int txSizeWithUnsignedInputs = 203;
|
||||
int txVsizeWithUnsignedInputs = 203;
|
||||
// If useCustomTxFee we allow overriding the estimated fee from preferences
|
||||
Coin txFeePerByte = useCustomTxFee ? getTxFeeForWithdrawalPerByte() : feeService.getTxFeePerByte();
|
||||
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;
|
||||
|
||||
@ -479,7 +499,10 @@ public class BtcWalletService extends WalletService {
|
||||
preferences.getIgnoreDustThreshold());
|
||||
List<TransactionInput> preparedBsqTxInputs = preparedBsqTx.getInputs();
|
||||
List<TransactionOutput> preparedBsqTxOutputs = preparedBsqTx.getOutputs();
|
||||
int numInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input
|
||||
// We don't know at this point what type the btc input would be (segwit/legacy).
|
||||
// We use legacy to be on the safe side.
|
||||
int numLegacyInputs = preparedBsqTxInputs.size() + 1; // We add 1 for the BTC fee input
|
||||
int numSegwitInputs = 0;
|
||||
Transaction resultTx = null;
|
||||
boolean isFeeOutsideTolerance;
|
||||
boolean opReturnIsOnlyOutput;
|
||||
@ -508,7 +531,9 @@ public class BtcWalletService extends WalletService {
|
||||
// signInputs needs to be false as it would try to sign all inputs (BSQ inputs are not in this wallet)
|
||||
sendRequest.signInputs = false;
|
||||
|
||||
sendRequest.fee = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs);
|
||||
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4);
|
||||
sendRequest.feePerKb = Coin.ZERO;
|
||||
sendRequest.ensureMinRequiredFee = false;
|
||||
|
||||
@ -528,15 +553,19 @@ public class BtcWalletService extends WalletService {
|
||||
if (opReturnData != null)
|
||||
resultTx.addOutput(new TransactionOutput(params, resultTx, Coin.ZERO, ScriptBuilder.createOpReturnScript(opReturnData).getProgram()));
|
||||
|
||||
numInputs = resultTx.getInputs().size();
|
||||
txSizeWithUnsignedInputs = resultTx.bitcoinSerialize().length;
|
||||
final long estimatedFeeAsLong = txFeePerByte.multiply(txSizeWithUnsignedInputs + sigSizePerInput * numInputs).value;
|
||||
Tuple2<Integer, Integer> numInputs = getNumInputs(resultTx);
|
||||
numLegacyInputs = numInputs.first;
|
||||
numSegwitInputs = numInputs.second;
|
||||
txVsizeWithUnsignedInputs = resultTx.getVsize();
|
||||
final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
|
||||
sigSizePerInput * numLegacyInputs +
|
||||
sigSizePerInput * numSegwitInputs / 4).value;
|
||||
// calculated fee must be inside of a tolerance range with tx fee
|
||||
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
|
||||
}
|
||||
while (opReturnIsOnlyOutput ||
|
||||
isFeeOutsideTolerance ||
|
||||
resultTx.getFee().value < txFeePerByte.multiply(resultTx.bitcoinSerialize().length).value);
|
||||
resultTx.getFee().value < txFeePerVbyte.multiply(resultTx.getVsize()).value);
|
||||
|
||||
// Sign all BTC inputs
|
||||
signAllBtcInputs(preparedBsqTxInputs.size(), resultTx);
|
||||
@ -548,6 +577,25 @@ public class BtcWalletService extends WalletService {
|
||||
return resultTx;
|
||||
}
|
||||
|
||||
private Tuple2<Integer, Integer> getNumInputs(Transaction tx) {
|
||||
int numLegacyInputs = 0;
|
||||
int numSegwitInputs = 0;
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
TransactionOutput connectedOutput = input.getConnectedOutput();
|
||||
if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) ||
|
||||
ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) {
|
||||
// If connectedOutput is null, we don't know here the input type. To avoid underpaying fees,
|
||||
// we treat it as a legacy input which will result in a higher fee estimation.
|
||||
numLegacyInputs++;
|
||||
} else if (ScriptPattern.isP2WPKH(connectedOutput.getScriptPubKey())) {
|
||||
numSegwitInputs++;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Inputs should spend a P2PKH, P2PK or P2WPKH ouput");
|
||||
}
|
||||
}
|
||||
return new Tuple2(numLegacyInputs, numSegwitInputs);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Commit tx
|
||||
@ -579,18 +627,17 @@ public class BtcWalletService extends WalletService {
|
||||
if (addressEntry.isPresent()) {
|
||||
return addressEntry.get();
|
||||
} else {
|
||||
// We still use non-segwit addresses for the trade protocol.
|
||||
// We try to use available and not yet used entries
|
||||
Optional<AddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> AddressEntry.Context.AVAILABLE == e.getContext())
|
||||
.filter(e -> isAddressUnused(e.getAddress()))
|
||||
.filter(e -> Script.ScriptType.P2PKH.equals(e.getAddress().getOutputScriptType()))
|
||||
.filter(e -> Script.ScriptType.P2WPKH.equals(e.getAddress().getOutputScriptType()))
|
||||
.findAny();
|
||||
if (emptyAvailableAddressEntry.isPresent()) {
|
||||
return addressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
||||
} else {
|
||||
DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2PKH));
|
||||
AddressEntry entry = new AddressEntry(key, context, offerId, false);
|
||||
DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(wallet.freshReceiveAddress(Script.ScriptType.P2WPKH));
|
||||
AddressEntry entry = new AddressEntry(key, context, offerId, true);
|
||||
addressEntryList.addAddressEntry(entry);
|
||||
return entry;
|
||||
}
|
||||
@ -810,7 +857,7 @@ public class BtcWalletService extends WalletService {
|
||||
);
|
||||
|
||||
log.info("newTransaction no. of inputs " + newTransaction.getInputs().size());
|
||||
log.info("newTransaction size in kB " + newTransaction.bitcoinSerialize().length / 1024);
|
||||
log.info("newTransaction vsize in vkB " + newTransaction.getVsize() / 1024);
|
||||
|
||||
if (!newTransaction.getInputs().isEmpty()) {
|
||||
Coin amount = Coin.valueOf(newTransaction.getInputs().stream()
|
||||
@ -821,13 +868,13 @@ public class BtcWalletService extends WalletService {
|
||||
try {
|
||||
Coin fee;
|
||||
int counter = 0;
|
||||
int txSize = 0;
|
||||
int txVsize = 0;
|
||||
Transaction tx;
|
||||
SendRequest sendRequest;
|
||||
Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte();
|
||||
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
|
||||
do {
|
||||
counter++;
|
||||
fee = txFeeForWithdrawalPerByte.multiply(txSize);
|
||||
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
|
||||
newTransaction.clearOutputs();
|
||||
newTransaction.addOutput(amount.subtract(fee), toAddress);
|
||||
|
||||
@ -840,7 +887,7 @@ public class BtcWalletService extends WalletService {
|
||||
sendRequest.changeAddress = toAddress;
|
||||
wallet.completeTx(sendRequest);
|
||||
tx = sendRequest.tx;
|
||||
txSize = tx.bitcoinSerialize().length;
|
||||
txVsize = tx.getVsize();
|
||||
printTx("FeeEstimationTransaction", tx);
|
||||
sendRequest.tx.getOutputs().forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString()));
|
||||
}
|
||||
@ -939,16 +986,16 @@ public class BtcWalletService extends WalletService {
|
||||
try {
|
||||
Coin fee;
|
||||
int counter = 0;
|
||||
int txSize = 0;
|
||||
int txVsize = 0;
|
||||
Transaction tx;
|
||||
Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte();
|
||||
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
|
||||
do {
|
||||
counter++;
|
||||
fee = txFeeForWithdrawalPerByte.multiply(txSize);
|
||||
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
|
||||
SendRequest sendRequest = getSendRequest(fromAddress, toAddress, amount, fee, aesKey, context);
|
||||
wallet.completeTx(sendRequest);
|
||||
tx = sendRequest.tx;
|
||||
txSize = tx.bitcoinSerialize().length;
|
||||
txVsize = tx.getVsize();
|
||||
printTx("FeeEstimationTransaction", tx);
|
||||
}
|
||||
while (feeEstimationNotSatisfied(counter, tx));
|
||||
@ -986,18 +1033,20 @@ public class BtcWalletService extends WalletService {
|
||||
try {
|
||||
Coin fee;
|
||||
int counter = 0;
|
||||
int txSize = 0;
|
||||
int txVsize = 0;
|
||||
Transaction tx;
|
||||
Coin txFeeForWithdrawalPerByte = getTxFeeForWithdrawalPerByte();
|
||||
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
|
||||
do {
|
||||
counter++;
|
||||
fee = txFeeForWithdrawalPerByte.multiply(txSize);
|
||||
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
|
||||
// We use a dummy address for the output
|
||||
final String dummyReceiver = LegacyAddress.fromKey(params, new ECKey()).toBase58();
|
||||
// We don't know here whether the output is segwit or not but we don't care too much because the size of
|
||||
// a segwit ouput is just 3 byte smaller than the size of a legacy ouput.
|
||||
final String dummyReceiver = SegwitAddress.fromKey(params, new ECKey()).toString();
|
||||
SendRequest sendRequest = getSendRequestForMultipleAddresses(fromAddresses, dummyReceiver, amount, fee, null, aesKey);
|
||||
wallet.completeTx(sendRequest);
|
||||
tx = sendRequest.tx;
|
||||
txSize = tx.bitcoinSerialize().length;
|
||||
txVsize = tx.getVsize();
|
||||
printTx("FeeEstimationTransactionForMultipleAddresses", tx);
|
||||
}
|
||||
while (feeEstimationNotSatisfied(counter, tx));
|
||||
@ -1013,16 +1062,18 @@ public class BtcWalletService extends WalletService {
|
||||
}
|
||||
|
||||
private boolean feeEstimationNotSatisfied(int counter, Transaction tx) {
|
||||
long targetFee = getTxFeeForWithdrawalPerByte().multiply(tx.bitcoinSerialize().length).value;
|
||||
long targetFee = getTxFeeForWithdrawalPerVbyte().multiply(tx.getVsize()).value;
|
||||
return counter < 10 &&
|
||||
(tx.getFee().value < targetFee ||
|
||||
tx.getFee().value - targetFee > 1000);
|
||||
}
|
||||
|
||||
public int getEstimatedFeeTxSize(List<Coin> outputValues, Coin txFee)
|
||||
public int getEstimatedFeeTxVsize(List<Coin> outputValues, Coin txFee)
|
||||
throws InsufficientMoneyException, AddressFormatException {
|
||||
Transaction transaction = new Transaction(params);
|
||||
Address dummyAddress = LegacyAddress.fromKey(params, new ECKey());
|
||||
// In reality txs have a mix of segwit/legacy ouputs, but we don't care too much because the size of
|
||||
// a segwit ouput is just 3 byte smaller than the size of a legacy ouput.
|
||||
Address dummyAddress = SegwitAddress.fromKey(params, new ECKey());
|
||||
outputValues.forEach(outputValue -> transaction.addOutput(outputValue, dummyAddress));
|
||||
|
||||
SendRequest sendRequest = SendRequest.forTx(transaction);
|
||||
@ -1035,7 +1086,7 @@ public class BtcWalletService extends WalletService {
|
||||
sendRequest.ensureMinRequiredFee = false;
|
||||
sendRequest.changeAddress = dummyAddress;
|
||||
wallet.completeTx(sendRequest);
|
||||
return transaction.bitcoinSerialize().length;
|
||||
return transaction.getVsize();
|
||||
}
|
||||
|
||||
|
||||
|
@ -36,8 +36,8 @@ import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.SegwitAddress;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.SignatureDecodeException;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
@ -76,6 +76,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class TradeWalletService {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class);
|
||||
private static final Coin MIN_DELAYED_PAYOUT_TX_FEE = Coin.valueOf(1000);
|
||||
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final Preferences preferences;
|
||||
@ -336,7 +337,7 @@ public class TradeWalletService {
|
||||
Transaction dummyTX = new Transaction(params);
|
||||
// The output is just used to get the right inputs and change outputs, so we use an anonymous ECKey, as it will never be used for anything.
|
||||
// We don't care about fee calculation differences between the real tx and that dummy tx as we use a static tx fee.
|
||||
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, LegacyAddress.fromKey(params, new ECKey()));
|
||||
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTX, dummyOutputAmount, SegwitAddress.fromKey(params, new ECKey()));
|
||||
dummyTX.addOutput(dummyOutput);
|
||||
|
||||
// Find the needed inputs to pay the output, optionally add 1 change output.
|
||||
@ -455,7 +456,7 @@ public class TradeWalletService {
|
||||
// First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx.
|
||||
// Similar to the way we did in the createTakerDepositTxInputs method.
|
||||
Transaction dummyTx = new Transaction(params);
|
||||
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, LegacyAddress.fromKey(params, new ECKey()));
|
||||
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, SegwitAddress.fromKey(params, new ECKey()));
|
||||
dummyTx.addOutput(dummyOutput);
|
||||
addAvailableInputsAndChangeOutputs(dummyTx, makerAddress, makerChangeAddress);
|
||||
// Normally we have only 1 input but we support multiple inputs if the user has paid in with several transactions.
|
||||
@ -502,12 +503,12 @@ public class TradeWalletService {
|
||||
|
||||
|
||||
// Add MultiSig output
|
||||
Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey);
|
||||
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
|
||||
|
||||
// Tx fee for deposit tx will be paid by buyer.
|
||||
TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount,
|
||||
p2SHMultiSigOutputScript.getProgram());
|
||||
preparedDepositTx.addOutput(p2SHMultiSigOutput);
|
||||
TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount,
|
||||
hashedMultiSigOutputScript.getProgram());
|
||||
preparedDepositTx.addOutput(hashedMultiSigOutput);
|
||||
|
||||
// We add the hash ot OP_RETURN with a 0 amount output
|
||||
TransactionOutput contractHashOutput = new TransactionOutput(params, preparedDepositTx, Coin.ZERO,
|
||||
@ -569,8 +570,9 @@ public class TradeWalletService {
|
||||
* @param buyerPubKey the public key of the buyer
|
||||
* @param sellerPubKey the public key of the seller
|
||||
* @throws SigningException if (one of) the taker input(s) was of an unrecognized type for signing
|
||||
* @throws TransactionVerificationException if a maker input wasn't signed, their MultiSig script or contract hash
|
||||
* doesn't match the taker's, or there was an unexpected problem with the final deposit tx or its signatures
|
||||
* @throws TransactionVerificationException if a non-P2WH maker-as-buyer input wasn't signed, the maker's MultiSig
|
||||
* script or contract hash doesn't match the taker's, or there was an unexpected problem with the final deposit tx
|
||||
* or its signatures
|
||||
* @throws WalletException if the taker's wallet is null or structurally inconsistent
|
||||
*/
|
||||
public Transaction takerSignsDepositTx(boolean takerIsSeller,
|
||||
@ -587,9 +589,9 @@ public class TradeWalletService {
|
||||
checkArgument(!sellerInputs.isEmpty());
|
||||
|
||||
// Check if maker's MultiSig script is identical to the takers
|
||||
Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey);
|
||||
if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(p2SHMultiSigOutputScript)) {
|
||||
throw new TransactionVerificationException("Maker's p2SHMultiSigOutputScript does not match to takers p2SHMultiSigOutputScript");
|
||||
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
|
||||
if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(hashedMultiSigOutputScript)) {
|
||||
throw new TransactionVerificationException("Maker's hashedMultiSigOutputScript does not match to takers hashedMultiSigOutputScript");
|
||||
}
|
||||
|
||||
// The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new
|
||||
@ -601,8 +603,12 @@ public class TradeWalletService {
|
||||
// We grab the signature from the makersDepositTx and apply it to the new tx input
|
||||
for (int i = 0; i < buyerInputs.size(); i++) {
|
||||
TransactionInput makersInput = makersDepositTx.getInputs().get(i);
|
||||
byte[] makersScriptSigProgram = getMakersScriptSigProgram(makersInput);
|
||||
byte[] makersScriptSigProgram = makersInput.getScriptSig().getProgram();
|
||||
TransactionInput input = getTransactionInput(depositTx, makersScriptSigProgram, buyerInputs.get(i));
|
||||
Script scriptPubKey = checkNotNull(input.getConnectedOutput()).getScriptPubKey();
|
||||
if (makersScriptSigProgram.length == 0 && !ScriptPattern.isP2WH(scriptPubKey)) {
|
||||
throw new TransactionVerificationException("Non-segwit inputs from maker not signed.");
|
||||
}
|
||||
if (!TransactionWitness.EMPTY.equals(makersInput.getWitness())) {
|
||||
input.setWitness(makersInput.getWitness());
|
||||
}
|
||||
@ -683,6 +689,21 @@ public class TradeWalletService {
|
||||
}
|
||||
|
||||
|
||||
public void sellerAddsBuyerWitnessesToDepositTx(Transaction myDepositTx,
|
||||
Transaction buyersDepositTxWithWitnesses) {
|
||||
int numberInputs = myDepositTx.getInputs().size();
|
||||
for (int i = 0; i < numberInputs; i++) {
|
||||
var txInput = myDepositTx.getInput(i);
|
||||
var witnessFromBuyer = buyersDepositTxWithWitnesses.getInput(i).getWitness();
|
||||
|
||||
if (TransactionWitness.EMPTY.equals(txInput.getWitness()) &&
|
||||
!TransactionWitness.EMPTY.equals(witnessFromBuyer)) {
|
||||
txInput.setWitness(witnessFromBuyer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Delayed payout tx
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -692,11 +713,11 @@ public class TradeWalletService {
|
||||
Coin minerFee,
|
||||
long lockTime)
|
||||
throws AddressFormatException, TransactionVerificationException {
|
||||
TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0);
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
Transaction delayedPayoutTx = new Transaction(params);
|
||||
delayedPayoutTx.addInput(p2SHMultiSigOutput);
|
||||
delayedPayoutTx.addInput(hashedMultiSigOutput);
|
||||
applyLockTime(lockTime, delayedPayoutTx);
|
||||
Coin outputAmount = p2SHMultiSigOutput.getValue().subtract(minerFee);
|
||||
Coin outputAmount = hashedMultiSigOutput.getValue().subtract(minerFee);
|
||||
delayedPayoutTx.addOutput(outputAmount, Address.fromString(params, donationAddressString));
|
||||
WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx);
|
||||
WalletService.verifyTransaction(delayedPayoutTx);
|
||||
@ -704,13 +725,17 @@ public class TradeWalletService {
|
||||
}
|
||||
|
||||
public byte[] signDelayedPayoutTx(Transaction delayedPayoutTx,
|
||||
Transaction preparedDepositTx,
|
||||
DeterministicKey myMultiSigKeyPair,
|
||||
byte[] buyerPubKey,
|
||||
byte[] sellerPubKey)
|
||||
throws AddressFormatException, TransactionVerificationException {
|
||||
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
Sha256Hash sigHash = delayedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
Sha256Hash sigHash;
|
||||
Coin delayedPayoutTxInputValue = preparedDepositTx.getOutput(0).getValue();
|
||||
sigHash = delayedPayoutTx.hashForWitnessSignature(0, redeemScript,
|
||||
delayedPayoutTxInputValue, Transaction.SigHash.ALL, false);
|
||||
checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null");
|
||||
if (myMultiSigKeyPair.isEncrypted()) {
|
||||
checkNotNull(aesKey);
|
||||
@ -722,24 +747,45 @@ public class TradeWalletService {
|
||||
return mySignature.encodeToDER();
|
||||
}
|
||||
|
||||
public Transaction finalizeUnconnectedDelayedPayoutTx(Transaction delayedPayoutTx,
|
||||
byte[] buyerPubKey,
|
||||
byte[] sellerPubKey,
|
||||
byte[] buyerSignature,
|
||||
byte[] sellerSignature,
|
||||
Coin inputValue)
|
||||
throws AddressFormatException, TransactionVerificationException, SignatureDecodeException {
|
||||
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
|
||||
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);
|
||||
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
|
||||
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
|
||||
TransactionInput input = delayedPayoutTx.getInput(0);
|
||||
input.setScriptSig(ScriptBuilder.createEmpty());
|
||||
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
|
||||
input.setWitness(witness);
|
||||
WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx);
|
||||
WalletService.verifyTransaction(delayedPayoutTx);
|
||||
|
||||
if (checkNotNull(inputValue).isLessThan(delayedPayoutTx.getOutputSum().add(MIN_DELAYED_PAYOUT_TX_FEE))) {
|
||||
throw new TransactionVerificationException("Delayed payout tx is paying less than the minimum allowed tx fee");
|
||||
}
|
||||
Script scriptPubKey = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, false);
|
||||
input.getScriptSig().correctlySpends(delayedPayoutTx, 0, witness, inputValue, scriptPubKey, Script.ALL_VERIFY_FLAGS);
|
||||
return delayedPayoutTx;
|
||||
}
|
||||
|
||||
public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx,
|
||||
byte[] buyerPubKey,
|
||||
byte[] sellerPubKey,
|
||||
byte[] buyerSignature,
|
||||
byte[] sellerSignature)
|
||||
throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException {
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature);
|
||||
ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature);
|
||||
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
|
||||
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
|
||||
|
||||
TransactionInput input = delayedPayoutTx.getInput(0);
|
||||
input.setScriptSig(inputScript);
|
||||
WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx);
|
||||
WalletService.verifyTransaction(delayedPayoutTx);
|
||||
finalizeUnconnectedDelayedPayoutTx(delayedPayoutTx, buyerPubKey, sellerPubKey, buyerSignature, sellerSignature, input.getValue());
|
||||
|
||||
WalletService.checkWalletConsistency(wallet);
|
||||
WalletService.checkScriptSig(delayedPayoutTx, input, 0);
|
||||
checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null");
|
||||
input.verify(input.getConnectedOutput());
|
||||
return delayedPayoutTx;
|
||||
@ -779,7 +825,15 @@ public class TradeWalletService {
|
||||
// MS redeemScript
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
// MS output from prev. tx is index 0
|
||||
Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
Sha256Hash sigHash;
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
if (ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey())) {
|
||||
sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
} else {
|
||||
Coin inputValue = hashedMultiSigOutput.getValue();
|
||||
sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript,
|
||||
inputValue, Transaction.SigHash.ALL, false);
|
||||
}
|
||||
checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null");
|
||||
if (multiSigKeyPair.isEncrypted()) {
|
||||
checkNotNull(aesKey);
|
||||
@ -822,7 +876,16 @@ public class TradeWalletService {
|
||||
// MS redeemScript
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
// MS output from prev. tx is index 0
|
||||
Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
|
||||
Sha256Hash sigHash;
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
} else {
|
||||
Coin inputValue = hashedMultiSigOutput.getValue();
|
||||
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
|
||||
inputValue, Transaction.SigHash.ALL, false);
|
||||
}
|
||||
checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null");
|
||||
if (multiSigKeyPair.isEncrypted()) {
|
||||
checkNotNull(aesKey);
|
||||
@ -832,10 +895,16 @@ public class TradeWalletService {
|
||||
Transaction.SigHash.ALL, false);
|
||||
TransactionSignature sellerTxSig = new TransactionSignature(sellerSignature, Transaction.SigHash.ALL, false);
|
||||
// Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer)
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
|
||||
redeemScript);
|
||||
TransactionInput input = payoutTx.getInput(0);
|
||||
input.setScriptSig(inputScript);
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
|
||||
redeemScript);
|
||||
input.setScriptSig(inputScript);
|
||||
} else {
|
||||
input.setScriptSig(ScriptBuilder.createEmpty());
|
||||
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
|
||||
input.setWitness(witness);
|
||||
}
|
||||
WalletService.printTx("payoutTx", payoutTx);
|
||||
WalletService.verifyTransaction(payoutTx);
|
||||
WalletService.checkWalletConsistency(wallet);
|
||||
@ -863,7 +932,16 @@ public class TradeWalletService {
|
||||
// MS redeemScript
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
// MS output from prev. tx is index 0
|
||||
Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
|
||||
Sha256Hash sigHash;
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
} else {
|
||||
Coin inputValue = hashedMultiSigOutput.getValue();
|
||||
sigHash = preparedPayoutTx.hashForWitnessSignature(0, redeemScript,
|
||||
inputValue, Transaction.SigHash.ALL, false);
|
||||
}
|
||||
checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null");
|
||||
if (myMultiSigKeyPair.isEncrypted()) {
|
||||
checkNotNull(aesKey);
|
||||
@ -895,9 +973,18 @@ public class TradeWalletService {
|
||||
TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature),
|
||||
Transaction.SigHash.ALL, false);
|
||||
// Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (seller, buyer)
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
boolean hashedMultiSigOutputIsLegacy = ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
|
||||
TransactionInput input = payoutTx.getInput(0);
|
||||
input.setScriptSig(inputScript);
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
|
||||
redeemScript);
|
||||
input.setScriptSig(inputScript);
|
||||
} else {
|
||||
input.setScriptSig(ScriptBuilder.createEmpty());
|
||||
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
|
||||
input.setWitness(witness);
|
||||
}
|
||||
WalletService.printTx("mediated payoutTx", payoutTx);
|
||||
WalletService.verifyTransaction(payoutTx);
|
||||
WalletService.checkWalletConsistency(wallet);
|
||||
@ -945,9 +1032,9 @@ public class TradeWalletService {
|
||||
byte[] arbitratorPubKey)
|
||||
throws AddressFormatException, TransactionVerificationException, WalletException, SignatureDecodeException {
|
||||
Transaction depositTx = new Transaction(params, depositTxSerialized);
|
||||
TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0);
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
Transaction payoutTx = new Transaction(params);
|
||||
payoutTx.addInput(p2SHMultiSigOutput);
|
||||
payoutTx.addInput(hashedMultiSigOutput);
|
||||
if (buyerPayoutAmount.isPositive()) {
|
||||
payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));
|
||||
}
|
||||
@ -957,7 +1044,15 @@ public class TradeWalletService {
|
||||
|
||||
// take care of sorting!
|
||||
Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey);
|
||||
Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
Sha256Hash sigHash;
|
||||
boolean hashedMultiSigOutputIsLegacy = !ScriptPattern.isP2SH(hashedMultiSigOutput.getScriptPubKey());
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
} else {
|
||||
Coin inputValue = hashedMultiSigOutput.getValue();
|
||||
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
|
||||
inputValue, Transaction.SigHash.ALL, false);
|
||||
}
|
||||
checkNotNull(tradersMultiSigKeyPair, "tradersMultiSigKeyPair must not be null");
|
||||
if (tradersMultiSigKeyPair.isEncrypted()) {
|
||||
checkNotNull(aesKey);
|
||||
@ -966,11 +1061,18 @@ public class TradeWalletService {
|
||||
TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false);
|
||||
TransactionSignature arbitratorTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(arbitratorSignature),
|
||||
Transaction.SigHash.ALL, false);
|
||||
// Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer)
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(arbitratorTxSig, tradersTxSig),
|
||||
redeemScript);
|
||||
TransactionInput input = payoutTx.getInput(0);
|
||||
input.setScriptSig(inputScript);
|
||||
// Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer)
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(
|
||||
ImmutableList.of(arbitratorTxSig, tradersTxSig),
|
||||
redeemScript);
|
||||
input.setScriptSig(inputScript);
|
||||
} else {
|
||||
input.setScriptSig(ScriptBuilder.createEmpty());
|
||||
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, arbitratorTxSig, tradersTxSig);
|
||||
input.setWitness(witness);
|
||||
}
|
||||
WalletService.printTx("disputed payoutTx", payoutTx);
|
||||
WalletService.verifyTransaction(payoutTx);
|
||||
WalletService.checkWalletConsistency(wallet);
|
||||
@ -995,21 +1097,23 @@ public class TradeWalletService {
|
||||
String sellerPrivateKeyAsHex,
|
||||
String buyerPubKeyAsHex,
|
||||
String sellerPubKeyAsHex,
|
||||
boolean hashedMultiSigOutputIsLegacy,
|
||||
TxBroadcaster.Callback callback)
|
||||
throws AddressFormatException, TransactionVerificationException, WalletException {
|
||||
byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey();
|
||||
byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey();
|
||||
|
||||
Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey);
|
||||
Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey,
|
||||
hashedMultiSigOutputIsLegacy);
|
||||
|
||||
Coin msOutput = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee);
|
||||
TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, null, msOutput, p2SHMultiSigOutputScript.getProgram());
|
||||
Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee);
|
||||
TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, null, msOutputValue, hashedMultiSigOutputScript.getProgram());
|
||||
Transaction depositTx = new Transaction(params);
|
||||
depositTx.addOutput(p2SHMultiSigOutput);
|
||||
depositTx.addOutput(hashedMultiSigOutput);
|
||||
|
||||
Transaction payoutTx = new Transaction(params);
|
||||
Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex);
|
||||
payoutTx.addInput(new TransactionInput(params, depositTx, p2SHMultiSigOutputScript.getProgram(), new TransactionOutPoint(params, 0, spendTxHash), msOutput));
|
||||
payoutTx.addInput(new TransactionInput(params, depositTx, null, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue));
|
||||
|
||||
if (buyerPayoutAmount.isPositive()) {
|
||||
payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));
|
||||
@ -1020,7 +1124,14 @@ public class TradeWalletService {
|
||||
|
||||
// take care of sorting!
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
Sha256Hash sigHash;
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
} else {
|
||||
Coin inputValue = msOutputValue;
|
||||
sigHash = payoutTx.hashForWitnessSignature(0, redeemScript,
|
||||
inputValue, Transaction.SigHash.ALL, false);
|
||||
}
|
||||
|
||||
ECKey buyerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(buyerPrivateKeyAsHex));
|
||||
checkNotNull(buyerPrivateKey, "key must not be null");
|
||||
@ -1032,10 +1143,18 @@ public class TradeWalletService {
|
||||
|
||||
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
|
||||
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
|
||||
|
||||
TransactionInput input = payoutTx.getInput(0);
|
||||
input.setScriptSig(inputScript);
|
||||
if (hashedMultiSigOutputIsLegacy) {
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig),
|
||||
redeemScript);
|
||||
input.setScriptSig(inputScript);
|
||||
} else {
|
||||
input.setScriptSig(ScriptBuilder.createEmpty());
|
||||
TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig);
|
||||
input.setWitness(witness);
|
||||
}
|
||||
|
||||
WalletService.printTx("payoutTx", payoutTx);
|
||||
WalletService.verifyTransaction(payoutTx);
|
||||
WalletService.checkWalletConsistency(wallet);
|
||||
@ -1092,28 +1211,32 @@ public class TradeWalletService {
|
||||
"input.getConnectedOutput().getParentTransaction() must not be null");
|
||||
checkNotNull(input.getValue(), "input.getValue() must not be null");
|
||||
|
||||
// bitcoinSerialize(false) is used just in case the serialized tx is parsed by a bisq node still using
|
||||
// bitcoinj 0.14. This is not supposed to happen ever since Version.TRADE_PROTOCOL_VERSION was set to 3,
|
||||
// but it costs nothing to be on the safe side.
|
||||
// The serialized tx is just used to obtain its hash, so the witness data is not relevant.
|
||||
return new RawTransactionInput(input.getOutpoint().getIndex(),
|
||||
input.getConnectedOutput().getParentTransaction().bitcoinSerialize(false),
|
||||
input.getValue().value);
|
||||
}
|
||||
|
||||
private byte[] getMakersScriptSigProgram(TransactionInput transactionInput) throws TransactionVerificationException {
|
||||
byte[] scriptProgram = transactionInput.getScriptSig().getProgram();
|
||||
if (scriptProgram.length == 0) {
|
||||
throw new TransactionVerificationException("Inputs from maker not signed.");
|
||||
}
|
||||
|
||||
return scriptProgram;
|
||||
}
|
||||
|
||||
private TransactionInput getTransactionInput(Transaction depositTx,
|
||||
byte[] scriptProgram,
|
||||
RawTransactionInput rawTransactionInput) {
|
||||
return new TransactionInput(params, depositTx, scriptProgram, new TransactionOutPoint(params,
|
||||
rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)),
|
||||
return new TransactionInput(params, depositTx, scriptProgram, getConnectedOutPoint(rawTransactionInput),
|
||||
Coin.valueOf(rawTransactionInput.value));
|
||||
}
|
||||
|
||||
private TransactionOutPoint getConnectedOutPoint(RawTransactionInput rawTransactionInput) {
|
||||
return new TransactionOutPoint(params, rawTransactionInput.index,
|
||||
new Transaction(params, rawTransactionInput.parentTransaction));
|
||||
}
|
||||
|
||||
public boolean isP2WH(RawTransactionInput rawTransactionInput) {
|
||||
return ScriptPattern.isP2WH(
|
||||
checkNotNull(getConnectedOutPoint(rawTransactionInput).getConnectedOutput()).getScriptPubKey());
|
||||
}
|
||||
|
||||
|
||||
// TODO: Once we have removed legacy arbitrator from dispute domain we can remove that method as well.
|
||||
// Atm it is still used by traderSignAndFinalizeDisputedPayoutTx which is used by ArbitrationManager.
|
||||
@ -1144,8 +1267,13 @@ public class TradeWalletService {
|
||||
return ScriptBuilder.createMultiSigOutputScript(2, keys);
|
||||
}
|
||||
|
||||
private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey) {
|
||||
return ScriptBuilder.createP2SHOutputScript(get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey));
|
||||
private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, boolean legacy) {
|
||||
Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey);
|
||||
if (legacy) {
|
||||
return ScriptBuilder.createP2SHOutputScript(redeemScript);
|
||||
} else {
|
||||
return ScriptBuilder.createP2WSHOutputScript(redeemScript);
|
||||
}
|
||||
}
|
||||
|
||||
private Transaction createPayoutTx(Transaction depositTx,
|
||||
@ -1153,9 +1281,9 @@ public class TradeWalletService {
|
||||
Coin sellerPayoutAmount,
|
||||
String buyerAddressString,
|
||||
String sellerAddressString) throws AddressFormatException {
|
||||
TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0);
|
||||
TransactionOutput hashedMultiSigOutput = depositTx.getOutput(0);
|
||||
Transaction transaction = new Transaction(params);
|
||||
transaction.addInput(p2SHMultiSigOutput);
|
||||
transaction.addInput(hashedMultiSigOutput);
|
||||
if (buyerPayoutAmount.isPositive()) {
|
||||
transaction.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));
|
||||
}
|
||||
@ -1187,13 +1315,10 @@ public class TradeWalletService {
|
||||
input.setScriptSig(ScriptBuilder.createInputScript(txSig, sigKey));
|
||||
}
|
||||
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) {
|
||||
// TODO: Consider using this alternative way to build the scriptCode (taken from bitcoinj master)
|
||||
// Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey)
|
||||
Script scriptCode = new ScriptBuilder().data(
|
||||
ScriptBuilder.createOutputScript(LegacyAddress.fromKey(transaction.getParams(), sigKey)).getProgram())
|
||||
.build();
|
||||
// scriptCode is expected to have the format of a legacy P2PKH output script
|
||||
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey);
|
||||
Coin value = input.getValue();
|
||||
TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, scriptCode, value,
|
||||
TransactionSignature txSig = transaction.calculateWitnessSignature(inputIndex, sigKey, aesKey, scriptCode, value,
|
||||
Transaction.SigHash.ALL, false);
|
||||
input.setScriptSig(ScriptBuilder.createEmpty());
|
||||
input.setWitness(TransactionWitness.redeemP2WPKH(txSig, sigKey));
|
||||
|
@ -37,7 +37,6 @@ import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Context;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
@ -325,13 +324,10 @@ public abstract class WalletService {
|
||||
}
|
||||
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) {
|
||||
try {
|
||||
// TODO: Consider using this alternative way to build the scriptCode (taken from bitcoinj master)
|
||||
// Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key);
|
||||
Script scriptCode = new ScriptBuilder().data(
|
||||
ScriptBuilder.createOutputScript(LegacyAddress.fromKey(tx.getParams(), key)).getProgram())
|
||||
.build();
|
||||
// scriptCode is expected to have the format of a legacy P2PKH output script
|
||||
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key);
|
||||
Coin value = txIn.getValue();
|
||||
TransactionSignature txSig = tx.calculateWitnessSignature(index, key, scriptCode, value,
|
||||
TransactionSignature txSig = tx.calculateWitnessSignature(index, key, aesKey, scriptCode, value,
|
||||
Transaction.SigHash.ALL, false);
|
||||
txIn.setScriptSig(ScriptBuilder.createEmpty());
|
||||
txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key));
|
||||
@ -479,10 +475,10 @@ public abstract class WalletService {
|
||||
return getBalanceForAddress(getAddressFromOutput(output));
|
||||
}
|
||||
|
||||
public Coin getTxFeeForWithdrawalPerByte() {
|
||||
public Coin getTxFeeForWithdrawalPerVbyte() {
|
||||
Coin fee = (preferences.isUseCustomWithdrawalTxFee()) ?
|
||||
Coin.valueOf(preferences.getWithdrawalTxFeeInBytes()) :
|
||||
feeService.getTxFeePerByte();
|
||||
Coin.valueOf(preferences.getWithdrawalTxFeeInVbytes()) :
|
||||
feeService.getTxFeePerVbyte();
|
||||
log.info("tx fee = " + fee.toFriendlyString());
|
||||
return fee;
|
||||
}
|
||||
@ -521,7 +517,7 @@ public abstract class WalletService {
|
||||
throws InsufficientMoneyException, AddressFormatException {
|
||||
SendRequest sendRequest = SendRequest.emptyWallet(Address.fromString(params, toAddress));
|
||||
sendRequest.fee = Coin.ZERO;
|
||||
sendRequest.feePerKb = getTxFeeForWithdrawalPerByte().multiply(1000);
|
||||
sendRequest.feePerKb = getTxFeeForWithdrawalPerVbyte().multiply(1000);
|
||||
sendRequest.aesKey = aesKey;
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
|
||||
printTx("empty btc wallet", sendResult.tx);
|
||||
@ -558,6 +554,10 @@ public abstract class WalletService {
|
||||
return isWalletReady() && chain != null ? chain.getBestChainHeight() : 0;
|
||||
}
|
||||
|
||||
public boolean isChainHeightSyncedWithinTolerance() {
|
||||
return walletsSetup.isChainHeightSyncedWithinTolerance();
|
||||
}
|
||||
|
||||
public Transaction getClonedTransaction(Transaction tx) {
|
||||
return new Transaction(params, tx.bitcoinSerialize());
|
||||
}
|
||||
|
@ -382,9 +382,9 @@ public class DaoFacade implements DaoSetupService {
|
||||
return BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight());
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getBlindVoteMiningFeeAndTxSize(Coin stake)
|
||||
public Tuple2<Coin, Integer> getBlindVoteMiningFeeAndTxVsize(Coin stake)
|
||||
throws WalletException, InsufficientMoneyException, TransactionVerificationException {
|
||||
return myBlindVoteListService.getMiningFeeAndTxSize(stake);
|
||||
return myBlindVoteListService.getMiningFeeAndTxVsize(stake);
|
||||
}
|
||||
|
||||
// Publish blindVote tx and broadcast blindVote to p2p network and store to blindVoteList.
|
||||
@ -532,12 +532,12 @@ public class DaoFacade implements DaoSetupService {
|
||||
lockupTxService.publishLockupTx(lockupAmount, lockTime, lockupReason, hash, resultHandler, exceptionHandler);
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getLockupTxMiningFeeAndTxSize(Coin lockupAmount,
|
||||
int lockTime,
|
||||
LockupReason lockupReason,
|
||||
byte[] hash)
|
||||
public Tuple2<Coin, Integer> getLockupTxMiningFeeAndTxVsize(Coin lockupAmount,
|
||||
int lockTime,
|
||||
LockupReason lockupReason,
|
||||
byte[] hash)
|
||||
throws InsufficientMoneyException, IOException, TransactionVerificationException, WalletException {
|
||||
return lockupTxService.getMiningFeeAndTxSize(lockupAmount, lockTime, lockupReason, hash);
|
||||
return lockupTxService.getMiningFeeAndTxVsize(lockupAmount, lockTime, lockupReason, hash);
|
||||
}
|
||||
|
||||
public void publishUnlockTx(String lockupTxId, Consumer<String> resultHandler,
|
||||
@ -545,9 +545,9 @@ public class DaoFacade implements DaoSetupService {
|
||||
unlockTxService.publishUnlockTx(lockupTxId, resultHandler, exceptionHandler);
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getUnlockTxMiningFeeAndTxSize(String lockupTxId)
|
||||
public Tuple2<Coin, Integer> getUnlockTxMiningFeeAndTxVsize(String lockupTxId)
|
||||
throws InsufficientMoneyException, TransactionVerificationException, WalletException {
|
||||
return unlockTxService.getMiningFeeAndTxSize(lockupTxId);
|
||||
return unlockTxService.getMiningFeeAndTxVsize(lockupTxId);
|
||||
}
|
||||
|
||||
public long getTotalLockupAmount() {
|
||||
@ -788,6 +788,7 @@ public class DaoFacade implements DaoSetupService {
|
||||
// This list need to be updated once a new address gets defined.
|
||||
allPastParamValues.add("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp"); // burning man 2019
|
||||
allPastParamValues.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV"); // burningman2
|
||||
allPastParamValues.add("34VLFgtFKAtwTdZ5rengTT2g2zC99sWQLC"); // burningman3 (https://github.com/bisq-network/roles/issues/80#issuecomment-723577776)
|
||||
}
|
||||
|
||||
return allPastParamValues;
|
||||
|
@ -108,7 +108,7 @@ public class BallotListService implements PersistedDataHost, DaoSetupService {
|
||||
private void registerProposalAsBallot(Proposal proposal) {
|
||||
Ballot ballot = new Ballot(proposal); // vote is null
|
||||
if (log.isInfoEnabled()) {
|
||||
log.info("We create a new ballot with a proposal and add it to our list. " +
|
||||
log.debug("We create a new ballot with a proposal and add it to our list. " +
|
||||
"Vote is null at that moment. proposalTxId={}", proposal.getTxId());
|
||||
}
|
||||
if (ballotList.contains(ballot)) {
|
||||
@ -129,13 +129,16 @@ public class BallotListService implements PersistedDataHost, DaoSetupService {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
BallotList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
ballotList.setAll(persisted.getList());
|
||||
listeners.forEach(l -> l.onListChanged(ballotList.getList()));
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
ballotList.setAll(persisted.getList());
|
||||
listeners.forEach(l -> l.onListChanged(ballotList.getList()));
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,12 +162,15 @@ public class MyBlindVoteListService implements PersistedDataHost, DaoStateListen
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
MyBlindVoteList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
myBlindVoteList.setAll(persisted.getList());
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
myBlindVoteList.setAll(persisted.getList());
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,14 +189,14 @@ public class MyBlindVoteListService implements PersistedDataHost, DaoStateListen
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Tuple2<Coin, Integer> getMiningFeeAndTxSize(Coin stake)
|
||||
public Tuple2<Coin, Integer> getMiningFeeAndTxVsize(Coin stake)
|
||||
throws InsufficientMoneyException, WalletException, TransactionVerificationException {
|
||||
// We set dummy opReturn data
|
||||
Coin blindVoteFee = BlindVoteConsensus.getFee(daoStateService, daoStateService.getChainHeight());
|
||||
Transaction dummyTx = getBlindVoteTx(stake, blindVoteFee, new byte[22]);
|
||||
Coin miningFee = dummyTx.getFee();
|
||||
int txSize = dummyTx.bitcoinSerialize().length;
|
||||
return new Tuple2<>(miningFee, txSize);
|
||||
int txVsize = dummyTx.getVsize();
|
||||
return new Tuple2<>(miningFee, txVsize);
|
||||
}
|
||||
|
||||
public void publishBlindVote(Coin stake, ResultHandler resultHandler, ExceptionHandler exceptionHandler) {
|
||||
|
@ -91,12 +91,12 @@ public class LockupTxService {
|
||||
}
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getMiningFeeAndTxSize(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash)
|
||||
public Tuple2<Coin, Integer> getMiningFeeAndTxVsize(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash)
|
||||
throws InsufficientMoneyException, WalletException, TransactionVerificationException, IOException {
|
||||
Transaction tx = getLockupTx(lockupAmount, lockTime, lockupReason, hash);
|
||||
Coin miningFee = tx.getFee();
|
||||
int txSize = tx.bitcoinSerialize().length;
|
||||
return new Tuple2<>(miningFee, txSize);
|
||||
int txVsize = tx.getVsize();
|
||||
return new Tuple2<>(miningFee, txVsize);
|
||||
}
|
||||
|
||||
private Transaction getLockupTx(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash)
|
||||
|
@ -55,12 +55,15 @@ public class MyReputationListService implements PersistedDataHost, DaoSetupServi
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
MyReputationList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
myReputationList.setAll(persisted.getList());
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
myReputationList.setAll(persisted.getList());
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,12 +89,12 @@ public class UnlockTxService {
|
||||
}
|
||||
}
|
||||
|
||||
public Tuple2<Coin, Integer> getMiningFeeAndTxSize(String lockupTxId)
|
||||
public Tuple2<Coin, Integer> getMiningFeeAndTxVsize(String lockupTxId)
|
||||
throws InsufficientMoneyException, WalletException, TransactionVerificationException {
|
||||
Transaction tx = getUnlockTx(lockupTxId);
|
||||
Coin miningFee = tx.getFee();
|
||||
int txSize = tx.bitcoinSerialize().length;
|
||||
return new Tuple2<>(miningFee, txSize);
|
||||
int txVsize = tx.getVsize();
|
||||
return new Tuple2<>(miningFee, txVsize);
|
||||
}
|
||||
|
||||
private Transaction getUnlockTx(String lockupTxId)
|
||||
|
@ -69,12 +69,15 @@ public class MyVoteListService implements PersistedDataHost {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
MyVoteList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
this.myVoteList.setAll(persisted.getList());
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
myVoteList.setAll(persisted.getList());
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,11 +96,12 @@ public class MyVoteListService implements PersistedDataHost {
|
||||
|
||||
public void applyRevealTxId(MyVote myVote, String voteRevealTxId) {
|
||||
myVote.setRevealTxId(voteRevealTxId);
|
||||
log.info("Applied revealTxId to myVote.\nmyVote={}\nvoteRevealTxId={}", myVote, voteRevealTxId);
|
||||
log.debug("Applied revealTxId to myVote.\nmyVote={}\nvoteRevealTxId={}", myVote, voteRevealTxId);
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public Tuple2<Long, Long> getMeritAndStakeForProposal(String proposalTxId, MyBlindVoteListService myBlindVoteListService) {
|
||||
public Tuple2<Long, Long> getMeritAndStakeForProposal(String proposalTxId,
|
||||
MyBlindVoteListService myBlindVoteListService) {
|
||||
long merit = 0;
|
||||
long stake = 0;
|
||||
List<MyVote> list = new ArrayList<>(myVoteList.getList());
|
||||
|
@ -55,12 +55,15 @@ public class MyProofOfBurnListService implements PersistedDataHost, DaoSetupServ
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
MyProofOfBurnList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
myProofOfBurnList.setAll(persisted.getList());
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
myProofOfBurnList.setAll(persisted.getList());
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,13 +107,16 @@ public class MyProposalListService implements PersistedDataHost, DaoStateListene
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
MyProposalList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
myProposalList.setAll(persisted.getList());
|
||||
listeners.forEach(l -> l.onListChanged(getList()));
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
myProposalList.setAll(persisted.getList());
|
||||
listeners.forEach(l -> l.onListChanged(getList()));
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,9 +73,9 @@ public class TempProposalStorageService extends MapStoreService<TempProposalStor
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readFromResources(String postFix) {
|
||||
protected void readFromResources(String postFix, Runnable completeHandler) {
|
||||
// We do not have a resource file for that store, so we just call the readStore method instead.
|
||||
readStore();
|
||||
readStore(persisted -> completeHandler.run());
|
||||
}
|
||||
|
||||
|
||||
|
@ -133,8 +133,8 @@ public class VoteRevealService implements DaoStateListener, DaoSetupService {
|
||||
private byte[] getHashOfBlindVoteList() {
|
||||
List<BlindVote> blindVotes = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService);
|
||||
byte[] hashOfBlindVoteList = VoteRevealConsensus.getHashOfBlindVoteList(blindVotes);
|
||||
log.info("blindVoteList for creating hash: " + blindVotes);
|
||||
log.info("Sha256Ripemd160 hash of hashOfBlindVoteList " + Utilities.bytesAsHexString(hashOfBlindVoteList));
|
||||
log.debug("blindVoteList for creating hash: {}", blindVotes);
|
||||
log.info("Sha256Ripemd160 hash of hashOfBlindVoteList {}", Utilities.bytesAsHexString(hashOfBlindVoteList));
|
||||
return hashOfBlindVoteList;
|
||||
}
|
||||
|
||||
|
@ -209,7 +209,7 @@ public abstract class BsqNode implements DaoSetupService {
|
||||
parseBlockchainComplete = true;
|
||||
daoStateService.onParseBlockChainComplete();
|
||||
|
||||
exportJsonFilesService.onParseBlockChainComplete();
|
||||
maybeExportToJson();
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@ -291,7 +291,7 @@ public abstract class BsqNode implements DaoSetupService {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
protected void maybeExportNewBlockToJson(Block block) {
|
||||
exportJsonFilesService.onNewBlock(block);
|
||||
protected void maybeExportToJson() {
|
||||
exportJsonFilesService.maybeExportToJson();
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package bisq.core.dao.node.explorer;
|
||||
|
||||
import bisq.core.dao.DaoSetupService;
|
||||
import bisq.core.dao.state.DaoStateService;
|
||||
import bisq.core.dao.state.model.DaoState;
|
||||
import bisq.core.dao.state.model.blockchain.Block;
|
||||
import bisq.core.dao.state.model.blockchain.PubKeyScript;
|
||||
import bisq.core.dao.state.model.blockchain.Tx;
|
||||
@ -26,6 +27,7 @@ import bisq.core.dao.state.model.blockchain.TxOutput;
|
||||
import bisq.core.dao.state.model.blockchain.TxType;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.file.FileUtil;
|
||||
import bisq.common.file.JsonFileManager;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
@ -35,11 +37,18 @@ import com.google.inject.Inject;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@ -47,13 +56,17 @@ import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Slf4j
|
||||
public class ExportJsonFilesService implements DaoSetupService {
|
||||
private final DaoStateService daoStateService;
|
||||
private final File storageDir;
|
||||
private boolean dumpBlockchainData;
|
||||
private JsonFileManager blockFileManager, txFileManager, txOutputFileManager, bsqStateFileManager;
|
||||
private File blockDir;
|
||||
private final boolean dumpBlockchainData;
|
||||
|
||||
private final ListeningExecutorService executor = Utilities.getListeningExecutorService("JsonExporter",
|
||||
1, 1, 1200);
|
||||
private JsonFileManager txFileManager, txOutputFileManager, bsqStateFileManager;
|
||||
|
||||
@Inject
|
||||
public ExportJsonFilesService(DaoStateService daoStateService,
|
||||
@ -75,135 +88,88 @@ public class ExportJsonFilesService implements DaoSetupService {
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (!dumpBlockchainData) {
|
||||
return;
|
||||
if (dumpBlockchainData) {
|
||||
File jsonDir = new File(Paths.get(storageDir.getAbsolutePath(), "json").toString());
|
||||
File txDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "tx").toString());
|
||||
File txOutputDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "txo").toString());
|
||||
File bsqStateDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "all").toString());
|
||||
try {
|
||||
if (txDir.exists())
|
||||
FileUtil.deleteDirectory(txDir);
|
||||
if (txOutputDir.exists())
|
||||
FileUtil.deleteDirectory(txOutputDir);
|
||||
if (bsqStateDir.exists())
|
||||
FileUtil.deleteDirectory(bsqStateDir);
|
||||
if (jsonDir.exists())
|
||||
FileUtil.deleteDirectory(jsonDir);
|
||||
} catch (IOException e) {
|
||||
log.error(e.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (!jsonDir.mkdir())
|
||||
log.warn("make jsonDir failed.\njsonDir=" + jsonDir.getAbsolutePath());
|
||||
|
||||
if (!txDir.mkdir())
|
||||
log.warn("make txDir failed.\ntxDir=" + txDir.getAbsolutePath());
|
||||
|
||||
if (!txOutputDir.mkdir())
|
||||
log.warn("make txOutputDir failed.\ntxOutputDir=" + txOutputDir.getAbsolutePath());
|
||||
|
||||
if (!bsqStateDir.mkdir())
|
||||
log.warn("make bsqStateDir failed.\nbsqStateDir=" + bsqStateDir.getAbsolutePath());
|
||||
|
||||
txFileManager = new JsonFileManager(txDir);
|
||||
txOutputFileManager = new JsonFileManager(txOutputDir);
|
||||
bsqStateFileManager = new JsonFileManager(bsqStateDir);
|
||||
}
|
||||
|
||||
File jsonDir = new File(Paths.get(storageDir.getAbsolutePath(), "json").toString());
|
||||
blockDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "block").toString());
|
||||
File txDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "tx").toString());
|
||||
File txOutputDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "txo").toString());
|
||||
File bsqStateDir = new File(Paths.get(storageDir.getAbsolutePath(), "json", "all").toString());
|
||||
|
||||
if (!jsonDir.mkdir())
|
||||
log.warn("make jsonDir failed.\njsonDir=" + jsonDir.getAbsolutePath());
|
||||
|
||||
if (!blockDir.mkdir())
|
||||
log.warn("make blockDir failed.\njsonDir=" + blockDir.getAbsolutePath());
|
||||
|
||||
if (!txDir.mkdir())
|
||||
log.warn("make txDir failed.\ntxDir=" + txDir.getAbsolutePath());
|
||||
|
||||
if (!txOutputDir.mkdir())
|
||||
log.warn("make txOutputDir failed.\ntxOutputDir=" + txOutputDir.getAbsolutePath());
|
||||
|
||||
if (!bsqStateDir.mkdir())
|
||||
log.warn("make bsqStateDir failed.\nbsqStateDir=" + bsqStateDir.getAbsolutePath());
|
||||
|
||||
blockFileManager = new JsonFileManager(blockDir);
|
||||
txFileManager = new JsonFileManager(txDir);
|
||||
txOutputFileManager = new JsonFileManager(txOutputDir);
|
||||
bsqStateFileManager = new JsonFileManager(bsqStateDir);
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
if (!dumpBlockchainData) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockFileManager.shutDown();
|
||||
txFileManager.shutDown();
|
||||
txOutputFileManager.shutDown();
|
||||
bsqStateFileManager.shutDown();
|
||||
dumpBlockchainData = false;
|
||||
}
|
||||
|
||||
public void onNewBlock(Block block) {
|
||||
if (!dumpBlockchainData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We do write the block on the main thread as the overhead to create a thread and risk for inconsistency is not
|
||||
// worth the potential performance gain.
|
||||
processBlock(block, true);
|
||||
}
|
||||
|
||||
private void processBlock(Block block, boolean doDumpDaoState) {
|
||||
int lastPersistedBlock = getLastPersistedBlock();
|
||||
if (block.getHeight() <= lastPersistedBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
long ts = System.currentTimeMillis();
|
||||
JsonBlock jsonBlock = getJsonBlock(block);
|
||||
blockFileManager.writeToDisc(Utilities.objectToJson(jsonBlock), String.valueOf(jsonBlock.getHeight()));
|
||||
|
||||
jsonBlock.getTxs().forEach(jsonTx -> {
|
||||
txFileManager.writeToDisc(Utilities.objectToJson(jsonTx), jsonTx.getId());
|
||||
|
||||
jsonTx.getOutputs().forEach(jsonTxOutput ->
|
||||
txOutputFileManager.writeToDisc(Utilities.objectToJson(jsonTxOutput), jsonTxOutput.getId()));
|
||||
});
|
||||
|
||||
log.info("Write json data for block {} took {} ms", block.getHeight(), System.currentTimeMillis() - ts);
|
||||
|
||||
if (doDumpDaoState) {
|
||||
dumpDaoState();
|
||||
if (dumpBlockchainData && txFileManager != null) {
|
||||
txFileManager.shutDown();
|
||||
txOutputFileManager.shutDown();
|
||||
bsqStateFileManager.shutDown();
|
||||
}
|
||||
}
|
||||
|
||||
public void onParseBlockChainComplete() {
|
||||
if (!dumpBlockchainData) {
|
||||
return;
|
||||
}
|
||||
public void maybeExportToJson() {
|
||||
if (dumpBlockchainData &&
|
||||
daoStateService.isParseBlockChainComplete()) {
|
||||
// We store the data we need once we write the data to disk (in the thread) locally.
|
||||
// Access to daoStateService is single threaded, we must not access daoStateService from the thread.
|
||||
List<JsonTxOutput> allJsonTxOutputs = new ArrayList<>();
|
||||
|
||||
int lastPersistedBlock = getLastPersistedBlock();
|
||||
List<Block> blocks = daoStateService.getBlocksFromBlockHeight(lastPersistedBlock + 1, Integer.MAX_VALUE);
|
||||
List<JsonTx> jsonTxs = daoStateService.getUnorderedTxStream()
|
||||
.map(tx -> {
|
||||
JsonTx jsonTx = getJsonTx(tx);
|
||||
allJsonTxOutputs.addAll(jsonTx.getOutputs());
|
||||
return jsonTx;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// We use a thread here to write all past blocks to avoid that the main thread gets blocked for too long.
|
||||
new Thread(() -> {
|
||||
Thread.currentThread().setName("Write all blocks to json");
|
||||
blocks.forEach(e -> processBlock(e, false));
|
||||
}).start();
|
||||
|
||||
dumpDaoState();
|
||||
}
|
||||
|
||||
private void dumpDaoState() {
|
||||
// TODO we should get rid of that data structure and use the individual jsonBlocks instead as we cannot cache data
|
||||
// here and re-write each time the full blockchain which is already > 200 MB
|
||||
// Once the webapp has impl the changes we can delete that here.
|
||||
long ts = System.currentTimeMillis();
|
||||
List<JsonBlock> jsonBlockList = daoStateService.getBlocks().stream()
|
||||
.map(this::getJsonBlock)
|
||||
.collect(Collectors.toList());
|
||||
JsonBlocks jsonBlocks = new JsonBlocks(daoStateService.getChainHeight(), jsonBlockList);
|
||||
|
||||
// We use here the thread write method as the data is quite large and write can take a bit
|
||||
bsqStateFileManager.writeToDiscThreaded(Utilities.objectToJson(jsonBlocks), "blocks");
|
||||
log.info("Dumping full bsqState with {} blocks took {} ms",
|
||||
jsonBlocks.getBlocks().size(), System.currentTimeMillis() - ts);
|
||||
}
|
||||
|
||||
private int getLastPersistedBlock() {
|
||||
// At start we use one block before genesis
|
||||
int result = daoStateService.getGenesisBlockHeight() - 1;
|
||||
String[] list = blockDir.list();
|
||||
if (list != null && list.length > 0) {
|
||||
List<Integer> blocks = Arrays.stream(list)
|
||||
.filter(e -> !e.endsWith(".tmp"))
|
||||
.map(e -> e.replace(".json", ""))
|
||||
.map(Integer::valueOf)
|
||||
.sorted()
|
||||
DaoState daoState = daoStateService.getClone();
|
||||
List<JsonBlock> jsonBlockList = daoState.getBlocks().stream()
|
||||
.map(this::getJsonBlock)
|
||||
.collect(Collectors.toList());
|
||||
if (!blocks.isEmpty()) {
|
||||
Integer lastBlockHeight = blocks.get(blocks.size() - 1);
|
||||
if (lastBlockHeight > result) {
|
||||
result = lastBlockHeight;
|
||||
JsonBlocks jsonBlocks = new JsonBlocks(daoState.getChainHeight(), jsonBlockList);
|
||||
|
||||
ListenableFuture<Void> future = executor.submit(() -> {
|
||||
bsqStateFileManager.writeToDisc(Utilities.objectToJson(jsonBlocks), "blocks");
|
||||
allJsonTxOutputs.forEach(jsonTxOutput -> txOutputFileManager.writeToDisc(Utilities.objectToJson(jsonTxOutput), jsonTxOutput.getId()));
|
||||
jsonTxs.forEach(jsonTx -> txFileManager.writeToDisc(Utilities.objectToJson(jsonTx), jsonTx.getId()));
|
||||
return null;
|
||||
});
|
||||
|
||||
Futures.addCallback(future, new FutureCallback<>() {
|
||||
public void onSuccess(Void ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.error(throwable.toString());
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonBlock getJsonBlock(Block block) {
|
||||
|
@ -168,7 +168,7 @@ public class FullNode extends BsqNode {
|
||||
}
|
||||
|
||||
private void onNewBlock(Block block) {
|
||||
maybeExportNewBlockToJson(block);
|
||||
maybeExportToJson();
|
||||
|
||||
if (p2pNetworkReady && parseBlockchainComplete)
|
||||
fullNodeNetworkService.publishNewBlock(block);
|
||||
|
@ -28,7 +28,6 @@ import bisq.core.dao.node.parser.BlockParser;
|
||||
import bisq.core.dao.node.parser.exceptions.RequiredReorgFromSnapshotException;
|
||||
import bisq.core.dao.state.DaoStateService;
|
||||
import bisq.core.dao.state.DaoStateSnapshotService;
|
||||
import bisq.core.dao.state.model.blockchain.Block;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.network.Connection;
|
||||
@ -40,7 +39,6 @@ import com.google.inject.Inject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@ -229,18 +227,19 @@ public class LiteNode extends BsqNode {
|
||||
}
|
||||
|
||||
// We received a new block
|
||||
private void onNewBlockReceived(RawBlock rawBlock) {
|
||||
int blockHeight = rawBlock.getHeight();
|
||||
log.debug("onNewBlockReceived: block at height {}, hash={}", blockHeight, rawBlock.getHash());
|
||||
private void onNewBlockReceived(RawBlock block) {
|
||||
int blockHeight = block.getHeight();
|
||||
log.debug("onNewBlockReceived: block at height {}, hash={}", blockHeight, block.getHash());
|
||||
|
||||
// We only update chainTipHeight if we get a newer block
|
||||
if (blockHeight > chainTipHeight)
|
||||
chainTipHeight = blockHeight;
|
||||
|
||||
try {
|
||||
Optional<Block> optionalBlock = doParseBlock(rawBlock);
|
||||
optionalBlock.ifPresent(this::maybeExportNewBlockToJson);
|
||||
doParseBlock(block);
|
||||
} catch (RequiredReorgFromSnapshotException ignore) {
|
||||
}
|
||||
|
||||
maybeExportToJson();
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.util.CollectionUtils;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
|
@ -27,7 +27,6 @@ import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.util.CollectionUtils;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
|
@ -55,12 +55,15 @@ public class UnconfirmedBsqChangeOutputListService implements PersistedDataHost
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
public void readPersisted(Runnable completeHandler) {
|
||||
if (DevEnv.isDaoActivated()) {
|
||||
UnconfirmedBsqChangeOutputList persisted = persistenceManager.getPersisted();
|
||||
if (persisted != null) {
|
||||
unconfirmedBsqChangeOutputList.setAll(persisted.getList());
|
||||
}
|
||||
persistenceManager.readPersisted(persisted -> {
|
||||
unconfirmedBsqChangeOutputList.setAll(persisted.getList());
|
||||
completeHandler.run();
|
||||
},
|
||||
completeHandler);
|
||||
} else {
|
||||
completeHandler.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ import javax.annotation.Nullable;
|
||||
public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
private final List<String> bannedOfferIds;
|
||||
private final List<String> bannedNodeAddress;
|
||||
private final List<String> bannedAutoConfExplorers;
|
||||
private final List<PaymentAccountFilter> bannedPaymentAccounts;
|
||||
private final List<String> bannedCurrencies;
|
||||
private final List<String> bannedPaymentMethods;
|
||||
@ -115,7 +116,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
signatureAsBase64,
|
||||
filter.getSignerPubKeyAsHex(),
|
||||
filter.getBannedPrivilegedDevPubKeys(),
|
||||
filter.isDisableAutoConf());
|
||||
filter.isDisableAutoConf(),
|
||||
filter.getBannedAutoConfExplorers());
|
||||
}
|
||||
|
||||
// Used for signature verification as we created the sig without the signatureAsBase64 field we set it to null again
|
||||
@ -143,7 +145,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
null,
|
||||
filter.getSignerPubKeyAsHex(),
|
||||
filter.getBannedPrivilegedDevPubKeys(),
|
||||
filter.isDisableAutoConf());
|
||||
filter.isDisableAutoConf(),
|
||||
filter.getBannedAutoConfExplorers());
|
||||
}
|
||||
|
||||
public Filter(List<String> bannedOfferIds,
|
||||
@ -166,7 +169,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
PublicKey ownerPubKey,
|
||||
String signerPubKeyAsHex,
|
||||
List<String> bannedPrivilegedDevPubKeys,
|
||||
boolean disableAutoConf) {
|
||||
boolean disableAutoConf,
|
||||
List<String> bannedAutoConfExplorers) {
|
||||
this(bannedOfferIds,
|
||||
bannedNodeAddress,
|
||||
bannedPaymentAccounts,
|
||||
@ -190,7 +194,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
null,
|
||||
signerPubKeyAsHex,
|
||||
bannedPrivilegedDevPubKeys,
|
||||
disableAutoConf);
|
||||
disableAutoConf,
|
||||
bannedAutoConfExplorers);
|
||||
}
|
||||
|
||||
|
||||
@ -222,7 +227,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
@Nullable String signatureAsBase64,
|
||||
String signerPubKeyAsHex,
|
||||
List<String> bannedPrivilegedDevPubKeys,
|
||||
boolean disableAutoConf) {
|
||||
boolean disableAutoConf,
|
||||
List<String> bannedAutoConfExplorers) {
|
||||
this.bannedOfferIds = bannedOfferIds;
|
||||
this.bannedNodeAddress = bannedNodeAddress;
|
||||
this.bannedPaymentAccounts = bannedPaymentAccounts;
|
||||
@ -247,6 +253,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
this.signerPubKeyAsHex = signerPubKeyAsHex;
|
||||
this.bannedPrivilegedDevPubKeys = bannedPrivilegedDevPubKeys;
|
||||
this.disableAutoConf = disableAutoConf;
|
||||
this.bannedAutoConfExplorers = bannedAutoConfExplorers;
|
||||
|
||||
// ownerPubKeyBytes can be null when called from tests
|
||||
if (ownerPubKeyBytes != null) {
|
||||
@ -283,7 +290,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
.setSignerPubKeyAsHex(signerPubKeyAsHex)
|
||||
.setCreationDate(creationDate)
|
||||
.addAllBannedPrivilegedDevPubKeys(bannedPrivilegedDevPubKeys)
|
||||
.setDisableAutoConf(disableAutoConf);
|
||||
.setDisableAutoConf(disableAutoConf)
|
||||
.addAllBannedAutoConfExplorers(bannedAutoConfExplorers);
|
||||
|
||||
Optional.ofNullable(signatureAsBase64).ifPresent(builder::setSignatureAsBase64);
|
||||
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
||||
@ -320,7 +328,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
proto.getSignatureAsBase64(),
|
||||
proto.getSignerPubKeyAsHex(),
|
||||
ProtoUtil.protocolStringListToList(proto.getBannedPrivilegedDevPubKeysList()),
|
||||
proto.getDisableAutoConf()
|
||||
proto.getDisableAutoConf(),
|
||||
ProtoUtil.protocolStringListToList(proto.getBannedAutoConfExplorersList())
|
||||
);
|
||||
}
|
||||
|
||||
@ -339,6 +348,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
return "Filter{" +
|
||||
"\n bannedOfferIds=" + bannedOfferIds +
|
||||
",\n bannedNodeAddress=" + bannedNodeAddress +
|
||||
",\n bannedAutoConfExplorers=" + bannedAutoConfExplorers +
|
||||
",\n bannedPaymentAccounts=" + bannedPaymentAccounts +
|
||||
",\n bannedCurrencies=" + bannedCurrencies +
|
||||
",\n bannedPaymentMethods=" + bannedPaymentMethods +
|
||||
@ -356,10 +366,11 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
",\n mediators=" + mediators +
|
||||
",\n refundAgents=" + refundAgents +
|
||||
",\n bannedAccountWitnessSignerPubKeys=" + bannedAccountWitnessSignerPubKeys +
|
||||
",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys +
|
||||
",\n btcFeeReceiverAddresses=" + btcFeeReceiverAddresses +
|
||||
",\n creationDate=" + creationDate +
|
||||
",\n bannedPrivilegedDevPubKeys=" + bannedPrivilegedDevPubKeys +
|
||||
",\n extraDataMap=" + extraDataMap +
|
||||
",\n ownerPubKey=" + ownerPubKey +
|
||||
",\n disableAutoConf=" + disableAutoConf +
|
||||
"\n}";
|
||||
}
|
||||
|
@ -216,13 +216,13 @@ public class FilterManager {
|
||||
addListener(filter -> {
|
||||
if (filter != null && filterWarningHandler != null) {
|
||||
if (filter.getSeedNodes() != null && !filter.getSeedNodes().isEmpty()) {
|
||||
log.info(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.seed")));
|
||||
log.info("One of the seed nodes got banned. {}", filter.getSeedNodes());
|
||||
// Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users.
|
||||
// filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.seed")));
|
||||
}
|
||||
|
||||
if (filter.getPriceRelayNodes() != null && !filter.getPriceRelayNodes().isEmpty()) {
|
||||
log.info(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.priceRelay")));
|
||||
log.info("One of the price relay nodes got banned. {}", filter.getPriceRelayNodes());
|
||||
// Let's keep that more silent. Might be used in case a node is unstable and we don't want to confuse users.
|
||||
// filterWarningHandler.accept(Res.get("popup.warning.nodeBanned", Res.get("popup.warning.priceRelay")));
|
||||
}
|
||||
@ -398,6 +398,12 @@ public class FilterManager {
|
||||
.anyMatch(e -> e.equals(nodeAddress.getFullAddress()));
|
||||
}
|
||||
|
||||
public boolean isAutoConfExplorerBanned(String address) {
|
||||
return getFilter() != null &&
|
||||
getFilter().getBannedAutoConfExplorers().stream()
|
||||
.anyMatch(e -> e.equals(address));
|
||||
}
|
||||
|
||||
public boolean requireUpdateToNewVersionForTrading() {
|
||||
if (getFilter() == null) {
|
||||
return false;
|
||||
@ -460,7 +466,7 @@ public class FilterManager {
|
||||
Filter currentFilter = getFilter();
|
||||
|
||||
if (!isFilterPublicKeyInList(newFilter)) {
|
||||
log.warn("isFilterPublicKeyInList failed. Filter={}", newFilter);
|
||||
log.warn("isFilterPublicKeyInList failed. Filter.getSignerPubKeyAsHex={}", newFilter.getSignerPubKeyAsHex());
|
||||
return;
|
||||
}
|
||||
if (!isSignatureValid(newFilter)) {
|
||||
|
@ -267,6 +267,76 @@ public class CurrencyUtil {
|
||||
return currencies;
|
||||
}
|
||||
|
||||
// https://github.com/bisq-network/proposals/issues/243
|
||||
public static List<TradeCurrency> getAllTransferwiseCurrencies() {
|
||||
ArrayList<TradeCurrency> currencies = new ArrayList<>(Arrays.asList(
|
||||
new FiatCurrency("ARS"),
|
||||
new FiatCurrency("AUD"),
|
||||
new FiatCurrency("XOF"),
|
||||
new FiatCurrency("BGN"),
|
||||
new FiatCurrency("CAD"),
|
||||
new FiatCurrency("CLP"),
|
||||
new FiatCurrency("HRK"),
|
||||
new FiatCurrency("CZK"),
|
||||
new FiatCurrency("DKK"),
|
||||
new FiatCurrency("EGP"),
|
||||
new FiatCurrency("EUR"),
|
||||
new FiatCurrency("GEL"),
|
||||
new FiatCurrency("HKD"),
|
||||
new FiatCurrency("HUF"),
|
||||
new FiatCurrency("IDR"),
|
||||
new FiatCurrency("ILS"),
|
||||
new FiatCurrency("JPY"),
|
||||
new FiatCurrency("KES"),
|
||||
new FiatCurrency("MYR"),
|
||||
new FiatCurrency("MXN"),
|
||||
new FiatCurrency("MAD"),
|
||||
new FiatCurrency("NPR"),
|
||||
new FiatCurrency("NZD"),
|
||||
new FiatCurrency("NGN"),
|
||||
new FiatCurrency("NOK"),
|
||||
new FiatCurrency("PKR"),
|
||||
new FiatCurrency("PEN"),
|
||||
new FiatCurrency("PHP"),
|
||||
new FiatCurrency("PLN"),
|
||||
new FiatCurrency("RON"),
|
||||
new FiatCurrency("RUB"),
|
||||
new FiatCurrency("SGD"),
|
||||
new FiatCurrency("ZAR"),
|
||||
new FiatCurrency("KRW"),
|
||||
new FiatCurrency("SEK"),
|
||||
new FiatCurrency("CHF"),
|
||||
new FiatCurrency("THB"),
|
||||
new FiatCurrency("TRY"),
|
||||
new FiatCurrency("UGX"),
|
||||
new FiatCurrency("AED"),
|
||||
new FiatCurrency("GBP"),
|
||||
new FiatCurrency("VND"),
|
||||
new FiatCurrency("ZMW")
|
||||
));
|
||||
|
||||
currencies.sort(Comparator.comparing(TradeCurrency::getCode));
|
||||
return currencies;
|
||||
}
|
||||
|
||||
public static List<TradeCurrency> getAllAmazonGiftCardCurrencies() {
|
||||
List<TradeCurrency> currencies = new ArrayList<>(Arrays.asList(
|
||||
new FiatCurrency("AUD"),
|
||||
new FiatCurrency("CAD"),
|
||||
new FiatCurrency("EUR"),
|
||||
new FiatCurrency("GBP"),
|
||||
new FiatCurrency("INR"),
|
||||
new FiatCurrency("JPY"),
|
||||
new FiatCurrency("SAR"),
|
||||
new FiatCurrency("SEK"),
|
||||
new FiatCurrency("SGD"),
|
||||
new FiatCurrency("TRY"),
|
||||
new FiatCurrency("USD")
|
||||
));
|
||||
currencies.sort(Comparator.comparing(TradeCurrency::getCode));
|
||||
return currencies;
|
||||
}
|
||||
|
||||
// https://www.revolut.com/help/getting-started/exchanging-currencies/what-fiat-currencies-are-supported-for-holding-and-exchange
|
||||
public static List<TradeCurrency> getAllRevolutCurrencies() {
|
||||
ArrayList<TradeCurrency> currencies = new ArrayList<>(Arrays.asList(
|
||||
@ -543,4 +613,8 @@ public class CurrencyUtil {
|
||||
else
|
||||
return Res.get(translationKey, currencyCode, Res.getBaseCurrencyCode());
|
||||
}
|
||||
|
||||
public static String getOfferVolumeCode(String currencyCode) {
|
||||
return Res.get("shared.offerVolumeCode", currencyCode);
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,9 @@ public class LanguageUtil {
|
||||
"vi", // Vietnamese
|
||||
"th", // Thai
|
||||
"ja", // Japanese
|
||||
"fa" // Persian
|
||||
"fa", // Persian
|
||||
"it", // Italian
|
||||
"cs" // Czech
|
||||
/*
|
||||
// not translated yet
|
||||
"el", // Greek
|
||||
@ -49,7 +51,6 @@ public class LanguageUtil {
|
||||
"hu", // Hungarian
|
||||
"ro", // Romanian
|
||||
"tr" // Turkish
|
||||
"it", // Italian
|
||||
"iw", // Hebrew
|
||||
"hi", // Hindi
|
||||
"ko", // Korean
|
||||
@ -77,7 +78,6 @@ public class LanguageUtil {
|
||||
"ms", // Malay
|
||||
"is", // Icelandic
|
||||
"et", // Estonian
|
||||
"cs", // Czech
|
||||
"ar", // Arabic
|
||||
"vi", // Vietnamese
|
||||
"th", // Thai
|
||||
|
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.network.p2p.inventory;
|
||||
|
||||
import bisq.core.dao.monitoring.BlindVoteStateMonitoringService;
|
||||
import bisq.core.dao.monitoring.DaoStateMonitoringService;
|
||||
import bisq.core.dao.monitoring.ProposalStateMonitoringService;
|
||||
import bisq.core.dao.monitoring.model.BlindVoteStateBlock;
|
||||
import bisq.core.dao.monitoring.model.DaoStateBlock;
|
||||
import bisq.core.dao.monitoring.model.ProposalStateBlock;
|
||||
import bisq.core.dao.state.DaoStateService;
|
||||
import bisq.core.filter.Filter;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.network.p2p.inventory.messages.GetInventoryRequest;
|
||||
import bisq.core.network.p2p.inventory.messages.GetInventoryResponse;
|
||||
import bisq.core.network.p2p.inventory.model.InventoryItem;
|
||||
import bisq.core.network.p2p.inventory.model.RequestInfo;
|
||||
|
||||
import bisq.network.p2p.network.Connection;
|
||||
import bisq.network.p2p.network.MessageListener;
|
||||
import bisq.network.p2p.network.NetworkNode;
|
||||
import bisq.network.p2p.network.Statistic;
|
||||
import bisq.network.p2p.peers.PeerManager;
|
||||
import bisq.network.p2p.storage.P2PDataStorage;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
import bisq.common.util.Profiler;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import com.google.common.base.Enums;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class GetInventoryRequestHandler implements MessageListener {
|
||||
private final NetworkNode networkNode;
|
||||
private final PeerManager peerManager;
|
||||
private final P2PDataStorage p2PDataStorage;
|
||||
private final DaoStateService daoStateService;
|
||||
private final DaoStateMonitoringService daoStateMonitoringService;
|
||||
private final ProposalStateMonitoringService proposalStateMonitoringService;
|
||||
private final BlindVoteStateMonitoringService blindVoteStateMonitoringService;
|
||||
private final FilterManager filterManager;
|
||||
private final int maxConnections;
|
||||
|
||||
@Inject
|
||||
public GetInventoryRequestHandler(NetworkNode networkNode,
|
||||
PeerManager peerManager,
|
||||
P2PDataStorage p2PDataStorage,
|
||||
DaoStateService daoStateService,
|
||||
DaoStateMonitoringService daoStateMonitoringService,
|
||||
ProposalStateMonitoringService proposalStateMonitoringService,
|
||||
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
|
||||
FilterManager filterManager,
|
||||
@Named(Config.MAX_CONNECTIONS) int maxConnections) {
|
||||
this.networkNode = networkNode;
|
||||
this.peerManager = peerManager;
|
||||
this.p2PDataStorage = p2PDataStorage;
|
||||
this.daoStateService = daoStateService;
|
||||
this.daoStateMonitoringService = daoStateMonitoringService;
|
||||
this.proposalStateMonitoringService = proposalStateMonitoringService;
|
||||
this.blindVoteStateMonitoringService = blindVoteStateMonitoringService;
|
||||
this.filterManager = filterManager;
|
||||
this.maxConnections = maxConnections;
|
||||
|
||||
this.networkNode.addMessageListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||
if (networkEnvelope instanceof GetInventoryRequest) {
|
||||
// Data
|
||||
GetInventoryRequest getInventoryRequest = (GetInventoryRequest) networkEnvelope;
|
||||
Map<InventoryItem, Integer> dataObjects = new HashMap<>();
|
||||
p2PDataStorage.getMapForDataResponse(getInventoryRequest.getVersion()).values().stream()
|
||||
.map(e -> e.getClass().getSimpleName())
|
||||
.forEach(className -> addClassNameToMap(dataObjects, className));
|
||||
p2PDataStorage.getMap().values().stream()
|
||||
.map(ProtectedStorageEntry::getProtectedStoragePayload)
|
||||
.map(e -> e.getClass().getSimpleName())
|
||||
.forEach(className -> addClassNameToMap(dataObjects, className));
|
||||
Map<InventoryItem, String> inventory = new HashMap<>();
|
||||
dataObjects.forEach((key, value) -> inventory.put(key, String.valueOf(value)));
|
||||
|
||||
// DAO
|
||||
int numBsqBlocks = daoStateService.getBlocks().size();
|
||||
inventory.put(InventoryItem.numBsqBlocks, String.valueOf(numBsqBlocks));
|
||||
|
||||
int daoStateChainHeight = daoStateService.getChainHeight();
|
||||
inventory.put(InventoryItem.daoStateChainHeight, String.valueOf(daoStateChainHeight));
|
||||
|
||||
LinkedList<DaoStateBlock> daoStateBlockChain = daoStateMonitoringService.getDaoStateBlockChain();
|
||||
if (!daoStateBlockChain.isEmpty()) {
|
||||
String daoStateHash = Utilities.bytesAsHexString(daoStateBlockChain.getLast().getMyStateHash().getHash());
|
||||
inventory.put(InventoryItem.daoStateHash, daoStateHash);
|
||||
}
|
||||
|
||||
LinkedList<ProposalStateBlock> proposalStateBlockChain = proposalStateMonitoringService.getProposalStateBlockChain();
|
||||
if (!proposalStateBlockChain.isEmpty()) {
|
||||
String proposalHash = Utilities.bytesAsHexString(proposalStateBlockChain.getLast().getMyStateHash().getHash());
|
||||
inventory.put(InventoryItem.proposalHash, proposalHash);
|
||||
}
|
||||
|
||||
LinkedList<BlindVoteStateBlock> blindVoteStateBlockChain = blindVoteStateMonitoringService.getBlindVoteStateBlockChain();
|
||||
if (!blindVoteStateBlockChain.isEmpty()) {
|
||||
String blindVoteHash = Utilities.bytesAsHexString(blindVoteStateBlockChain.getLast().getMyStateHash().getHash());
|
||||
inventory.put(InventoryItem.blindVoteHash, blindVoteHash);
|
||||
}
|
||||
|
||||
// network
|
||||
inventory.put(InventoryItem.maxConnections, String.valueOf(maxConnections));
|
||||
inventory.put(InventoryItem.numConnections, String.valueOf(networkNode.getAllConnections().size()));
|
||||
inventory.put(InventoryItem.peakNumConnections, String.valueOf(peerManager.getPeakNumConnections()));
|
||||
inventory.put(InventoryItem.numAllConnectionsLostEvents, String.valueOf(peerManager.getNumAllConnectionsLostEvents()));
|
||||
peerManager.maybeResetNumAllConnectionsLostEvents();
|
||||
inventory.put(InventoryItem.sentBytes, String.valueOf(Statistic.totalSentBytesProperty().get()));
|
||||
inventory.put(InventoryItem.sentBytesPerSec, String.valueOf(Statistic.totalSentBytesPerSecProperty().get()));
|
||||
inventory.put(InventoryItem.receivedBytes, String.valueOf(Statistic.totalReceivedBytesProperty().get()));
|
||||
inventory.put(InventoryItem.receivedBytesPerSec, String.valueOf(Statistic.totalReceivedBytesPerSecProperty().get()));
|
||||
inventory.put(InventoryItem.receivedMessagesPerSec, String.valueOf(Statistic.numTotalReceivedMessagesPerSecProperty().get()));
|
||||
inventory.put(InventoryItem.sentMessagesPerSec, String.valueOf(Statistic.numTotalSentMessagesPerSecProperty().get()));
|
||||
|
||||
// node
|
||||
inventory.put(InventoryItem.version, Version.VERSION);
|
||||
inventory.put(InventoryItem.commitHash, RequestInfo.COMMIT_HASH);
|
||||
inventory.put(InventoryItem.usedMemory, String.valueOf(Profiler.getUsedMemoryInBytes()));
|
||||
inventory.put(InventoryItem.jvmStartTime, String.valueOf(ManagementFactory.getRuntimeMXBean().getStartTime()));
|
||||
|
||||
Filter filter = filterManager.getFilter();
|
||||
if (filter != null) {
|
||||
inventory.put(InventoryItem.filteredSeeds, Joiner.on("," + System.getProperty("line.separator")).join(filter.getSeedNodes()));
|
||||
}
|
||||
|
||||
log.info("Send inventory {} to {}", inventory, connection.getPeersNodeAddressOptional());
|
||||
GetInventoryResponse getInventoryResponse = new GetInventoryResponse(inventory);
|
||||
networkNode.sendMessage(connection, getInventoryResponse);
|
||||
}
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
networkNode.removeMessageListener(this);
|
||||
}
|
||||
|
||||
private void addClassNameToMap(Map<InventoryItem, Integer> dataObjects, String className) {
|
||||
Optional<InventoryItem> optionalEnum = Enums.getIfPresent(InventoryItem.class, className);
|
||||
if (optionalEnum.isPresent()) {
|
||||
InventoryItem key = optionalEnum.get();
|
||||
dataObjects.putIfAbsent(key, 0);
|
||||
int prev = dataObjects.get(key);
|
||||
dataObjects.put(key, prev + 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,7 +15,9 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.network.p2p.inventory;
|
||||
package bisq.core.network.p2p.inventory;
|
||||
|
||||
import bisq.core.network.p2p.inventory.model.InventoryItem;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.network.NetworkNode;
|
||||
@ -41,11 +43,12 @@ public class GetInventoryRequestManager {
|
||||
}
|
||||
|
||||
public void request(NodeAddress nodeAddress,
|
||||
Consumer<Map<String, Integer>> resultHandler,
|
||||
Consumer<Map<InventoryItem, String>> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
if (requesterMap.containsKey(nodeAddress)) {
|
||||
log.warn("There is still an open request pending for {}", nodeAddress.getFullAddress());
|
||||
return;
|
||||
log.warn("There was still a pending request for {}. We shut it down and make a new request",
|
||||
nodeAddress.getFullAddress());
|
||||
requesterMap.get(nodeAddress).shutDown();
|
||||
}
|
||||
|
||||
GetInventoryRequester getInventoryRequester = new GetInventoryRequester(networkNode,
|
@ -15,12 +15,16 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.network.p2p.inventory;
|
||||
package bisq.core.network.p2p.inventory;
|
||||
|
||||
import bisq.core.network.p2p.inventory.messages.GetInventoryRequest;
|
||||
import bisq.core.network.p2p.inventory.messages.GetInventoryResponse;
|
||||
import bisq.core.network.p2p.inventory.model.InventoryItem;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.inventory.messages.GetInventoryRequest;
|
||||
import bisq.network.p2p.inventory.messages.GetInventoryResponse;
|
||||
import bisq.network.p2p.network.CloseConnectionReason;
|
||||
import bisq.network.p2p.network.Connection;
|
||||
import bisq.network.p2p.network.ConnectionListener;
|
||||
import bisq.network.p2p.network.MessageListener;
|
||||
import bisq.network.p2p.network.NetworkNode;
|
||||
|
||||
@ -36,18 +40,18 @@ import java.util.function.Consumer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class GetInventoryRequester implements MessageListener {
|
||||
public class GetInventoryRequester implements MessageListener, ConnectionListener {
|
||||
private final static int TIMEOUT_SEC = 90;
|
||||
|
||||
private final NetworkNode networkNode;
|
||||
private final NodeAddress nodeAddress;
|
||||
private final Consumer<Map<String, Integer>> resultHandler;
|
||||
private final Consumer<Map<InventoryItem, String>> resultHandler;
|
||||
private final ErrorMessageHandler errorMessageHandler;
|
||||
private Timer timer;
|
||||
|
||||
public GetInventoryRequester(NetworkNode networkNode,
|
||||
NodeAddress nodeAddress,
|
||||
Consumer<Map<String, Integer>> resultHandler,
|
||||
Consumer<Map<InventoryItem, String>> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
this.networkNode = networkNode;
|
||||
this.nodeAddress = nodeAddress;
|
||||
@ -57,12 +61,16 @@ public class GetInventoryRequester implements MessageListener {
|
||||
|
||||
public void request() {
|
||||
networkNode.addMessageListener(this);
|
||||
networkNode.addConnectionListener(this);
|
||||
|
||||
timer = UserThread.runAfter(this::onTimeOut, TIMEOUT_SEC);
|
||||
networkNode.sendMessage(nodeAddress, new GetInventoryRequest(Version.VERSION));
|
||||
|
||||
GetInventoryRequest getInventoryRequest = new GetInventoryRequest(Version.VERSION);
|
||||
networkNode.sendMessage(nodeAddress, getInventoryRequest);
|
||||
}
|
||||
|
||||
private void onTimeOut() {
|
||||
errorMessageHandler.handleErrorMessage("Timeout got triggered (" + TIMEOUT_SEC + " sec)");
|
||||
errorMessageHandler.handleErrorMessage("Request timeout");
|
||||
shutDown();
|
||||
}
|
||||
|
||||
@ -72,8 +80,11 @@ public class GetInventoryRequester implements MessageListener {
|
||||
connection.getPeersNodeAddressOptional().ifPresent(peer -> {
|
||||
if (peer.equals(nodeAddress)) {
|
||||
GetInventoryResponse getInventoryResponse = (GetInventoryResponse) networkEnvelope;
|
||||
resultHandler.accept(getInventoryResponse.getNumPayloadsMap());
|
||||
resultHandler.accept(getInventoryResponse.getInventory());
|
||||
shutDown();
|
||||
|
||||
// We shut down our connection after work as our node is not helpful for the network.
|
||||
UserThread.runAfter(() -> connection.shutDown(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER), 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -85,5 +96,27 @@ public class GetInventoryRequester implements MessageListener {
|
||||
timer = null;
|
||||
}
|
||||
networkNode.removeMessageListener(this);
|
||||
networkNode.removeConnectionListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(CloseConnectionReason closeConnectionReason,
|
||||
Connection connection) {
|
||||
connection.getPeersNodeAddressOptional().ifPresent(address -> {
|
||||
if (address.equals(nodeAddress)) {
|
||||
if (!closeConnectionReason.isIntended) {
|
||||
errorMessageHandler.handleErrorMessage("Connected closed because of " + closeConnectionReason.name());
|
||||
}
|
||||
shutDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.network.p2p.inventory.messages;
|
||||
package bisq.core.network.p2p.inventory.messages;
|
||||
|
||||
|
||||
import bisq.common.app.Version;
|
@ -15,42 +15,60 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.network.p2p.inventory.messages;
|
||||
package bisq.core.network.p2p.inventory.messages;
|
||||
|
||||
import bisq.core.network.p2p.inventory.model.InventoryItem;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
|
||||
import com.google.common.base.Enums;
|
||||
import com.google.common.base.Optional;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class GetInventoryResponse extends NetworkEnvelope {
|
||||
private final Map<String, Integer> numPayloadsMap;
|
||||
private final Map<InventoryItem, String> inventory;
|
||||
|
||||
public GetInventoryResponse(Map<String, Integer> numPayloadsMap) {
|
||||
this(numPayloadsMap, Version.getP2PMessageVersion());
|
||||
public GetInventoryResponse(Map<InventoryItem, String> inventory) {
|
||||
this(inventory, Version.getP2PMessageVersion());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private GetInventoryResponse(Map<String, Integer> numPayloadsMap, int messageVersion) {
|
||||
private GetInventoryResponse(Map<InventoryItem, String> inventory, int messageVersion) {
|
||||
super(messageVersion);
|
||||
|
||||
this.numPayloadsMap = numPayloadsMap;
|
||||
this.inventory = inventory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
// For protobuf we use a map with a string key
|
||||
Map<String, String> map = new HashMap<>();
|
||||
inventory.forEach((key, value) -> map.put(key.getKey(), value));
|
||||
return getNetworkEnvelopeBuilder()
|
||||
.setGetInventoryResponse(protobuf.GetInventoryResponse.newBuilder()
|
||||
.putAllNumPayloadsMap(numPayloadsMap))
|
||||
.putAllInventory(map))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static GetInventoryResponse fromProto(protobuf.GetInventoryResponse proto, int messageVersion) {
|
||||
return new GetInventoryResponse(proto.getNumPayloadsMapMap(), messageVersion);
|
||||
// For protobuf we use a map with a string key
|
||||
Map<String, String> map = proto.getInventoryMap();
|
||||
Map<InventoryItem, String> inventory = new HashMap<>();
|
||||
map.forEach((key, value) -> {
|
||||
Optional<InventoryItem> optional = Enums.getIfPresent(InventoryItem.class, key);
|
||||
if (optional.isPresent()) {
|
||||
inventory.put(optional.get(), value);
|
||||
}
|
||||
});
|
||||
return new GetInventoryResponse(inventory, messageVersion);
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.network.p2p.inventory.model;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public class Average {
|
||||
public static Map<InventoryItem, Double> of(Set<RequestInfo> requestInfoSet) {
|
||||
Map<InventoryItem, Double> averageValuesPerItem = new HashMap<>();
|
||||
Arrays.asList(InventoryItem.values()).forEach(inventoryItem -> {
|
||||
if (inventoryItem.isNumberValue()) {
|
||||
averageValuesPerItem.put(inventoryItem, getAverage(requestInfoSet, inventoryItem));
|
||||
}
|
||||
});
|
||||
return averageValuesPerItem;
|
||||
}
|
||||
|
||||
public static double getAverage(Set<RequestInfo> requestInfoSet, InventoryItem inventoryItem) {
|
||||
return requestInfoSet.stream()
|
||||
.map(RequestInfo::getDataMap)
|
||||
.filter(map -> map.containsKey(inventoryItem))
|
||||
.map(map -> map.get(inventoryItem).getValue())
|
||||
.filter(Objects::nonNull)
|
||||
.mapToDouble(Double::parseDouble)
|
||||
.average()
|
||||
.orElse(0d);
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.network.p2p.inventory.model;
|
||||
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class DeviationByIntegerDiff implements DeviationType {
|
||||
private final int warnTrigger;
|
||||
private final int alertTrigger;
|
||||
|
||||
public DeviationByIntegerDiff(int warnTrigger, int alertTrigger) {
|
||||
this.warnTrigger = warnTrigger;
|
||||
this.alertTrigger = alertTrigger;
|
||||
}
|
||||
|
||||
public DeviationSeverity getDeviationSeverity(Collection<List<RequestInfo>> collection,
|
||||
@Nullable String value,
|
||||
InventoryItem inventoryItem) {
|
||||
DeviationSeverity deviationSeverity = DeviationSeverity.OK;
|
||||
if (value == null) {
|
||||
return deviationSeverity;
|
||||
}
|
||||
|
||||
Map<String, Integer> sameItemsByValue = new HashMap<>();
|
||||
collection.stream()
|
||||
.filter(list -> !list.isEmpty())
|
||||
.map(list -> list.get(list.size() - 1)) // We use last item only
|
||||
.map(RequestInfo::getDataMap)
|
||||
.map(e -> e.get(inventoryItem).getValue())
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(e -> {
|
||||
sameItemsByValue.putIfAbsent(e, 0);
|
||||
int prev = sameItemsByValue.get(e);
|
||||
sameItemsByValue.put(e, prev + 1);
|
||||
});
|
||||
if (sameItemsByValue.size() > 1) {
|
||||
List<Tuple2<String, Integer>> sameItems = new ArrayList<>();
|
||||
sameItemsByValue.forEach((k, v) -> sameItems.add(new Tuple2<>(k, v)));
|
||||
sameItems.sort(Comparator.comparing(o -> o.second));
|
||||
Collections.reverse(sameItems);
|
||||
String majority = sameItems.get(0).first;
|
||||
if (!majority.equals(value)) {
|
||||
int majorityAsInt = Integer.parseInt(majority);
|
||||
int valueAsInt = Integer.parseInt(value);
|
||||
int diff = Math.abs(majorityAsInt - valueAsInt);
|
||||
if (diff >= alertTrigger) {
|
||||
deviationSeverity = DeviationSeverity.ALERT;
|
||||
} else if (diff >= warnTrigger) {
|
||||
deviationSeverity = DeviationSeverity.WARN;
|
||||
} else {
|
||||
deviationSeverity = DeviationSeverity.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
return deviationSeverity;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.network.p2p.inventory.model;
|
||||
|
||||
public class DeviationByPercentage implements DeviationType {
|
||||
private final double lowerAlertTrigger;
|
||||
private final double upperAlertTrigger;
|
||||
private final double lowerWarnTrigger;
|
||||
private final double upperWarnTrigger;
|
||||
|
||||
// In case want to see the % deviation but not trigger any warnings or alerts
|
||||
public DeviationByPercentage() {
|
||||
this(0, Double.MAX_VALUE, 0, Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
public DeviationByPercentage(double lowerAlertTrigger,
|
||||
double upperAlertTrigger,
|
||||
double lowerWarnTrigger,
|
||||
double upperWarnTrigger) {
|
||||
this.lowerAlertTrigger = lowerAlertTrigger;
|
||||
this.upperAlertTrigger = upperAlertTrigger;
|
||||
this.lowerWarnTrigger = lowerWarnTrigger;
|
||||
this.upperWarnTrigger = upperWarnTrigger;
|
||||
}
|
||||
|
||||
public DeviationSeverity getDeviationSeverity(double deviation) {
|
||||
if (deviation <= lowerAlertTrigger || deviation >= upperAlertTrigger) {
|
||||
return DeviationSeverity.ALERT;
|
||||
}
|
||||
|
||||
if (deviation <= lowerWarnTrigger || deviation >= upperWarnTrigger) {
|
||||
return DeviationSeverity.WARN;
|
||||
}
|
||||
|
||||
return DeviationSeverity.OK;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.network.p2p.inventory.model;
|
||||
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class DeviationOfHashes implements DeviationType {
|
||||
public DeviationSeverity getDeviationSeverity(Collection<List<RequestInfo>> collection,
|
||||
@Nullable String value,
|
||||
InventoryItem inventoryItem,
|
||||
String currentBlockHeight) {
|
||||
DeviationSeverity deviationSeverity = DeviationSeverity.OK;
|
||||
if (value == null) {
|
||||
return deviationSeverity;
|
||||
}
|
||||
|
||||
Map<String, Integer> sameHashesPerHashListByHash = new HashMap<>();
|
||||
collection.stream()
|
||||
.filter(list -> !list.isEmpty())
|
||||
.map(list -> list.get(list.size() - 1)) // We use last item only
|
||||
.map(RequestInfo::getDataMap)
|
||||
.filter(map -> currentBlockHeight.equals(map.get(InventoryItem.daoStateChainHeight).getValue()))
|
||||
.map(map -> map.get(inventoryItem).getValue())
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(v -> {
|
||||
sameHashesPerHashListByHash.putIfAbsent(v, 0);
|
||||
int prev = sameHashesPerHashListByHash.get(v);
|
||||
sameHashesPerHashListByHash.put(v, prev + 1);
|
||||
});
|
||||
if (sameHashesPerHashListByHash.size() > 1) {
|
||||
List<Tuple2<String, Integer>> sameHashesPerHashList = new ArrayList<>();
|
||||
sameHashesPerHashListByHash.forEach((k, v) -> sameHashesPerHashList.add(new Tuple2<>(k, v)));
|
||||
sameHashesPerHashList.sort(Comparator.comparing(o -> o.second));
|
||||
Collections.reverse(sameHashesPerHashList);
|
||||
|
||||
// It could be that first and any following list entry has same number of hashes, but we ignore that as
|
||||
// it is reason enough to alert the operators in case not all hashes are the same.
|
||||
if (sameHashesPerHashList.get(0).first.equals(value)) {
|
||||
// We are in the majority group.
|
||||
// We also set a warning to make sure the operators act quickly and to check if there are
|
||||
// more severe issues.
|
||||
deviationSeverity = DeviationSeverity.WARN;
|
||||
} else {
|
||||
deviationSeverity = DeviationSeverity.ALERT;
|
||||
}
|
||||
}
|
||||
return deviationSeverity;
|
||||
}
|
||||
}
|
@ -15,14 +15,11 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.apitest.scenario;
|
||||
package bisq.core.network.p2p.inventory.model;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import bisq.apitest.method.MethodTest;
|
||||
|
||||
@Slf4j
|
||||
public class ScenarioTest extends MethodTest {
|
||||
public enum DeviationSeverity {
|
||||
IGNORED,
|
||||
OK,
|
||||
WARN,
|
||||
ALERT
|
||||
}
|
@ -15,20 +15,7 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.price.spot.providers;
|
||||
|
||||
import bisq.price.AbstractExchangeRateProviderTest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class HitbtcTest extends AbstractExchangeRateProviderTest {
|
||||
|
||||
@Test
|
||||
public void doGet_successfulCall() {
|
||||
doGet_successfulCall(new Hitbtc());
|
||||
}
|
||||
package bisq.core.network.p2p.inventory.model;
|
||||
|
||||
public interface DeviationType {
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.network.p2p.inventory.model;
|
||||
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public enum InventoryItem {
|
||||
// Percentage deviation
|
||||
OfferPayload("OfferPayload",
|
||||
true,
|
||||
new DeviationByPercentage(0.8, 1.2, 0.9, 1.1), 5),
|
||||
MailboxStoragePayload("MailboxStoragePayload",
|
||||
true,
|
||||
new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2),
|
||||
TradeStatistics3("TradeStatistics3",
|
||||
true,
|
||||
new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2),
|
||||
AccountAgeWitness("AccountAgeWitness",
|
||||
true,
|
||||
new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2),
|
||||
SignedWitness("SignedWitness",
|
||||
true,
|
||||
new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 2),
|
||||
|
||||
// Should be same value
|
||||
Alert("Alert",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 1), 2),
|
||||
Filter("Filter",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 1), 2),
|
||||
Mediator("Mediator",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 1), 2),
|
||||
RefundAgent("RefundAgent",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 1), 2),
|
||||
|
||||
// Should be very close values
|
||||
TempProposalPayload("TempProposalPayload",
|
||||
true,
|
||||
new DeviationByIntegerDiff(3, 5), 2),
|
||||
ProposalPayload("ProposalPayload",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 2), 2),
|
||||
BlindVotePayload("BlindVotePayload",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 2), 2),
|
||||
|
||||
// Should be very close values
|
||||
daoStateChainHeight("daoStateChainHeight",
|
||||
true,
|
||||
new DeviationByIntegerDiff(2, 4), 3),
|
||||
numBsqBlocks("numBsqBlocks",
|
||||
true,
|
||||
new DeviationByIntegerDiff(2, 4), 3),
|
||||
|
||||
// Has to be same values at same block
|
||||
daoStateHash("daoStateHash",
|
||||
false,
|
||||
new DeviationOfHashes(), 1),
|
||||
proposalHash("proposalHash",
|
||||
false,
|
||||
new DeviationOfHashes(), 1),
|
||||
blindVoteHash("blindVoteHash",
|
||||
false,
|
||||
new DeviationOfHashes(), 1),
|
||||
|
||||
// Percentage deviation
|
||||
maxConnections("maxConnections",
|
||||
true,
|
||||
new DeviationByPercentage(0.33, 3, 0.4, 2.5), 2),
|
||||
numConnections("numConnections",
|
||||
true,
|
||||
new DeviationByPercentage(0, 3, 0, 2.5), 2),
|
||||
peakNumConnections("peakNumConnections",
|
||||
true,
|
||||
new DeviationByPercentage(0, 3, 0, 2.5), 2),
|
||||
numAllConnectionsLostEvents("numAllConnectionsLostEvents",
|
||||
true,
|
||||
new DeviationByIntegerDiff(1, 2), 1),
|
||||
sentBytesPerSec("sentBytesPerSec",
|
||||
true,
|
||||
new DeviationByPercentage(), 5),
|
||||
receivedBytesPerSec("receivedBytesPerSec",
|
||||
true,
|
||||
new DeviationByPercentage(), 5),
|
||||
receivedMessagesPerSec("receivedMessagesPerSec",
|
||||
true,
|
||||
new DeviationByPercentage(), 5),
|
||||
sentMessagesPerSec("sentMessagesPerSec",
|
||||
true,
|
||||
new DeviationByPercentage(), 5),
|
||||
|
||||
// No deviation check
|
||||
sentBytes("sentBytes", true),
|
||||
receivedBytes("receivedBytes", true),
|
||||
|
||||
// No deviation check
|
||||
version("version", false),
|
||||
commitHash("commitHash", false),
|
||||
usedMemory("usedMemory", true),
|
||||
jvmStartTime("jvmStartTime", true),
|
||||
filteredSeeds("filteredSeeds", false);
|
||||
|
||||
@Getter
|
||||
private final String key;
|
||||
@Getter
|
||||
private final boolean isNumberValue;
|
||||
@Getter
|
||||
@Nullable
|
||||
private DeviationType deviationType;
|
||||
|
||||
// The number of past requests we check to see if there have been repeated alerts or warnings. The higher the
|
||||
// number the more repeated alert need to have happened to cause a notification alert.
|
||||
// Smallest number is 1, as that takes only the last request data and does not look further back.
|
||||
@Getter
|
||||
private int deviationTolerance = 1;
|
||||
|
||||
InventoryItem(String key, boolean isNumberValue) {
|
||||
this.key = key;
|
||||
this.isNumberValue = isNumberValue;
|
||||
}
|
||||
|
||||
InventoryItem(String key, boolean isNumberValue, @NotNull DeviationType deviationType, int deviationTolerance) {
|
||||
this(key, isNumberValue);
|
||||
this.deviationType = deviationType;
|
||||
this.deviationTolerance = deviationTolerance;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Tuple2<Double, Double> getDeviationAndAverage(Map<InventoryItem, Double> averageValues,
|
||||
@Nullable String value) {
|
||||
if (averageValues.containsKey(this) && value != null) {
|
||||
double averageValue = averageValues.get(this);
|
||||
return new Tuple2<>(getDeviation(value, averageValue), averageValue);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getDeviation(@Nullable String value, double average) {
|
||||
if (deviationType != null && value != null && average != 0 && isNumberValue) {
|
||||
return Double.parseDouble(value) / average;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public DeviationSeverity getDeviationSeverity(Double deviation,
|
||||
Collection<List<RequestInfo>> collection,
|
||||
@Nullable String value,
|
||||
String currentBlockHeight) {
|
||||
if (deviationType == null || deviation == null || value == null) {
|
||||
return DeviationSeverity.OK;
|
||||
}
|
||||
|
||||
if (deviationType instanceof DeviationByPercentage) {
|
||||
return ((DeviationByPercentage) deviationType).getDeviationSeverity(deviation);
|
||||
} else if (deviationType instanceof DeviationByIntegerDiff) {
|
||||
return ((DeviationByIntegerDiff) deviationType).getDeviationSeverity(collection, value, this);
|
||||
} else if (deviationType instanceof DeviationOfHashes) {
|
||||
return ((DeviationOfHashes) deviationType).getDeviationSeverity(collection, value, this, currentBlockHeight);
|
||||
} else {
|
||||
return DeviationSeverity.OK;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user