mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-22 14:42:37 +01:00
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:
commit
29c2e0002d
122 changed files with 5351 additions and 1194 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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 ]
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ public class CancelOfferTest extends AbstractOfferTest {
|
|||
.setMarketPriceMargin(0.00)
|
||||
.setPrice("0")
|
||||
.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent())
|
||||
.setMakerFeeCurrencyCode("bsq")
|
||||
.build();
|
||||
|
||||
// Create some offers.
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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, () ->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
108
common/src/main/java/bisq/common/util/ReflectionUtils.java
Normal file
108
common/src/main/java/bisq/common/util/ReflectionUtils.java
Normal 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 "";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) -> {
|
||||
|
|
|
@ -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)
|
||||
|
|
45
core/src/main/java/bisq/core/api/model/BalancesInfo.java
Normal file
45
core/src/main/java/bisq/core/api/model/BalancesInfo.java
Normal 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" +
|
||||
'}';
|
||||
}
|
||||
}
|
94
core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java
Normal file
94
core/src/main/java/bisq/core/api/model/BsqBalanceInfo.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
75
core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java
Normal file
75
core/src/main/java/bisq/core/api/model/BtcBalanceInfo.java
Normal 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
250
core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
Normal file
250
core/src/main/java/bisq/core/api/model/PaymentAccountForm.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
75
core/src/main/java/bisq/core/api/model/TxFeeRateInfo.java
Normal file
75
core/src/main/java/bisq/core/api/model/TxFeeRateInfo.java
Normal 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" +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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(() -> {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(() -> {
|
||||
|
|
70
core/src/main/java/bisq/core/btc/model/BsqTransferModel.java
Normal file
70
core/src/main/java/bisq/core/btc/model/BsqTransferModel.java
Normal 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" +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
139
core/src/main/java/bisq/core/util/AveragePriceUtil.java
Normal file
139
core/src/main/java/bisq/core/util/AveragePriceUtil.java
Normal 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();
|
||||
}
|
||||
}
|
140
core/src/main/java/bisq/core/util/InlierUtil.java
Normal file
140
core/src/main/java/bisq/core/util/InlierUtil.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(","));
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue