Merge remote-tracking branch 'bisq-network/release/v1.5.0' into upgrade-javafax-14

This commit is contained in:
cd2357 2020-11-27 16:59:23 +01:00
commit 72a719dcc9
No known key found for this signature in database
GPG Key ID: F26C56748514D0D3
359 changed files with 18100 additions and 5032 deletions

View File

@ -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

View File

@ -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");
}
}

View File

@ -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";
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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);
}

View File

@ -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 {

View File

@ -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());
}

View File

@ -0,0 +1,82 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.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());
}
}

View File

@ -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());
}

View File

@ -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());

View File

@ -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)

View File

@ -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)));
}
}

View File

@ -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;
}
}

View File

@ -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)));
}
}

View File

@ -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)));
}
}

View File

@ -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() {

View 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();
}
}

View 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();
}
}

View 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);
}
}

View 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();
}
}

View File

@ -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")
}
}

View File

@ -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");

View 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, ' ');
}

View File

@ -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));
}

View File

@ -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);
}
}

View File

@ -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)

View 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");
}
}

View File

@ -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";

View File

@ -72,7 +72,7 @@ public enum BaseCurrencyNetwork {
return "BTC_REGTEST".equals(name());
}
public long getDefaultMinFeePerByte() {
public long getDefaultMinFeePerVbyte() {
return 2;
}
}

View File

@ -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}";

View File

@ -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);
}

View File

@ -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(() -> {
}));
});
}

View File

@ -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;
}

View File

@ -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];
}
}

View File

@ -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);

View File

@ -35,7 +35,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AccountAgeWitnessStore extends PersistableNetworkPayloadStore<AccountAgeWitness> {
AccountAgeWitnessStore() {
public AccountAgeWitnessStore() {
}

View File

@ -133,7 +133,7 @@ public class PrivateNotificationManager {
}
public void removePrivateNotification() {
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
p2PService.removeMailboxMsg(decryptedMessageWithPubKey);
}
private boolean isKeyValid(String privKeyString) {

View File

@ -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
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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,

View 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()));
}
}

View File

@ -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

View File

@ -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");
}
}

View File

@ -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);
}

View 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" +
'}';
}
}

View File

@ -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);
}
}
}

View File

@ -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"));

View File

@ -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();

View File

@ -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();

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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 {

View File

@ -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));

View File

@ -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() {

View File

@ -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;

View File

@ -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);
});
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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)

View File

@ -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());

View File

@ -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();
}

View File

@ -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));

View File

@ -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());
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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)

View File

@ -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());

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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) {

View File

@ -168,7 +168,7 @@ public class FullNode extends BsqNode {
}
private void onNewBlock(Block block) {
maybeExportNewBlockToJson(block);
maybeExportToJson();
if (p2pNetworkReady && parseBlockchainComplete)
fullNodeNetworkService.publishNewBlock(block);

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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}";
}

View File

@ -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)) {

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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,

View File

@ -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) {
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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 {
}

View File

@ -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