Merge branch 'master' of github.com:bisq-network/bisq into hotfix/v1.5.1

# Conflicts:
#	build.gradle
#	core/src/main/java/bisq/core/btc/setup/WalletConfig.java
#	desktop/package/linux/Dockerfile
#	desktop/package/linux/package.sh
#	desktop/package/linux/release.sh
#	desktop/package/macosx/create_app.sh
#	desktop/package/macosx/finalize.sh
#	desktop/package/macosx/insert_snapshot_version.sh
#	desktop/package/windows/package.bat
#	desktop/package/windows/release.bat
#	relay/src/main/resources/version.txt
#	seednode/src/main/java/bisq/seednode/SeedNodeMain.java
This commit is contained in:
Christoph Atteneder 2020-12-08 22:19:11 +01:00
commit 29c2e0002d
No known key found for this signature in database
GPG key ID: CD5DC1C529CDFD3B
122 changed files with 5351 additions and 1194 deletions

View file

@ -48,7 +48,7 @@ To run all test cases in a package:
To run a single test case:
$ ./gradlew :apitest:test --tests "bisq.apitest.method.GetBalanceTest" -DrunApiTests=true
$ ./gradlew :apitest:test --tests "bisq.apitest.scenario.WalletTest" -DrunApiTests=true
To run test cases from Intellij, add two JVM arguments to your JUnit launchers:

View file

@ -93,8 +93,6 @@
@test "test getbalance while wallet unlocked for 8s" {
run ./bisq-cli --password=xyz getbalance
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "0.00000000" ]
sleep 8
}
@ -145,8 +143,6 @@
@test "test getbalance when wallet available & unlocked with 0 btc balance" {
run ./bisq-cli --password=xyz getbalance
[ "$status" -eq 0 ]
echo "actual output: $output" >&2
[ "$output" = "0.00000000" ]
}
@test "test getfundingaddresses" {
@ -154,6 +150,11 @@
[ "$status" -eq 0 ]
}
@test "test getunusedbsqaddress" {
run ./bisq-cli --password=xyz getfundingaddresses
[ "$status" -eq 0 ]
}
@test "test getaddressbalance missing address argument" {
run ./bisq-cli --password=xyz getaddressbalance
[ "$status" -eq 1 ]
@ -168,15 +169,8 @@
[ "$output" = "Error: address bogus not found in wallet" ]
}
@test "test createpaymentacct PerfectMoneyDummy (missing name, nbr, ccy params)" {
run ./bisq-cli --password=xyz createpaymentacct PERFECT_MONEY
[ "$status" -eq 1 ]
echo "actual output: $output" >&2
[ "$output" = "Error: incorrect parameter count, expecting payment method id, account name, account number, currency code" ]
}
@test "test createpaymentacct PERFECT_MONEY PerfectMoneyDummy 0123456789 USD" {
run ./bisq-cli --password=xyz createpaymentacct PERFECT_MONEY PerfectMoneyDummy 0123456789 USD
@test "test getpaymentmethods" {
run ./bisq-cli --password=xyz getpaymentmethods
[ "$status" -eq 0 ]
}

View file

@ -1,99 +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.apitest.method;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import protobuf.PaymentAccount;
import protobuf.PerfectMoneyAccountPayload;
import java.util.List;
import java.util.stream.Collectors;
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;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.util.Comparator.comparing;
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 {
static final String PERFECT_MONEY_ACCT_NAME = "Perfect Money USD";
static final String PERFECT_MONEY_ACCT_NUMBER = "0123456789";
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testCreatePerfectMoneyUSDPaymentAccount() {
var perfectMoneyPaymentAccountRequest = createCreatePerfectMoneyPaymentAccountRequest(
PERFECT_MONEY_ACCT_NAME,
PERFECT_MONEY_ACCT_NUMBER,
"USD");
//noinspection ResultOfMethodCallIgnored
grpcStubs(alicedaemon).paymentAccountsService.createPaymentAccount(perfectMoneyPaymentAccountRequest);
var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build();
var reply = grpcStubs(alicedaemon).paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest);
// The daemon is running against the regtest/dao setup files, and was set up with
// two dummy accounts ("PerfectMoney dummy", "ETH dummy") before any tests ran.
// We just added 1 test account, making 3 total.
assertEquals(3, reply.getPaymentAccountsCount());
// Sort the returned list by creation date; the last item in the sorted
// list will be the payment acct we just created.
List<PaymentAccount> paymentAccountList = reply.getPaymentAccountsList().stream()
.sorted(comparing(PaymentAccount::getCreationDate))
.collect(Collectors.toList());
PaymentAccount paymentAccount = paymentAccountList.get(2);
PerfectMoneyAccountPayload perfectMoneyAccount = paymentAccount
.getPaymentAccountPayload()
.getPerfectMoneyAccountPayload();
assertEquals(PERFECT_MONEY_ACCT_NAME, paymentAccount.getAccountName());
assertEquals("USD",
paymentAccount.getSelectedTradeCurrency().getFiatCurrency().getCurrency().getCurrencyCode());
assertEquals(PERFECT_MONEY_ACCT_NUMBER, perfectMoneyAccount.getAccountNr());
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -1,73 +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.apitest.method;
import bisq.proto.grpc.GetBalanceRequest;
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;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
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 {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, alicedaemon);
// Have to generate 1 regtest block for alice's wallet to show 10 BTC balance.
bitcoinCli.generateBlocks(1);
// Give the alicedaemon time to parse the new block.
MILLISECONDS.sleep(1500);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetBalance() {
// All tests depend on the DAO / regtest environment, and Alice's wallet is
// initialized with 10 BTC during the scaffolding setup.
var balance = grpcStubs(alicedaemon).walletsService
.getBalance(GetBalanceRequest.newBuilder().build()).getBalance();
assertEquals(1000000000, balance);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -17,36 +17,60 @@
package bisq.apitest.method;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.proto.CoreProtoResolver;
import bisq.common.util.Utilities;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
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.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
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.SendBsqRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
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.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.stream;
import static java.util.Comparator.comparing;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -70,6 +94,8 @@ public class MethodTest extends ApiTestCase {
protected static PaymentAccount alicesDummyAcct;
protected static PaymentAccount bobsDummyAcct;
private static final CoreProtoResolver CORE_PROTO_RESOLVER = new CoreProtoResolver();
public static void startSupportingApps(boolean registerDisputeAgents,
boolean generateBtcBlock,
Enum<?>... supportingApps) {
@ -102,9 +128,12 @@ public class MethodTest extends ApiTestCase {
}
// Convenience methods for building gRPC request objects
protected final GetBalancesRequest createGetBalancesRequest(String currencyCode) {
return GetBalancesRequest.newBuilder().setCurrencyCode(currencyCode).build();
}
protected final GetBalanceRequest createBalanceRequest() {
return GetBalanceRequest.newBuilder().build();
protected final GetAddressBalanceRequest createGetAddressBalanceRequest(String address) {
return GetAddressBalanceRequest.newBuilder().setAddress(address).build();
}
protected final SetWalletPasswordRequest createSetWalletPasswordRequest(String password) {
@ -127,6 +156,14 @@ public class MethodTest extends ApiTestCase {
return LockWalletRequest.newBuilder().build();
}
protected final GetUnusedBsqAddressRequest createGetUnusedBsqAddressRequest() {
return GetUnusedBsqAddressRequest.newBuilder().build();
}
protected final SendBsqRequest createSendBsqRequest(String address, String amount) {
return SendBsqRequest.newBuilder().setAddress(address).setAmount(amount).build();
}
protected final GetFundingAddressesRequest createGetFundingAddressesRequest() {
return GetFundingAddressesRequest.newBuilder().build();
}
@ -143,8 +180,14 @@ public class MethodTest extends ApiTestCase {
return CancelOfferRequest.newBuilder().setId(offerId).build();
}
protected final TakeOfferRequest createTakeOfferRequest(String offerId, String paymentAccountId) {
return TakeOfferRequest.newBuilder().setOfferId(offerId).setPaymentAccountId(paymentAccountId).build();
protected final TakeOfferRequest createTakeOfferRequest(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
}
protected final GetTradeRequest createGetTradeRequest(String tradeId) {
@ -173,9 +216,21 @@ public class MethodTest extends ApiTestCase {
}
// Convenience methods for calling frequently used & thoroughly tested gRPC services.
protected final BalancesInfo getBalances(BisqAppConfig bisqAppConfig, String currencyCode) {
return grpcStubs(bisqAppConfig).walletsService.getBalances(
createGetBalancesRequest(currencyCode)).getBalances();
}
protected final long getBalance(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getBalance(createBalanceRequest()).getBalance();
protected final BsqBalanceInfo getBsqBalances(BisqAppConfig bisqAppConfig) {
return getBalances(bisqAppConfig, "bsq").getBsq();
}
protected final BtcBalanceInfo getBtcBalances(BisqAppConfig bisqAppConfig) {
return getBalances(bisqAppConfig, "btc").getBtc();
}
protected final AddressBalanceInfo getAddressBalance(BisqAppConfig bisqAppConfig, String address) {
return grpcStubs(bisqAppConfig).walletsService.getAddressBalance(createGetAddressBalanceRequest(address)).getAddressBalanceInfo();
}
protected final void unlockWallet(BisqAppConfig bisqAppConfig, String password, long timeout) {
@ -188,6 +243,15 @@ public class MethodTest extends ApiTestCase {
grpcStubs(bisqAppConfig).walletsService.lockWallet(createLockWalletRequest());
}
protected final String getUnusedBsqAddress(BisqAppConfig bisqAppConfig) {
return grpcStubs(bisqAppConfig).walletsService.getUnusedBsqAddress(createGetUnusedBsqAddressRequest()).getAddress();
}
protected final void sendBsq(BisqAppConfig bisqAppConfig, String address, String amount) {
//noinspection ResultOfMethodCallIgnored
grpcStubs(bisqAppConfig).walletsService.sendBsq(createSendBsqRequest(address, amount));
}
protected final String getUnusedBtcAddress(BisqAppConfig bisqAppConfig) {
//noinspection OptionalGetWithoutIsPresent
return grpcStubs(bisqAppConfig).walletsService.getFundingAddresses(createGetFundingAddressesRequest())
@ -199,26 +263,53 @@ public class MethodTest extends ApiTestCase {
.getAddress();
}
protected final CreatePaymentAccountRequest createCreatePerfectMoneyPaymentAccountRequest(
String accountName,
String accountNumber,
String currencyCode) {
return CreatePaymentAccountRequest.newBuilder()
.setPaymentMethodId(PERFECT_MONEY.getId())
.setAccountName(accountName)
.setAccountNumber(accountNumber)
.setCurrencyCode(currencyCode)
.build();
protected final List<PaymentMethod> getPaymentMethods(BisqAppConfig bisqAppConfig) {
var req = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentMethods(req).getPaymentMethodsList();
}
protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
var req = GetPaymentAccountsRequest.newBuilder().build();
protected final File getPaymentAccountForm(BisqAppConfig bisqAppConfig, String paymentMethodId) {
// We take seemingly unnecessary steps to get a File object, but the point is to
// test the API, and we do not directly ask bisq.core.api.model.PaymentAccountForm
// for an empty json form (file).
var req = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
String jsonString = grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccountForm(req)
.getPaymentAccountFormJson();
// Write the json string to a file here in the test case.
File jsonFile = PaymentAccountForm.getTmpJsonFile(paymentMethodId);
try (PrintWriter out = new PrintWriter(jsonFile, UTF_8)) {
out.println(jsonString);
} catch (IOException ex) {
fail("Could not create tmp payment account form.", ex);
}
return jsonFile;
}
protected final bisq.core.payment.PaymentAccount createPaymentAccount(BisqAppConfig bisqAppConfig,
String jsonString) {
var req = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(jsonString)
.build();
var paymentAccountsService = grpcStubs(bisqAppConfig).paymentAccountsService;
PaymentAccount paymentAccount = paymentAccountsService.getPaymentAccounts(req)
// Normally, we can do asserts on the protos from the gRPC service, but in this
// case we need to return a bisq.core.payment.PaymentAccount so it can be cast
// to its sub type.
return fromProto(paymentAccountsService.createPaymentAccount(req).getPaymentAccount());
}
protected static List<PaymentAccount> getPaymentAccounts(BisqAppConfig bisqAppConfig) {
var req = GetPaymentAccountsRequest.newBuilder().build();
return grpcStubs(bisqAppConfig).paymentAccountsService.getPaymentAccounts(req)
.getPaymentAccountsList()
.stream()
.sorted(comparing(PaymentAccount::getCreationDate))
.collect(Collectors.toList()).get(0);
.collect(Collectors.toList());
}
protected static PaymentAccount getDefaultPerfectDummyPaymentAccount(BisqAppConfig bisqAppConfig) {
PaymentAccount paymentAccount = getPaymentAccounts(bisqAppConfig).get(0);
assertEquals("PerfectMoney dummy", paymentAccount.getAccountName());
return paymentAccount;
}
@ -267,6 +358,27 @@ public class MethodTest extends ApiTestCase {
var req = createWithdrawFundsRequest(tradeId, address);
grpcStubs(bisqAppConfig).tradesService.withdrawFunds(req);
}
protected final TxFeeRateInfo getTxFeeRate(BisqAppConfig bisqAppConfig) {
var req = GetTxFeeRateRequest.newBuilder().build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.getTxFeeRate(req).getTxFeeRateInfo());
}
protected final TxFeeRateInfo setTxFeeRate(BisqAppConfig bisqAppConfig, long feeRate) {
var req = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(feeRate)
.build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.setTxFeeRatePreference(req).getTxFeeRateInfo());
}
protected final TxFeeRateInfo unsetTxFeeRate(BisqAppConfig bisqAppConfig) {
var req = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return TxFeeRateInfo.fromProto(
grpcStubs(bisqAppConfig).walletsService.unsetTxFeeRatePreference(req).getTxFeeRateInfo());
}
// Static conveniences for test methods and test case fixture setups.
protected static RegisterDisputeAgentRequest createRegisterDisputeAgentRequest(String disputeAgentType) {
@ -275,10 +387,18 @@ public class MethodTest extends ApiTestCase {
.setRegistrationKey(DEV_PRIVILEGE_PRIV_KEY).build();
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@SuppressWarnings({"ResultOfMethodCallIgnored", "SameParameterValue"})
protected static void registerDisputeAgents(BisqAppConfig bisqAppConfig) {
var disputeAgentsService = grpcStubs(bisqAppConfig).disputeAgentsService;
disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(MEDIATOR));
disputeAgentsService.registerDisputeAgent(createRegisterDisputeAgentRequest(REFUND_AGENT));
}
protected static String encodeToHex(String s) {
return Utilities.bytesAsHexString(s.getBytes(UTF_8));
}
private bisq.core.payment.PaymentAccount fromProto(PaymentAccount proto) {
return bisq.core.payment.PaymentAccount.fromProto(proto, CORE_PROTO_RESOLVER);
}
}

View file

@ -73,22 +73,35 @@ public abstract class AbstractOfferTest extends MethodTest {
protected final OfferInfo createAliceOffer(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount) {
return createMarketBasedPricedOffer(aliceStubs, paymentAccount, direction, currencyCode, amount);
long amount,
String makerFeeCurrencyCode) {
return createMarketBasedPricedOffer(aliceStubs,
paymentAccount,
direction,
currencyCode,
amount,
makerFeeCurrencyCode);
}
protected final OfferInfo createBobOffer(PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount) {
return createMarketBasedPricedOffer(bobStubs, paymentAccount, direction, currencyCode, amount);
long amount,
String makerFeeCurrencyCode) {
return createMarketBasedPricedOffer(bobStubs,
paymentAccount,
direction,
currencyCode,
amount,
makerFeeCurrencyCode);
}
protected final OfferInfo createMarketBasedPricedOffer(GrpcStubs grpcStubs,
PaymentAccount paymentAccount,
String direction,
String currencyCode,
long amount) {
long amount,
String makerFeeCurrencyCode) {
var req = CreateOfferRequest.newBuilder()
.setPaymentAccountId(paymentAccount.getId())
.setDirection(direction)
@ -99,6 +112,7 @@ public abstract class AbstractOfferTest extends MethodTest {
.setMarketPriceMargin(0.00)
.setPrice("0")
.setBuyerSecurityDeposit(getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
return grpcStubs.offersService.createOffer(req).getOffer();
}

View file

@ -54,6 +54,7 @@ public class CancelOfferTest extends AbstractOfferTest {
.setMarketPriceMargin(0.00)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode("bsq")
.build();
// Create some offers.

View file

@ -38,6 +38,8 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
private static final String MAKER_FEE_CURRENCY_CODE = "bsq";
@Test
@Order(1)
public void testCreateAUDBTCBuyOfferUsingFixedPrice16000() {
@ -51,6 +53,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
.setMarketPriceMargin(0.00)
.setPrice("16000")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
String newOfferId = newOffer.getId();
@ -64,6 +67,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("AUD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -76,6 +80,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("AUD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
}
@Test
@ -91,6 +96,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
.setMarketPriceMargin(0.00)
.setPrice("10000.1234")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
String newOfferId = newOffer.getId();
@ -104,6 +110,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -116,6 +123,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
}
@Test
@ -131,6 +139,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
.setMarketPriceMargin(0.00)
.setPrice("9500.1234")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
String newOfferId = newOffer.getId();
@ -144,6 +153,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("EUR", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -156,5 +166,6 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("EUR", newOffer.getCounterCurrencyCode());
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
}
}

View file

@ -50,6 +50,8 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
private static final double MKT_PRICE_MARGIN_ERROR_TOLERANCE = 0.0050; // 0.50%
private static final double MKT_PRICE_MARGIN_WARNING_TOLERANCE = 0.0001; // 0.01%
private static final String MAKER_FEE_CURRENCY_CODE = "btc";
@Test
@Order(1)
public void testCreateUSDBTCBuyOffer5PctPriceMargin() {
@ -64,6 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
String newOfferId = newOffer.getId();
@ -76,6 +79,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -87,6 +91,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("USD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@ -105,6 +110,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
String newOfferId = newOffer.getId();
@ -117,6 +123,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -128,6 +135,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("NZD", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@ -146,6 +154,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
@ -159,6 +168,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -170,6 +180,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("GBP", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}
@ -188,6 +199,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
.setMarketPriceMargin(priceMarginPctInput)
.setPrice("0")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode(MAKER_FEE_CURRENCY_CODE)
.build();
var newOffer = aliceStubs.offersService.createOffer(req).getOffer();
@ -201,6 +213,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = getOffer(newOfferId);
assertEquals(newOfferId, newOffer.getId());
@ -212,6 +225,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertEquals(alicesDummyAcct.getId(), newOffer.getPaymentAccountId());
assertEquals("BTC", newOffer.getBaseCurrencyCode());
assertEquals("BRL", newOffer.getCounterCurrencyCode());
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
}

View file

@ -52,6 +52,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
.setMarketPriceMargin(0.00)
.setPrice("10000.0000")
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
.setMakerFeeCurrencyCode("bsq")
.build();
@SuppressWarnings("ResultOfMethodCallIgnored")
Throwable exception = assertThrows(StatusRuntimeException.class, () ->

View file

@ -0,0 +1,204 @@
package bisq.apitest.method.payment;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.stream.JsonWriter;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInfo;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.*;
import bisq.apitest.method.MethodTest;
@Slf4j
public class AbstractPaymentAccountTest extends MethodTest {
static final String PROPERTY_NAME_JSON_COMMENTS = "_COMMENTS_";
static final List<String> PROPERTY_VALUE_JSON_COMMENTS = new ArrayList<>() {{
add("Do not manually edit the paymentMethodId field.");
add("Edit the salt field only if you are recreating a payment"
+ " account on a new installation and wish to preserve the account age.");
}};
static final String PROPERTY_NAME_PAYMENT_METHOD_ID = "paymentMethodId";
static final String PROPERTY_NAME_ACCOUNT_ID = "accountId";
static final String PROPERTY_NAME_ACCOUNT_NAME = "accountName";
static final String PROPERTY_NAME_ACCOUNT_NR = "accountNr";
static final String PROPERTY_NAME_ACCOUNT_TYPE = "accountType";
static final String PROPERTY_NAME_ANSWER = "answer";
static final String PROPERTY_NAME_BANK_ACCOUNT_NAME = "bankAccountName";
static final String PROPERTY_NAME_BANK_ACCOUNT_NUMBER = "bankAccountNumber";
static final String PROPERTY_NAME_BANK_ACCOUNT_TYPE = "bankAccountType";
static final String PROPERTY_NAME_BANK_BRANCH_CODE = "bankBranchCode";
static final String PROPERTY_NAME_BANK_BRANCH_NAME = "bankBranchName";
static final String PROPERTY_NAME_BANK_CODE = "bankCode";
@SuppressWarnings("unused")
static final String PROPERTY_NAME_BANK_ID = "bankId";
static final String PROPERTY_NAME_BANK_NAME = "bankName";
static final String PROPERTY_NAME_BRANCH_ID = "branchId";
static final String PROPERTY_NAME_BIC = "bic";
static final String PROPERTY_NAME_COUNTRY = "country";
static final String PROPERTY_NAME_CITY = "city";
static final String PROPERTY_NAME_CONTACT = "contact";
static final String PROPERTY_NAME_EMAIL = "email";
static final String PROPERTY_NAME_EMAIL_OR_MOBILE_NR = "emailOrMobileNr";
static final String PROPERTY_NAME_EXTRA_INFO = "extraInfo";
static final String PROPERTY_NAME_HOLDER_EMAIL = "holderEmail";
static final String PROPERTY_NAME_HOLDER_NAME = "holderName";
static final String PROPERTY_NAME_HOLDER_TAX_ID = "holderTaxId";
static final String PROPERTY_NAME_IBAN = "iban";
static final String PROPERTY_NAME_MOBILE_NR = "mobileNr";
static final String PROPERTY_NAME_NATIONAL_ACCOUNT_ID = "nationalAccountId";
static final String PROPERTY_NAME_PAY_ID = "payid";
static final String PROPERTY_NAME_POSTAL_ADDRESS = "postalAddress";
static final String PROPERTY_NAME_PROMPT_PAY_ID = "promptPayId";
static final String PROPERTY_NAME_QUESTION = "question";
static final String PROPERTY_NAME_REQUIREMENTS = "requirements";
static final String PROPERTY_NAME_SALT = "salt";
static final String PROPERTY_NAME_SORT_CODE = "sortCode";
static final String PROPERTY_NAME_STATE = "state";
static final String PROPERTY_NAME_USERNAME = "userName";
static final Gson GSON = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls()
.create();
static final Map<String, Object> COMPLETED_FORM_MAP = new HashMap<>();
// A payment account serializer / deserializer.
static final PaymentAccountForm PAYMENT_ACCOUNT_FORM = new PaymentAccountForm();
@BeforeEach
public void setup() {
Res.setup();
}
protected final File getEmptyForm(TestInfo testInfo, String paymentMethodId) {
// This would normally be done in @BeforeEach, but these test cases might be
// called from a single 'scenario' test case, and the @BeforeEach -> clear()
// would be skipped.
COMPLETED_FORM_MAP.clear();
File emptyForm = getPaymentAccountForm(alicedaemon, paymentMethodId);
// A short cut over the API:
// File emptyForm = PAYMENT_ACCOUNT_FORM.getPaymentAccountForm(paymentMethodId);
log.debug("{} Empty form saved to {}",
testName(testInfo),
PAYMENT_ACCOUNT_FORM.getClickableURI(emptyForm));
emptyForm.deleteOnExit();
return emptyForm;
}
protected final void verifyEmptyForm(File jsonForm, String paymentMethodId, String... fields) {
@SuppressWarnings("unchecked")
Map<String, Object> emptyForm = (Map<String, Object>) GSON.fromJson(
PAYMENT_ACCOUNT_FORM.toJsonString(jsonForm),
Object.class);
assertNotNull(emptyForm);
assertEquals(PROPERTY_VALUE_JSON_COMMENTS, emptyForm.get(PROPERTY_NAME_JSON_COMMENTS));
assertEquals(paymentMethodId, emptyForm.get(PROPERTY_NAME_PAYMENT_METHOD_ID));
assertEquals("your accountname", emptyForm.get(PROPERTY_NAME_ACCOUNT_NAME));
for (String field : fields) {
assertEquals("your " + field.toLowerCase(), emptyForm.get(field));
}
}
protected final void verifyCommonFormEntries(PaymentAccount paymentAccount) {
// All PaymentAccount subclasses have paymentMethodId and an accountName fields.
assertNotNull(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAYMENT_METHOD_ID), paymentAccount.getPaymentMethod().getId());
assertTrue(paymentAccount.getCreationDate().getTime() > 0);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NAME), paymentAccount.getAccountName());
}
protected final void verifyAccountSingleTradeCurrency(String expectedCurrencyCode, PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getSingleTradeCurrency());
assertEquals(expectedCurrencyCode, paymentAccount.getSingleTradeCurrency().getCode());
}
protected final void verifyAccountTradeCurrencies(List<TradeCurrency> expectedTradeCurrencies,
PaymentAccount paymentAccount) {
assertNotNull(paymentAccount.getTradeCurrencies());
assertArrayEquals(expectedTradeCurrencies.toArray(), paymentAccount.getTradeCurrencies().toArray());
}
protected final void verifyUserPayloadHasPaymentAccountWithId(String paymentAccountId) {
var getPaymentAccountsRequest = GetPaymentAccountsRequest.newBuilder().build();
var reply = grpcStubs(alicedaemon)
.paymentAccountsService.getPaymentAccounts(getPaymentAccountsRequest);
Optional<protobuf.PaymentAccount> paymentAccount = reply.getPaymentAccountsList().stream()
.filter(a -> a.getId().equals(paymentAccountId))
.findFirst();
assertTrue(paymentAccount.isPresent());
}
protected final String getCompletedFormAsJsonString() {
File completedForm = fillPaymentAccountForm();
String jsonString = PAYMENT_ACCOUNT_FORM.toJsonString(completedForm);
log.debug("Completed form: {}", jsonString);
return jsonString;
}
private File fillPaymentAccountForm() {
File tmpJsonForm = null;
try {
tmpJsonForm = File.createTempFile("temp_acct_form_",
".json",
Paths.get(getProperty("java.io.tmpdir")).toFile());
tmpJsonForm.deleteOnExit();
JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(tmpJsonForm), UTF_8));
writer.beginObject();
writer.name(PROPERTY_NAME_JSON_COMMENTS);
writer.beginArray();
for (String s : PROPERTY_VALUE_JSON_COMMENTS) {
writer.value(s);
}
writer.endArray();
for (Map.Entry<String, Object> entry : COMPLETED_FORM_MAP.entrySet()) {
String k = entry.getKey();
Object v = entry.getValue();
writer.name(k);
writer.value(v.toString());
}
writer.endObject();
writer.close();
} catch (IOException ex) {
log.error("", ex);
fail(format("Could not write json file from form entries %s", COMPLETED_FORM_MAP));
}
return tmpJsonForm;
}
}

View file

@ -0,0 +1,853 @@
/*
* 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.payment;
import bisq.core.payment.AdvancedCashAccount;
import bisq.core.payment.AliPayAccount;
import bisq.core.payment.AustraliaPayid;
import bisq.core.payment.CashDepositAccount;
import bisq.core.payment.ChaseQuickPayAccount;
import bisq.core.payment.ClearXchangeAccount;
import bisq.core.payment.F2FAccount;
import bisq.core.payment.FasterPaymentsAccount;
import bisq.core.payment.HalCashAccount;
import bisq.core.payment.InteracETransferAccount;
import bisq.core.payment.JapanBankAccount;
import bisq.core.payment.MoneyBeamAccount;
import bisq.core.payment.MoneyGramAccount;
import bisq.core.payment.NationalBankAccount;
import bisq.core.payment.PerfectMoneyAccount;
import bisq.core.payment.PopmoneyAccount;
import bisq.core.payment.PromptPayAccount;
import bisq.core.payment.RevolutAccount;
import bisq.core.payment.SameBankAccount;
import bisq.core.payment.SepaAccount;
import bisq.core.payment.SepaInstantAccount;
import bisq.core.payment.SpecificBanksAccount;
import bisq.core.payment.SwishAccount;
import bisq.core.payment.TransferwiseAccount;
import bisq.core.payment.USPostalMoneyOrderAccount;
import bisq.core.payment.UpholdAccount;
import bisq.core.payment.WeChatPayAccount;
import bisq.core.payment.WesternUnionAccount;
import bisq.core.payment.payload.BankAccountPayload;
import bisq.core.payment.payload.CashDepositAccountPayload;
import bisq.core.payment.payload.SameBankAccountPayload;
import bisq.core.payment.payload.SpecificBanksAccountPayload;
import java.io.File;
import java.util.Objects;
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.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.Scaffold.BitcoinCoreApp.bitcoind;
import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.core.locale.CurrencyUtil.*;
import static bisq.core.payment.payload.PaymentMethod.*;
import static org.junit.Assert.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 AbstractPaymentAccountTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
public void testCreateAdvancedCashAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, ADVANCED_CASH_ID);
verifyEmptyForm(emptyForm,
ADVANCED_CASH_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ADVANCED_CASH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Advanced Cash Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "0000 1111 2222");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Advanced Cash Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AdvancedCashAccount paymentAccount = (AdvancedCashAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountTradeCurrencies(getAllAdvancedCashCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateAliPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, ALI_PAY_ID);
verifyEmptyForm(emptyForm,
ALI_PAY_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, ALI_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Ali Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "2222 3333 4444");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
AliPayAccount paymentAccount = (AliPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("CNY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateAustraliaPayidAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, AUSTRALIA_PAYID_ID);
verifyEmptyForm(emptyForm,
AUSTRALIA_PAYID_ID,
PROPERTY_NAME_BANK_ACCOUNT_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, AUSTRALIA_PAYID_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Australia Pay ID Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAY_ID, "123 456 789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Credit Union Australia");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Australia Pay ID Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
AustraliaPayid paymentAccount = (AustraliaPayid) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("AUD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PAY_ID), paymentAccount.getPayid());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateCashDepositAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CASH_DEPOSIT_ID);
verifyEmptyForm(emptyForm,
CASH_DEPOSIT_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_ID,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_EMAIL,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID,
PROPERTY_NAME_REQUIREMENTS);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CASH_DEPOSIT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Cash Deposit Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "4444 5555 6666");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ID, "0001");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "BoF");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "99-8888-7654");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "FR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_EMAIL, "jean@johnson.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jean Johnson");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_REQUIREMENTS, "Requirements...");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
CashDepositAccount paymentAccount = (CashDepositAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
CashDepositAccountPayload payload = (CashDepositAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ID), payload.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_EMAIL), payload.getHolderEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_REQUIREMENTS), payload.getRequirements());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateBrazilNationalBankAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, NATIONAL_BANK_ID);
verifyEmptyForm(emptyForm,
NATIONAL_BANK_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, NATIONAL_BANK_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Banco do Brasil");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "456789-87");
// No BankId is required for BR.
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Banco do Brasil");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "456789-10");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Joao da Silva");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Banco do Brasil Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
NationalBankAccount paymentAccount = (NationalBankAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
BankAccountPayload payload = (BankAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
// When no BankId is required, getBankId() returns bankName.
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateChaseQuickPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CHASE_QUICK_PAY_ID);
verifyEmptyForm(emptyForm,
CHASE_QUICK_PAY_ID,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CHASE_QUICK_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Quick Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "johndoe@quickpay.com");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
ChaseQuickPayAccount paymentAccount = (ChaseQuickPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateClearXChangeAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, CLEAR_X_CHANGE_ID);
verifyEmptyForm(emptyForm,
CLEAR_X_CHANGE_ID,
PROPERTY_NAME_EMAIL_OR_MOBILE_NR,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, CLEAR_X_CHANGE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "USD Zelle Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL_OR_MOBILE_NR, "jane@doe.com");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Zelle Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
ClearXchangeAccount paymentAccount = (ClearXchangeAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL_OR_MOBILE_NR), paymentAccount.getEmailOrMobileNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateF2FAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, F2F_ID);
verifyEmptyForm(emptyForm,
F2F_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_CITY,
PROPERTY_NAME_CONTACT,
PROPERTY_NAME_EXTRA_INFO);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, F2F_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Cara a Cara");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "BR");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Rio de Janeiro");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_CONTACT, "Freddy Beira Mar");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EXTRA_INFO, "So fim de semana");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
F2FAccount paymentAccount = (F2FAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("BRL", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CONTACT), paymentAccount.getContact());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EXTRA_INFO), paymentAccount.getExtraInfo());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateFasterPaymentsAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, FASTER_PAYMENTS_ID);
verifyEmptyForm(emptyForm,
FASTER_PAYMENTS_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_SORT_CODE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, FASTER_PAYMENTS_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Faster Payments Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "9999 8888 7777");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SORT_CODE, "3127");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Faster Payments Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
FasterPaymentsAccount paymentAccount = (FasterPaymentsAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SORT_CODE), paymentAccount.getSortCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateHalCashAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, HAL_CASH_ID);
verifyEmptyForm(emptyForm,
HAL_CASH_ID,
PROPERTY_NAME_MOBILE_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, HAL_CASH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Hal Cash Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "798 123 456");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
HalCashAccount paymentAccount = (HalCashAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateInteracETransferAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, INTERAC_E_TRANSFER_ID);
verifyEmptyForm(emptyForm,
INTERAC_E_TRANSFER_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_QUESTION,
PROPERTY_NAME_ANSWER);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, INTERAC_E_TRANSFER_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Interac Transfer Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_QUESTION, "What is my dog's name?");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ANSWER, "Fido");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Interac Transfer Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
InteracETransferAccount paymentAccount = (InteracETransferAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("CAD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_QUESTION), paymentAccount.getQuestion());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ANSWER), paymentAccount.getAnswer());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateJapanBankAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, JAPAN_BANK_ID);
verifyEmptyForm(emptyForm,
JAPAN_BANK_ID,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BANK_CODE,
PROPERTY_NAME_BANK_BRANCH_CODE,
PROPERTY_NAME_BANK_BRANCH_NAME,
PROPERTY_NAME_BANK_ACCOUNT_NAME,
PROPERTY_NAME_BANK_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_ACCOUNT_NUMBER);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, JAPAN_BANK_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Fukuoka Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "Bank of Kyoto");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_CODE, "FKBKJPJT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_CODE, "8100-8727");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_BRANCH_NAME, "Fukuoka Branch");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NAME, "Fukuoka Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_TYPE, "Yen Account");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_ACCOUNT_NUMBER, "8100-8727-0000");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
JapanBankAccount paymentAccount = (JapanBankAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("JPY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_CODE), paymentAccount.getBankCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), paymentAccount.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_CODE), paymentAccount.getBankBranchCode());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_BRANCH_NAME), paymentAccount.getBankBranchName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NAME), paymentAccount.getBankAccountName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_TYPE), paymentAccount.getBankAccountType());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_ACCOUNT_NUMBER), paymentAccount.getBankAccountNumber());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateMoneyBeamAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, MONEY_BEAM_ID);
verifyEmptyForm(emptyForm,
MONEY_BEAM_ID,
PROPERTY_NAME_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_BEAM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Beam Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "MB 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Money Beam Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
MoneyBeamAccount paymentAccount = (MoneyBeamAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateMoneyGramAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, MONEY_GRAM_ID);
verifyEmptyForm(emptyForm,
MONEY_GRAM_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_EMAIL,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_STATE);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, MONEY_GRAM_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Money Gram Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "John Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "john@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "NY");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
MoneyGramAccount paymentAccount = (MoneyGramAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountTradeCurrencies(getAllMoneyGramCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreatePerfectMoneyAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PERFECT_MONEY_ID);
verifyEmptyForm(emptyForm,
PERFECT_MONEY_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PERFECT_MONEY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Perfect Money Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "PM 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Perfect Money Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
PerfectMoneyAccount paymentAccount = (PerfectMoneyAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreatePopmoneyAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, POPMONEY_ID);
verifyEmptyForm(emptyForm,
POPMONEY_ID,
PROPERTY_NAME_ACCOUNT_ID,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, POPMONEY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Pop Money Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "POPMONEY 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
PopmoneyAccount paymentAccount = (PopmoneyAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreatePromptPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, PROMPT_PAY_ID);
verifyEmptyForm(emptyForm,
PROMPT_PAY_ID,
PROPERTY_NAME_PROMPT_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, PROMPT_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Prompt Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PROMPT_PAY_ID, "PP 0000 1111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Prompt Pay Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
PromptPayAccount paymentAccount = (PromptPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("THB", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_PROMPT_PAY_ID), paymentAccount.getPromptPayId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateRevolutAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, REVOLUT_ID);
verifyEmptyForm(emptyForm,
REVOLUT_ID,
PROPERTY_NAME_USERNAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, REVOLUT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Revolut Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_USERNAME, "revolut123");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
RevolutAccount paymentAccount = (RevolutAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountTradeCurrencies(getAllRevolutCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_USERNAME), paymentAccount.getUserName());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateSameBankAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SAME_BANK_ID);
verifyEmptyForm(emptyForm,
SAME_BANK_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SAME_BANK_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Same Bank Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Same Bank Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
SameBankAccount paymentAccount = (SameBankAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
SameBankAccountPayload payload = (SameBankAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
// The bankId == bankName because bank id is not required in the UK.
assertEquals(payload.getBankId(), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateSepaInstantAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SEPA_INSTANT_ID);
verifyEmptyForm(emptyForm,
SEPA_INSTANT_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_IBAN,
PROPERTY_NAME_BIC);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_INSTANT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa Instant");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SepaInstantAccount paymentAccount = (SepaInstantAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
// bankId == bic
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateSepaAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SEPA_ID);
verifyEmptyForm(emptyForm,
SEPA_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_IBAN,
PROPERTY_NAME_BIC);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SEPA_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Conta Sepa");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "PT");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jose da Silva");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_IBAN, "909-909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BIC, "909");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Conta Sepa Salt"));
String jsonString = getCompletedFormAsJsonString();
SepaAccount paymentAccount = (SepaAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
verifyAccountSingleTradeCurrency("EUR", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_IBAN), paymentAccount.getIban());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBic());
// bankId == bic
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BIC), paymentAccount.getBankId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateSpecificBanksAccount(TestInfo testInfo) {
// TODO Supporting set of accepted banks may require some refactoring
// of the SpecificBanksAccount and SpecificBanksAccountPayload classes, i.e.,
// public void setAcceptedBanks(String... bankNames) { ... }
File emptyForm = getEmptyForm(testInfo, SPECIFIC_BANKS_ID);
verifyEmptyForm(emptyForm,
SPECIFIC_BANKS_ID,
PROPERTY_NAME_ACCOUNT_NR,
PROPERTY_NAME_ACCOUNT_TYPE,
PROPERTY_NAME_BANK_NAME,
PROPERTY_NAME_BRANCH_ID,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_HOLDER_TAX_ID,
PROPERTY_NAME_NATIONAL_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SPECIFIC_BANKS_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Specific Banks Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "000 1 4567");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_TYPE, "Checking");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BANK_NAME, "HSBC");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_BRANCH_ID, "111");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "GB");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_TAX_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_NATIONAL_ACCOUNT_ID, "123456789");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
SpecificBanksAccount paymentAccount = (SpecificBanksAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("GBP", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
SpecificBanksAccountPayload payload = (SpecificBanksAccountPayload) paymentAccount.getPaymentAccountPayload();
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), payload.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_TYPE), payload.getAccountType());
// The bankId == bankName because bank id is not required in the UK.
assertEquals(payload.getBankId(), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BANK_NAME), payload.getBankName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_BRANCH_ID), payload.getBranchId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), payload.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_TAX_ID), payload.getHolderTaxId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_NATIONAL_ACCOUNT_ID), payload.getNationalAccountId());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateSwishAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, SWISH_ID);
verifyEmptyForm(emptyForm,
SWISH_ID,
PROPERTY_NAME_MOBILE_NR,
PROPERTY_NAME_HOLDER_NAME);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, SWISH_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Swish Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_MOBILE_NR, "+46 7 6060 0101");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Swish Acct Holder");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Swish Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
SwishAccount paymentAccount = (SwishAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("SEK", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_MOBILE_NR), paymentAccount.getMobileNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateTransferwiseAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, TRANSFERWISE_ID);
verifyEmptyForm(emptyForm,
TRANSFERWISE_ID,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, TRANSFERWISE_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Transferwise Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jan@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
TransferwiseAccount paymentAccount = (TransferwiseAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountTradeCurrencies(getAllTransferwiseCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateUpholdAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, UPHOLD_ID);
verifyEmptyForm(emptyForm,
UPHOLD_ID,
PROPERTY_NAME_ACCOUNT_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, UPHOLD_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Uphold Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_ID, "UA 9876");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored Uphold Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
UpholdAccount paymentAccount = (UpholdAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountTradeCurrencies(getAllUpholdCurrencies(), paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_ID), paymentAccount.getAccountId());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateUSPostalMoneyOrderAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, US_POSTAL_MONEY_ORDER_ID);
verifyEmptyForm(emptyForm,
US_POSTAL_MONEY_ORDER_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_POSTAL_ADDRESS);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, US_POSTAL_MONEY_ORDER_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Bubba's Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Bubba");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_POSTAL_ADDRESS, "000 Westwood Terrace Austin, TX 78700");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
USPostalMoneyOrderAccount paymentAccount = (USPostalMoneyOrderAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getHolderName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_POSTAL_ADDRESS), paymentAccount.getPostalAddress());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateWeChatPayAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, WECHAT_PAY_ID);
verifyEmptyForm(emptyForm,
WECHAT_PAY_ID,
PROPERTY_NAME_ACCOUNT_NR);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WECHAT_PAY_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "WeChat Pay Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NR, "WC 1234");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, encodeToHex("Restored WeChat Pay Acct Salt"));
String jsonString = getCompletedFormAsJsonString();
WeChatPayAccount paymentAccount = (WeChatPayAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("CNY", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_ACCOUNT_NR), paymentAccount.getAccountNr());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_SALT), paymentAccount.getSaltAsHex());
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@Test
public void testCreateWesternUnionAccount(TestInfo testInfo) {
File emptyForm = getEmptyForm(testInfo, WESTERN_UNION_ID);
verifyEmptyForm(emptyForm,
WESTERN_UNION_ID,
PROPERTY_NAME_HOLDER_NAME,
PROPERTY_NAME_CITY,
PROPERTY_NAME_STATE,
PROPERTY_NAME_COUNTRY,
PROPERTY_NAME_EMAIL);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_PAYMENT_METHOD_ID, WESTERN_UNION_ID);
COMPLETED_FORM_MAP.put(PROPERTY_NAME_ACCOUNT_NAME, "Western Union Acct");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_HOLDER_NAME, "Jane Doe");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_CITY, "Fargo");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_STATE, "North Dakota");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_COUNTRY, "US");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_EMAIL, "jane@doe.info");
COMPLETED_FORM_MAP.put(PROPERTY_NAME_SALT, "");
String jsonString = getCompletedFormAsJsonString();
WesternUnionAccount paymentAccount = (WesternUnionAccount) createPaymentAccount(alicedaemon, jsonString);
verifyUserPayloadHasPaymentAccountWithId(paymentAccount.getId());
verifyAccountSingleTradeCurrency("USD", paymentAccount);
verifyCommonFormEntries(paymentAccount);
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_HOLDER_NAME), paymentAccount.getFullName());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_CITY), paymentAccount.getCity());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_STATE), paymentAccount.getState());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_EMAIL), paymentAccount.getEmail());
assertEquals(COMPLETED_FORM_MAP.get(PROPERTY_NAME_COUNTRY),
Objects.requireNonNull(paymentAccount.getCountry()).code);
log.debug("Deserialized {}: {}", paymentAccount.getClass().getSimpleName(), paymentAccount);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,55 @@
package bisq.apitest.method.payment;
import protobuf.PaymentMethod;
import java.util.List;
import java.util.stream.Collectors;
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.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 org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import bisq.apitest.method.MethodTest;
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class GetPaymentMethodsTest extends MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetPaymentMethods() {
List<String> paymentMethodIds = getPaymentMethods(alicedaemon)
.stream()
.map(PaymentMethod::getId)
.collect(Collectors.toList());
assertTrue(paymentMethodIds.size() >= 20);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -27,13 +27,25 @@ public class AbstractTradeTest extends AbstractOfferTest {
EXPECTED_PROTOCOL_STATUS.init();
}
protected final TradeInfo takeAlicesOffer(String offerId, String paymentAccountId) {
return bobStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade();
protected final TradeInfo takeAlicesOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return bobStubs.tradesService.takeOffer(
createTakeOfferRequest(offerId,
paymentAccountId,
takerFeeCurrencyCode))
.getTrade();
}
@SuppressWarnings("unused")
protected final TradeInfo takeBobsOffer(String offerId, String paymentAccountId) {
return aliceStubs.tradesService.takeOffer(createTakeOfferRequest(offerId, paymentAccountId)).getTrade();
protected final TradeInfo takeBobsOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode) {
return aliceStubs.tradesService.takeOffer(
createTakeOfferRequest(offerId,
paymentAccountId,
takerFeeCurrencyCode))
.getTrade();
}
protected final void verifyExpectedProtocolStatus(TradeInfo trade) {

View file

@ -17,6 +17,8 @@
package bisq.apitest.method.trade;
import bisq.proto.grpc.BtcBalanceInfo;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
@ -37,6 +39,7 @@ 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_PAID;
@ -49,6 +52,9 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
// Alice is buyer, Bob is seller.
// Maker and Taker fees are in BSQ.
private static final String TRADE_FEE_CURRENCY_CODE = "bsq";
@Test
@Order(1)
public void testTakeAlicesBuyOffer(final TestInfo testInfo) {
@ -56,17 +62,20 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
var alicesOffer = createAliceOffer(alicesDummyAcct,
"buy",
"usd",
12500000);
12500000,
TRADE_FEE_CURRENCY_CODE);
var offerId = alicesOffer.getId();
assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc());
// Wait for Alice's AddToOfferBook task.
// Wait times vary; my logs show >= 2 second delay.
sleep(3000);
sleep(3000); // TODO loop instead of hard code wait time
assertEquals(1, getOpenOffersCount(aliceStubs, "buy", "usd"));
var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId());
var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId(), TRADE_FEE_CURRENCY_CODE);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
assertFalse(trade.getIsCurrencyForTakerFeeBtc());
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
@ -147,8 +156,9 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
.setPhase(PAYOUT_PUBLISHED);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after keeping funds", trade);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
log.info("{} Alice's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(getBalance(alicedaemon)));
formatSatoshis(currentBalance.getAvailableBalance()));
}
}

View file

@ -17,6 +17,8 @@
package bisq.apitest.method.trade;
import bisq.proto.grpc.BtcBalanceInfo;
import io.grpc.StatusRuntimeException;
import lombok.extern.slf4j.Slf4j;
@ -35,6 +37,7 @@ 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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_PAID;
import static protobuf.OpenOffer.State.AVAILABLE;
@ -46,6 +49,9 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
// Alice is seller, Bob is buyer.
// Maker and Taker fees are in BTC.
private static final String TRADE_FEE_CURRENCY_CODE = "btc";
@Test
@Order(1)
public void testTakeAlicesSellOffer(final TestInfo testInfo) {
@ -53,18 +59,21 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
var alicesOffer = createAliceOffer(alicesDummyAcct,
"sell",
"usd",
12500000);
12500000,
TRADE_FEE_CURRENCY_CODE);
var offerId = alicesOffer.getId();
assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc());
// 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);
sleep(3000); // TODO loop instead of hard code wait time
assertEquals(1, getOpenOffersCount(bobStubs, "sell", "usd"));
var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId());
var trade = takeAlicesOffer(offerId, bobsDummyAcct.getId(), TRADE_FEE_CURRENCY_CODE);
assertNotNull(trade);
assertEquals(offerId, trade.getTradeId());
assertTrue(trade.getIsCurrencyForTakerFeeBtc());
// Cache the trade id for the other tests.
tradeId = trade.getTradeId();
@ -148,8 +157,9 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
.setWithdrawn(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after withdrawing funds to external wallet", trade);
BtcBalanceInfo currentBalance = getBtcBalances(bobdaemon);
log.info("{} Bob's current available balance: {} BTC",
testName(testInfo),
formatSatoshis(getBalance(bobdaemon)));
formatSatoshis(currentBalance.getAvailableBalance()));
}
}

View file

@ -0,0 +1,244 @@
package bisq.apitest.method.wallet;
import bisq.proto.grpc.BsqBalanceInfo;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
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.TestInfo;
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.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.cli.TableFormat.formatBsqBalanceInfoTbl;
import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_MAINNET;
import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_REGTEST;
import static org.bitcoinj.core.NetworkParameters.PAYMENT_PROTOCOL_ID_TESTNET;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.config.BisqAppConfig;
import bisq.apitest.method.MethodTest;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class BsqWalletTest extends MethodTest {
// Alice's regtest BSQ wallet is initialized with 1,000,000 BSQ.
private static final bisq.core.api.model.BsqBalanceInfo ALICES_INITIAL_BSQ_BALANCES =
expectedBsqBalanceModel(100000000,
0,
0,
0,
0,
0);
// Bob's regtest BSQ wallet is initialized with 1,500,000 BSQ.
private static final bisq.core.api.model.BsqBalanceInfo BOBS_INITIAL_BSQ_BALANCES =
expectedBsqBalanceModel(150000000,
0,
0,
0,
0,
0);
private static final String SEND_BSQ_AMOUNT = "25000.50";
@BeforeAll
public static void setUp() {
startSupportingApps(false,
true,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testGetUnusedBsqAddress() {
var request = createGetUnusedBsqAddressRequest();
String address = grpcStubs(alicedaemon).walletsService.getUnusedBsqAddress(request).getAddress();
assertFalse(address.isEmpty());
assertTrue(address.startsWith("B"));
NetworkParameters networkParameters = LegacyAddress.getParametersFromAddress(address.substring(1));
String addressNetwork = networkParameters.getPaymentProtocolId();
assertNotEquals(PAYMENT_PROTOCOL_ID_MAINNET, addressNetwork);
// TODO Fix bug causing the regtest bsq address network to be evaluated as 'testnet' here.
assertTrue(addressNetwork.equals(PAYMENT_PROTOCOL_ID_TESTNET)
|| addressNetwork.equals(PAYMENT_PROTOCOL_ID_REGTEST));
}
@Test
@Order(2)
public void testInitialBsqBalances(final TestInfo testInfo) {
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
log.debug("{} -> Alice's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(alicesBsqBalances));
verifyBsqBalances(ALICES_INITIAL_BSQ_BALANCES, alicesBsqBalances);
BsqBalanceInfo bobsBsqBalances = getBsqBalances(bobdaemon);
log.debug("{} -> Bob's BSQ Initial Balances -> \n{}",
testName(testInfo),
formatBsqBalanceInfoTbl(bobsBsqBalances));
verifyBsqBalances(BOBS_INITIAL_BSQ_BALANCES, bobsBsqBalances);
}
@Test
@Order(3)
public void testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(final TestInfo testInfo) {
String bobsBsqAddress = getUnusedBsqAddress(bobdaemon);
sendBsq(alicedaemon, bobsBsqAddress, SEND_BSQ_AMOUNT);
sleep(2000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForNonZeroUnverifiedBalance(bobdaemon);
log.debug("BSQ Balances Before BTC Block Gen...");
printBobAndAliceBsqBalances(testInfo,
bobsBsqBalances,
alicesBsqBalances,
alicedaemon);
verifyBsqBalances(expectedBsqBalanceModel(150000000,
2500050,
0,
0,
0,
0),
bobsBsqBalances);
verifyBsqBalances(expectedBsqBalanceModel(97499950,
97499950,
97499950,
0,
0,
0),
alicesBsqBalances);
}
@Test
@Order(4)
public void testBalancesAfterSendingBsqAndGeneratingBtcBlock(final TestInfo testInfo) {
// There is a wallet persist delay; we have to
// wait for both wallets to be saved to disk.
genBtcBlocksThenWait(1, 4000);
BsqBalanceInfo alicesBsqBalances = getBsqBalances(alicedaemon);
BsqBalanceInfo bobsBsqBalances = waitForNewAvailableConfirmedBalance(bobdaemon, 150000000);
log.debug("See Available Confirmed BSQ Balances...");
printBobAndAliceBsqBalances(testInfo,
bobsBsqBalances,
alicesBsqBalances,
alicedaemon);
verifyBsqBalances(expectedBsqBalanceModel(152500050,
0,
0,
0,
0,
0),
bobsBsqBalances);
verifyBsqBalances(expectedBsqBalanceModel(97499950,
0,
0,
0,
0,
0),
alicesBsqBalances);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
private void verifyBsqBalances(bisq.core.api.model.BsqBalanceInfo expected,
BsqBalanceInfo actual) {
assertEquals(expected.getAvailableConfirmedBalance(), actual.getAvailableConfirmedBalance());
assertEquals(expected.getUnverifiedBalance(), actual.getUnverifiedBalance());
assertEquals(expected.getUnconfirmedChangeBalance(), actual.getUnconfirmedChangeBalance());
assertEquals(expected.getLockedForVotingBalance(), actual.getLockedForVotingBalance());
assertEquals(expected.getLockupBondsBalance(), actual.getLockupBondsBalance());
assertEquals(expected.getUnlockingBondsBalance(), actual.getUnlockingBondsBalance());
}
private BsqBalanceInfo waitForNonZeroUnverifiedBalance(BisqAppConfig daemon) {
// A BSQ recipient needs to wait for her daemon to detect a new tx.
// Loop here until her unverifiedBalance != 0, or give up after 15 seconds.
// A slow test is preferred over a flaky test.
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
for (int numRequests = 1; numRequests <= 15 && bsqBalance.getUnverifiedBalance() == 0; numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
}
return bsqBalance;
}
private BsqBalanceInfo waitForNewAvailableConfirmedBalance(BisqAppConfig daemon,
long staleBalance) {
BsqBalanceInfo bsqBalance = getBsqBalances(daemon);
for (int numRequests = 1;
numRequests <= 15 && bsqBalance.getAvailableConfirmedBalance() == staleBalance;
numRequests++) {
sleep(1000);
bsqBalance = getBsqBalances(daemon);
}
return bsqBalance;
}
@SuppressWarnings("SameParameterValue")
private void printBobAndAliceBsqBalances(final TestInfo testInfo,
BsqBalanceInfo bobsBsqBalances,
BsqBalanceInfo alicesBsqBalances,
BisqAppConfig senderApp) {
log.debug("{} -> Bob's BSQ Balances After {} {} BSQ-> \n{}",
testName(testInfo),
senderApp.equals(bobdaemon) ? "Sending" : "Receiving",
SEND_BSQ_AMOUNT,
formatBsqBalanceInfoTbl(bobsBsqBalances));
log.debug("{} -> Alice's Balances After {} {} BSQ-> \n{}",
testName(testInfo),
senderApp.equals(alicedaemon) ? "Sending" : "Receiving",
SEND_BSQ_AMOUNT,
formatBsqBalanceInfoTbl(alicesBsqBalances));
}
@SuppressWarnings("SameParameterValue")
private static bisq.core.api.model.BsqBalanceInfo expectedBsqBalanceModel(long availableConfirmedBalance,
long unverifiedBalance,
long unconfirmedChangeBalance,
long lockedForVotingBalance,
long lockupBondsBalance,
long unlockingBondsBalance) {
return bisq.core.api.model.BsqBalanceInfo.valueOf(availableConfirmedBalance,
unverifiedBalance,
unconfirmedChangeBalance,
lockedForVotingBalance,
lockupBondsBalance,
unlockingBondsBalance);
}
}

View file

@ -0,0 +1,76 @@
package bisq.apitest.method.wallet;
import bisq.core.api.model.TxFeeRateInfo;
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.TestInfo;
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.method.MethodTest;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class BtcTxFeeRateTest extends MethodTest {
@BeforeAll
public static void setUp() {
startSupportingApps(false,
true,
bitcoind,
seednode,
alicedaemon);
}
@Test
@Order(1)
public void testGetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = getTxFeeRate(alicedaemon);
log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo);
assertFalse(txFeeRateInfo.isUseCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
}
@Test
@Order(2)
public void testSetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = setTxFeeRate(alicedaemon, 10);
log.debug("{} -> Fee rates with custom preference: {}", testName(testInfo), txFeeRateInfo);
assertTrue(txFeeRateInfo.isUseCustomTxFeeRate());
assertEquals(10, txFeeRateInfo.getCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
}
@Test
@Order(3)
public void testUnsetTxFeeRate(final TestInfo testInfo) {
TxFeeRateInfo txFeeRateInfo = unsetTxFeeRate(alicedaemon);
log.debug("{} -> Fee rate with no preference: {}", testName(testInfo), txFeeRateInfo);
assertFalse(txFeeRateInfo.isUseCustomTxFeeRate());
assertTrue(txFeeRateInfo.getFeeServiceRate() > 0);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,107 @@
package bisq.apitest.method.wallet;
import bisq.proto.grpc.BtcBalanceInfo;
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.TestInfo;
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.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.cli.TableFormat.formatAddressBalanceTbl;
import static bisq.cli.TableFormat.formatBtcBalanceInfoTbl;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.method.MethodTest;
@Disabled
@Slf4j
@TestMethodOrder(OrderAnnotation.class)
public class BtcWalletTest extends MethodTest {
// All api tests depend on the DAO / regtest environment, and Bob & Alice's wallets
// are initialized with 10 BTC during the scaffolding setup.
private static final bisq.core.api.model.BtcBalanceInfo INITIAL_BTC_BALANCES =
bisq.core.api.model.BtcBalanceInfo.valueOf(1000000000,
0,
1000000000,
0);
@BeforeAll
public static void setUp() {
startSupportingApps(false,
true,
bitcoind,
seednode,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testInitialBtcBalances(final TestInfo testInfo) {
// Bob & Alice's regtest Bisq wallets were initialized with 10 BTC.
BtcBalanceInfo alicesBalances = getBtcBalances(alicedaemon);
log.info("{} Alice's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(alicesBalances));
BtcBalanceInfo bobsBalances = getBtcBalances(bobdaemon);
log.info("{} Bob's BTC Balances:\n{}", testName(testInfo), formatBtcBalanceInfoTbl(bobsBalances));
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), alicesBalances.getAvailableBalance());
assertEquals(INITIAL_BTC_BALANCES.getAvailableBalance(), bobsBalances.getAvailableBalance());
}
@Test
@Order(2)
public void testFundAlicesBtcWallet(final TestInfo testInfo) {
String newAddress = getUnusedBtcAddress(alicedaemon);
bitcoinCli.sendToAddress(newAddress, "2.5");
genBtcBlocksThenWait(1, 1500);
BtcBalanceInfo btcBalanceInfo = getBtcBalances(alicedaemon);
// New balance is 12.5 BTC
assertEquals(1250000000, btcBalanceInfo.getAvailableBalance());
log.info("{} -> Alice's Funded Address Balance -> \n{}",
testName(testInfo),
formatAddressBalanceTbl(singletonList(getAddressBalance(alicedaemon, newAddress))));
// New balance is 12.5 BTC
btcBalanceInfo = getBtcBalances(alicedaemon);
bisq.core.api.model.BtcBalanceInfo alicesExpectedBalances =
bisq.core.api.model.BtcBalanceInfo.valueOf(1250000000,
0,
1250000000,
0);
verifyBtcBalances(alicesExpectedBalances, btcBalanceInfo);
log.info("{} -> Alice's BTC Balances After Sending 2.5 BTC -> \n{}",
testName(testInfo),
formatBtcBalanceInfoTbl(btcBalanceInfo));
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
private void verifyBtcBalances(bisq.core.api.model.BtcBalanceInfo expected,
BtcBalanceInfo actual) {
assertEquals(expected.getAvailableBalance(), actual.getAvailableBalance());
assertEquals(expected.getReservedBalance(), actual.getReservedBalance());
assertEquals(expected.getTotalAvailableBalance(), actual.getTotalAvailableBalance());
assertEquals(expected.getLockedBalance(), actual.getLockedBalance());
}
}

View file

@ -1,4 +1,4 @@
package bisq.apitest.method;
package bisq.apitest.method.wallet;
import io.grpc.StatusRuntimeException;
@ -18,6 +18,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import bisq.apitest.method.MethodTest;
@SuppressWarnings("ResultOfMethodCallIgnored")
@Disabled
@Slf4j
@ -44,7 +48,7 @@ public class WalletProtectionTest extends MethodTest {
@Test
@Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@ -53,9 +57,9 @@ public class WalletProtectionTest extends MethodTest {
public void testUnlockWalletFor4Seconds() {
var request = createUnlockWalletRequest("first-password", 4);
grpcStubs(alicedaemon).walletsService.unlockWallet(request);
getBalance(alicedaemon); // should not throw 'wallet locked' exception
getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@ -65,7 +69,7 @@ public class WalletProtectionTest extends MethodTest {
var request = createUnlockWalletRequest("first-password", 3);
grpcStubs(alicedaemon).walletsService.unlockWallet(request);
sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@ -75,7 +79,7 @@ public class WalletProtectionTest extends MethodTest {
unlockWallet(alicedaemon, "first-password", 60);
var request = createLockWalletRequest();
grpcStubs(alicedaemon).walletsService.lockWallet(request);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBalance(alicedaemon));
Throwable exception = assertThrows(StatusRuntimeException.class, () -> getBtcBalances(alicedaemon));
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
}
@ -95,7 +99,7 @@ public class WalletProtectionTest extends MethodTest {
sleep(500); // override unlock timeout after 0.5s
unlockWallet(alicedaemon, "first-password", 6);
sleep(5000);
getBalance(alicedaemon); // getbalance 5s after resetting unlock timeout to 6s
getBtcBalances(alicedaemon); // getbalance 5s after overriding timeout to 6s
}
@Test
@ -105,7 +109,7 @@ public class WalletProtectionTest extends MethodTest {
"first-password", "second-password");
grpcStubs(alicedaemon).walletsService.setWalletPassword(request);
unlockWallet(alicedaemon, "second-password", 2);
getBalance(alicedaemon);
getBtcBalances(alicedaemon);
sleep(2500); // allow time for wallet save
}
@ -124,7 +128,7 @@ public class WalletProtectionTest extends MethodTest {
public void testRemoveNewWalletPassword() {
var request = createRemoveWalletPasswordRequest("second-password");
grpcStubs(alicedaemon).walletsService.removeWalletPassword(request);
getBalance(alicedaemon); // should not throw 'wallet locked' exception
getBtcBalances(alicedaemon); // should not throw 'wallet locked' exception
}
@AfterAll

View file

@ -1,76 +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.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 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 MethodTest {
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, alicedaemon);
bitcoinCli.generateBlocks(1);
MILLISECONDS.sleep(1500);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testFundWallet() {
// 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
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -0,0 +1,90 @@
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.TestInfo;
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.payment.AbstractPaymentAccountTest;
import bisq.apitest.method.payment.CreatePaymentAccountTest;
import bisq.apitest.method.payment.GetPaymentMethodsTest;
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class PaymentAccountTest extends AbstractPaymentAccountTest {
// Two dummy (usd +eth) accounts are set up as defaults in regtest / dao mode,
// then we add 28 more payment accounts in testCreatePaymentAccount().
private static final int EXPECTED_NUM_PAYMENT_ACCOUNTS = 2 + 28;
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, alicedaemon);
} catch (Exception ex) {
fail(ex);
}
}
@Test
@Order(1)
public void testGetPaymentMethods() {
GetPaymentMethodsTest test = new GetPaymentMethodsTest();
test.testGetPaymentMethods();
}
@Test
@Order(2)
public void testCreatePaymentAccount(TestInfo testInfo) {
CreatePaymentAccountTest test = new CreatePaymentAccountTest();
test.testCreateAdvancedCashAccount(testInfo);
test.testCreateAliPayAccount(testInfo);
test.testCreateAustraliaPayidAccount(testInfo);
test.testCreateCashDepositAccount(testInfo);
test.testCreateBrazilNationalBankAccount(testInfo);
test.testCreateChaseQuickPayAccount(testInfo);
test.testCreateClearXChangeAccount(testInfo);
test.testCreateF2FAccount(testInfo);
test.testCreateFasterPaymentsAccount(testInfo);
test.testCreateHalCashAccount(testInfo);
test.testCreateInteracETransferAccount(testInfo);
test.testCreateJapanBankAccount(testInfo);
test.testCreateMoneyBeamAccount(testInfo);
test.testCreateMoneyGramAccount(testInfo);
test.testCreatePerfectMoneyAccount(testInfo);
test.testCreatePopmoneyAccount(testInfo);
test.testCreatePromptPayAccount(testInfo);
test.testCreateRevolutAccount(testInfo);
test.testCreateSameBankAccount(testInfo);
test.testCreateSepaInstantAccount(testInfo);
test.testCreateSepaAccount(testInfo);
test.testCreateSpecificBanksAccount(testInfo);
test.testCreateSwishAccount(testInfo);
test.testCreateTransferwiseAccount(testInfo);
test.testCreateUpholdAccount(testInfo);
test.testCreateUSPostalMoneyOrderAccount(testInfo);
test.testCreateWeChatPayAccount(testInfo);
test.testCreateWesternUnionAccount(testInfo);
assertEquals(EXPECTED_NUM_PAYMENT_ACCOUNTS, getPaymentAccounts(alicedaemon).size());
}
@AfterAll
public static void tearDown() {
tearDownScaffold();
}
}

View file

@ -34,7 +34,6 @@ 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;
@ -71,13 +70,6 @@ public class StartupTest extends MethodTest {
test.testRegisterRefundAgent();
}
@Test
@Order(3)
public void testCreatePaymentAccount() {
CreatePaymentAccountTest test = new CreatePaymentAccountTest();
test.testCreatePerfectMoneyUSDPaymentAccount();
}
@AfterAll
public static void tearDown() {
tearDownScaffold();

View file

@ -24,60 +24,65 @@ 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.TestInfo;
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.bobdaemon;
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;
import bisq.apitest.method.wallet.BsqWalletTest;
import bisq.apitest.method.wallet.BtcTxFeeRateTest;
import bisq.apitest.method.wallet.BtcWalletTest;
import bisq.apitest.method.wallet.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.
// 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.
@BeforeAll
public static void setUp() {
try {
setUpScaffold(bitcoind, seednode, alicedaemon);
genBtcBlocksThenWait(1, 1500);
} catch (Exception ex) {
fail(ex);
}
startSupportingApps(true,
true,
bitcoind,
seednode,
arbdaemon,
alicedaemon,
bobdaemon);
}
@Test
@Order(1)
public void testFundWallet() {
// The regtest Bisq wallet was initialized with 10 BTC.
long balance = getBalance(alicedaemon);
assertEquals(1000000000, balance);
public void testBtcWalletFunding(final TestInfo testInfo) {
BtcWalletTest btcWalletTest = new BtcWalletTest();
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
btcWalletTest.testInitialBtcBalances(testInfo);
btcWalletTest.testFundAlicesBtcWallet(testInfo);
}
@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.
public void testBsqWalletFunding(final TestInfo testInfo) {
BsqWalletTest bsqWalletTest = new BsqWalletTest();
bsqWalletTest.testGetUnusedBsqAddress();
bsqWalletTest.testInitialBsqBalances(testInfo);
bsqWalletTest.testSendBsqAndCheckBalancesBeforeGeneratingBtcBlock(testInfo);
bsqWalletTest.testBalancesAfterSendingBsqAndGeneratingBtcBlock(testInfo);
}
@Test
@Order(3)
public void testWalletProtection() {
WalletProtectionTest walletProtectionTest = new WalletProtectionTest();
walletProtectionTest.testSetWalletPassword();
@ -92,6 +97,16 @@ public class WalletTest extends MethodTest {
walletProtectionTest.testRemoveNewWalletPassword();
}
@Test
@Order(4)
public void testTxFeeRateMethods(final TestInfo testInfo) {
BtcTxFeeRateTest test = new BtcTxFeeRateTest();
test.testGetTxFeeRate(testInfo);
test.testSetTxFeeRate(testInfo);
test.testUnsetTxFeeRate(testInfo);
}
@AfterAll
public static void tearDown() {
tearDownScaffold();

View file

@ -23,42 +23,58 @@ import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
import io.grpc.StatusRuntimeException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo;
import static bisq.cli.CurrencyFormat.toSatoshis;
import static bisq.cli.NegativeNumberOptions.hasNegativeNumberOptions;
import static bisq.cli.TableFormat.formatAddressBalanceTbl;
import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.cli.TableFormat.formatPaymentAcctTbl;
import static bisq.cli.TableFormat.*;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.lang.System.exit;
@ -84,12 +100,19 @@ public class CliMain {
confirmpaymentreceived,
keepfunds,
withdrawfunds,
getpaymentmethods,
getpaymentacctform,
createpaymentacct,
getpaymentaccts,
getversion,
getbalance,
getaddressbalance,
getfundingaddresses,
getunusedbsqaddress,
sendbsq,
gettxfeerate,
settxfeerate,
unsettxfeerate,
lockwallet,
unlockwallet,
removewalletpassword,
@ -183,10 +206,25 @@ public class CliMain {
return;
}
case getbalance: {
var request = GetBalanceRequest.newBuilder().build();
var reply = walletsService.getBalance(request);
var btcBalance = formatSatoshis(reply.getBalance());
out.println(btcBalance);
var currencyCode = nonOptionArgs.size() == 2
? nonOptionArgs.get(1)
: "";
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
var reply = walletsService.getBalances(request);
switch (currencyCode.toUpperCase()) {
case "BSQ":
out.println(formatBsqBalanceInfoTbl(reply.getBalances().getBsq()));
break;
case "BTC":
out.println(formatBtcBalanceInfoTbl(reply.getBalances().getBtc()));
break;
case "":
default:
out.println(formatBalancesTbls(reply.getBalances()));
break;
}
return;
}
case getaddressbalance: {
@ -205,11 +243,73 @@ public class CliMain {
out.println(formatAddressBalanceTbl(reply.getAddressBalanceInfoList()));
return;
}
case getunusedbsqaddress: {
var request = GetUnusedBsqAddressRequest.newBuilder().build();
var reply = walletsService.getUnusedBsqAddress(request);
out.println(reply.getAddress());
return;
}
case sendbsq: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no bsq address specified");
var address = nonOptionArgs.get(1);
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("no bsq amount specified");
var amount = nonOptionArgs.get(2);
try {
Double.parseDouble(amount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", amount));
}
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.build();
walletsService.sendBsq(request);
out.printf("%s BSQ sent to %s%n", amount, address);
return;
}
case gettxfeerate: {
var request = GetTxFeeRateRequest.newBuilder().build();
var reply = walletsService.getTxFeeRate(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
return;
}
case settxfeerate: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no tx fee rate specified");
long txFeeRate;
try {
txFeeRate = Long.parseLong(nonOptionArgs.get(2));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("'%s' is not a number", nonOptionArgs.get(2)));
}
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
var reply = walletsService.setTxFeeRatePreference(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
return;
}
case unsettxfeerate: {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
var reply = walletsService.unsetTxFeeRatePreference(request);
out.println(formatTxFeeRateInfo(reply.getTxFeeRateInfo()));
return;
}
case createoffer: {
if (nonOptionArgs.size() < 9)
throw new IllegalArgumentException("incorrect parameter count,"
+ " expecting payment acct id, buy | sell, currency code, amount, min amount,"
+ " use-market-based-price, fixed-price | mkt-price-margin, security-deposit");
+ " use-market-based-price, fixed-price | mkt-price-margin, security-deposit"
+ " [,maker-fee-currency-code = bsq|btc]");
var paymentAcctId = nonOptionArgs.get(1);
var direction = nonOptionArgs.get(2);
@ -223,7 +323,11 @@ public class CliMain {
marketPriceMargin = new BigDecimal(nonOptionArgs.get(7));
else
fixedPrice = nonOptionArgs.get(7);
var securityDeposit = new BigDecimal(nonOptionArgs.get(8));
var makerFeeCurrencyCode = nonOptionArgs.size() == 10
? nonOptionArgs.get(9)
: "btc";
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
@ -235,6 +339,7 @@ public class CliMain {
.setMarketPriceMargin(marketPriceMargin.doubleValue())
.setBuyerSecurityDeposit(securityDeposit.doubleValue())
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.build();
var reply = offersService.createOffer(request);
out.println(formatOfferTable(singletonList(reply.getOffer()), currencyCode));
@ -278,26 +383,40 @@ public class CliMain {
.setCurrencyCode(currencyCode)
.build();
var reply = offersService.getOffers(request);
out.println(formatOfferTable(reply.getOffersList(), currencyCode));
List<OfferInfo> offers = reply.getOffersList();
if (offers.isEmpty())
out.printf("no %s %s offers found%n", direction, currencyCode);
else
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");
throw new IllegalArgumentException("incorrect parameter count, " +
" expecting offer id, payment acct id [,taker fee currency code = bsq|btc]");
var offerId = nonOptionArgs.get(1);
var paymentAccountId = nonOptionArgs.get(2);
var takerFeeCurrencyCode = nonOptionArgs.size() == 4
? nonOptionArgs.get(3)
: "btc";
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
var reply = tradesService.takeOffer(request);
out.printf("trade '%s' successfully taken", reply.getTrade().getShortId());
out.printf("trade %s successfully taken%n", reply.getTrade().getTradeId());
return;
}
case gettrade: {
// TODO make short-id a valid argument
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("incorrect parameter count, expecting trade id, [,showcontract = true|false]");
throw new IllegalArgumentException("incorrect parameter count, "
+ " expecting trade id [,showcontract = true|false]");
var tradeId = nonOptionArgs.get(1);
var showContract = false;
@ -323,7 +442,7 @@ public class CliMain {
.setTradeId(tradeId)
.build();
tradesService.confirmPaymentStarted(request);
out.printf("trade '%s' payment started message sent", tradeId);
out.printf("trade %s payment started message sent%n", tradeId);
return;
}
case confirmpaymentreceived: {
@ -335,7 +454,7 @@ public class CliMain {
.setTradeId(tradeId)
.build();
tradesService.confirmPaymentReceived(request);
out.printf("trade '%s' payment received message sent", tradeId);
out.printf("trade %s payment received message sent%n", tradeId);
return;
}
case keepfunds: {
@ -347,12 +466,13 @@ public class CliMain {
.setTradeId(tradeId)
.build();
tradesService.keepFunds(request);
out.printf("funds from trade '%s' saved in bisq wallet", tradeId);
out.printf("funds from trade %s saved in bisq wallet%n", tradeId);
return;
}
case withdrawfunds: {
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("incorrect parameter count, expecting trade id, bitcoin wallet address");
throw new IllegalArgumentException("incorrect parameter count, "
+ " expecting trade id, bitcoin wallet address");
var tradeId = nonOptionArgs.get(1);
var address = nonOptionArgs.get(2);
@ -361,33 +481,70 @@ public class CliMain {
.setAddress(address)
.build();
tradesService.withdrawFunds(request);
out.printf("funds from trade '%s' sent to btc address '%s'", tradeId, address);
out.printf("funds from trade %s sent to btc address %s%n", tradeId, address);
return;
}
case getpaymentmethods: {
var request = GetPaymentMethodsRequest.newBuilder().build();
var reply = paymentAccountsService.getPaymentMethods(request);
reply.getPaymentMethodsList().forEach(p -> out.println(p.getId()));
return;
}
case getpaymentacctform: {
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("incorrect parameter count, expecting payment method id");
var paymentMethodId = nonOptionArgs.get(1);
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
String jsonString = paymentAccountsService.getPaymentAccountForm(request)
.getPaymentAccountFormJson();
File jsonFile = saveFileToDisk(paymentMethodId.toLowerCase(),
".json",
jsonString);
out.printf("payment account form %s%nsaved to %s%n",
jsonString, jsonFile.getAbsolutePath());
out.println("Edit the file, and use as the argument to a 'createpaymentacct' command.");
return;
}
case createpaymentacct: {
if (nonOptionArgs.size() < 5)
if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException(
"incorrect parameter count, expecting payment method id,"
+ " account name, account number, currency code");
"incorrect parameter count, expecting path to payment account form");
var paymentMethodId = nonOptionArgs.get(1);
var accountName = nonOptionArgs.get(2);
var accountNumber = nonOptionArgs.get(3);
var currencyCode = nonOptionArgs.get(4);
var paymentAccountFormPath = Paths.get(nonOptionArgs.get(1));
if (!paymentAccountFormPath.toFile().exists())
throw new IllegalStateException(
format("payment account form '%s' could not be found",
paymentAccountFormPath.toString()));
String jsonString;
try {
jsonString = new String(Files.readAllBytes(paymentAccountFormPath));
} catch (IOException e) {
throw new IllegalStateException(
format("could not read %s", paymentAccountFormPath.toString()));
}
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.setAccountName(accountName)
.setAccountNumber(accountNumber)
.setCurrencyCode(currencyCode).build();
paymentAccountsService.createPaymentAccount(request);
out.printf("payment account %s saved", accountName);
.setPaymentAccountForm(jsonString)
.build();
var reply = paymentAccountsService.createPaymentAccount(request);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(reply.getPaymentAccount())));
return;
}
case getpaymentaccts: {
var request = GetPaymentAccountsRequest.newBuilder().build();
var reply = paymentAccountsService.getPaymentAccounts(request);
out.println(formatPaymentAcctTbl(reply.getPaymentAccountsList()));
List<PaymentAccount> paymentAccounts = reply.getPaymentAccountsList();
if (paymentAccounts.size() > 0)
out.println(formatPaymentAcctTbl(paymentAccounts));
else
out.println("no payment accounts are saved");
return;
}
case lockwallet: {
@ -470,6 +627,26 @@ public class CliMain {
return Method.valueOf(methodName.toLowerCase());
}
private static File saveFileToDisk(String prefix,
@SuppressWarnings("SameParameterValue") String suffix,
String text) {
String timestamp = Long.toUnsignedString(new Date().getTime());
String relativeFileName = prefix + "_" + timestamp + suffix;
try {
Path path = Paths.get(relativeFileName);
if (!Files.exists(path)) {
try (PrintWriter out = new PrintWriter(path.toString())) {
out.println(text);
}
return path.toAbsolutePath().toFile();
} else {
throw new IllegalStateException(format("could not overwrite existing file '%s'", relativeFileName));
}
} catch (FileNotFoundException e) {
throw new IllegalStateException(format("could not create file '%s'", relativeFileName));
}
}
private static void printHelp(OptionParser parser, PrintStream stream) {
try {
stream.println("Bisq RPC Client");
@ -482,23 +659,30 @@ public class CliMain {
stream.format(rowFormat, "Method", "Params", "Description");
stream.format(rowFormat, "------", "------", "------------");
stream.format(rowFormat, "getversion", "", "Get server version");
stream.format(rowFormat, "getbalance", "", "Get server wallet balance");
stream.format(rowFormat, "getbalance [,currency code = bsq|btc]", "", "Get server wallet balances");
stream.format(rowFormat, "getaddressbalance", "address", "Get server wallet address balance");
stream.format(rowFormat, "getfundingaddresses", "", "Get BTC funding addresses");
stream.format(rowFormat, "getunusedbsqaddress", "", "Get unused BSQ address");
stream.format(rowFormat, "sendbsq", "address, amount", "Send BSQ");
stream.format(rowFormat, "gettxfeerate", "", "Get current tx fee rate in sats/byte");
stream.format(rowFormat, "settxfeerate", "satoshis (per byte)", "Set custom tx fee rate in sats/byte");
stream.format(rowFormat, "unsettxfeerate", "", "Unset custom tx fee rate");
stream.format(rowFormat, "createoffer", "payment acct id, buy | sell, currency code, \\", "Create and place an offer");
stream.format(rowFormat, "", "amount (btc), min amount, use mkt based price, \\", "");
stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), \\", "");
stream.format(rowFormat, "", "security deposit (%)", "");
stream.format(rowFormat, "", "fixed price (btc) | mkt price margin (%), security deposit (%) \\", "");
stream.format(rowFormat, "", "[,maker fee currency code = bsq|btc]", "");
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, "takeoffer", "offer id, [,taker fee currency code = bsq|btc]", "Take offer with id");
stream.format(rowFormat, "gettrade", "trade id [,showcontract = true|false]", "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, "getpaymentmethods", "", "Get list of supported payment account method ids");
stream.format(rowFormat, "getpaymentacctform", "payment method id", "Get a new payment account form");
stream.format(rowFormat, "createpaymentacct", "path to payment account form", "Create a new payment account");
stream.format(rowFormat, "getpaymentaccts", "", "Get user payment accounts");
stream.format(rowFormat, "lockwallet", "", "Remove wallet password from memory, locking the wallet");
stream.format(rowFormat, "unlockwallet", "password timeout",

View file

@ -29,9 +29,18 @@ class ColumnHeaderConstants {
// 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_ADDRESS = padEnd("%-3s Address", 52, ' ');
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_AVAILABLE_BALANCE = "Available Balance";
static final String COL_HEADER_AVAILABLE_CONFIRMED_BALANCE = "Available Confirmed Balance";
static final String COL_HEADER_UNCONFIRMED_CHANGE_BALANCE = "Unconfirmed Change Balance";
static final String COL_HEADER_RESERVED_BALANCE = "Reserved Balance";
static final String COL_HEADER_TOTAL_AVAILABLE_BALANCE = "Total Available Balance";
static final String COL_HEADER_LOCKED_BALANCE = "Locked Balance";
static final String COL_HEADER_LOCKED_FOR_VOTING_BALANCE = "Locked For Voting Balance";
static final String COL_HEADER_LOCKUP_BONDS_BALANCE = "Lockup Bonds Balance";
static final String COL_HEADER_UNLOCKING_BONDS_BALANCE = "Unlocking Bonds Balance";
static final String COL_HEADER_UNVERIFIED_BALANCE = "Unverified Balance";
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";

View file

@ -17,6 +17,8 @@
package bisq.cli;
import bisq.proto.grpc.TxFeeRateInfo;
import com.google.common.annotations.VisibleForTesting;
import java.text.DecimalFormat;
@ -36,13 +38,31 @@ public class CurrencyFormat {
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000);
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000");
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,##0.00");
static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00");
@VisibleForTesting
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatSatoshis(long sats) {
return BTC_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
public static String formatBsq(long sats) {
return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR));
}
public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) {
if (txFeeRateInfo.getUseCustomTxFeeRate())
return format("custom tx fee rate: %s sats/byte, network rate: %s sats/byte",
formatFeeSatoshis(txFeeRateInfo.getCustomTxFeeRate()),
formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()));
else
return format("tx fee rate: %s sats/byte",
formatFeeSatoshis(txFeeRateInfo.getFeeServiceRate()));
}
static String formatAmountRange(long minAmount, long amount) {
return minAmount != amount
? formatSatoshis(minAmount) + " - " + formatSatoshis(amount)
@ -78,4 +98,9 @@ public class CurrencyFormat {
throw new IllegalArgumentException(format("'%s' is not a number", btc));
}
}
@SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
private static String formatFeeSatoshis(long sats) {
return BTC_TX_FEE_FORMAT.format(BigDecimal.valueOf(sats).divide(SATOSHI_DIVISOR));
}
}

View file

@ -18,10 +18,15 @@
package bisq.cli;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.OfferInfo;
import protobuf.PaymentAccount;
import com.google.common.annotations.VisibleForTesting;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -30,28 +35,28 @@ 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 bisq.cli.CurrencyFormat.*;
import static com.google.common.base.Strings.padEnd;
import static java.lang.String.format;
import static java.util.Collections.max;
import static java.util.Comparator.comparing;
import static java.util.TimeZone.getTimeZone;
class TableFormat {
@VisibleForTesting
public class TableFormat {
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
+ COL_HEADER_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n");
String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // left justify
+ " %" + COL_HEADER_BALANCE.length() + "s" // right justify
+ " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // right justify
public static String formatAddressBalanceTbl(List<AddressBalanceInfo> addressBalanceInfo) {
String headerFormatString = COL_HEADER_ADDRESS + COL_HEADER_DELIMITER
+ COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_CONFIRMATIONS + COL_HEADER_DELIMITER + "\n";
String headerLine = format(headerFormatString, "BTC");
String colDataFormat = "%-" + COL_HEADER_ADDRESS.length() + "s" // lt justify
+ " %" + (COL_HEADER_AVAILABLE_BALANCE.length() - 1) + "s" // rt justify
+ " %" + COL_HEADER_CONFIRMATIONS.length() + "d"; // lt justify
return headerLine
+ addressBalanceInfo.stream()
.map(info -> format(colDataFormat,
@ -61,15 +66,58 @@ class TableFormat {
.collect(Collectors.joining("\n"));
}
static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
public static String formatBalancesTbls(BalancesInfo balancesInfo) {
return "BTC" + "\n"
+ formatBtcBalanceInfoTbl(balancesInfo.getBtc()) + "\n"
+ "BSQ" + "\n"
+ formatBsqBalanceInfoTbl(balancesInfo.getBsq());
}
public static String formatBsqBalanceInfoTbl(BsqBalanceInfo bsqBalanceInfo) {
String headerLine = COL_HEADER_AVAILABLE_CONFIRMED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_UNVERIFIED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_UNCONFIRMED_CHANGE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKED_FOR_VOTING_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKUP_BONDS_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_UNLOCKING_BONDS_BALANCE + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%" + COL_HEADER_AVAILABLE_CONFIRMED_BALANCE.length() + "s" // rt justify
+ " %" + (COL_HEADER_UNVERIFIED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_UNCONFIRMED_CHANGE_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKED_FOR_VOTING_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKUP_BONDS_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_UNLOCKING_BONDS_BALANCE.length() + 1) + "s"; // rt justify
return headerLine + format(colDataFormat,
formatBsq(bsqBalanceInfo.getAvailableConfirmedBalance()),
formatBsq(bsqBalanceInfo.getUnverifiedBalance()),
formatBsq(bsqBalanceInfo.getUnconfirmedChangeBalance()),
formatBsq(bsqBalanceInfo.getLockedForVotingBalance()),
formatBsq(bsqBalanceInfo.getLockupBondsBalance()),
formatBsq(bsqBalanceInfo.getUnlockingBondsBalance()));
}
public static String formatBtcBalanceInfoTbl(BtcBalanceInfo btcBalanceInfo) {
String headerLine = COL_HEADER_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_RESERVED_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_TOTAL_AVAILABLE_BALANCE + COL_HEADER_DELIMITER
+ COL_HEADER_LOCKED_BALANCE + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%" + COL_HEADER_AVAILABLE_BALANCE.length() + "s" // rt justify
+ " %" + (COL_HEADER_RESERVED_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_TOTAL_AVAILABLE_BALANCE.length() + 1) + "s" // rt justify
+ " %" + (COL_HEADER_LOCKED_BALANCE.length() + 1) + "s"; // rt justify
return headerLine + format(colDataFormat,
formatSatoshis(btcBalanceInfo.getAvailableBalance()),
formatSatoshis(btcBalanceInfo.getReservedBalance()),
formatSatoshis(btcBalanceInfo.getTotalAvailableBalance()),
formatSatoshis(btcBalanceInfo.getLockedBalance()));
}
static String formatOfferTable(List<OfferInfo> offerInfo, String fiatCurrency) {
// Some column values might be longer than header, so we need to calculate them.
int paymentMethodColWidth = getLengthOfLongestColumn(
COL_HEADER_PAYMENT_METHOD.length(),
offerInfo.stream()
.map(OfferInfo::getPaymentMethodShortName)
.collect(Collectors.toList()));
String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrency
+ COL_HEADER_AMOUNT + COL_HEADER_DELIMITER

View file

@ -110,7 +110,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
// 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 also check if we have called read already to avoid a very early write attempt before we have ever
// read the data, which would lead to a write of empty data
// (fixes https://github.com/bisq-network/bisq/issues/4844).
if (persistenceManager.readCalled.get() &&
(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.
@ -184,6 +188,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
private Timer timer;
private ExecutorService writeToDiskExecutor;
public final AtomicBoolean initCalled = new AtomicBoolean(false);
public final AtomicBoolean readCalled = new AtomicBoolean(false);
///////////////////////////////////////////////////////////////////////////////////////////
@ -303,6 +308,8 @@ public class PersistenceManager<T extends PersistableEnvelope> {
return null;
}
readCalled.set(true);
File storageFile = new File(dir, fileName);
if (!storageFile.exists()) {
return null;

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();
@ -74,6 +74,9 @@ public class CommonSetup {
} else if (throwable instanceof ClassCastException &&
"sun.awt.image.BufImgSurfaceData cannot be cast to sun.java2d.xr.XRSurfaceData".equals(throwable.getMessage())) {
log.warn(throwable.getMessage());
} else if (throwable instanceof UnsupportedOperationException &&
"The system tray is not supported on the current platform.".equals(throwable.getMessage())) {
log.warn(throwable.getMessage());
} else {
log.error("Uncaught Exception from thread " + Thread.currentThread().getName());
log.error("throwableMessage= " + throwable.getMessage());

View file

@ -0,0 +1,108 @@
/*
* 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.common.util;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import static java.util.Arrays.stream;
import static org.apache.commons.lang3.StringUtils.capitalize;
public class ReflectionUtils {
/**
* Recursively loads a list of fields for a given class and its superclasses,
* using a filter predicate to exclude any unwanted fields.
*
* @param fields The list of fields being loaded for a class hierarchy.
* @param clazz The lowest level class in a hierarchy; excluding Object.class.
* @param isExcludedField The field exclusion predicate.
*/
public static void loadFieldListForClassHierarchy(List<Field> fields,
Class<?> clazz,
Predicate<Field> isExcludedField) {
fields.addAll(stream(clazz.getDeclaredFields())
.filter(f -> !isExcludedField.test(f))
.collect(Collectors.toList()));
Class<?> superclass = clazz.getSuperclass();
if (!Objects.equals(superclass, Object.class))
loadFieldListForClassHierarchy(fields,
superclass,
isExcludedField);
}
/**
* Returns an Optional of a setter method for a given field and a class hierarchy,
* or Optional.empty() if it does not exist.
*
* @param field The field used to find a setter method.
* @param clazz The lowest level class in a hierarchy; excluding Object.class.
* @return Optional<Method> of the setter method for a field in the class hierarchy,
* or Optional.empty() if it does not exist.
*/
public static Optional<Method> getSetterMethodForFieldInClassHierarchy(Field field,
Class<?> clazz) {
Optional<Method> setter = stream(clazz.getDeclaredMethods())
.filter((m) -> isSetterForField(m, field))
.findFirst();
if (setter.isPresent())
return setter;
Class<?> superclass = clazz.getSuperclass();
if (!Objects.equals(superclass, Object.class)) {
setter = getSetterMethodForFieldInClassHierarchy(field, superclass);
if (setter.isPresent())
return setter;
}
return Optional.empty();
}
public static boolean isSetterForField(Method m, Field f) {
return m.getName().startsWith("set")
&& m.getName().endsWith(capitalize(f.getName()))
&& m.getReturnType().getName().equals("void")
&& m.getParameterCount() == 1
&& m.getParameterTypes()[0].getName().equals(f.getType().getName());
}
public static boolean isSetterOnClass(Method setter, Class<?> clazz) {
return clazz.equals(setter.getDeclaringClass());
}
public static String getVisibilityModifierAsString(Field field) {
if (Modifier.isPrivate(field.getModifiers()))
return "private";
else if (Modifier.isProtected(field.getModifiers()))
return "protected";
else if (Modifier.isPublic(field.getModifiers()))
return "public";
else
return "";
}
}

View file

@ -104,6 +104,7 @@ public class AccountAgeWitnessService {
private String presentation;
private String hash = "";
private long daysUntilLimitLifted = 0;
SignState(String presentation) {
this.presentation = presentation;
@ -114,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);
}
}
@ -527,11 +533,19 @@ public class AccountAgeWitnessService {
Coin tradeAmount,
ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer);
// In case we don't find the witness we check if the trade amount is above the
// TOLERATED_SMALL_TRADE_AMOUNT (0.01 BTC) and only in that case return false.
return findWitness(offer)
.map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler))
.orElse(false);
.orElse(isToleratedSmalleAmount(tradeAmount));
}
private boolean isToleratedSmalleAmount(Coin tradeAmount) {
return tradeAmount.value <= OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Package scope verification subroutines
///////////////////////////////////////////////////////////////////////////////////////////
@ -806,7 +820,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

@ -18,15 +18,20 @@
package bisq.core.api;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.monetary.Price;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.trade.Trade;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.common.app.Version;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
@ -40,6 +45,8 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/**
* Provides high level interface to functionality of core Bisq features.
* E.g. useful for different APIs to access data of different domains of Bisq.
@ -107,6 +114,7 @@ public class CoreApi {
long minAmountAsLong,
double buyerSecurityDeposit,
String paymentAccountId,
String makerFeeCurrencyCode,
Consumer<Offer> resultHandler) {
coreOffersService.createAndPlaceOffer(currencyCode,
directionAsString,
@ -117,6 +125,7 @@ public class CoreApi {
minAmountAsLong,
buyerSecurityDeposit,
paymentAccountId,
makerFeeCurrencyCode,
resultHandler);
}
@ -150,20 +159,22 @@ public class CoreApi {
// PaymentAccounts
///////////////////////////////////////////////////////////////////////////////////////////
public void createPaymentAccount(String paymentMethodId,
String accountName,
String accountNumber,
String currencyCode) {
paymentAccountsService.createPaymentAccount(paymentMethodId,
accountName,
accountNumber,
currencyCode);
public PaymentAccount createPaymentAccount(String jsonString) {
return paymentAccountsService.createPaymentAccount(jsonString);
}
public Set<PaymentAccount> getPaymentAccounts() {
return paymentAccountsService.getPaymentAccounts();
}
public List<PaymentMethod> getFiatPaymentMethods() {
return paymentAccountsService.getFiatPaymentMethods();
}
public String getPaymentAccountForm(String paymentMethodId) {
return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Prices
///////////////////////////////////////////////////////////////////////////////////////////
@ -178,10 +189,12 @@ public class CoreApi {
public void takeOffer(String offerId,
String paymentAccountId,
String takerFeeCurrencyCode,
Consumer<Trade> resultHandler) {
Offer offer = coreOffersService.getOffer(offerId);
coreTradesService.takeOffer(offer,
paymentAccountId,
takerFeeCurrencyCode,
resultHandler);
}
@ -197,8 +210,8 @@ public class CoreApi {
coreTradesService.keepFunds(tradeId);
}
public void withdrawFunds(String tradeId, String address) {
coreTradesService.withdrawFunds(tradeId, address);
public void withdrawFunds(String tradeId, String address, @Nullable String memo) {
coreTradesService.withdrawFunds(tradeId, address, memo);
}
public Trade getTrade(String tradeId) {
@ -213,8 +226,8 @@ public class CoreApi {
// Wallets
///////////////////////////////////////////////////////////////////////////////////////////
public long getAvailableBalance() {
return walletsService.getAvailableBalance();
public BalancesInfo getBalances(String currencyCode) {
return walletsService.getBalances(currencyCode);
}
public long getAddressBalance(String addressString) {
@ -229,6 +242,31 @@ public class CoreApi {
return walletsService.getFundingAddresses();
}
public String getUnusedBsqAddress() {
return walletsService.getUnusedBsqAddress();
}
public void sendBsq(String address, String amount, TxBroadcaster.Callback callback) {
walletsService.sendBsq(address, amount, callback);
}
public void getTxFeeRate(ResultHandler resultHandler) {
walletsService.getTxFeeRate(resultHandler);
}
public void setTxFeeRatePreference(long txFeeRate,
ResultHandler resultHandler) {
walletsService.setTxFeeRatePreference(txFeeRate, resultHandler);
}
public void unsetTxFeeRatePreference(ResultHandler resultHandler) {
walletsService.unsetTxFeeRatePreference(resultHandler);
}
public TxFeeRateInfo getMostRecentTxFeeRateInfo() {
return walletsService.getMostRecentTxFeeRateInfo();
}
public void setWalletPassword(String password, String newPassword) {
walletsService.setWalletPassword(password, newPassword);
}

View file

@ -22,6 +22,7 @@ import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferBookService;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount;
import bisq.core.user.User;
@ -55,16 +56,19 @@ class CoreOffersService {
private final CreateOfferService createOfferService;
private final OfferBookService offerBookService;
private final OpenOfferManager openOfferManager;
private final OfferUtil offerUtil;
private final User user;
@Inject
public CoreOffersService(CreateOfferService createOfferService,
OfferBookService offerBookService,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
User user) {
this.createOfferService = createOfferService;
this.offerBookService = offerBookService;
this.openOfferManager = openOfferManager;
this.offerUtil = offerUtil;
this.user = user;
}
@ -105,7 +109,11 @@ class CoreOffersService {
long minAmountAsLong,
double buyerSecurityDeposit,
String paymentAccountId,
String makerFeeCurrencyCode,
Consumer<Offer> resultHandler) {
offerUtil.maybeSetFeePaymentCurrencyPreference(makerFeeCurrencyCode);
String upperCaseCurrencyCode = currencyCode.toUpperCase();
String offerId = createOfferService.getRandomOfferId();
Direction direction = Direction.valueOf(directionAsString.toUpperCase());

View file

@ -18,117 +18,65 @@
package bisq.core.api;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.FiatCurrency;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountFactory;
import bisq.core.payment.PerfectMoneyAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.user.User;
import bisq.common.config.Config;
import javax.inject.Inject;
import java.io.File;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.payment.payload.PaymentMethod.*;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
class CorePaymentAccountsService {
private final Config config;
private final AccountAgeWitnessService accountAgeWitnessService;
private final PaymentAccountForm paymentAccountForm;
private final User user;
@Inject
public CorePaymentAccountsService(Config config,
AccountAgeWitnessService accountAgeWitnessService,
public CorePaymentAccountsService(AccountAgeWitnessService accountAgeWitnessService,
PaymentAccountForm paymentAccountForm,
User user) {
this.config = config;
this.accountAgeWitnessService = accountAgeWitnessService;
this.paymentAccountForm = paymentAccountForm;
this.user = user;
}
void createPaymentAccount(String paymentMethodId,
String accountName,
String accountNumber,
String currencyCode) {
PaymentAccount paymentAccount = getNewPaymentAccount(paymentMethodId,
accountName,
accountNumber,
currencyCode);
PaymentAccount createPaymentAccount(String jsonString) {
PaymentAccount paymentAccount = paymentAccountForm.toPaymentAccount(jsonString);
user.addPaymentAccountIfNotExists(paymentAccount);
// Don't do this on mainnet until thoroughly tested.
if (config.baseCurrencyNetwork.isRegtest())
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
log.info("Payment account {} saved", paymentAccount.getId());
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
log.info("Saved payment account with id {} and payment method {}.",
paymentAccount.getId(),
paymentAccount.getPaymentAccountPayload().getPaymentMethodId());
return paymentAccount;
}
Set<PaymentAccount> getPaymentAccounts() {
return user.getPaymentAccounts();
}
private PaymentAccount getNewPaymentAccount(String paymentMethodId,
String accountName,
String accountNumber,
String currencyCode) {
PaymentAccount paymentAccount = null;
PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId);
List<PaymentMethod> getFiatPaymentMethods() {
return PaymentMethod.getPaymentMethods().stream()
.filter(paymentMethod -> !paymentMethod.isAsset())
.sorted(Comparator.comparing(PaymentMethod::getId))
.collect(Collectors.toList());
}
switch (paymentMethod.getId()) {
case UPHOLD_ID:
case MONEY_BEAM_ID:
case POPMONEY_ID:
case REVOLUT_ID:
//noinspection DuplicateBranchesInSwitch
log.error("PaymentMethod {} not supported yet.", paymentMethod);
break;
case PERFECT_MONEY_ID:
// Create and persist a PerfectMoney dummy payment account. There is no
// guard against creating accounts with duplicate names & numbers, only
// the uuid and creation date are unique.
paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
paymentAccount.init();
paymentAccount.setAccountName(accountName);
((PerfectMoneyAccount) paymentAccount).setAccountNr(accountNumber);
paymentAccount.setSingleTradeCurrency(new FiatCurrency(currencyCode));
break;
case SEPA_ID:
case SEPA_INSTANT_ID:
case FASTER_PAYMENTS_ID:
case NATIONAL_BANK_ID:
case SAME_BANK_ID:
case SPECIFIC_BANKS_ID:
case JAPAN_BANK_ID:
case ALI_PAY_ID:
case WECHAT_PAY_ID:
case SWISH_ID:
case CLEAR_X_CHANGE_ID:
case CHASE_QUICK_PAY_ID:
case INTERAC_E_TRANSFER_ID:
case US_POSTAL_MONEY_ORDER_ID:
case MONEY_GRAM_ID:
case WESTERN_UNION_ID:
case CASH_DEPOSIT_ID:
case HAL_CASH_ID:
case F2F_ID:
case PROMPT_PAY_ID:
case ADVANCED_CASH_ID:
default:
log.error("PaymentMethod {} not supported yet.", paymentMethod);
break;
}
String getPaymentAccountFormAsString(String paymentMethodId) {
File jsonForm = getPaymentAccountForm(paymentMethodId);
return paymentAccountForm.toJsonString(jsonForm);
}
checkNotNull(paymentAccount,
"Could not create payment account with paymentMethodId "
+ paymentMethodId + ".");
return paymentAccount;
File getPaymentAccountForm(String paymentMethodId) {
return paymentAccountForm.getPaymentAccountForm(paymentMethodId);
}
}

View file

@ -20,6 +20,7 @@ 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.OfferUtil;
import bisq.core.offer.takeoffer.TakeOfferModel;
import bisq.core.trade.Tradable;
import bisq.core.trade.Trade;
@ -40,6 +41,8 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
import static java.lang.String.format;
@ -52,6 +55,7 @@ class CoreTradesService {
private final CoreWalletsService coreWalletsService;
private final BtcWalletService btcWalletService;
private final OfferUtil offerUtil;
private final ClosedTradableManager closedTradableManager;
private final TakeOfferModel takeOfferModel;
private final TradeManager tradeManager;
@ -61,6 +65,7 @@ class CoreTradesService {
@Inject
public CoreTradesService(CoreWalletsService coreWalletsService,
BtcWalletService btcWalletService,
OfferUtil offerUtil,
ClosedTradableManager closedTradableManager,
TakeOfferModel takeOfferModel,
TradeManager tradeManager,
@ -68,6 +73,7 @@ class CoreTradesService {
User user) {
this.coreWalletsService = coreWalletsService;
this.btcWalletService = btcWalletService;
this.offerUtil = offerUtil;
this.closedTradableManager = closedTradableManager;
this.takeOfferModel = takeOfferModel;
this.tradeManager = tradeManager;
@ -77,7 +83,11 @@ class CoreTradesService {
void takeOffer(Offer offer,
String paymentAccountId,
String takerFeeCurrencyCode,
Consumer<Trade> resultHandler) {
offerUtil.maybeSetFeePaymentCurrencyPreference(takerFeeCurrencyCode);
var paymentAccount = user.getPaymentAccount(paymentAccountId);
if (paymentAccount == null)
throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId));
@ -146,7 +156,7 @@ class CoreTradesService {
tradeManager.onTradeCompleted(trade);
}
void withdrawFunds(String tradeId, String toAddress) {
void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) {
// An encrypted wallet must be unlocked for this operation.
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
@ -176,6 +186,7 @@ class CoreTradesService {
fee,
coreWalletsService.getKey(),
trade,
memo,
() -> {
},
(errorMessage, throwable) -> {

View file

@ -18,15 +18,34 @@
package bisq.core.api;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.BsqBalanceInfo;
import bisq.core.api.model.BtcBalanceInfo;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.btc.Balances;
import bisq.core.btc.exceptions.BsqChangeBelowDustException;
import bisq.core.btc.exceptions.TransactionVerificationException;
import bisq.core.btc.exceptions.WalletException;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.model.BsqTransferModel;
import bisq.core.btc.wallet.BsqTransferService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.btc.wallet.WalletsManager;
import bisq.core.provider.fee.FeeService;
import bisq.core.user.Preferences;
import bisq.core.util.coin.BsqFormatter;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.handlers.ResultHandler;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.crypto.KeyCrypterScrypt;
@ -35,6 +54,11 @@ import javax.inject.Inject;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
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 org.bouncycastle.crypto.params.KeyParameter;
@ -47,6 +71,8 @@ import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput;
import static bisq.core.util.ParsingUtils.parseToCoin;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
@ -55,7 +81,12 @@ class CoreWalletsService {
private final Balances balances;
private final WalletsManager walletsManager;
private final BsqWalletService bsqWalletService;
private final BsqTransferService bsqTransferService;
private final BsqFormatter bsqFormatter;
private final BtcWalletService btcWalletService;
private final FeeService feeService;
private final Preferences preferences;
@Nullable
private Timer lockTimer;
@ -63,13 +94,25 @@ class CoreWalletsService {
@Nullable
private KeyParameter tempAesKey;
private final ListeningExecutorService executor = Utilities.getSingleThreadListeningExecutor("CoreWalletsService");
@Inject
public CoreWalletsService(Balances balances,
WalletsManager walletsManager,
BtcWalletService btcWalletService) {
BsqWalletService bsqWalletService,
BsqTransferService bsqTransferService,
BsqFormatter bsqFormatter,
BtcWalletService btcWalletService,
FeeService feeService,
Preferences preferences) {
this.balances = balances;
this.walletsManager = walletsManager;
this.bsqWalletService = bsqWalletService;
this.bsqTransferService = bsqTransferService;
this.bsqFormatter = bsqFormatter;
this.btcWalletService = btcWalletService;
this.feeService = feeService;
this.preferences = preferences;
}
@Nullable
@ -78,15 +121,21 @@ class CoreWalletsService {
return tempAesKey;
}
long getAvailableBalance() {
BalancesInfo getBalances(String currencyCode) {
verifyWalletCurrencyCodeIsValid(currencyCode);
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
var balance = balances.getAvailableBalance().get();
if (balance == null)
if (balances.getAvailableBalance().get() == null)
throw new IllegalStateException("balance is not yet available");
return balance.getValue();
switch (currencyCode.trim().toUpperCase()) {
case "BSQ":
return new BalancesInfo(getBsqBalances(), BtcBalanceInfo.EMPTY);
case "BTC":
return new BalancesInfo(BsqBalanceInfo.EMPTY, getBtcBalances());
default:
return new BalancesInfo(getBsqBalances(), getBtcBalances());
}
}
long getAddressBalance(String addressString) {
@ -134,6 +183,75 @@ class CoreWalletsService {
.collect(Collectors.toList());
}
String getUnusedBsqAddress() {
return bsqWalletService.getUnusedBsqAddressAsString();
}
void sendBsq(String address,
String amount,
TxBroadcaster.Callback callback) {
try {
LegacyAddress legacyAddress = getValidBsqLegacyAddress(address);
Coin receiverAmount = getValidBsqTransferAmount(amount);
BsqTransferModel model = bsqTransferService.getBsqTransferModel(legacyAddress, receiverAmount);
bsqTransferService.sendFunds(model, callback);
} catch (InsufficientMoneyException
| BsqChangeBelowDustException
| TransactionVerificationException
| WalletException ex) {
log.error("", ex);
throw new IllegalStateException(ex);
}
}
void getTxFeeRate(ResultHandler resultHandler) {
try {
@SuppressWarnings({"unchecked", "Convert2MethodRef"})
ListenableFuture<Void> future =
(ListenableFuture<Void>) executor.submit(() -> feeService.requestFees());
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Void ignored) {
resultHandler.handleResult();
}
@Override
public void onFailure(Throwable t) {
log.error("", t);
throw new IllegalStateException("could not request fees from fee service", t);
}
}, MoreExecutors.directExecutor());
} catch (Exception ex) {
log.error("", ex);
throw new IllegalStateException("could not request fees from fee service", ex);
}
}
void setTxFeeRatePreference(long txFeeRate,
ResultHandler resultHandler) {
if (txFeeRate <= 0)
throw new IllegalStateException("cannot create transactions without fees");
preferences.setUseCustomWithdrawalTxFee(true);
Coin satsPerByte = Coin.valueOf(txFeeRate);
preferences.setWithdrawalTxFeeInVbytes(satsPerByte.value);
getTxFeeRate(resultHandler);
}
void unsetTxFeeRatePreference(ResultHandler resultHandler) {
preferences.setUseCustomWithdrawalTxFee(false);
getTxFeeRate(resultHandler);
}
TxFeeRateInfo getMostRecentTxFeeRateInfo() {
return new TxFeeRateInfo(
preferences.isUseCustomWithdrawalTxFee(),
preferences.getWithdrawalTxFeeInVbytes(),
feeService.getTxFeePerVbyte().value,
feeService.getLastRequest());
}
int getNumConfirmationsForMostRecentTransaction(String addressString) {
Address address = getAddressEntry(addressString).getAddress();
TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address);
@ -244,6 +362,76 @@ class CoreWalletsService {
throw new IllegalStateException("wallet is locked");
}
// Throws a RuntimeException if wallet currency code is not BSQ or BTC.
private void verifyWalletCurrencyCodeIsValid(String currencyCode) {
if (currencyCode == null || currencyCode.isEmpty())
return;
if (!currencyCode.equalsIgnoreCase("BSQ")
&& !currencyCode.equalsIgnoreCase("BTC"))
throw new IllegalStateException(format("wallet does not support %s", currencyCode));
}
private BsqBalanceInfo getBsqBalances() {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
var availableConfirmedBalance = bsqWalletService.getAvailableConfirmedBalance();
var unverifiedBalance = bsqWalletService.getUnverifiedBalance();
var unconfirmedChangeBalance = bsqWalletService.getUnconfirmedChangeBalance();
var lockedForVotingBalance = bsqWalletService.getLockedForVotingBalance();
var lockupBondsBalance = bsqWalletService.getLockupBondsBalance();
var unlockingBondsBalance = bsqWalletService.getUnlockingBondsBalance();
return new BsqBalanceInfo(availableConfirmedBalance.value,
unverifiedBalance.value,
unconfirmedChangeBalance.value,
lockedForVotingBalance.value,
lockupBondsBalance.value,
unlockingBondsBalance.value);
}
private BtcBalanceInfo getBtcBalances() {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
var availableBalance = balances.getAvailableBalance().get();
if (availableBalance == null)
throw new IllegalStateException("balance is not yet available");
var reservedBalance = balances.getReservedBalance().get();
if (reservedBalance == null)
throw new IllegalStateException("reserved balance is not yet available");
var lockedBalance = balances.getLockedBalance().get();
if (lockedBalance == null)
throw new IllegalStateException("locked balance is not yet available");
return new BtcBalanceInfo(availableBalance.value,
reservedBalance.value,
availableBalance.add(reservedBalance).value,
lockedBalance.value);
}
// Returns a LegacyAddress for the string, or a RuntimeException if invalid.
private LegacyAddress getValidBsqLegacyAddress(String address) {
try {
return bsqFormatter.getAddressFromBsqAddress(address);
} catch (Throwable t) {
log.error("", t);
throw new IllegalStateException(format("%s is not a valid bsq address", address));
}
}
// Returns a Coin for the amount string, or a RuntimeException if invalid.
private Coin getValidBsqTransferAmount(String amount) {
Coin amountAsCoin = parseToCoin(amount, bsqFormatter);
if (amountAsCoin.isLessThan(getMinNonDustOutput()))
throw new IllegalStateException(format("%s bsq is an invalid send amount", amount));
return amountAsCoin;
}
private KeyCrypterScrypt getKeyCrypterScrypt() {
KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt();
if (keyCrypterScrypt == null)

View file

@ -0,0 +1,45 @@
package bisq.core.api.model;
import bisq.common.Payload;
import lombok.Getter;
@Getter
public class BalancesInfo implements Payload {
// Getter names are shortened for readability's sake, i.e.,
// balancesInfo.getBtc().getAvailableBalance() is cleaner than
// balancesInfo.getBtcBalanceInfo().getAvailableBalance().
private final BsqBalanceInfo bsq;
private final BtcBalanceInfo btc;
public BalancesInfo(BsqBalanceInfo bsq, BtcBalanceInfo btc) {
this.bsq = bsq;
this.btc = btc;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.BalancesInfo toProtoMessage() {
return bisq.proto.grpc.BalancesInfo.newBuilder()
.setBsq(bsq.toProtoMessage())
.setBtc(btc.toProtoMessage())
.build();
}
public static BalancesInfo fromProto(bisq.proto.grpc.BalancesInfo proto) {
return new BalancesInfo(BsqBalanceInfo.fromProto(proto.getBsq()),
BtcBalanceInfo.fromProto(proto.getBtc()));
}
@Override
public String toString() {
return "BalancesInfo{" + "\n" +
" " + bsq.toString() + "\n" +
", " + btc.toString() + "\n" +
'}';
}
}

View file

@ -0,0 +1,94 @@
package bisq.core.api.model;
import bisq.common.Payload;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
@Getter
public class BsqBalanceInfo implements Payload {
public static final BsqBalanceInfo EMPTY = new BsqBalanceInfo(-1,
-1,
-1,
-1,
-1,
-1);
// All balances are in BSQ satoshis.
private final long availableConfirmedBalance;
private final long unverifiedBalance;
private final long unconfirmedChangeBalance;
private final long lockedForVotingBalance;
private final long lockupBondsBalance;
private final long unlockingBondsBalance;
public BsqBalanceInfo(long availableConfirmedBalance,
long unverifiedBalance,
long unconfirmedChangeBalance,
long lockedForVotingBalance,
long lockupBondsBalance,
long unlockingBondsBalance) {
this.availableConfirmedBalance = availableConfirmedBalance;
this.unverifiedBalance = unverifiedBalance;
this.unconfirmedChangeBalance = unconfirmedChangeBalance;
this.lockedForVotingBalance = lockedForVotingBalance;
this.lockupBondsBalance = lockupBondsBalance;
this.unlockingBondsBalance = unlockingBondsBalance;
}
@VisibleForTesting
public static BsqBalanceInfo valueOf(long availableConfirmedBalance,
long unverifiedBalance,
long unconfirmedChangeBalance,
long lockedForVotingBalance,
long lockupBondsBalance,
long unlockingBondsBalance) {
// Convenience for creating a model instance instead of a proto.
return new BsqBalanceInfo(availableConfirmedBalance,
unverifiedBalance,
unconfirmedChangeBalance,
lockedForVotingBalance,
lockupBondsBalance,
unlockingBondsBalance);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.BsqBalanceInfo toProtoMessage() {
return bisq.proto.grpc.BsqBalanceInfo.newBuilder()
.setAvailableConfirmedBalance(availableConfirmedBalance)
.setUnverifiedBalance(unverifiedBalance)
.setUnconfirmedChangeBalance(unconfirmedChangeBalance)
.setLockedForVotingBalance(lockedForVotingBalance)
.setLockupBondsBalance(lockupBondsBalance)
.setUnlockingBondsBalance(unlockingBondsBalance)
.build();
}
public static BsqBalanceInfo fromProto(bisq.proto.grpc.BsqBalanceInfo proto) {
return new BsqBalanceInfo(proto.getAvailableConfirmedBalance(),
proto.getUnverifiedBalance(),
proto.getUnconfirmedChangeBalance(),
proto.getLockedForVotingBalance(),
proto.getLockupBondsBalance(),
proto.getUnlockingBondsBalance());
}
@Override
public String toString() {
return "BsqBalanceInfo{" +
"availableConfirmedBalance=" + availableConfirmedBalance +
", unverifiedBalance=" + unverifiedBalance +
", unconfirmedChangeBalance=" + unconfirmedChangeBalance +
", lockedForVotingBalance=" + lockedForVotingBalance +
", lockupBondsBalance=" + lockupBondsBalance +
", unlockingBondsBalance=" + unlockingBondsBalance +
'}';
}
}

View file

@ -0,0 +1,75 @@
package bisq.core.api.model;
import bisq.common.Payload;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
@Getter
public class BtcBalanceInfo implements Payload {
public static final BtcBalanceInfo EMPTY = new BtcBalanceInfo(-1,
-1,
-1,
-1);
// All balances are in BTC satoshis.
private final long availableBalance;
private final long reservedBalance;
private final long totalAvailableBalance; // available + reserved
private final long lockedBalance;
public BtcBalanceInfo(long availableBalance,
long reservedBalance,
long totalAvailableBalance,
long lockedBalance) {
this.availableBalance = availableBalance;
this.reservedBalance = reservedBalance;
this.totalAvailableBalance = totalAvailableBalance;
this.lockedBalance = lockedBalance;
}
@VisibleForTesting
public static BtcBalanceInfo valueOf(long availableBalance,
long reservedBalance,
long totalAvailableBalance,
long lockedBalance) {
// Convenience for creating a model instance instead of a proto.
return new BtcBalanceInfo(availableBalance,
reservedBalance,
totalAvailableBalance,
lockedBalance);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.BtcBalanceInfo toProtoMessage() {
return bisq.proto.grpc.BtcBalanceInfo.newBuilder()
.setAvailableBalance(availableBalance)
.setReservedBalance(reservedBalance)
.setTotalAvailableBalance(totalAvailableBalance)
.setLockedBalance(lockedBalance)
.build();
}
public static BtcBalanceInfo fromProto(bisq.proto.grpc.BtcBalanceInfo proto) {
return new BtcBalanceInfo(proto.getAvailableBalance(),
proto.getReservedBalance(),
proto.getTotalAvailableBalance(),
proto.getLockedBalance());
}
@Override
public String toString() {
return "BtcBalanceInfo{" +
"availableBalance=" + availableBalance +
", reservedBalance=" + reservedBalance +
", totalAvailableBalance=" + totalAvailableBalance +
", lockedBalance=" + lockedBalance +
'}';
}
}

View file

@ -46,6 +46,7 @@ public class OfferInfo implements Payload {
private final long volume;
private final long minVolume;
private final long buyerSecurityDeposit;
private final boolean isCurrencyForMakerFeeBtc;
private final String paymentAccountId; // only used when creating offer
private final String paymentMethodId;
private final String paymentMethodShortName;
@ -67,6 +68,7 @@ public class OfferInfo implements Payload {
this.volume = builder.volume;
this.minVolume = builder.minVolume;
this.buyerSecurityDeposit = builder.buyerSecurityDeposit;
this.isCurrencyForMakerFeeBtc = builder.isCurrencyForMakerFeeBtc;
this.paymentAccountId = builder.paymentAccountId;
this.paymentMethodId = builder.paymentMethodId;
this.paymentMethodShortName = builder.paymentMethodShortName;
@ -88,6 +90,7 @@ public class OfferInfo implements Payload {
.withVolume(Objects.requireNonNull(offer.getVolume()).getValue())
.withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue())
.withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value)
.withIsCurrencyForMakerFeeBtc(offer.isCurrencyForMakerFeeBtc())
.withPaymentAccountId(offer.getMakerPaymentAccountId())
.withPaymentMethodId(offer.getPaymentMethod().getId())
.withPaymentMethodShortName(offer.getPaymentMethod().getShortName())
@ -115,6 +118,7 @@ public class OfferInfo implements Payload {
.setVolume(volume)
.setMinVolume(minVolume)
.setBuyerSecurityDeposit(buyerSecurityDeposit)
.setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc)
.setPaymentAccountId(paymentAccountId)
.setPaymentMethodId(paymentMethodId)
.setPaymentMethodShortName(paymentMethodShortName)
@ -125,9 +129,28 @@ public class OfferInfo implements Payload {
.build();
}
@SuppressWarnings({"unused", "SameReturnValue"})
@SuppressWarnings("unused")
public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) {
return null; // TODO
return new OfferInfo.OfferInfoBuilder()
.withId(proto.getId())
.withDirection(proto.getDirection())
.withPrice(proto.getPrice())
.withUseMarketBasedPrice(proto.getUseMarketBasedPrice())
.withMarketPriceMargin(proto.getMarketPriceMargin())
.withAmount(proto.getAmount())
.withMinAmount(proto.getMinAmount())
.withVolume(proto.getVolume())
.withMinVolume(proto.getMinVolume())
.withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit())
.withIsCurrencyForMakerFeeBtc(proto.getIsCurrencyForMakerFeeBtc())
.withPaymentAccountId(proto.getPaymentAccountId())
.withPaymentMethodId(proto.getPaymentMethodId())
.withPaymentMethodShortName(proto.getPaymentMethodShortName())
.withBaseCurrencyCode(proto.getBaseCurrencyCode())
.withCounterCurrencyCode(proto.getCounterCurrencyCode())
.withDate(proto.getDate())
.withState(proto.getState())
.build();
}
/*
@ -147,6 +170,7 @@ public class OfferInfo implements Payload {
private long volume;
private long minVolume;
private long buyerSecurityDeposit;
private boolean isCurrencyForMakerFeeBtc;
private String paymentAccountId;
private String paymentMethodId;
private String paymentMethodShortName;
@ -205,6 +229,11 @@ public class OfferInfo implements Payload {
return this;
}
public OfferInfoBuilder withIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) {
this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc;
return this;
}
public OfferInfoBuilder withPaymentAccountId(String paymentAccountId) {
this.paymentAccountId = paymentAccountId;
return this;

View file

@ -0,0 +1,250 @@
/*
* 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.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountFactory;
import bisq.core.payment.payload.PaymentMethod;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javax.inject.Singleton;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Map;
import java.lang.reflect.Type;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.payment.payload.PaymentMethod.getPaymentMethodById;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* <p>
* An instance of this class can write new payment account forms (editable json files),
* and de-serialize edited json files into {@link bisq.core.payment.PaymentAccount}
* instances.
* </p>
* <p>
* Example use case: (1) ask for a blank Hal Cash account form, (2) edit it, (3) derive a
* {@link bisq.core.payment.HalCashAccount} instance from the edited json file.
* </p>
* <br>
* <p>
* (1) Ask for a hal cash account form: Pass a {@link bisq.core.payment.payload.PaymentMethod#HAL_CASH_ID}
* to {@link bisq.core.api.model.PaymentAccountForm#getPaymentAccountForm(String)} to
* get the json Hal Cash payment account form:
* <pre>
* {
* "_COMMENTS_": [
* "Do not manually edit the paymentMethodId field.",
* "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age."
* ],
* "paymentMethodId": "HAL_CASH",
* "accountName": "Your accountname",
* "mobileNr": "Your mobilenr"
* "salt": ""
* }
* </pre>
* </p>
* <p>
* (2) Save the Hal Cash payment account form to disk, and edit it:
* <pre>
* {
* "_COMMENTS_": [
* "Do not manually edit the paymentMethodId field.",
* "Edit the salt field only if you are recreating a payment account on a new installation and wish to preserve the account age."
* ],
* "paymentMethodId": "HAL_CASH",
* "accountName": "Hal Cash Acct",
* "mobileNr": "798 123 456"
* "salt": ""
* }
* </pre>
* </p>
* (3) De-serialize the edited json account form: Pass the edited json file to
* {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(File)}, or
* a json string to {@link bisq.core.api.model.PaymentAccountForm#toPaymentAccount(String)}
* and get a {@link bisq.core.payment.HalCashAccount} instance.
* <pre>
* PaymentAccount(
* paymentMethod=PaymentMethod(id=HAL_CASH,
* maxTradePeriod=86400000,
* maxTradeLimit=50000000),
* id=e33c9d94-1a1a-43fd-aa11-fcaacbb46100,
* creationDate=Mon Nov 16 12:26:43 BRST 2020,
* paymentAccountPayload=HalCashAccountPayload(mobileNr=798 123 456),
* accountName=Hal Cash Acct,
* tradeCurrencies=[FiatCurrency(currency=EUR)],
* selectedTradeCurrency=FiatCurrency(currency=EUR)
* )
* </pre>
*/
@Singleton
@Slf4j
public class PaymentAccountForm {
private final GsonBuilder gsonBuilder = new GsonBuilder()
.setPrettyPrinting()
.serializeNulls();
// A list of PaymentAccount fields to exclude from json forms.
private final String[] excludedFields = new String[]{
"log",
"id",
"acceptedCountryCodes",
"countryCode",
"creationDate",
"excludeFromJsonDataMap",
"maxTradePeriod",
"paymentAccountPayload",
"paymentMethod",
"paymentMethodId", // This field will be included, but handled differently.
"selectedTradeCurrency",
"tradeCurrencies",
"HOLDER_NAME",
"SALT" // This field will be included, but handled differently.
};
/**
* Returns a blank payment account form (json) for the given paymentMethodId.
*
* @param paymentMethodId Determines what kind of json form to return.
* @return A uniquely named tmp file used to define new payment account details.
*/
public File getPaymentAccountForm(String paymentMethodId) {
PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId);
File file = getTmpJsonFile(paymentMethodId);
try (OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(checkNotNull(file), false), UTF_8)) {
PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
Class<? extends PaymentAccount> clazz = paymentAccount.getClass();
Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz, excludedFields)).create();
String json = gson.toJson(paymentAccount); // serializes target to json
outputStreamWriter.write(json);
} catch (Exception ex) {
String errMsg = format("cannot create a payment account form for a %s payment method", paymentMethodId);
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException(errMsg);
}
return file;
}
/**
* De-serialize a PaymentAccount json form into a new PaymentAccount instance.
*
* @param jsonForm The file representing a new payment account form.
* @return A populated PaymentAccount subclass instance.
*/
@SuppressWarnings("unused")
@VisibleForTesting
public PaymentAccount toPaymentAccount(File jsonForm) {
String jsonString = toJsonString(jsonForm);
return toPaymentAccount(jsonString);
}
/**
* De-serialize a PaymentAccount json string into a new PaymentAccount instance.
*
* @param jsonString The json data representing a new payment account form.
* @return A populated PaymentAccount subclass instance.
*/
public PaymentAccount toPaymentAccount(String jsonString) {
Class<? extends PaymentAccount> clazz = getPaymentAccountClassFromJson(jsonString);
Gson gson = gsonBuilder.registerTypeAdapter(clazz, new PaymentAccountTypeAdapter(clazz)).create();
return gson.fromJson(jsonString, clazz);
}
public String toJsonString(File jsonFile) {
try {
checkNotNull(jsonFile, "json file cannot be null");
return new String(Files.readAllBytes(Paths.get(jsonFile.getAbsolutePath())));
} catch (IOException ex) {
String errMsg = format("cannot read json string from file '%s'",
jsonFile.getAbsolutePath());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException(errMsg);
}
}
@VisibleForTesting
public URI getClickableURI(File jsonFile) {
try {
return new URI("file",
"",
jsonFile.toURI().getPath(),
null,
null);
} catch (URISyntaxException ex) {
String errMsg = format("cannot create clickable url to file '%s'",
jsonFile.getAbsolutePath());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException(errMsg);
}
}
@VisibleForTesting
public static File getTmpJsonFile(String paymentMethodId) {
File file;
try {
// Creates a tmp file that includes a random number string between the
// prefix and suffix, i.e., sepa_form_13243546575879.json, so there is
// little chance this will fail because the tmp file already exists.
file = File.createTempFile(paymentMethodId.toLowerCase() + "_form_",
".json",
Paths.get(getProperty("java.io.tmpdir")).toFile());
} catch (IOException ex) {
String errMsg = format("cannot create json file for a %s payment method",
paymentMethodId);
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException(errMsg);
}
return file;
}
private Class<? extends PaymentAccount> getPaymentAccountClassFromJson(String json) {
Map<String, Object> jsonMap = gsonBuilder.create().fromJson(json, (Type) Object.class);
String paymentMethodId = checkNotNull((String) jsonMap.get("paymentMethodId"),
format("cannot not find a paymentMethodId in json string: %s", json));
return getPaymentAccountClass(paymentMethodId);
}
private Class<? extends PaymentAccount> getPaymentAccountClass(String paymentMethodId) {
PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId);
return PaymentAccountFactory.getPaymentAccount(paymentMethod).getClass();
}
}

View file

@ -0,0 +1,362 @@
/*
* 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.locale.Country;
import bisq.core.locale.FiatCurrency;
import bisq.core.payment.CountryBasedPaymentAccount;
import bisq.core.payment.MoneyGramAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentAccountPayload;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import static bisq.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy;
import static bisq.common.util.ReflectionUtils.getVisibilityModifierAsString;
import static bisq.common.util.ReflectionUtils.isSetterOnClass;
import static bisq.common.util.ReflectionUtils.loadFieldListForClassHierarchy;
import static bisq.common.util.Utilities.decodeFromHex;
import static bisq.core.locale.CountryUtil.findCountryByCode;
import static bisq.core.locale.CurrencyUtil.getCurrencyByCountryCode;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static java.util.Comparator.comparing;
@Slf4j
class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
private static final String[] JSON_COMMENTS = new String[]{
"Do not manually edit the paymentMethodId field.",
"Edit the salt field only if you are recreating a payment"
+ " account on a new installation and wish to preserve the account age."
};
private final Class<? extends PaymentAccount> paymentAccountType;
private final Class<? extends PaymentAccountPayload> paymentAccountPayloadType;
private final Map<Field, Optional<Method>> fieldSettersMap;
private final Predicate<Field> isExcludedField;
/**
* Constructor used when de-serializing a json payment account form into a
* PaymentAccount instance.
*
* @param paymentAccountType the PaymentAccount subclass being instantiated
*/
public PaymentAccountTypeAdapter(Class<? extends PaymentAccount> paymentAccountType) {
this(paymentAccountType, new String[]{});
}
/**
* Constructor used when serializing a PaymentAccount subclass instance into a json
* payment account json form.
*
* @param paymentAccountType the PaymentAccount subclass being serialized
* @param excludedFields a string array of field names to exclude from the serialized
* payment account json form.
*/
public PaymentAccountTypeAdapter(Class<? extends PaymentAccount> paymentAccountType, String[] excludedFields) {
this.paymentAccountType = paymentAccountType;
this.paymentAccountPayloadType = getPaymentAccountPayloadType();
this.isExcludedField = (f) -> Arrays.stream(excludedFields).anyMatch(e -> e.equals(f.getName()));
this.fieldSettersMap = getFieldSetterMap();
}
@Override
public void write(JsonWriter out, PaymentAccount account) throws IOException {
// We write a blank payment acct form for a payment method id.
// We're not serializing a real payment account instance here.
out.beginObject();
// All json forms start with immutable _COMMENTS_ and paymentMethodId fields.
out.name("_COMMENTS_");
out.beginArray();
for (String s : JSON_COMMENTS) {
out.value(s);
}
out.endArray();
out.name("paymentMethodId");
out.value(account.getPaymentMethod().getId());
// Write the editable, PaymentAccount subclass specific fields.
writeInnerMutableFields(out, account);
// The last field in all json forms is the empty, editable salt field.
out.name("salt");
out.value("");
out.endObject();
}
private void writeInnerMutableFields(JsonWriter out, PaymentAccount account) {
fieldSettersMap.forEach((field, value) -> {
try {
// Write out a json element if there is a @Setter for this field.
if (value.isPresent()) {
log.debug("Append form with settable field: {} {} {} setter: {}",
getVisibilityModifierAsString(field),
field.getType().getSimpleName(),
field.getName(),
value);
String fieldName = field.getName();
out.name(fieldName);
out.value("your " + fieldName.toLowerCase());
}
} catch (Exception ex) {
String errMsg = format("cannot create a new %s json form",
account.getClass().getSimpleName());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
});
}
@Override
public PaymentAccount read(JsonReader in) throws IOException {
PaymentAccount account = initNewPaymentAccount();
in.beginObject();
while (in.hasNext()) {
String currentFieldName = in.nextName();
// Some of the fields are common to all payment account types.
if (didReadCommonField(in, account, currentFieldName))
continue;
// If the account is a subclass of CountryBasedPaymentAccount, set the
// account's Country, and use the Country to derive and set the account's
// FiatCurrency.
if (didReadCountryField(in, account, currentFieldName))
continue;
Optional<Field> field = fieldSettersMap.keySet().stream()
.filter(k -> k.getName().equals(currentFieldName)).findFirst();
field.ifPresentOrElse((f) -> invokeSetterMethod(account, f, in), () -> {
throw new IllegalStateException(
format("programmer error: cannot de-serialize json to a '%s' "
+ " because there is no %s field.",
account.getClass().getSimpleName(),
currentFieldName));
});
}
in.endObject();
return account;
}
private void invokeSetterMethod(PaymentAccount account, Field field, JsonReader jsonReader) {
Optional<Method> setter = fieldSettersMap.get(field);
if (setter.isPresent()) {
try {
// The setter might be on the PaymentAccount instance, or its
// PaymentAccountPayload instance.
if (isSetterOnPaymentAccountClass(setter.get(), account)) {
setter.get().invoke(account, nextStringOrNull(jsonReader));
} else if (isSetterOnPaymentAccountPayloadClass(setter.get(), account)) {
setter.get().invoke(account.getPaymentAccountPayload(), nextStringOrNull(jsonReader));
} else {
String errMsg = format("programmer error: cannot de-serialize json to a '%s' using reflection"
+ " because the setter method's declaring class was not found.",
account.getClass().getSimpleName());
throw new IllegalStateException(errMsg);
}
} catch (IllegalAccessException | InvocationTargetException ex) {
String errMsg = format("cannot set field value for %s on %s",
field.getName(),
account.getClass().getSimpleName());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
} else {
throw new IllegalStateException(
format("programmer error: cannot de-serialize json to a '%s' "
+ " because there is no setter method for field %s.",
account.getClass().getSimpleName(),
field.getName()));
}
}
private boolean isSetterOnPaymentAccountClass(Method setter, PaymentAccount account) {
return isSetterOnClass(setter, account.getClass());
}
private boolean isSetterOnPaymentAccountPayloadClass(Method setter, PaymentAccount account) {
return isSetterOnClass(setter, account.getPaymentAccountPayload().getClass())
|| isSetterOnClass(setter, account.getPaymentAccountPayload().getClass().getSuperclass());
}
private Map<Field, Optional<Method>> getFieldSetterMap() {
List<Field> orderedFields = getOrderedFields();
Map<Field, Optional<Method>> map = new LinkedHashMap<>();
for (Field field : orderedFields) {
Optional<Method> setter = getSetterMethodForFieldInClassHierarchy(field, paymentAccountType)
.or(() -> getSetterMethodForFieldInClassHierarchy(field, paymentAccountPayloadType));
map.put(field, setter);
}
return Collections.unmodifiableMap(map);
}
private List<Field> getOrderedFields() {
List<Field> fields = new ArrayList<>();
loadFieldListForClassHierarchy(fields, paymentAccountType, isExcludedField);
loadFieldListForClassHierarchy(fields, paymentAccountPayloadType, isExcludedField);
fields.sort(comparing(Field::getName));
return fields;
}
private String nextStringOrNull(JsonReader in) {
try {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
} else {
return in.nextString();
}
} catch (IOException ex) {
String errMsg = "cannot see next string in json reader";
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}
@SuppressWarnings("unused")
private Long nextLongOrNull(JsonReader in) {
try {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
} else {
return in.nextLong();
}
} catch (IOException ex) {
String errMsg = "cannot see next long in json reader";
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}
private boolean didReadCommonField(JsonReader in,
PaymentAccount account,
String fieldName) throws IOException {
switch (fieldName) {
case "_COMMENTS_":
case "paymentMethodId":
// Skip over the the comments and paymentMethodId, which is already
// set on the PaymentAccount instance.
in.skipValue();
return true;
case "accountName":
// Set the acct name using the value read from json.
account.setAccountName(nextStringOrNull(in));
return true;
case "salt":
// Set the acct salt using the value read from json.
String saltAsHex = nextStringOrNull(in);
if (saltAsHex != null && !saltAsHex.trim().isEmpty()) {
account.setSalt(decodeFromHex(saltAsHex));
}
return true;
default:
return false;
}
}
private boolean didReadCountryField(JsonReader in, PaymentAccount account, String fieldName) {
if (!fieldName.equals("country"))
return false;
String countryCode = nextStringOrNull(in);
Optional<Country> country = findCountryByCode(countryCode);
if (country.isPresent()) {
if (account.isCountryBasedPaymentAccount()) {
((CountryBasedPaymentAccount) account).setCountry(country.get());
FiatCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
account.setSingleTradeCurrency(fiatCurrency);
} else if (account.isMoneyGramAccount()) {
((MoneyGramAccount) account).setCountry(country.get());
} else {
String errMsg = format("cannot set the country on a %s",
paymentAccountType.getSimpleName());
log.error(StringUtils.capitalize(errMsg) + ".");
throw new IllegalStateException("programmer error: " + errMsg);
}
return true;
} else {
throw new IllegalArgumentException(
format("'%s' is an invalid country code.", countryCode));
}
}
private Class<? extends PaymentAccountPayload> getPaymentAccountPayloadType() {
try {
Package pkg = PaymentAccountPayload.class.getPackage();
//noinspection unchecked
return (Class<? extends PaymentAccountPayload>) Class.forName(pkg.getName()
+ "." + paymentAccountType.getSimpleName() + "Payload");
} catch (Exception ex) {
String errMsg = format("cannot get the payload class for %s",
paymentAccountType.getSimpleName());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}
private PaymentAccount initNewPaymentAccount() {
try {
Constructor<?> constructor = paymentAccountType.getDeclaredConstructor();
PaymentAccount paymentAccount = (PaymentAccount) constructor.newInstance();
paymentAccount.init();
return paymentAccount;
} catch (NoSuchMethodException
| IllegalAccessException
| InstantiationException
| InvocationTargetException ex) {
String errMsg = format("cannot instantiate a new %s",
paymentAccountType.getSimpleName());
log.error(StringUtils.capitalize(errMsg) + ".", ex);
throw new IllegalStateException("programmer error: " + errMsg);
}
}
}

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.api.model;
import bisq.common.Payload;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode
@Getter
public class TxFeeRateInfo implements Payload {
private final boolean useCustomTxFeeRate;
private final long customTxFeeRate;
private final long feeServiceRate;
private final long lastFeeServiceRequestTs;
public TxFeeRateInfo(boolean useCustomTxFeeRate,
long customTxFeeRate,
long feeServiceRate,
long lastFeeServiceRequestTs) {
this.useCustomTxFeeRate = useCustomTxFeeRate;
this.customTxFeeRate = customTxFeeRate;
this.feeServiceRate = feeServiceRate;
this.lastFeeServiceRequestTs = lastFeeServiceRequestTs;
}
//////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
//////////////////////////////////////////////////////////////////////////////////////
@Override
public bisq.proto.grpc.TxFeeRateInfo toProtoMessage() {
return bisq.proto.grpc.TxFeeRateInfo.newBuilder()
.setUseCustomTxFeeRate(useCustomTxFeeRate)
.setCustomTxFeeRate(customTxFeeRate)
.setFeeServiceRate(feeServiceRate)
.setLastFeeServiceRequestTs(lastFeeServiceRequestTs)
.build();
}
@SuppressWarnings("unused")
public static TxFeeRateInfo fromProto(bisq.proto.grpc.TxFeeRateInfo proto) {
return new TxFeeRateInfo(proto.getUseCustomTxFeeRate(),
proto.getCustomTxFeeRate(),
proto.getFeeServiceRate(),
proto.getLastFeeServiceRequestTs());
}
@Override
public String toString() {
return "TxFeeRateInfo{" + "\n" +
" useCustomTxFeeRate=" + useCustomTxFeeRate + "\n" +
", customTxFeeRate=" + customTxFeeRate + "sats/byte" + "\n" +
", feeServiceRate=" + feeServiceRate + "sats/byte" + "\n" +
", lastFeeServiceRequestTs=" + lastFeeServiceRequestTs + "\n" +
'}';
}
}

View file

@ -21,6 +21,7 @@ import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoSetup;
import bisq.core.dao.node.full.RpcService;
import bisq.core.offer.OpenOfferManager;
import bisq.core.setup.CorePersistedDataHost;
import bisq.core.setup.CoreSetup;
@ -229,6 +230,7 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(XmrTxProofService.class).shutDown();
injector.getInstance(RpcService.class).shutDown();
injector.getInstance(DaoSetup.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown();
injector.getInstance(OpenOfferManager.class).shutDown(() -> {

View file

@ -111,29 +111,38 @@ public class WalletAppSetup {
ObjectProperty<Throwable> walletServiceException = new SimpleObjectProperty<>();
btcInfoBinding = EasyBind.combine(walletsSetup.downloadPercentageProperty(),
walletsSetup.chainHeightProperty(),
feeService.feeUpdateCounterProperty(),
walletServiceException,
(downloadPercentage, feeUpdate, exception) -> {
(downloadPercentage, chainHeight, feeUpdate, exception) -> {
String result;
if (exception == null) {
double percentage = (double) downloadPercentage;
btcSyncProgress.set(percentage);
int bestChainHeight = walletsSetup.getChain() != null ?
walletsSetup.getChain().getBestChainHeight() :
0;
String chainHeightAsString = bestChainHeight > 0 ?
String.valueOf(bestChainHeight) :
"";
if (percentage == 1) {
result = Res.get("mainView.footer.btcInfo",
Res.get("mainView.footer.btcInfo.synchronizedWith"),
getBtcNetworkAsString(),
feeService.getFeeTextForDisplay());
String synchronizedWith = Res.get("mainView.footer.btcInfo.synchronizedWith",
getBtcNetworkAsString(), chainHeightAsString);
String info = feeService.isFeeAvailable() ?
Res.get("mainView.footer.btcFeeRate", feeService.getTxFeePerVbyte().value) :
"";
result = Res.get("mainView.footer.btcInfo", synchronizedWith, info);
getBtcSplashSyncIconId().set("image-connection-synced");
downloadCompleteHandler.run();
} else if (percentage > 0.0) {
result = Res.get("mainView.footer.btcInfo",
Res.get("mainView.footer.btcInfo.synchronizingWith"),
getBtcNetworkAsString() + ": " + FormattingUtils.formatToPercentWithSymbol(percentage), "");
String synchronizingWith = Res.get("mainView.footer.btcInfo.synchronizingWith",
getBtcNetworkAsString(), chainHeightAsString,
FormattingUtils.formatToPercentWithSymbol(percentage));
result = Res.get("mainView.footer.btcInfo", synchronizingWith, "");
} else {
result = Res.get("mainView.footer.btcInfo",
Res.get("mainView.footer.btcInfo.connectingTo"),
getBtcNetworkAsString(), "");
getBtcNetworkAsString());
}
} else {
result = Res.get("mainView.footer.btcInfo",
@ -259,6 +268,7 @@ public class WalletAppSetup {
}
});
}
private String getBtcNetworkAsString() {
String postFix;
if (config.ignoreLocalBtcNode)

View file

@ -22,6 +22,7 @@ import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoSetup;
import bisq.core.dao.node.full.RpcService;
import bisq.core.offer.OpenOfferManager;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
@ -86,6 +87,7 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable {
try {
if (injector != null) {
JsonFileManager.shutDownAllInstances();
injector.getInstance(RpcService.class).shutDown();
injector.getInstance(DaoSetup.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> {

View file

@ -0,0 +1,70 @@
package bisq.core.btc.model;
import bisq.core.dao.state.model.blockchain.TxType;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.Transaction;
import lombok.Getter;
@Getter
public final class BsqTransferModel {
private final LegacyAddress receiverAddress;
private final Coin receiverAmount;
private final Transaction preparedSendTx;
private final Transaction txWithBtcFee;
private final Transaction signedTx;
private final Coin miningFee;
private final int txSize;
private final TxType txType;
public BsqTransferModel(LegacyAddress receiverAddress,
Coin receiverAmount,
Transaction preparedSendTx,
Transaction txWithBtcFee,
Transaction signedTx) {
this.receiverAddress = receiverAddress;
this.receiverAmount = receiverAmount;
this.preparedSendTx = preparedSendTx;
this.txWithBtcFee = txWithBtcFee;
this.signedTx = signedTx;
this.miningFee = signedTx.getFee();
this.txSize = signedTx.bitcoinSerialize().length;
this.txType = TxType.TRANSFER_BSQ;
}
public String getReceiverAddressAsString() {
return receiverAddress.toString();
}
public double getTxSizeInKb() {
return txSize / 1000d;
}
public String toShortString() {
return "{" + "\n" +
" receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" +
", receiverAmount=" + receiverAmount + "\n" +
", txWithBtcFee.txId=" + txWithBtcFee.getTxId() + "\n" +
", miningFee=" + miningFee + "\n" +
", txSizeInKb=" + getTxSizeInKb() + "\n" +
'}';
}
@Override
public String toString() {
return "BsqTransferModel{" + "\n" +
" receiverAddress='" + getReceiverAddressAsString() + '\'' + "\n" +
", receiverAmount=" + receiverAmount + "\n" +
", preparedSendTx=" + preparedSendTx + "\n" +
", txWithBtcFee=" + txWithBtcFee + "\n" +
", signedTx=" + signedTx + "\n" +
", miningFee=" + miningFee + "\n" +
", txSize=" + txSize + "\n" +
", txSizeInKb=" + getTxSizeInKb() + "\n" +
", txType=" + txType + "\n" +
'}';
}
}

View file

@ -128,8 +128,10 @@ public class WalletConfig extends AbstractIdleService {
protected DownloadProgressTracker downloadListener;
protected InputStream checkpoints;
protected String userAgent, version;
@Nullable protected DeterministicSeed restoreFromSeed;
@Nullable protected PeerDiscovery discovery;
@Nullable
protected DeterministicSeed restoreFromSeed;
@Nullable
protected PeerDiscovery discovery;
protected volatile Context context;
@ -308,7 +310,7 @@ public class WalletConfig extends AbstractIdleService {
}
vChain = new BlockChain(params, vStore);
vPeerGroup = createPeerGroup();
if (minBroadcastConnections > 0 )
if (minBroadcastConnections > 0)
vPeerGroup.setMinBroadcastConnections(minBroadcastConnections);
if (this.userAgent != null)
vPeerGroup.setUserAgent(userAgent, version);
@ -363,7 +365,9 @@ public class WalletConfig extends AbstractIdleService {
}, MoreExecutors.directExecutor());
}
private Wallet createOrLoadWallet(boolean shouldReplayWallet, File walletFile, boolean isBsqWallet) throws Exception {
private Wallet createOrLoadWallet(boolean shouldReplayWallet,
File walletFile,
boolean isBsqWallet) throws Exception {
Wallet wallet;
maybeMoveOldWalletOutOfTheWay(walletFile);
@ -575,4 +579,8 @@ public class WalletConfig extends AbstractIdleService {
}
migratedWalletToSegwit.set(true);
}
public boolean stateStartingOrRunning() {
return state() == State.STARTING || state() == State.RUNNING;
}
}

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());
@ -487,7 +488,7 @@ public class WalletsSetup {
@Nullable
public BlockChain getChain() {
return walletConfig != null ? walletConfig.chain() : null;
return walletConfig != null && walletConfig.stateStartingOrRunning() ? walletConfig.chain() : null;
}
public PeerGroup getPeerGroup() {
@ -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

@ -0,0 +1,59 @@
package bisq.core.btc.wallet;
import bisq.core.btc.exceptions.BsqChangeBelowDustException;
import bisq.core.btc.exceptions.TransactionVerificationException;
import bisq.core.btc.exceptions.WalletException;
import bisq.core.btc.model.BsqTransferModel;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class BsqTransferService {
private final WalletsManager walletsManager;
private final BsqWalletService bsqWalletService;
private final BtcWalletService btcWalletService;
@Inject
public BsqTransferService(WalletsManager walletsManager,
BsqWalletService bsqWalletService,
BtcWalletService btcWalletService) {
this.walletsManager = walletsManager;
this.bsqWalletService = bsqWalletService;
this.btcWalletService = btcWalletService;
}
public BsqTransferModel getBsqTransferModel(LegacyAddress address,
Coin receiverAmount)
throws TransactionVerificationException,
WalletException,
BsqChangeBelowDustException,
InsufficientMoneyException {
Transaction preparedSendTx = bsqWalletService.getPreparedSendBsqTx(address.toString(), receiverAmount);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true);
Transaction signedTx = bsqWalletService.signTx(txWithBtcFee);
return new BsqTransferModel(address,
receiverAmount,
preparedSendTx,
txWithBtcFee,
signedTx);
}
public void sendFunds(BsqTransferModel bsqTransferModel, TxBroadcaster.Callback callback) {
log.info("Publishing BSQ transfer {}", bsqTransferModel.toShortString());
walletsManager.publishAndCommitBsqTx(bsqTransferModel.getTxWithBtcFee(),
bsqTransferModel.getTxType(),
callback);
}
}

View file

@ -1130,12 +1130,15 @@ public class BtcWalletService extends WalletService {
Coin fee,
@Nullable KeyParameter aesKey,
@SuppressWarnings("SameParameterValue") AddressEntry.Context context,
@Nullable String memo,
FutureCallback<Transaction> callback) throws AddressFormatException,
AddressEntryException, InsufficientMoneyException {
SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context);
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
if (memo != null) {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
return sendResult.tx.getTxId().toString();
}
@ -1146,13 +1149,16 @@ public class BtcWalletService extends WalletService {
Coin fee,
@Nullable String changeAddress,
@Nullable KeyParameter aesKey,
@Nullable String memo,
FutureCallback<Transaction> callback) throws AddressFormatException,
AddressEntryException, InsufficientMoneyException {
SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey);
Wallet.SendResult sendResult = wallet.sendCoins(request);
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
if (memo != null) {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
return sendResult.tx;
}

View file

@ -569,19 +569,21 @@ public class TradeWalletService {
* @param takerIsSeller the flag indicating if we are in the taker as seller role or the opposite
* @param contractHash the hash of the contract to be added to the OP_RETURN output
* @param makersDepositTxSerialized the prepared deposit transaction signed by the maker
* @param msOutputAmount the MultiSig output amount, as determined by the taker
* @param buyerInputs the connected outputs for all inputs of the buyer
* @param sellerInputs the connected outputs for all inputs of the seller
* @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 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
* script, contract hash or output amount 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,
byte[] contractHash,
byte[] makersDepositTxSerialized,
Coin msOutputAmount,
List<RawTransactionInput> buyerInputs,
List<RawTransactionInput> sellerInputs,
byte[] buyerPubKey,
@ -592,10 +594,15 @@ public class TradeWalletService {
checkArgument(!buyerInputs.isEmpty());
checkArgument(!sellerInputs.isEmpty());
// Check if maker's MultiSig script is identical to the takers
// Check if maker's MultiSig script is identical to the taker's
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");
throw new TransactionVerificationException("Maker's hashedMultiSigOutputScript does not match taker's hashedMultiSigOutputScript");
}
// Check if maker's MultiSig output value is identical to the taker's
if (!makersDepositTx.getOutput(0).getValue().equals(msOutputAmount)) {
throw new TransactionVerificationException("Maker's MultiSig output amount does not match taker's MultiSig output amount");
}
// The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new
@ -646,7 +653,7 @@ public class TradeWalletService {
TransactionOutput makersContractHashOutput = makersDepositTx.getOutputs().get(1);
log.debug("makersContractHashOutput {}", makersContractHashOutput);
if (!makersContractHashOutput.getScriptPubKey().equals(contractHashOutput.getScriptPubKey())) {
throw new TransactionVerificationException("Maker's transaction output for the contract hash is not matching takers version.");
throw new TransactionVerificationException("Maker's transaction output for the contract hash is not matching taker's version.");
}
// Add all outputs from makersDepositTx to depositTx
@ -1117,7 +1124,7 @@ public class TradeWalletService {
Transaction payoutTx = new Transaction(params);
Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex);
payoutTx.addInput(new TransactionInput(params, depositTx, null, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue));
payoutTx.addInput(new TransactionInput(params, payoutTx, new byte[]{}, new TransactionOutPoint(params, 0, spendTxHash), msOutputValue));
if (buyerPayoutAmount.isPositive()) {
payoutTx.addOutput(buyerPayoutAmount, Address.fromString(params, buyerAddressString));

View file

@ -505,6 +505,20 @@ public abstract class WalletService {
return getNumTxOutputsForAddress(address) == 0;
}
// BISQ issue #4039: Prevent dust outputs from being created.
// Check the outputs of a proposed transaction. If any are below the dust threshold,
// add up the dust, log the details, and return the cumulative dust amount.
public Coin getDust(Transaction proposedTransaction) {
Coin dust = Coin.ZERO;
for (TransactionOutput transactionOutput : proposedTransaction.getOutputs()) {
if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) {
dust = dust.add(transactionOutput.getValue());
log.info("Dust TXO = {}", transactionOutput.toString());
}
}
return dust;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Empty complete Wallet
@ -554,6 +568,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

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

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

@ -121,6 +121,20 @@ public class RpcService {
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void shutDown() {
if (daemon != null) {
daemon.shutdown();
log.info("daemon shut down");
}
if (client != null) {
client.close();
log.info("client closed");
}
executor.shutdown();
}
void setup(ResultHandler resultHandler, Consumer<Throwable> errorHandler) {
ListenableFuture<Void> future = executor.submit(() -> {
try {

View file

@ -348,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 +
@ -365,12 +366,12 @@ 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 bannedAutoConfExplorers=" + bannedAutoConfExplorers +
"\n}";
}
}

View file

@ -141,7 +141,7 @@ public class GetInventoryRequestHandler implements MessageListener {
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.resetNumAllConnectionsLostEvents();
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()));

View file

@ -70,7 +70,7 @@ public class GetInventoryRequester implements MessageListener, ConnectionListene
}
private void onTimeOut() {
errorMessageHandler.handleErrorMessage("Timeout got triggered (" + TIMEOUT_SEC + " sec)");
errorMessageHandler.handleErrorMessage("Request timeout");
shutDown();
}

View file

@ -32,7 +32,7 @@ public enum InventoryItem {
// Percentage deviation
OfferPayload("OfferPayload",
true,
new DeviationByPercentage(0.9, 1.1, 0.95, 1.05), 5),
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),
@ -102,7 +102,7 @@ public enum InventoryItem {
new DeviationByPercentage(0, 3, 0, 2.5), 2),
numAllConnectionsLostEvents("numAllConnectionsLostEvents",
true,
new DeviationByIntegerDiff(1, 2), 3),
new DeviationByIntegerDiff(1, 2), 1),
sentBytesPerSec("sentBytesPerSec",
true,
new DeviationByPercentage(), 5),
@ -137,6 +137,7 @@ public enum InventoryItem {
// 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;

View file

@ -29,7 +29,7 @@ import org.jetbrains.annotations.Nullable;
@Getter
public class RequestInfo {
// Carries latest commit hash of feature changes (not latest commit as that is then the commit for editing that field)
public static final String COMMIT_HASH = "d789282b";
public static final String COMMIT_HASH = "7f83d1b3";
private final long requestStartTime;
@Setter
@ -56,6 +56,10 @@ public class RequestInfo {
null;
}
public boolean hasError() {
return errorMessage != null && !errorMessage.isEmpty();
}
@Value
public static class Data {
private final String value;

View file

@ -30,8 +30,10 @@ import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.ReferralIdService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.AutoConfirmSettings;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.CoinUtil;
@ -39,6 +41,7 @@ import bisq.network.p2p.P2PService;
import bisq.common.app.Capabilities;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
@ -49,6 +52,7 @@ import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import lombok.extern.slf4j.Slf4j;
@ -63,6 +67,7 @@ import static bisq.core.btc.wallet.Restrictions.isDust;
import static bisq.core.offer.OfferPayload.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
/**
* This class holds utility methods for creating, editing and taking an Offer.
@ -78,6 +83,10 @@ public class OfferUtil {
private final PriceFeedService priceFeedService;
private final P2PService p2PService;
private final ReferralIdService referralIdService;
private final TradeStatisticsManager tradeStatisticsManager;
private final Predicate<String> isValidFeePaymentCurrencyCode = (c) ->
c.equalsIgnoreCase("BSQ") || c.equalsIgnoreCase("BTC");
@Inject
public OfferUtil(AccountAgeWitnessService accountAgeWitnessService,
@ -86,7 +95,8 @@ public class OfferUtil {
Preferences preferences,
PriceFeedService priceFeedService,
P2PService p2PService,
ReferralIdService referralIdService) {
ReferralIdService referralIdService,
TradeStatisticsManager tradeStatisticsManager) {
this.accountAgeWitnessService = accountAgeWitnessService;
this.bsqWalletService = bsqWalletService;
this.filterManager = filterManager;
@ -94,6 +104,20 @@ public class OfferUtil {
this.priceFeedService = priceFeedService;
this.p2PService = p2PService;
this.referralIdService = referralIdService;
this.tradeStatisticsManager = tradeStatisticsManager;
}
public void maybeSetFeePaymentCurrencyPreference(String feeCurrencyCode) {
if (!feeCurrencyCode.isEmpty()) {
if (!isValidFeePaymentCurrencyCode.test(feeCurrencyCode))
throw new IllegalStateException(format("%s cannot be used to pay trade fees",
feeCurrencyCode.toUpperCase()));
if (feeCurrencyCode.equalsIgnoreCase("BSQ") && preferences.isPayFeeInBtc())
preferences.setPayFeeInBtc(false);
else if (feeCurrencyCode.equalsIgnoreCase("BTC") && !preferences.isPayFeeInBtc())
preferences.setPayFeeInBtc(true);
}
}
/**
@ -268,8 +292,14 @@ public class OfferUtil {
public Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee,
boolean isCurrencyForMakerFeeBtc,
CoinFormatter bsqFormatter) {
String countryCode = preferences.getUserCountry().code;
String userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
String userCurrencyCode = preferences.getPreferredTradeCurrency().getCode();
if (CurrencyUtil.isCryptoCurrency(userCurrencyCode)) {
// In case the user has selected a altcoin as preferredTradeCurrency
// we derive the fiat currency from the user country
String countryCode = preferences.getUserCountry().code;
userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
}
return getFeeInUserFiatCurrency(makerFee,
isCurrencyForMakerFeeBtc,
userCurrencyCode,
@ -329,8 +359,6 @@ public class OfferUtil {
boolean isCurrencyForMakerFeeBtc,
String userCurrencyCode,
CoinFormatter bsqFormatter) {
// We use the users currency derived from his selected country. We don't use the
// preferredTradeCurrency from preferences as that can be also set to an altcoin.
MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode);
if (marketPrice != null && makerFee != null) {
long marketPriceAsLong = roundDoubleToLong(
@ -340,16 +368,16 @@ public class OfferUtil {
if (isCurrencyForMakerFeeBtc) {
return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee));
} else {
Optional<Price> optionalBsqPrice = priceFeedService.getBsqPrice();
if (optionalBsqPrice.isPresent()) {
Price bsqPrice = optionalBsqPrice.get();
String inputValue = bsqFormatter.formatCoin(makerFee);
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
return Optional.of(userCurrencyPrice.getVolumeByAmount(requiredBtc));
} else {
return Optional.empty();
}
// We use the current market price for the fiat currency and the 30 day average BSQ price
Tuple2<Price, Price> tuple = AveragePriceUtil.getAveragePriceTuple(preferences,
tradeStatisticsManager,
30);
Price bsqPrice = tuple.second;
String inputValue = bsqFormatter.formatCoin(makerFee);
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
Volume volumeByAmount = userCurrencyPrice.getVolumeByAmount(requiredBtc);
return Optional.of(volumeByAmount);
}
} else {
return Optional.empty();

View file

@ -580,6 +580,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
// Don't allow trade start if BitcoinJ is not fully synced (bisq issue #4764)
if (!btcWalletService.isChainHeightSyncedWithinTolerance()) {
errorMessage = "We got a handleOfferAvailabilityRequest but our chain is not synced.";
log.info(errorMessage);
sendAckMessage(request, peer, false, errorMessage);
return;
}
if (stopped) {
errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call.";
log.debug(errorMessage);

View file

@ -173,10 +173,18 @@ public abstract class PaymentAccount implements PersistablePayload {
return paymentAccountPayload.getOwnerId();
}
public boolean isCountryBasedPaymentAccount() {
return this instanceof CountryBasedPaymentAccount;
}
public boolean isHalCashAccount() {
return this instanceof HalCashAccount;
}
public boolean isMoneyGramAccount() {
return this instanceof MoneyGramAccount;
}
/**
* Return an Optional of the trade currency for this payment account, or
* Optional.empty() if none is found. If this payment account has a selected

View file

@ -20,7 +20,6 @@ package bisq.core.provider.fee;
import bisq.core.dao.governance.param.Param;
import bisq.core.dao.governance.period.PeriodService;
import bisq.core.dao.state.DaoStateService;
import bisq.core.locale.Res;
import bisq.common.UserThread;
import bisq.common.config.Config;
@ -45,6 +44,7 @@ import java.time.Instant;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@ -96,6 +96,7 @@ public class FeeService {
private final IntegerProperty feeUpdateCounter = new SimpleIntegerProperty(0);
private long txFeePerVbyte = BTC_DEFAULT_TX_FEE;
private Map<String, Long> timeStampMap;
@Getter
private long lastRequest;
private long minFeePerVByte;
private long epochInSecondAtLastRequest;
@ -192,10 +193,7 @@ public class FeeService {
return feeUpdateCounter;
}
public String getFeeTextForDisplay() {
// only show the fee rate if it has been initialized from the service (see feeUpdateCounter)
if (feeUpdateCounter.get() > 0)
return Res.get("mainView.footer.btcFeeRate", txFeePerVbyte);
return "";
public boolean isFeeAvailable() {
return feeUpdateCounter.get() > 0;
}
}

View file

@ -479,8 +479,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// Complete trade
///////////////////////////////////////////////////////////////////////////////////////////
public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey,
Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) {
public void onWithdrawRequest(String toAddress,
Coin amount,
Coin fee,
KeyParameter aesKey,
Trade trade,
@Nullable String memo,
ResultHandler resultHandler,
FaultHandler faultHandler) {
String fromAddress = btcWalletService.getOrCreateAddressEntry(trade.getId(),
AddressEntry.Context.TRADE_PAYOUT).getAddressString();
FutureCallback<Transaction> callback = new FutureCallback<>() {
@ -504,7 +510,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
};
try {
btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey, AddressEntry.Context.TRADE_PAYOUT, callback);
btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey,
AddressEntry.Context.TRADE_PAYOUT, memo, callback);
} catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) {
e.printStackTrace();
log.error(e.getMessage());

View file

@ -20,6 +20,7 @@ package bisq.core.trade.protocol.tasks.buyer_as_taker;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.model.RawTransactionInput;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.Offer;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.trade.protocol.tasks.TradeTask;
@ -72,6 +73,10 @@ public class BuyerAsTakerSignsDepositTx extends TradeTask {
processModel.getBtcWalletService().setCoinLockedInMultiSigAddressEntry(buyerMultiSigAddressEntry, multiSigValue.value);
walletService.saveAddressEntryList();
Offer offer = trade.getOffer();
Coin msOutputAmount = offer.getBuyerSecurityDeposit().add(offer.getSellerSecurityDeposit()).add(trade.getTxFee())
.add(checkNotNull(trade.getTradeAmount()));
TradingPeer tradingPeer = processModel.getTradingPeer();
byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey();
checkArgument(Arrays.equals(buyerMultiSigPubKey, buyerMultiSigAddressEntry.getPubKey()),
@ -83,6 +88,7 @@ public class BuyerAsTakerSignsDepositTx extends TradeTask {
false,
contractHash,
processModel.getPreparedDepositTx(),
msOutputAmount,
buyerInputs,
sellerInputs,
buyerMultiSigPubKey,

View file

@ -20,6 +20,7 @@ package bisq.core.trade.protocol.tasks.seller_as_taker;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.model.RawTransactionInput;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.Offer;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.TradingPeer;
@ -70,12 +71,17 @@ public class SellerAsTakerSignsDepositTx extends TradeTask {
processModel.getBtcWalletService().setCoinLockedInMultiSigAddressEntry(sellerMultiSigAddressEntry, multiSigValue.value);
walletService.saveAddressEntryList();
Offer offer = trade.getOffer();
Coin msOutputAmount = offer.getBuyerSecurityDeposit().add(offer.getSellerSecurityDeposit()).add(trade.getTxFee())
.add(checkNotNull(trade.getTradeAmount()));
TradingPeer tradingPeer = processModel.getTradingPeer();
Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx(
true,
trade.getContractHash(),
processModel.getPreparedDepositTx(),
msOutputAmount,
checkNotNull(tradingPeer.getRawTransactionInputs()),
sellerInputs,
tradingPeer.getMultiSigPubKey(),

View file

@ -40,11 +40,14 @@ import javax.inject.Singleton;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import java.time.Instant;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@ -135,6 +138,24 @@ public class TradeStatisticsManager {
.collect(Collectors.toCollection(ArrayList::new));
cryptoCurrencyList.add(0, new CurrencyTuple(Res.getBaseCurrencyCode(), Res.getBaseCurrencyName(), 8));
jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(cryptoCurrencyList), "crypto_currency_list");
Instant yearAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(365));
Set<String> activeCurrencies = observableTradeStatisticsSet.stream()
.filter(e -> e.getDate().toInstant().isAfter(yearAgo))
.map(p -> p.getCurrency())
.collect(Collectors.toSet());
ArrayList<CurrencyTuple> activeFiatCurrencyList = fiatCurrencyList.stream()
.filter(e -> activeCurrencies.contains(e.code))
.map(e -> new CurrencyTuple(e.code, e.name, 8))
.collect(Collectors.toCollection(ArrayList::new));
jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(activeFiatCurrencyList), "active_fiat_currency_list");
ArrayList<CurrencyTuple> activeCryptoCurrencyList = cryptoCurrencyList.stream()
.filter(e -> activeCurrencies.contains(e.code))
.map(e -> new CurrencyTuple(e.code, e.name, 8))
.collect(Collectors.toCollection(ArrayList::new));
jsonFileManager.writeToDiscThreaded(Utilities.objectToJson(activeCryptoCurrencyList), "active_crypto_currency_list");
}
List<TradeStatisticsForJson> list = observableTradeStatisticsSet.stream()

View file

@ -863,7 +863,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
}
public long getWithdrawalTxFeeInVbytes() {
return Math.max(prefPayload.getWithdrawalTxFeeInVbytes(), Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte());
return Math.max(prefPayload.getWithdrawalTxFeeInVbytes(),
Config.baseCurrencyNetwork().getDefaultMinFeePerVbyte());
}
public boolean isDaoFullNode() {

View file

@ -0,0 +1,139 @@
/*
* 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.util;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.utils.Fiat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.stream.Collectors;
public class AveragePriceUtil {
private static final double HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER = 10;
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
TradeStatisticsManager tradeStatisticsManager,
int days) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
Date pastXDays = getPastDate(days);
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("BSQ"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
bsqAllTradePastXDays;
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("USD"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
removeOutliers(usdAllTradePastXDays, percentToTrim) :
usdAllTradePastXDays;
Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays));
Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays));
return new Tuple2<>(usdPrice, bsqPrice);
}
private static List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
List<Double> yValues = list.stream()
.filter(TradeStatistics3::isValid)
.map(e -> (double) e.getPrice())
.collect(Collectors.toList());
Tuple2<Double, Double> tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER);
double lowerBound = tuple.first;
double upperBound = tuple.second;
return list.stream()
.filter(e -> e.getPrice() > lowerBound)
.filter(e -> e.getPrice() < upperBound)
.collect(Collectors.toList());
}
private static long getBTCAverage(List<TradeStatistics3> list) {
long accumulatedVolume = 0;
long accumulatedAmount = 0;
for (TradeStatistics3 item : list) {
accumulatedVolume += item.getTradeVolume().getValue();
accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded
}
long averagePrice;
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0;
return averagePrice;
}
private static long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) {
// Use next USD/BTC print as price to calculate BSQ/USD rate
// Store each trade as amount of USD and amount of BSQ traded
List<Tuple2<Double, Double>> usdBsqList = new ArrayList<>(bsqList.size());
usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
for (TradeStatistics3 item : bsqList) {
// Find usdprice for trade item
usdBTCPrice = usdList.stream()
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
Fiat.SMALLEST_UNIT_EXPONENT))
.findFirst()
.orElse(usdBTCPrice);
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount));
}
long averagePrice;
var usdTraded = usdBsqList.stream()
.mapToDouble(item -> item.first)
.sum();
var bsqTraded = usdBsqList.stream()
.mapToDouble(item -> item.second)
.sum();
var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d;
var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT);
averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0;
return averagePrice;
}
private static Date getPastDate(int days) {
Calendar cal = new GregorianCalendar();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
return cal.getTime();
}
}

View file

@ -0,0 +1,140 @@
/*
* 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.util;
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
import bisq.common.util.Tuple2;
import javafx.collections.FXCollections;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
public class InlierUtil {
/* Finds the minimum and maximum inlier values. The returned values may be NaN.
* See `computeInlierThreshold` for the definition of inlier.
*/
public static Tuple2<Double, Double> findInlierRange(
List<Double> yValues,
double percentToTrim,
double howManyStdDevsConstituteOutlier
) {
Tuple2<Double, Double> inlierThreshold =
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
DoubleSummaryStatistics inlierStatistics =
yValues
.stream()
.filter(y -> withinBounds(inlierThreshold, y))
.mapToDouble(Double::doubleValue)
.summaryStatistics();
var inlierMin = inlierStatistics.getMin();
var inlierMax = inlierStatistics.getMax();
return new Tuple2<>(inlierMin, inlierMax);
}
private static boolean withinBounds(Tuple2<Double, Double> bounds, double number) {
var lowerBound = bounds.first;
var upperBound = bounds.second;
return (lowerBound <= number) && (number <= upperBound);
}
/* Computes the lower and upper inlier thresholds. A point lying outside
* these thresholds is considered an outlier, and a point lying within
* is considered an inlier.
* The thresholds are found by trimming the dataset (see method `trim`),
* then adding or subtracting a multiple of its (trimmed) standard
* deviation from its (trimmed) mean.
*/
private static Tuple2<Double, Double> computeInlierThreshold(
List<Double> numbers, double percentToTrim, double howManyStdDevsConstituteOutlier
) {
if (howManyStdDevsConstituteOutlier <= 0) {
throw new IllegalArgumentException(
"howManyStdDevsConstituteOutlier should be a positive number");
}
List<Double> trimmed = trim(percentToTrim, numbers);
DoubleSummaryStatisticsWithStdDev summaryStatistics =
trimmed.stream()
.collect(
DoubleSummaryStatisticsWithStdDev::new,
DoubleSummaryStatisticsWithStdDev::accept,
DoubleSummaryStatisticsWithStdDev::combine);
double mean = summaryStatistics.getAverage();
double stdDev = summaryStatistics.getStandardDeviation();
var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier);
var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier);
return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold);
}
/* Sorts the data and discards given percentage from the left and right sides each.
* E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded.
* Used in calculating trimmed mean (and in turn trimmed standard deviation),
* which is more robust to outliers than a simple mean.
*/
private static List<Double> trim(double percentToTrim, List<Double> numbers) {
var minPercentToTrim = 0;
var maxPercentToTrim = 50;
if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) {
throw new IllegalArgumentException(
String.format(
"The percentage of data points to trim must be in the range [%d,%d].",
minPercentToTrim, maxPercentToTrim));
}
var totalPercentTrim = percentToTrim * 2;
if (totalPercentTrim == 0) {
return numbers;
}
if (totalPercentTrim == 100) {
return FXCollections.emptyObservableList();
}
if (numbers.isEmpty()) {
return numbers;
}
var count = numbers.size();
int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0?
if (countToDropFromEachSide == 0) {
return numbers;
}
var sorted = numbers.stream().sorted();
var oneSideTrimmed = sorted.skip(countToDropFromEachSide);
// Here, having already trimmed the left-side, we are implicitly trimming
// the right-side by specifying a limit to the stream's length.
// An explicit right-side drop/trim/skip is not supported by the Stream API.
var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count?
var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim);
return bothSidesTrimmed.collect(Collectors.toList());
}
}

View file

@ -21,7 +21,7 @@ import bisq.core.dao.governance.param.Param;
import bisq.core.dao.governance.proposal.ProposalValidationException;
import bisq.core.locale.GlobalSettings;
import bisq.core.locale.Res;
import bisq.core.provider.price.MarketPrice;
import bisq.core.monetary.Price;
import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.validation.BtcAddressValidator;
@ -121,10 +121,10 @@ public class BsqFormatter implements CoinFormatter {
return amountFormat.format(MathUtils.scaleDownByPowerOf10(amount.value, 2)) + " BSQ";
}
public String formatMarketCap(MarketPrice bsqPriceMarketPrice, MarketPrice fiatMarketPrice, Coin issuedAmount) {
if (bsqPriceMarketPrice != null && fiatMarketPrice != null) {
double marketCap = bsqPriceMarketPrice.getPrice() * fiatMarketPrice.getPrice() * (MathUtils.scaleDownByPowerOf10(issuedAmount.value, 2));
return marketCapFormat.format(MathUtils.doubleToLong(marketCap)) + " " + fiatMarketPrice.getCurrencyCode();
public String formatMarketCap(Price usdBsqPrice, Coin issuedAmount) {
if (usdBsqPrice != null && issuedAmount != null) {
double marketCap = usdBsqPrice.getValue() * (MathUtils.scaleDownByPowerOf10(issuedAmount.value, 6));
return marketCapFormat.format(MathUtils.doubleToLong(marketCap)) + " USD";
} else {
return "";
}

View file

@ -71,6 +71,7 @@ shared.amountWithCur=Amount in {0}
shared.volumeWithCur=Volume in {0}
shared.currency=Currency
shared.market=Market
shared.deviation=Deviation
shared.paymentMethod=Payment method
shared.tradeCurrency=Trade currency
shared.offerType=Offer type
@ -249,14 +250,14 @@ mainView.balance.locked=Locked in trades
mainView.balance.reserved.short=Reserved
mainView.balance.locked.short=Locked
mainView.footer.usingTor=(using Tor)
mainView.footer.usingTor=(via Tor)
mainView.footer.localhostBitcoinNode=(localhost)
mainView.footer.btcInfo={0} {1} {2}
mainView.footer.btcFeeRate=/ Current fee rate: {0} sat/vB
mainView.footer.btcInfo={0} {1}
mainView.footer.btcFeeRate=/ Fee rate: {0} sat/vB
mainView.footer.btcInfo.initializing=Connecting to Bitcoin network
mainView.footer.bsqInfo.synchronizing=/ Synchronizing DAO
mainView.footer.btcInfo.synchronizingWith=Synchronizing with
mainView.footer.btcInfo.synchronizedWith=Synced with
mainView.footer.btcInfo.synchronizingWith=Synchronizing with {0} at block: {1} / {2}
mainView.footer.btcInfo.synchronizedWith=Synced with {0} at block {1}
mainView.footer.btcInfo.connectingTo=Connecting to
mainView.footer.btcInfo.connectionFailed=Connection failed to
mainView.footer.p2pInfo=Bitcoin network peers: {0} / Bisq network peers: {1}
@ -337,10 +338,10 @@ offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n {
offerbook.availableOffers=Available offers
offerbook.filterByCurrency=Filter by currency
offerbook.filterByPaymentMethod=Filter by payment method
offerbook.timeSinceSigning=Signed since
offerbook.timeSinceSigning=Account info
offerbook.timeSinceSigning.info=This account was verified and {0}
offerbook.timeSinceSigning.info.arbitrator=signed by an arbitrator and can sign peer accounts
offerbook.timeSinceSigning.info.peer=signed by a peer, waiting for limits to be lifted
offerbook.timeSinceSigning.info.peer=signed by a peer, waiting %d days for limits to be lifted
offerbook.timeSinceSigning.info.peerLimitLifted=signed by a peer and limits were lifted
offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts (limits lifted)
offerbook.timeSinceSigning.info.banned=account was banned
@ -351,9 +352,12 @@ offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\
{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts.
offerbook.timeSinceSigning.notSigned=Not signed yet
offerbook.timeSinceSigning.notSigned.ageDays={0} days
offerbook.timeSinceSigning.notSigned.noNeed=N/A
shared.notSigned=This account hasn't been signed yet
shared.notSigned.noNeed=This account type doesn't use signing
shared.notSigned=This account has not been signed yet and was created {0} days ago
shared.notSigned.noNeed=This account type does not require signing
shared.notSigned.noNeedDays=This account type does not require signing and was created {0} days ago
shared.notSigned.noNeedAlts=Altcoin accounts do not feature signing or aging
offerbook.nrOffers=No. of offers: {0}
offerbook.volume={0} (min - max)
@ -557,6 +561,8 @@ portfolio.tab.history=History
portfolio.tab.failed=Failed
portfolio.tab.editOpenOffer=Edit offer
portfolio.closedTrades.deviation.help=Percentage price deviation from market
portfolio.pending.invalidDelayedPayoutTx=There is an issue with a missing or invalid transaction.\n\n\
Please do NOT send the fiat or altcoin payment. Contact Bisq \
developers on Keybase [HYPERLINK:https://keybase.io/team/bisq] or on the \
@ -1254,6 +1260,7 @@ settings.net.creationDateColumn=Established
settings.net.connectionTypeColumn=In/Out
settings.net.sentDataLabel=Sent data statistics
settings.net.receivedDataLabel=Received data statistics
settings.net.chainHeightLabel=Latest BTC block height
settings.net.roundTripTimeColumn=Roundtrip
settings.net.sentBytesColumn=Sent
settings.net.receivedBytesColumn=Received
@ -1268,6 +1275,7 @@ settings.net.needRestart=You need to restart the application to apply that chang
settings.net.notKnownYet=Not known yet...
settings.net.sentData=Sent data: {0}, {1} messages, {2} messages/sec
settings.net.receivedData=Received data: {0}, {1} messages, {2} messages/sec
settings.net.chainHeight=Bisq: {0} | Peers: {1}
settings.net.ips=[IP address:port | host name:port | onion address:port] (comma separated). Port can be omitted if default is used (8333).
settings.net.seedNode=Seed node
settings.net.directPeer=Peer (direct)
@ -1276,7 +1284,7 @@ settings.net.inbound=inbound
settings.net.outbound=outbound
settings.net.reSyncSPVChainLabel=Resync SPV chain
settings.net.reSyncSPVChainButton=Delete SPV file and resync
settings.net.reSyncSPVSuccess=The SPV chain file will be deleted on the next startup. You need to restart your application now.\n\n\
settings.net.reSyncSPVSuccess=Are you sure you want to do an SPV resync? If you proceed, the SPV chain file will be deleted on the next startup.\n\n\
After the restart it can take a while to resync with the network and you will only see all transactions once the resync is completed.\n\n\
Depending on the number of transactions and the age of your wallet the resync can take up to a few hours and consumes 100% of CPU. \
Do not interrupt the process otherwise you have to repeat it.
@ -2228,7 +2236,7 @@ dao.wallet.send.setDestinationAddress=Fill in your destination address
dao.wallet.send.send=Send BSQ funds
dao.wallet.send.sendBtc=Send BTC funds
dao.wallet.send.sendFunds.headline=Confirm withdrawal request
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired transaction fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount?
dao.wallet.chainHeightSynced=Latest verified block: {0}
dao.wallet.chainHeightSyncing=Awaiting blocks... Verified {0} blocks out of {1}
dao.wallet.tx.type=Type
@ -2386,9 +2394,9 @@ dao.factsAndFigures.menuItem.transactions=BSQ Transactions
dao.factsAndFigures.dashboard.avgPrice90=90 days average BSQ/BTC trade price
dao.factsAndFigures.dashboard.avgPrice30=30 days average BSQ/BTC trade price
dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average USD/BSQ trade price
dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average USD/BSQ trade price
dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on trade price)
dao.factsAndFigures.dashboard.avgUSDPrice90=90 days volume weighted average USD/BSQ price
dao.factsAndFigures.dashboard.avgUSDPrice30=30 days volume weighted average USD/BSQ price
dao.factsAndFigures.dashboard.marketCap=Market capitalisation (based on 30 days average USD/BSQ price)
dao.factsAndFigures.dashboard.availableAmount=Total available BSQ
dao.factsAndFigures.supply.issuedVsBurnt=BSQ issued v. BSQ burnt
@ -2462,7 +2470,7 @@ disputeSummaryWindow.openDate=Ticket opening date
disputeSummaryWindow.role=Trader's role
disputeSummaryWindow.payout=Trade amount payout
disputeSummaryWindow.payout.getsTradeAmount=BTC {0} gets trade amount payout
disputeSummaryWindow.payout.getsAll=BTC {0} gets all
disputeSummaryWindow.payout.getsAll=Max. payout to BTC {0}
disputeSummaryWindow.payout.custom=Custom payout
disputeSummaryWindow.payoutAmount.buyer=Buyer's payout amount
disputeSummaryWindow.payoutAmount.seller=Seller's payout amount
@ -2740,6 +2748,8 @@ popup.warning.noMediatorsAvailable=There are no mediators available.
popup.warning.notFullyConnected=You need to wait until you are fully connected to the network.\nThat might take up to about 2 minutes at startup.
popup.warning.notSufficientConnectionsToBtcNetwork=You need to wait until you have at least {0} connections to the Bitcoin network.
popup.warning.downloadNotComplete=You need to wait until the download of missing Bitcoin blocks is complete.
popup.warning.chainNotSynced=The Bisq wallet blockchain height is not synced correctly. If you recently started the application, please wait until one Bitcoin block has been published.\n\n\
You can check the blockchain height in Settings/Network Info. If more than one block passes and this problem persists it may be stalled, in which case you should do an SPV resync. [HYPERLINK:https://bisq.wiki/Resyncing_SPV_file]
popup.warning.removeOffer=Are you sure you want to remove that offer?\nThe maker fee of {0} will be lost if you remove that offer.
popup.warning.tooLargePercentageValue=You cannot set a percentage of 100% or larger.
popup.warning.examplePercentageValue=Please enter a percentage number like \"5.4\" for 5.4%
@ -2794,7 +2804,7 @@ popup.warning.openOffer.makerFeeTxRejected=The maker fee transaction for offer w
popup.warning.trade.txRejected.tradeFee=trade fee
popup.warning.trade.txRejected.deposit=deposit
popup.warning.trade.txRejected=The {0} transaction for trade with ID {1} was rejected by the Bitcoin network.\n\
Transaction ID={2}}\n\
Transaction ID={2}\n\
The trade has been moved to failed trades.\n\
Please go to \"Settings/Network info\" and do a SPV resync.\n\
For further help please contact the Bisq support channel at the Bisq Keybase team.

View file

@ -100,6 +100,7 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
req.getMinAmount(),
req.getBuyerSecurityDeposit(),
req.getPaymentAccountId(),
req.getMakerFeeCurrencyCode(),
offer -> {
// This result handling consumer's accept operation will return
// the new offer to the gRPC client after async placement is done.

View file

@ -19,13 +19,20 @@ package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.proto.grpc.CreatePaymentAccountReply;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetPaymentAccountFormReply;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsReply;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsReply;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.PaymentAccountsGrpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
@ -45,24 +52,86 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
@Override
public void createPaymentAccount(CreatePaymentAccountRequest req,
StreamObserver<CreatePaymentAccountReply> responseObserver) {
coreApi.createPaymentAccount(req.getPaymentMethodId(),
req.getAccountName(),
req.getAccountNumber(),
req.getCurrencyCode());
var reply = CreatePaymentAccountReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
try {
PaymentAccount paymentAccount = coreApi.createPaymentAccount(req.getPaymentAccountForm());
var reply = CreatePaymentAccountReply.newBuilder()
.setPaymentAccount(paymentAccount.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void getPaymentAccounts(GetPaymentAccountsRequest req,
StreamObserver<GetPaymentAccountsReply> responseObserver) {
var paymentAccounts = coreApi.getPaymentAccounts().stream()
.map(PaymentAccount::toProtoMessage)
.collect(Collectors.toList());
var reply = GetPaymentAccountsReply.newBuilder()
.addAllPaymentAccounts(paymentAccounts).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
try {
var paymentAccounts = coreApi.getPaymentAccounts().stream()
.map(PaymentAccount::toProtoMessage)
.collect(Collectors.toList());
var reply = GetPaymentAccountsReply.newBuilder()
.addAllPaymentAccounts(paymentAccounts).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void getPaymentMethods(GetPaymentMethodsRequest req,
StreamObserver<GetPaymentMethodsReply> responseObserver) {
try {
var paymentMethods = coreApi.getFiatPaymentMethods().stream()
.map(PaymentMethod::toProtoMessage)
.collect(Collectors.toList());
var reply = GetPaymentMethodsReply.newBuilder()
.addAllPaymentMethods(paymentMethods).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void getPaymentAccountForm(GetPaymentAccountFormRequest req,
StreamObserver<GetPaymentAccountFormReply> responseObserver) {
try {
var paymentAccountFormJson = coreApi.getPaymentAccountForm(req.getPaymentMethodId());
var reply = GetPaymentAccountFormReply.newBuilder()
.setPaymentAccountFormJson(paymentAccountFormJson)
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalArgumentException cause) {
var ex = new StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
}

View file

@ -79,6 +79,7 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
try {
coreApi.takeOffer(req.getOfferId(),
req.getPaymentAccountId(),
req.getTakerFeeCurrencyCode(),
trade -> {
TradeInfo tradeInfo = toTradeInfo(trade);
var reply = TakeOfferReply.newBuilder()
@ -143,7 +144,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
public void withdrawFunds(WithdrawFundsRequest req,
StreamObserver<WithdrawFundsReply> responseObserver) {
try {
coreApi.withdrawFunds(req.getTradeId(), req.getAddress());
//TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI)
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null);
var reply = WithdrawFundsReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();

View file

@ -19,32 +19,50 @@ package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.btc.exceptions.TxBroadcastException;
import bisq.core.btc.wallet.TxBroadcaster;
import bisq.proto.grpc.GetAddressBalanceReply;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalanceReply;
import bisq.proto.grpc.GetBalanceRequest;
import bisq.proto.grpc.GetBalancesReply;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesReply;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetTxFeeRateReply;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressReply;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.LockWalletReply;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.RemoveWalletPasswordReply;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqReply;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceReply;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordReply;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.UnlockWalletReply;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceReply;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.WalletsGrpc;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
import org.bitcoinj.core.Transaction;
import javax.inject.Inject;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
private final CoreApi coreApi;
@ -54,17 +72,13 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
this.coreApi = coreApi;
}
// TODO we need to support 3 or 4 balance types: available, reserved, lockedInTrade
// and maybe total wallet balance (available+reserved). To not duplicate the methods,
// we should pass an enum type. Enums in proto are a bit cumbersome as they are
// global so you quickly run into namespace conflicts if not always prefixes which
// makes it more verbose. In the core code base we move to the strategy to store the
// enum name and map it. This gives also more flexibility with updates.
@Override
public void getBalance(GetBalanceRequest req, StreamObserver<GetBalanceReply> responseObserver) {
public void getBalances(GetBalancesRequest req, StreamObserver<GetBalancesReply> responseObserver) {
try {
long availableBalance = coreApi.getAvailableBalance();
var reply = GetBalanceReply.newBuilder().setBalance(availableBalance).build();
var balances = coreApi.getBalances(req.getCurrencyCode());
var reply = GetBalancesReply.newBuilder()
.setBalances(balances.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
@ -110,6 +124,109 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
}
}
@Override
public void getUnusedBsqAddress(GetUnusedBsqAddressRequest req,
StreamObserver<GetUnusedBsqAddressReply> responseObserver) {
try {
String address = coreApi.getUnusedBsqAddress();
var reply = GetUnusedBsqAddressReply.newBuilder()
.setAddress(address)
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void sendBsq(SendBsqRequest req,
StreamObserver<SendBsqReply> responseObserver) {
try {
coreApi.sendBsq(req.getAddress(), req.getAmount(), new TxBroadcaster.Callback() {
@Override
public void onSuccess(Transaction tx) {
log.info("Successfully published BSQ tx: id {}, output sum {} sats, fee {} sats, size {} bytes",
tx.getTxId().toString(),
tx.getOutputSum(),
tx.getFee(),
tx.getMessageSize());
var reply = SendBsqReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void onFailure(TxBroadcastException ex) {
throw new IllegalStateException(ex);
}
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void getTxFeeRate(GetTxFeeRateRequest req,
StreamObserver<GetTxFeeRateReply> responseObserver) {
try {
coreApi.getTxFeeRate(() -> {
TxFeeRateInfo txFeeRateInfo = coreApi.getMostRecentTxFeeRateInfo();
var reply = GetTxFeeRateReply.newBuilder()
.setTxFeeRateInfo(txFeeRateInfo.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void setTxFeeRatePreference(SetTxFeeRatePreferenceRequest req,
StreamObserver<SetTxFeeRatePreferenceReply> responseObserver) {
try {
coreApi.setTxFeeRatePreference(req.getTxFeeRatePreference(), () -> {
TxFeeRateInfo txFeeRateInfo = coreApi.getMostRecentTxFeeRateInfo();
var reply = SetTxFeeRatePreferenceReply.newBuilder()
.setTxFeeRateInfo(txFeeRateInfo.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void unsetTxFeeRatePreference(UnsetTxFeeRatePreferenceRequest req,
StreamObserver<UnsetTxFeeRatePreferenceReply> responseObserver) {
try {
coreApi.unsetTxFeeRatePreference(() -> {
TxFeeRateInfo txFeeRateInfo = coreApi.getMostRecentTxFeeRateInfo();
var reply = UnsetTxFeeRatePreferenceReply.newBuilder()
.setTxFeeRateInfo(txFeeRateInfo.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
});
} catch (IllegalStateException cause) {
var ex = new StatusRuntimeException(Status.UNKNOWN.withDescription(cause.getMessage()));
responseObserver.onError(ex);
throw ex;
}
}
@Override
public void setWalletPassword(SetWalletPasswordRequest req,
StreamObserver<SetWalletPasswordReply> responseObserver) {

View file

@ -1700,7 +1700,9 @@ textfield */
-fx-font-size: 0.880em;
}
#price-chart .axis-tick-mark-text-node, #volume-chart .axis-tick-mark-text-node {
#price-chart .axis-tick-mark-text-node,
#volume-chart .axis-tick-mark-text-node,
#charts-dao .axis-tick-mark-text-node {
-fx-text-alignment: center;
}
@ -1726,13 +1728,13 @@ textfield */
/* The .chart-line-symbol rules change the color of the legend symbol */
#charts-dao .default-color0.chart-series-line { -fx-stroke: -bs-chart-dao-line1; }
#charts-dao .default-color0.chart-line-symbol { -fx-background-color: -bs-chart-dao-line1, -bs-background-color; }
#charts-dao .default-color0.chart-line-symbol { -fx-background-color: -bs-chart-dao-line1, -bs-chart-dao-line1; }
#charts-dao .default-color1.chart-series-line { -fx-stroke: -bs-chart-dao-line2; }
#charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-background-color; }
#charts-dao .default-color1.chart-line-symbol { -fx-background-color: -bs-chart-dao-line2, -bs-chart-dao-line2; }
#charts-dao .chart-series-line {
-fx-stroke-width: 1px;
-fx-stroke-width: 3px;
}
#charts .default-color0.chart-series-area-fill {

View file

@ -75,8 +75,10 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
// TODO Copied form OpenJFX, check license issues and way how we integrated it
@ -261,7 +263,7 @@ public class StaticProgressIndicatorSkin extends SkinBase<TxConfidenceIndicator>
* CssMetaData of its super classes.
*/
@SuppressWarnings("SameReturnValue")
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
public static ObservableList<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
@ -316,7 +318,7 @@ public class StaticProgressIndicatorSkin extends SkinBase<TxConfidenceIndicator>
* {@inheritDoc}
*/
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
public ObservableList<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
@ -691,7 +693,7 @@ public class StaticProgressIndicatorSkin extends SkinBase<TxConfidenceIndicator>
*/
@SuppressWarnings({"deprecation", "unchecked", "ConstantConditions"})
private static class StyleableProperties {
static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static final ObservableList<CssMetaData<? extends Styleable, ?>> STYLEABLES;
private static final CssMetaData<TxConfidenceIndicator, Paint> PROGRESS_COLOR =
new CssMetaData<>(
@ -746,7 +748,6 @@ public class StaticProgressIndicatorSkin extends SkinBase<TxConfidenceIndicator>
return skin.spinEnabled == null || !skin.spinEnabled.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(TxConfidenceIndicator node) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin();
@ -755,13 +756,12 @@ public class StaticProgressIndicatorSkin extends SkinBase<TxConfidenceIndicator>
};
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(SkinBase.getClassCssMetaData());
final ObservableList<CssMetaData<? extends Styleable, ?>> styleables =
FXCollections.observableArrayList(SkinBase.getClassCssMetaData());
styleables.add(PROGRESS_COLOR);
styleables.add(INDETERMINATE_SEGMENT_COUNT);
styleables.add(SPIN_ENABLED);
STYLEABLES = Collections.unmodifiableList(styleables);
STYLEABLES = FXCollections.unmodifiableObservableList(styleables);
}
}
}

View file

@ -43,6 +43,7 @@ import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@ -88,7 +89,7 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
paymentAccounts.setAll(user.getPaymentAccounts().stream()
.filter(paymentAccount -> paymentAccount.getPaymentMethod().isAsset())
.collect(Collectors.toList()));
paymentAccounts.sort((o1, o2) -> o1.getCreationDate().compareTo(o2.getCreationDate()));
paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName));
}
}

View file

@ -91,7 +91,7 @@ class FiatAccountsDataModel extends ActivatableDataModel {
.filter(paymentAccount -> !paymentAccount.getPaymentMethod().isAsset())
.collect(Collectors.toList());
paymentAccounts.setAll(list);
paymentAccounts.sort(Comparator.comparing(PaymentAccount::getCreationDate));
paymentAccounts.sort(Comparator.comparing(PaymentAccount::getAccountName));
}
}

View file

@ -20,28 +20,25 @@ package bisq.desktop.main.dao.economy.dashboard;
import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.TextFieldWithIcon;
import bisq.desktop.util.AxisInlierUtils;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
@ -75,11 +72,7 @@ import java.time.format.FormatStyle;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -114,7 +107,7 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
private Coin availableAmount;
private int gridRow = 0;
double howManyStdDevsConstituteOutlier = 10;
private Price avg30DayUSDPrice;
///////////////////////////////////////////////////////////////////////////////////////////
@ -145,6 +138,7 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
updatePrice();
updateAveragePriceFields(avgPrice90TextField, avgPrice30TextField, false);
updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true);
updateMarketCap();
};
}
@ -188,6 +182,7 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
updateChartData();
updateAveragePriceFields(avgPrice90TextField, avgPrice30TextField, false);
updateAveragePriceFields(avgUSDPrice90TextField, avgUSDPrice30TextField, true);
updateMarketCap();
}
@ -333,14 +328,16 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
Price bsqPrice = optionalBsqPrice.get();
marketPriceLabel.setText(FormattingUtils.formatPrice(bsqPrice) + " BSQ/BTC");
marketCapTextField.setText(bsqFormatter.formatMarketCap(priceFeedService.getMarketPrice("BSQ"),
priceFeedService.getMarketPrice(preferences.getPreferredTradeCurrency().getCode()),
availableAmount));
updateChartData();
} else {
marketPriceLabel.setText(Res.get("shared.na"));
}
}
private void updateMarketCap() {
if (avg30DayUSDPrice != null) {
marketCapTextField.setText(bsqFormatter.formatMarketCap(avg30DayUSDPrice, availableAmount));
} else {
marketCapTextField.setText(Res.get("shared.na"));
}
}
@ -369,107 +366,21 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
}
private long updateAveragePriceField(TextField textField, int days, boolean isUSDField) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
Date pastXDays = getPastDate(days);
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("BSQ"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
bsqAllTradePastXDays;
Tuple2<Price, Price> tuple = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, days);
Price usdPrice = tuple.first;
Price bsqPrice = tuple.second;
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("USD"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
removeOutliers(usdAllTradePastXDays, percentToTrim) :
usdAllTradePastXDays;
long average = isUSDField ? getUSDAverage(bsqTradePastXDays, usdTradePastXDays) :
getBTCAverage(bsqTradePastXDays);
Price avgPrice = isUSDField ? Price.valueOf("USD", average) :
Price.valueOf("BSQ", average);
String avg = FormattingUtils.formatPrice(avgPrice);
if (isUSDField) {
textField.setText(avg + " USD/BSQ");
textField.setText(usdPrice + " USD/BSQ");
if (days == 30) {
avg30DayUSDPrice = usdPrice;
}
} else {
textField.setText(avg + " BSQ/BTC");
textField.setText(bsqPrice + " BSQ/BTC");
}
return average;
}
private List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
List<Double> yValues = list.stream()
.filter(TradeStatistics3::isValid)
.map(e -> (double) e.getPrice())
.collect(Collectors.toList());
Tuple2<Double, Double> tuple = AxisInlierUtils.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
double lowerBound = tuple.first;
double upperBound = tuple.second;
return list.stream()
.filter(e -> e.getPrice() > lowerBound)
.filter(e -> e.getPrice() < upperBound)
.collect(Collectors.toList());
}
private long getBTCAverage(List<TradeStatistics3> list) {
long accumulatedVolume = 0;
long accumulatedAmount = 0;
for (TradeStatistics3 item : list) {
accumulatedVolume += item.getTradeVolume().getValue();
accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded
}
long averagePrice;
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0;
return averagePrice;
}
private long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) {
// Use next USD/BTC print as price to calculate BSQ/USD rate
// Store each trade as amount of USD and amount of BSQ traded
List<Tuple2<Double, Double>> usdBsqList = new ArrayList<>(bsqList.size());
usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
for (TradeStatistics3 item : bsqList) {
// Find usdprice for trade item
usdBTCPrice = usdList.stream()
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
Fiat.SMALLEST_UNIT_EXPONENT))
.findFirst()
.orElse(usdBTCPrice);
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount));
}
long averagePrice;
var usdTraded = usdBsqList.stream()
.mapToDouble(item -> item.first)
.sum();
var bsqTraded = usdBsqList.stream()
.mapToDouble(item -> item.second)
.sum();
var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d;
var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT);
averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0;
return averagePrice;
}
private Date getPastDate(int days) {
Calendar cal = new GregorianCalendar();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
return cal.getTime();
Price average = isUSDField ? usdPrice : bsqPrice;
return average.getValue();
}
}

View file

@ -42,7 +42,6 @@ import org.bitcoinj.core.Coin;
import javax.inject.Inject;
import javafx.scene.Node;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
@ -53,6 +52,7 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.geometry.Insets;
import javafx.geometry.Side;
@ -61,6 +61,8 @@ import javafx.collections.ListChangeListener;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
@ -82,7 +84,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@ -96,6 +97,7 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
private static final String MONTH = "month";
private static final String DAY = "day";
private static final DecimalFormat dFmt = new DecimalFormat(",###");
private final DaoFacade daoFacade;
private DaoStateService daoStateService;
@ -108,8 +110,6 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
private XYChart.Series<Number, Number> seriesBSQIssuedMonthly, seriesBSQBurntMonthly, seriesBSQBurntDaily,
seriesBSQBurntDailyMA;
private XYChart.Series<Number, Number> seriesBSQIssuedMonthly2;
private ListChangeListener<XYChart.Data<Number, Number>> changeListenerBSQBurntDaily;
private NumberAxis yAxisBSQBurntDaily;
@ -123,6 +123,9 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
private static final Map<String, TemporalAdjuster> ADJUSTERS = new HashMap<>();
private static final double monthDurationAvg = 2635200; // 3600 * 24 * 30.5;
private static List<Number> chart1XBounds = List.of();
private static NumberAxis xAxisChart1;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
@ -144,9 +147,9 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
initializeSeries();
createSupplyIncreasedVsDecreasedInformation();
createSupplyIncreasedInformation();
createSupplyReducedInformation();
createSupplyIncreasedVsDecreasedInformation(); // chart #1
createSupplyIncreasedInformation(); // chart #2
createSupplyReducedInformation(); // chart #3
createSupplyLockedInformation();
}
@ -199,8 +202,6 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
// Because Series cannot be reused in multiple charts, we create a
// "second" Series and populate it at the same time as the original.
// Some other solutions: https://stackoverflow.com/questions/49770442
seriesBSQIssuedMonthly2 = new XYChart.Series<>();
seriesBSQIssuedMonthly2.setName(issuedLabel);
seriesBSQBurntMonthly = new XYChart.Series<>();
seriesBSQBurntMonthly.setName(burntLabel);
@ -250,11 +251,6 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
Res.get("dao.factsAndFigures.supply.compRequestIssueAmount")).second;
reimbursementAmountTextField = addTopLabelReadOnlyTextField(root, gridRow, 1,
Res.get("dao.factsAndFigures.supply.reimbursementAmount")).second;
var chart = createBSQIssuedChart(seriesBSQIssuedMonthly2);
var chartPane = wrapInChartPane(chart);
root.getChildren().add(chartPane);
}
private void createSupplyReducedInformation() {
@ -291,85 +287,56 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
Res.get("dao.factsAndFigures.supply.totalConfiscatedAmount")).second;
}
// chart #1 (top)
private Node createBSQIssuedVsBurntChart(
XYChart.Series<Number, Number> seriesBSQIssuedMonthly,
XYChart.Series<Number, Number> seriesBSQBurntMonthly
XYChart.Series<Number, Number> seriesBSQIssuedMonthly,
XYChart.Series<Number, Number> seriesBSQBurntMonthly
) {
Supplier<NumberAxis> makeXAxis = () -> {
NumberAxis xAxis = new NumberAxis();
configureAxis(xAxis);
xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("MMM uu"));
return xAxis;
};
Supplier<NumberAxis> makeYAxis = () -> {
NumberAxis yAxis = new NumberAxis();
configureYAxis(yAxis);
yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter);
return yAxis;
};
var chart = new LineChart<>(makeXAxis.get(), makeYAxis.get());
configureChart(chart);
chart.setCreateSymbols(false);
chart.getData().addAll(List.of(seriesBSQIssuedMonthly, seriesBSQBurntMonthly));
chart.setLegendVisible(true);
return chart;
}
private Node createBSQIssuedChart(XYChart.Series<Number, Number> series) {
NumberAxis xAxis = new NumberAxis();
configureAxis(xAxis);
xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("MMM uu"));
xAxisChart1 = new NumberAxis();
configureAxis(xAxisChart1);
xAxisChart1.setLabel("Month");
xAxisChart1.setTickLabelFormatter(getMonthTickLabelFormatter("MM\nyyyy"));
addTickMarkLabelCssClass(xAxisChart1, "axis-tick-mark-text-node");
NumberAxis yAxis = new NumberAxis();
configureYAxis(yAxis);
yAxis.setLabel("BSQ");
yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter);
AreaChart<Number, Number> chart = new AreaChart<>(xAxis, yAxis);
var chart = new LineChart<>(xAxisChart1, yAxis);
configureChart(chart);
chart.setCreateSymbols(false);
chart.setLegendVisible(true);
chart.setCreateSymbols(true);
chart.getData().add(series);
chart.getData().addAll(seriesBSQIssuedMonthly, seriesBSQBurntMonthly);
return chart;
}
// chart #3 (bottom)
private Node createBSQBurntChart(
XYChart.Series<Number, Number> seriesBSQBurntDaily,
XYChart.Series<Number, Number> seriesBSQBurntDailyMA
) {
Supplier<NumberAxis> makeXAxis = () -> {
NumberAxis xAxis = new NumberAxis();
configureAxis(xAxis);
xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("d MMM"));
return xAxis;
};
NumberAxis xAxis = new NumberAxis();
configureAxis(xAxis);
xAxis.setTickLabelFormatter(getTimestampTickLabelFormatter("dd/MMM\nyyyy"));
addTickMarkLabelCssClass(xAxis, "axis-tick-mark-text-node");
Supplier<NumberAxis> makeYAxis = () -> {
NumberAxis yAxis = new NumberAxis();
configureYAxis(yAxis);
yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter);
return yAxis;
};
NumberAxis yAxis = new NumberAxis();
configureYAxis(yAxis);
yAxis.setLabel("BSQ");
yAxis.setTickLabelFormatter(BSQPriceTickLabelFormatter);
var yAxis = makeYAxis.get();
initializeChangeListener(yAxis);
var chart = new LineChart<>(makeXAxis.get(), yAxis);
var chart = new LineChart<>(xAxis, yAxis);
configureChart(chart);
chart.setCreateSymbols(false);
chart.getData().addAll(List.of(seriesBSQBurntDaily, seriesBSQBurntDailyMA));
chart.setLegendVisible(true);
chart.getData().addAll(seriesBSQBurntDaily, seriesBSQBurntDailyMA);
return chart;
}
@ -381,20 +348,68 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
yAxisBSQBurntDaily, chartMaxNumberOfTicks, chartPercentToTrim, chartHowManyStdDevsConstituteOutlier);
}
public static List<Number> getListXMinMax (List<XYChart.Data<Number, Number>> bsqList) {
long min = Long.MAX_VALUE, max = 0;
for (XYChart.Data<Number, ?> data : bsqList) {
min = Math.min(data.getXValue().longValue(), min);
max = Math.max(data.getXValue().longValue(), max);
}
return List.of(min, max);
}
private void configureYAxis(NumberAxis axis) {
configureAxis(axis);
axis.setForceZeroInRange(true);
axis.setTickLabelGap(5);
axis.setSide(Side.RIGHT);
}
private void configureAxis(NumberAxis axis) {
axis.setForceZeroInRange(false);
axis.setAutoRanging(true);
axis.setTickMarkVisible(false);
axis.setTickMarkVisible(true);
axis.setMinorTickVisible(false);
axis.setTickLabelGap(6);
}
// grab the axis tick mark label (text object) and add a CSS class.
private void addTickMarkLabelCssClass(NumberAxis axis, String cssClass) {
axis.getChildrenUnmodifiable().addListener((ListChangeListener<Node>) c -> {
while (c.next()) {
if (c.wasAdded()) {
for (Node mark : c.getAddedSubList()) {
if (mark instanceof Text) {
mark.getStyleClass().add(cssClass);
}
}
}
}
});
}
// rounds the tick timestamp to the nearest month
private StringConverter<Number> getMonthTickLabelFormatter(String datePattern) {
return new StringConverter<>() {
@Override
public String toString(Number timestamp) {
double tsd = timestamp.doubleValue();
if ((chart1XBounds.size() == 2) &&
((tsd - monthDurationAvg / 2 < chart1XBounds.get(0).doubleValue()) ||
(tsd + monthDurationAvg / 2 > chart1XBounds.get(1).doubleValue()))) {
return "";
}
LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(timestamp.longValue(),
0, OffsetDateTime.now(ZoneId.systemDefault()).getOffset());
if (localDateTime.getDayOfMonth() > 15) {
localDateTime = localDateTime.with(TemporalAdjusters.firstDayOfNextMonth());
}
return localDateTime.format(DateTimeFormatter.ofPattern(datePattern, GlobalSettings.getLocale()));
}
@Override
public Number fromString(String string) {
return 0;
}
};
}
private StringConverter<Number> getTimestampTickLabelFormatter(String datePattern) {
@ -417,7 +432,7 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
new StringConverter<>() {
@Override
public String toString(Number marketPrice) {
return bsqFormatter.formatBSQSatoshisWithCode(marketPrice.longValue());
return dFmt.format(Double.parseDouble(bsqFormatter.formatBSQSatoshis(marketPrice.longValue())));
}
@Override
@ -427,7 +442,6 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
};
private <X, Y> void configureChart(XYChart<X, Y> chart) {
chart.setLegendVisible(false);
chart.setAnimated(false);
chart.setId("charts-dao");
@ -487,9 +501,16 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
var updatedBurntBsqDaily = updateBSQBurntDaily(sortedBurntTxs);
updateBSQBurntDailyMA(updatedBurntBsqDaily);
updateBSQBurntMonthly(sortedBurntTxs);
List<Number> xMinMaxB = updateBSQBurntMonthly(sortedBurntTxs);
updateBSQIssuedMonthly();
List<Number> xMinMaxI = updateBSQIssuedMonthly();
chart1XBounds = List.of(Math.min(xMinMaxB.get(0).doubleValue(), xMinMaxI.get(0).doubleValue()) - monthDurationAvg,
Math.max(xMinMaxB.get(1).doubleValue(), xMinMaxI.get(1).doubleValue()) + monthDurationAvg);
xAxisChart1.setAutoRanging(false);
xAxisChart1.setLowerBound(chart1XBounds.get(0).doubleValue());
xAxisChart1.setUpperBound(chart1XBounds.get(1).doubleValue());
xAxisChart1.setTickUnit(monthDurationAvg);
}
private List<Tx> getSortedBurntTxs() {
@ -534,7 +555,7 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
return updatedBurntBsqDaily;
}
private void updateBSQBurntMonthly(List<Tx> sortedBurntTxs) {
private List<Number> updateBSQBurntMonthly(List<Tx> sortedBurntTxs) {
seriesBSQBurntMonthly.getData().clear();
var burntBsqByMonth =
@ -563,6 +584,7 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
.collect(Collectors.toList());
seriesBSQBurntMonthly.getData().setAll(updatedBurntBsqMonthly);
return getListXMinMax(updatedBurntBsqMonthly);
}
private void updateBSQBurntDailyMA(List<XYChart.Data<Number, Number>> updatedBurntBsq) {
@ -602,7 +624,7 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
seriesBSQBurntDailyMA.getData().setAll(burntBsqMA);
}
private void updateBSQIssuedMonthly() {
private List<Number> updateBSQIssuedMonthly() {
Function<Integer, LocalDate> blockTimeFn = memoize(height ->
Instant.ofEpochMilli(daoFacade.getBlockTime(height)).atZone(ZoneId.systemDefault())
.toLocalDate()
@ -630,7 +652,8 @@ public class SupplyView extends ActivatableView<GridPane, Void> implements DaoSt
.collect(Collectors.toList());
seriesBSQIssuedMonthly.getData().setAll(updatedAddedBSQ);
seriesBSQIssuedMonthly2.getData().setAll(updatedAddedBSQ);
return getListXMinMax(updatedAddedBSQ);
}
private void activateButtons() {

View file

@ -27,6 +27,7 @@ import bisq.desktop.main.dao.wallet.BsqBalanceUtil;
import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.deposit.DepositView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.WalletPasswordWindow;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.BsqAddressValidator;
@ -53,6 +54,7 @@ import bisq.core.util.validation.BtcAddressValidator;
import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
@ -67,6 +69,8 @@ import javafx.scene.layout.GridPane;
import javafx.beans.value.ChangeListener;
import java.util.concurrent.TimeUnit;
import static bisq.desktop.util.FormBuilder.addButtonAfterGroup;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
@ -86,6 +90,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
private final BtcValidator btcValidator;
private final BsqAddressValidator bsqAddressValidator;
private final BtcAddressValidator btcAddressValidator;
private final WalletPasswordWindow walletPasswordWindow;
private int gridRow = 0;
private InputTextField amountInputTextField, btcAmountInputTextField;
@ -113,7 +118,8 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
BsqValidator bsqValidator,
BtcValidator btcValidator,
BsqAddressValidator bsqAddressValidator,
BtcAddressValidator btcAddressValidator) {
BtcAddressValidator btcAddressValidator,
WalletPasswordWindow walletPasswordWindow) {
this.bsqWalletService = bsqWalletService;
this.btcWalletService = btcWalletService;
this.walletsManager = walletsManager;
@ -127,6 +133,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
this.btcValidator = btcValidator;
this.bsqAddressValidator = bsqAddressValidator;
this.btcAddressValidator = btcAddressValidator;
this.walletPasswordWindow = walletPasswordWindow;
}
@Override
@ -362,7 +369,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
amountFormatter.formatCoinWithCode(receiverAmount)))
.actionButtonText(Res.get("shared.yes"))
.onAction(() -> {
walletsManager.publishAndCommitBsqTx(txWithBtcFee, txType, new TxBroadcaster.Callback() {
doWithdraw(txWithBtcFee, txType, new TxBroadcaster.Callback() {
@Override
public void onSuccess(Transaction transaction) {
log.debug("Successfully sent tx with id {}", txWithBtcFee.getTxId().toString());
@ -378,5 +385,19 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
.closeButtonText(Res.get("shared.cancel"))
.show();
}
private void doWithdraw(Transaction txWithBtcFee, TxType txType, TxBroadcaster.Callback callback) {
if (btcWalletService.isEncrypted()) {
UserThread.runAfter(() -> walletPasswordWindow.onAesKey(aesKey ->
sendFunds(txWithBtcFee, txType, callback))
.show(), 300, TimeUnit.MILLISECONDS);
} else {
sendFunds(txWithBtcFee, txType, callback);
}
}
private void sendFunds(Transaction txWithBtcFee, TxType txType, TxBroadcaster.Callback callback) {
walletsManager.publishAndCommitBsqTx(txWithBtcFee, txType, callback);
}
}

View file

@ -266,7 +266,7 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
// Private
///////////////////////////////////////////////////////////////////////////////////////////
// If chain height from wallet of from the BSQ blockchain parsing changed we update our state.
// If chain height from wallet or from the BSQ blockchain parsing changed we update our state.
private void onUpdateAnyChainHeight() {
int currentBlockHeight = daoFacade.getChainHeight();
if (walletChainHeight > 0) {
@ -276,8 +276,7 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
chainSyncIndicator.setVisible(!synced);
chainSyncIndicator.setManaged(!synced);
if (synced) {
chainHeightLabel.setText(Res.get("dao.wallet.chainHeightSynced",
currentBlockHeight));
chainHeightLabel.setText(Res.get("dao.wallet.chainHeightSynced", currentBlockHeight));
} else {
chainSyncIndicator.setProgress(progress);
if (walletChainHeight > currentBlockHeight) {
@ -287,12 +286,13 @@ public class BsqTxView extends ActivatableView<GridPane, Void> implements BsqBal
currentBlockHeight,
walletChainHeight));
} else {
// But when restoring from seed, we receive the latest block height
// from the seed nodes while BitcoinJ has not received all blocks yet and
// is still syncing
chainHeightLabel.setText(Res.get("dao.wallet.chainHeightSyncing",
walletChainHeight,
currentBlockHeight));
// Our wallet chain height is behind our BSQ chain height. That can be the case at SPV resync or if
// we updated manually our DaoStateStore with a newer version. We do not want to show sync state
// as we do not know at that moment if we are missing blocks. Once Btc wallet has synced we will
// trigger a check and request more blocks in case we are the lite node.
chainSyncIndicator.setVisible(false);
chainSyncIndicator.setManaged(false);
chainHeightLabel.setText(Res.get("dao.wallet.chainHeightSynced", currentBlockHeight));
}
}
} else {

View file

@ -56,7 +56,6 @@ import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.wallet.Wallet;
import javax.inject.Inject;
@ -334,9 +333,9 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}
checkNotNull(feeEstimationTransaction, "feeEstimationTransaction must not be null");
Coin dust = getDust(feeEstimationTransaction);
Coin dust = btcWalletService.getDust(feeEstimationTransaction);
Coin fee = feeEstimationTransaction.getFee().add(dust);
Coin receiverAmount = Coin.ZERO;
Coin receiverAmount;
// amountAsCoin is what the user typed into the withdrawal field.
// this can be interpreted as either the senders amount or receivers amount depending
// on a radio button "fee excluded / fee included".
@ -495,14 +494,19 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private void sendFunds(Coin amount, Coin fee, KeyParameter aesKey, FutureCallback<Transaction> callback) {
try {
String memo = withdrawMemoTextField.getText();
if (memo.isEmpty()) {
memo = null;
}
Transaction transaction = btcWalletService.sendFundsForMultipleAddresses(fromAddresses,
withdrawToTextField.getText(),
amount,
fee,
null,
aesKey,
memo,
callback);
transaction.setMemo(withdrawMemoTextField.getText());
reset();
updateList();
} catch (AddressFormatException e) {
@ -669,21 +673,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}
});
}
// BISQ issue #4039: prevent dust outputs from being created.
// check the outputs of a proposed transaction, if any are below the dust threshold
// add up the dust, noting the details in the log.
// returns the 'dust amount' to indicate if any dust was detected.
private Coin getDust(Transaction transaction) {
Coin dust = Coin.ZERO;
for (TransactionOutput transactionOutput : transaction.getOutputs()) {
if (transactionOutput.getValue().isLessThan(Restrictions.getMinNonDustOutput())) {
dust = dust.add(transactionOutput.getValue());
log.info("dust TXO = {}", transactionOutput.toString());
}
}
return dust;
}
}

View file

@ -93,7 +93,6 @@ import java.text.DecimalFormat;
import javafx.util.Callback;
import javafx.util.StringConverter;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@ -127,7 +126,7 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
private HBox bottomHBox;
private ListChangeListener<OfferBookListItem> changeListener;
private ListChangeListener<CurrencyListItem> currencyListItemsListener;
private final double chartDataFactor = 3;
private final double dataLimitFactor = 3;
private final double initialOfferTableViewHeight = 121;
private final double pixelsPerOfferTableRow = (initialOfferTableViewHeight - 30) / 5.0; // initial visible row count=5, header height=30
private final Function<Double, Double> offerTableViewHeight = (screenSize) -> {
@ -387,48 +386,78 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
seriesSell.getData().clear();
areaChart.getData().clear();
double buyMinValue = model.getBuyData().stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.min()
.orElse(Double.MAX_VALUE);
List<Double> leftMnMx, rightMnMx;
boolean isCrypto = CurrencyUtil.isCryptoCurrency(model.getCurrencyCode());
if (isCrypto) { // crypto: left-sell, right-buy,
leftMnMx = minMaxFilterLeft(model.getSellData());
rightMnMx = minMaxFilterRight(model.getBuyData());
} else { // fiat: left-buy, right-sell
leftMnMx = minMaxFilterLeft(model.getBuyData());
rightMnMx = minMaxFilterRight(model.getSellData());
}
// Hide buy offers that are more than a factor of chartDataFactor higher than the lowest buy offer
double buyMaxValue = model.getBuyData().stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.filter(o -> o < buyMinValue * chartDataFactor)
.max()
.orElse(Double.MIN_VALUE);
double sellMaxValue = model.getSellData().stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.max()
.orElse(Double.MIN_VALUE);
// Hide sell offers that are less than a factor of chartDataFactor lower than the highest sell offer
double sellMinValue = model.getSellData().stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.filter(o -> o > sellMaxValue / chartDataFactor)
.min()
.orElse(Double.MAX_VALUE);
double minValue = Double.min(buyMinValue, sellMinValue);
double maxValue = Double.max(buyMaxValue, sellMaxValue);
double minValue = Double.min(leftMnMx.get(0).doubleValue(), rightMnMx.get(0).doubleValue());
double maxValue = Double.max(leftMnMx.get(1).doubleValue(), rightMnMx.get(1).doubleValue());
if (minValue == Double.MAX_VALUE || maxValue == Double.MIN_VALUE) { // no filtering
seriesBuy.getData().addAll(model.getBuyData());
seriesSell.getData().addAll(model.getSellData());
} else { // apply filtering
seriesBuy.getData().addAll(model.getBuyData().stream()
.filter(o -> o.getXValue().doubleValue() < buyMinValue * 3)
.collect(Collectors.toList()));
seriesSell.getData().addAll(model.getSellData().stream()
.filter(o -> o.getXValue().doubleValue() > sellMaxValue / 3)
.collect(Collectors.toList()));
if (isCrypto) { // crypto: left-sell, right-buy
seriesBuy.getData().addAll(filterRight(model.getBuyData(), rightMnMx.get(0)));
seriesSell.getData().addAll(filterLeft(model.getSellData(), leftMnMx.get(1)));
} else { // fiat: left-buy, right-sell
seriesBuy.getData().addAll(filterLeft(model.getBuyData(), leftMnMx.get(1)));
seriesSell.getData().addAll(filterRight(model.getSellData(), rightMnMx.get(0)));
}
}
areaChart.getData().addAll(List.of(seriesBuy, seriesSell));
}
private List<Double> minMaxFilterLeft(List<XYChart.Data<Number, Number>> data) {
double maxValue = data.stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.max()
.orElse(Double.MIN_VALUE);
// Hide sell offers that are less than a div-factor of dataLimitFactor
// lower than the highest sell offer.
double minValue = data.stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.filter(o -> o > maxValue / dataLimitFactor)
.min()
.orElse(Double.MAX_VALUE);
return List.of(minValue, maxValue);
}
private List<Double> minMaxFilterRight(List<XYChart.Data<Number, Number>> data) {
double minValue = data.stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.min()
.orElse(Double.MAX_VALUE);
// Hide sell offers that are more than dataLimitFactor factor higher
// than the lowest sell offer
double maxValue = data.stream()
.mapToDouble(o -> o.getXValue().doubleValue())
.filter(o -> o < minValue * dataLimitFactor)
.max()
.orElse(Double.MIN_VALUE);
return List.of(minValue, maxValue);
}
private List<XYChart.Data<Number, Number>> filterLeft(List<XYChart.Data<Number, Number>> data, double maxValue) {
return data.stream()
.filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor)
.collect(Collectors.toList());
}
private List<XYChart.Data<Number, Number>> filterRight(List<XYChart.Data<Number, Number>> data, double minValue) {
return data.stream()
.filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor)
.collect(Collectors.toList());
}
private Tuple4<TableView<OfferListItem>, VBox, Button, Label> getOfferTable(OfferPayload.Direction direction) {
TableView<OfferListItem> tableView = new TableView<>();
tableView.setMinHeight(initialOfferTableViewHeight);
@ -668,12 +697,12 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
private void reverseTableColumns() {
ObservableList<TableColumn<OfferListItem, ?>> columns = FXCollections.observableArrayList(buyOfferTableView.getColumns());
buyOfferTableView.getColumns().clear();
Collections.reverse(columns);
FXCollections.reverse(columns);
buyOfferTableView.getColumns().addAll(columns);
columns = FXCollections.observableArrayList(sellOfferTableView.getColumns());
sellOfferTableView.getColumns().clear();
Collections.reverse(columns);
FXCollections.reverse(columns);
sellOfferTableView.getColumns().addAll(columns);
}

View file

@ -95,6 +95,8 @@ import javafx.collections.transformation.SortedList;
import javafx.util.Callback;
import javafx.util.StringConverter;
import java.text.DecimalFormat;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
@ -442,11 +444,13 @@ public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesCharts
public String toString(Number object) {
String currencyCode = model.getCurrencyCode();
double doubleValue = (double) object;
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
final double value = MathUtils.scaleDownByPowerOf10(doubleValue, 8);
return FormattingUtils.formatRoundedDoubleWithPrecision(value, 8);
return FormattingUtils.formatRoundedDoubleWithPrecision(value, 8).replaceFirst("0{3}$", "");
} else {
return FormattingUtils.formatPrice(Price.valueOf(currencyCode, MathUtils.doubleToLong(doubleValue)));
DecimalFormat df = new DecimalFormat(",###");
return df.format(Double.parseDouble(FormattingUtils.formatPrice(Price.valueOf(currencyCode, MathUtils.doubleToLong(doubleValue)))));
}
}

View file

@ -65,7 +65,6 @@ import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
@ -292,7 +291,7 @@ class TradesChartsViewModel extends ActivatableViewModel {
long accumulatedVolume = 0;
long accumulatedAmount = 0;
long numTrades = set.size();
List<Long> tradePrices = new ArrayList<>(set.size());
ObservableList<Long> tradePrices = FXCollections.observableArrayList();
for (TradeStatistics3 item : set) {
long tradePriceAsLong = item.getTradePrice().getValue();
@ -304,13 +303,14 @@ class TradesChartsViewModel extends ActivatableViewModel {
accumulatedAmount += item.getTradeAmount().getValue();
tradePrices.add(item.getTradePrice().getValue());
}
Collections.sort(tradePrices);
FXCollections.sort(tradePrices);
List<TradeStatistics3> list = new ArrayList<>(set);
list.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong));
if (list.size() > 0) {
open = list.get(0).getTradePrice().getValue();
close = list.get(list.size() - 1).getTradePrice().getValue();
ObservableList<TradeStatistics3> obsList = FXCollections.observableArrayList(list);
obsList.sort(Comparator.comparingLong(TradeStatistics3::getDateAsLong));
if (obsList.size() > 0) {
open = obsList.get(0).getTradePrice().getValue();
close = obsList.get(obsList.size() - 1).getTradePrice().getValue();
}
long averagePrice;

View file

@ -91,6 +91,7 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Comparator.comparing;
public abstract class MutableOfferDataModel extends OfferDataModel implements BsqBalanceListener {
private final CreateOfferService createOfferService;
@ -330,7 +331,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
setTradeCurrencyFromPaymentAccount(paymentAccount);
setSuggestedSecurityDeposit(getPaymentAccount());
if (amount.get() != null)
if (amount.get() != null && this.allowAmountUpdate)
this.amount.set(Coin.valueOf(Math.min(amount.get().value, getMaxTradeLimit())));
}
}
@ -612,6 +613,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
private void fillPaymentAccounts() {
if (user.getPaymentAccounts() != null)
paymentAccounts.setAll(new HashSet<>(user.getPaymentAccounts()));
paymentAccounts.sort(comparing(PaymentAccount::getAccountName));
}
protected void setAmount(Coin amount) {

View file

@ -459,7 +459,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
}
private void updateOfferElementsStyle() {
GridPane.setColumnSpan(firstRowHBox, 1);
GridPane.setColumnSpan(firstRowHBox, 2);
final String activeInputStyle = "input-with-border";
final String readOnlyInputStyle = "input-with-border-readonly";
@ -991,7 +991,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
paymentGroupBox = new HBox();
paymentGroupBox.setAlignment(Pos.CENTER_LEFT);
paymentGroupBox.setSpacing(62);
paymentGroupBox.setSpacing(12);
paymentGroupBox.setPadding(new Insets(10, 0, 18, 0));
final Tuple3<VBox, Label, ComboBox<PaymentAccount>> tradingAccountBoxTuple = addTopLabelComboBox(
@ -1007,12 +1007,15 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
GridPane.setMargin(paymentGroupBox, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0));
gridPane.getChildren().add(paymentGroupBox);
tradingAccountBoxTuple.first.setMinWidth(800);
paymentAccountsComboBox = tradingAccountBoxTuple.third;
paymentAccountsComboBox.setMinWidth(300);
paymentAccountsComboBox.setMinWidth(tradingAccountBoxTuple.first.getMinWidth());
paymentAccountsComboBox.setPrefWidth(tradingAccountBoxTuple.first.getMinWidth());
editOfferElements.add(tradingAccountBoxTuple.first);
// we display either currencyComboBox (multi currency account) or currencyTextField (single)
currencyComboBox = currencyBoxTuple.third;
currencyComboBox.setMaxWidth(tradingAccountBoxTuple.first.getMinWidth() / 2);
editOfferElements.add(currencySelection);
currencyComboBox.setConverter(new StringConverter<>() {
@Override

View file

@ -376,7 +376,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
private void updateSigningStateColumn() {
if (model.hasSelectionAccountSigning()) {
if (!tableView.getColumns().contains(signingStateColumn)) {
tableView.getColumns().add(tableView.getColumns().indexOf(paymentMethodColumn) + 1, signingStateColumn);
tableView.getColumns().add(tableView.getColumns().indexOf(depositColumn) + 1, signingStateColumn);
}
} else {
tableView.getColumns().remove(signingStateColumn);
@ -1112,6 +1112,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
if (needsSigning) {
if (accountAgeWitnessService.hasSignedWitness(item.getOffer())) {
// either signed & limits lifted, or waiting for limits to be lifted
AccountAgeWitnessService.SignState signState = accountAgeWitnessService.getSignState(item.getOffer());
icon = GUIUtil.getIconForSignState(signState);
info = Res.get("offerbook.timeSinceSigning.info",
@ -1121,10 +1122,9 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
timeSinceSigning = Res.get("offerbook.timeSinceSigning.daysSinceSigning",
daysSinceSigning);
} else {
// either banned, unsigned
AccountAgeWitnessService.SignState signState = accountAgeWitnessService.getSignState(item.getOffer());
icon = GUIUtil.getIconForSignState(signState);
if (!signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) {
info = Res.get("offerbook.timeSinceSigning.info", signState.getPresentation());
long daysSinceSigning = TimeUnit.MILLISECONDS.toDays(
@ -1132,15 +1132,22 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
timeSinceSigning = Res.get("offerbook.timeSinceSigning.daysSinceSigning",
daysSinceSigning);
} else {
info = Res.get("shared.notSigned");
timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned");
long accountAge = TimeUnit.MILLISECONDS.toDays(accountAgeWitnessService.getAccountAge(item.getOffer()));
info = Res.get("shared.notSigned", accountAge);
timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned", accountAge);
}
}
} else {
icon = MaterialDesignIcon.INFORMATION_OUTLINE;
info = Res.get("shared.notSigned.noNeed");
timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned.noNeed");
if (CurrencyUtil.isFiatCurrency(item.getOffer().getCurrencyCode())) {
icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE;
long days = TimeUnit.MILLISECONDS.toDays(accountAgeWitnessService.getAccountAge(item.getOffer()));
info = Res.get("shared.notSigned.noNeedDays", days);
timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", days);
} else { // altcoins
icon = MaterialDesignIcon.INFORMATION_OUTLINE;
info = Res.get("shared.notSigned.noNeedAlts");
timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned.noNeed");
}
}
InfoAutoTooltipLabel label = new InfoAutoTooltipLabel(timeSinceSigning, icon, ContentDisplay.RIGHT, info);

View file

@ -26,6 +26,7 @@ import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.filter.FilterManager;
import bisq.core.locale.BankUtil;
import bisq.core.locale.CountryUtil;
@ -99,6 +100,7 @@ class OfferBookViewModel extends ActivatableViewModel {
private final User user;
private final OfferBook offerBook;
final Preferences preferences;
private final WalletsSetup walletsSetup;
private final P2PService p2PService;
final PriceFeedService priceFeedService;
private final ClosedTradableManager closedTradableManager;
@ -142,6 +144,7 @@ class OfferBookViewModel extends ActivatableViewModel {
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
@ -156,6 +159,7 @@ class OfferBookViewModel extends ActivatableViewModel {
this.user = user;
this.offerBook = offerBook;
this.preferences = preferences;
this.walletsSetup = walletsSetup;
this.p2PService = p2PService;
this.priceFeedService = priceFeedService;
this.closedTradableManager = closedTradableManager;
@ -532,6 +536,7 @@ class OfferBookViewModel extends ActivatableViewModel {
boolean canCreateOrTakeOffer() {
return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) &&
GUIUtil.isChainHeightSyncedWithinToleranceOrShowPopup(walletsSetup) &&
GUIUtil.isBootstrappedOrShowPopup(p2PService);
}

View file

@ -27,6 +27,7 @@ import bisq.core.filter.FilterManager;
import bisq.core.filter.PaymentAccountFilter;
import bisq.core.locale.Res;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.config.Config;
@ -36,6 +37,8 @@ import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
@ -49,7 +52,6 @@ import javafx.geometry.HPos;
import javafx.geometry.Insets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ -223,12 +225,16 @@ public class FilterWindow extends Overlay<FilterWindow> {
);
// We remove first the old filter
// We delay a bit with adding as it seems that the instant add/remove calls lead to issues that the
// remove msg was rejected (P2P storage should handle it but seems there are edge cases where its not
// working as expected)
if (filterManager.canRemoveDevFilter(privKeyString)) {
filterManager.removeDevFilter(privKeyString);
UserThread.runAfter(() -> addDevFilter(removeFilterMessageButton, privKeyString, newFilter),
5);
} else {
addDevFilter(removeFilterMessageButton, privKeyString, newFilter);
}
filterManager.addDevFilter(newFilter, privKeyString);
removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null);
hide();
} else {
new Popup().warning(Res.get("shared.invalidKey")).onClose(this::blurAgain).show();
}
@ -258,6 +264,12 @@ public class FilterWindow extends Overlay<FilterWindow> {
GridPane.setMargin(hBox, new Insets(10, 0, 0, 0));
}
private void addDevFilter(Button removeFilterMessageButton, String privKeyString, Filter newFilter) {
filterManager.addDevFilter(newFilter, privKeyString);
removeFilterMessageButton.setDisable(filterManager.getDevFilter() == null);
hide();
}
private void setupFieldFromList(InputTextField field, List<String> values) {
if (values != null)
field.setText(String.join(", ", values));
@ -283,7 +295,7 @@ public class FilterWindow extends Overlay<FilterWindow> {
private List<String> readAsList(InputTextField field) {
if (field.getText().isEmpty()) {
return Collections.emptyList();
return FXCollections.emptyObservableList();
} else {
return Arrays.asList(StringUtils.deleteWhitespace(field.getText()).split(","));
}

View file

@ -34,17 +34,18 @@
<TableView fx:id="tableView" VBox.vgrow="ALWAYS">
<columns>
<TableColumn fx:id="tradeIdColumn" minWidth="120" maxWidth="120"/>
<TableColumn fx:id="dateColumn" minWidth="180"/>
<TableColumn fx:id="marketColumn" minWidth="100"/>
<TableColumn fx:id="tradeIdColumn" minWidth="110" maxWidth="120"/>
<TableColumn fx:id="dateColumn" minWidth="170"/>
<TableColumn fx:id="marketColumn" minWidth="75"/>
<TableColumn fx:id="priceColumn" minWidth="100"/>
<TableColumn fx:id="amountColumn" minWidth="130"/>
<TableColumn fx:id="volumeColumn" minWidth="130"/>
<TableColumn fx:id="deviationColumn" minWidth="70"/>
<TableColumn fx:id="amountColumn" minWidth="110"/>
<TableColumn fx:id="volumeColumn" minWidth="110"/>
<TableColumn fx:id="txFeeColumn" visible="false"/>
<TableColumn fx:id="tradeFeeColumn" visible="false"/>
<TableColumn fx:id="buyerSecurityDepositColumn" visible="false"/>
<TableColumn fx:id="sellerSecurityDepositColumn" visible="false"/>
<TableColumn fx:id="directionColumn" minWidth="80"/>
<TableColumn fx:id="directionColumn" minWidth="70"/>
<TableColumn fx:id="stateColumn" minWidth="80"/>
<TableColumn fx:id="avatarColumn" minWidth="40" maxWidth="40"/>
</columns>

View file

@ -21,6 +21,7 @@ import bisq.desktop.common.view.ActivatableViewAndModel;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.components.InputTextField;
import bisq.desktop.components.PeerInfoIcon;
@ -79,10 +80,37 @@ import java.util.function.Function;
public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTradesViewModel> {
private final boolean useDevPrivilegeKeys;
private enum ColumnNames {
TRADE_ID(Res.get("shared.tradeId")),
DATE(Res.get("shared.dateTime")),
MARKET(Res.get("shared.market")),
PRICE(Res.get("shared.price")),
DEVIATION(Res.get("shared.deviation")),
AMOUNT(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())),
VOLUME(Res.get("shared.amount")),
TX_FEE(Res.get("shared.txFee")),
TRADE_FEE(Res.get("shared.tradeFee")),
BUYER_SEC(Res.get("shared.buyerSecurityDeposit")),
SELLER_SEC(Res.get("shared.sellerSecurityDeposit")),
OFFER_TYPE(Res.get("shared.offerType")),
STATUS(Res.get("shared.state"));
private final String text;
ColumnNames(String text) {
this.text = text;
}
@Override
public String toString() {
return text;
}
}
@FXML
TableView<ClosedTradableListItem> tableView;
@FXML
TableColumn<ClosedTradableListItem, ClosedTradableListItem> priceColumn, amountColumn, volumeColumn, txFeeColumn, tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn,
TableColumn<ClosedTradableListItem, ClosedTradableListItem> priceColumn, deviationColumn, amountColumn, volumeColumn,
txFeeColumn, tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn,
marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, avatarColumn;
@FXML
HBox footerBox;
@ -120,18 +148,20 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
@Override
public void initialize() {
txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee")));
tradeFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.tradeFee")));
buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.buyerSecurityDeposit")));
sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.sellerSecurityDeposit")));
priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price")));
amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())));
volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount")));
marketColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.market")));
directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType")));
dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime")));
tradeIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.tradeId")));
stateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.state")));
txFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TX_FEE.toString()));
tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE.toString()));
buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString()));
sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.SELLER_SEC.toString()));
priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString()));
deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(),
Res.get("portfolio.closedTrades.deviation.help")).getGraphic());
amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString()));
volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString()));
marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString()));
directionColumn.setGraphic(new AutoTooltipLabel(ColumnNames.OFFER_TYPE.toString()));
dateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DATE.toString()));
tradeIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_ID.toString()));
stateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString()));
avatarColumn.setText("");
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
@ -145,6 +175,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
setBuyerSecurityDepositColumnCellFactory();
setSellerSecurityDepositColumnCellFactory();
setPriceColumnCellFactory();
setDeviationColumnCellFactory();
setVolumeColumnCellFactory();
setDateColumnCellFactory();
setMarketColumnCellFactory();
@ -159,6 +190,9 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
priceColumn.setComparator(nullsFirstComparing(o ->
o instanceof Trade ? ((Trade) o).getTradePrice() : o.getOffer().getPrice()
));
deviationColumn.setComparator(Comparator.comparing(o ->
o.getTradable().getOffer().isUseMarketBasedPrice() ? o.getTradable().getOffer().getMarketPriceMargin() : 1,
Comparator.nullsFirst(Comparator.naturalOrder())));
volumeColumn.setComparator(nullsFirstComparingAsTrade(Trade::getTradeVolume));
amountColumn.setComparator(nullsFirstComparingAsTrade(Trade::getTradeAmount));
avatarColumn.setComparator(nullsFirstComparingAsTrade(o ->
@ -217,25 +251,27 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
exportButton.setOnAction(event -> {
final ObservableList<TableColumn<ClosedTradableListItem, ?>> tableColumns = tableView.getColumns();
CSVEntryConverter<ClosedTradableListItem> headerConverter = transactionsListItem -> {
String[] columns = new String[12];
for (int i = 0; i < columns.length; i++)
columns[i] = ((AutoTooltipLabel) tableColumns.get(i).getGraphic()).getText();
String[] columns = new String[ColumnNames.values().length];
for (ColumnNames m : ColumnNames.values()) {
columns[m.ordinal()] = m.toString();
}
return columns;
};
CSVEntryConverter<ClosedTradableListItem> contentConverter = item -> {
String[] columns = new String[12];
columns[0] = model.getTradeId(item);
columns[1] = model.getDate(item);
columns[2] = model.getMarketLabel(item);
columns[3] = model.getPrice(item);
columns[4] = model.getAmount(item);
columns[5] = model.getVolume(item);
columns[6] = model.getTxFee(item);
columns[7] = model.getMakerFee(item);
columns[8] = model.getBuyerSecurityDeposit(item);
columns[9] = model.getSellerSecurityDeposit(item);
columns[10] = model.getDirectionLabel(item);
columns[11] = model.getState(item);
String[] columns = new String[ColumnNames.values().length];
columns[ColumnNames.TRADE_ID.ordinal()] = model.getTradeId(item);
columns[ColumnNames.DATE.ordinal()] = model.getDate(item);
columns[ColumnNames.MARKET.ordinal()] = model.getMarketLabel(item);
columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item);
columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item);
columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item);
columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item);
columns[ColumnNames.TX_FEE.ordinal()] = model.getTxFee(item);
columns[ColumnNames.TRADE_FEE.ordinal()] = model.getMakerFee(item);
columns[ColumnNames.BUYER_SEC.ordinal()] = model.getBuyerSecurityDeposit(item);
columns[ColumnNames.SELLER_SEC.ordinal()] = model.getSellerSecurityDeposit(item);
columns[ColumnNames.OFFER_TYPE.ordinal()] = model.getDirectionLabel(item);
columns[ColumnNames.STATUS.ordinal()] = model.getState(item);
return columns;
};
@ -461,6 +497,24 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
});
}
private void setDeviationColumnCellFactory() {
deviationColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
deviationColumn.setCellFactory(
new Callback<>() {
@Override
public TableCell<ClosedTradableListItem, ClosedTradableListItem> call(
TableColumn<ClosedTradableListItem, ClosedTradableListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(final ClosedTradableListItem item, boolean empty) {
super.updateItem(item, empty);
setGraphic(new AutoTooltipLabel(model.getPriceDeviation(item)));
}
};
}
});
}
private void setVolumeColumnCellFactory() {
volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
volumeColumn.setCellFactory(

View file

@ -78,6 +78,17 @@ class ClosedTradesViewModel extends ActivatableWithDataModel<ClosedTradesDataMod
return FormattingUtils.formatPrice(tradable.getOffer().getPrice());
}
String getPriceDeviation(ClosedTradableListItem item) {
if (item == null)
return "";
Tradable tradable = item.getTradable();
if (tradable.getOffer().isUseMarketBasedPrice()) {
return FormattingUtils.formatPercentagePrice(tradable.getOffer().getMarketPriceMargin());
} else {
return Res.get("shared.na");
}
}
String getVolume(ClosedTradableListItem item) {
if (item != null && item.getTradable() instanceof Trade)
return DisplayUtils.formatVolumeWithCode(((Trade) item.getTradable()).getTradeVolume());

Some files were not shown because too many files have changed in this diff Show more