Refactor API & add registerdisputeagent method to CLI

This commit contains most of the changes suggested by @chimp1984 in
his api-suggestions branch.  See
961703ecea

A new 'registerdisputeagent' method was also added to MainCli, finishing
the work to support registration of mediators and refund agents on
arbitration daemons running in regtest mode.  This method cannot be
used to register dispute agents on mainnet;  users will see an error
msg if they try.
This commit is contained in:
ghubstan 2020-09-18 11:33:16 -03:00
parent b22ab6a1fa
commit e2bd89f39a
No known key found for this signature in database
GPG Key ID: E35592D6800A861E
18 changed files with 317 additions and 143 deletions

View File

@ -48,14 +48,14 @@
run ./bisq-cli --password="xyz" getversion run ./bisq-cli --password="xyz" getversion
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
echo "actual output: $output" >&2 echo "actual output: $output" >&2
[ "$output" = "1.3.8" ] [ "$output" = "1.3.9" ]
} }
@test "test getversion" { @test "test getversion" {
run ./bisq-cli --password=xyz getversion run ./bisq-cli --password=xyz getversion
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
echo "actual output: $output" >&2 echo "actual output: $output" >&2
[ "$output" = "1.3.8" ] [ "$output" = "1.3.9" ]
} }
@test "test setwalletpassword \"a b c\"" { @test "test setwalletpassword \"a b c\"" {
@ -166,15 +166,15 @@
[ "$output" = "Error: address bogus not found in wallet" ] [ "$output" = "Error: address bogus not found in wallet" ]
} }
@test "test createpaymentacct PerfectMoneyDummy (missing nbr, ccy params)" { @test "test createpaymentacct PerfectMoneyDummy (missing name, nbr, ccy params)" {
run ./bisq-cli --password=xyz createpaymentacct PerfectMoneyDummy run ./bisq-cli --password=xyz createpaymentacct PERFECT_MONEY
[ "$status" -eq 1 ] [ "$status" -eq 1 ]
echo "actual output: $output" >&2 echo "actual output: $output" >&2
[ "$output" = "Error: incorrect parameter count, expecting account name, account number, currency code" ] [ "$output" = "Error: incorrect parameter count, expecting payment method id, account name, account number, currency code" ]
} }
@test "test createpaymentacct PerfectMoneyDummy 0123456789 USD" { @test "test createpaymentacct PERFECT_MONEY PerfectMoneyDummy 0123456789 USD" {
run ./bisq-cli --password=xyz createpaymentacct PerfectMoneyDummy 0123456789 USD run ./bisq-cli --password=xyz createpaymentacct PERFECT_MONEY PerfectMoneyDummy 0123456789 USD
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
} }

View File

@ -25,6 +25,7 @@ import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest; import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.LockWalletRequest; import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest; import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SetWalletPasswordRequest; import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.UnlockWalletRequest; import bisq.proto.grpc.UnlockWalletRequest;
@ -69,7 +70,8 @@ public class CliMain {
lockwallet, lockwallet,
unlockwallet, unlockwallet,
removewalletpassword, removewalletpassword,
setwalletpassword setwalletpassword,
registerdisputeagent
} }
public static void main(String[] args) { public static void main(String[] args) {
@ -114,9 +116,9 @@ public class CliMain {
} }
var methodName = nonOptionArgs.get(0); var methodName = nonOptionArgs.get(0);
final Method method; Method method;
try { try {
method = Method.valueOf(methodName); method = getMethodFromCmd(methodName);
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
throw new IllegalArgumentException(format("'%s' is not a supported method", methodName)); throw new IllegalArgumentException(format("'%s' is not a supported method", methodName));
} }
@ -128,6 +130,7 @@ public class CliMain {
throw new IllegalArgumentException("missing required 'password' option"); throw new IllegalArgumentException("missing required 'password' option");
GrpcStubs grpcStubs = new GrpcStubs(host, port, password); GrpcStubs grpcStubs = new GrpcStubs(host, port, password);
var disputeAgentsService = grpcStubs.disputeAgentsService;
var versionService = grpcStubs.versionService; var versionService = grpcStubs.versionService;
var offersService = grpcStubs.offersService; var offersService = grpcStubs.offersService;
var paymentAccountsService = grpcStubs.paymentAccountsService; var paymentAccountsService = grpcStubs.paymentAccountsService;
@ -166,34 +169,38 @@ public class CliMain {
} }
case getoffers: { case getoffers: {
if (nonOptionArgs.size() < 3) if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException("incorrect parameter count, expecting direction (buy|sell), currency code"); throw new IllegalArgumentException("incorrect parameter count,"
+ " expecting direction (buy|sell), currency code");
var direction = nonOptionArgs.get(1); var direction = nonOptionArgs.get(1);
var fiatCurrency = nonOptionArgs.get(2); var fiatCurrency = nonOptionArgs.get(2);
var request = GetOffersRequest.newBuilder() var request = GetOffersRequest.newBuilder()
.setDirection(direction) .setDirection(direction)
.setFiatCurrencyCode(fiatCurrency) .setCurrencyCode(fiatCurrency)
.build(); .build();
var reply = offersService.getOffers(request); var reply = offersService.getOffers(request);
out.println(formatOfferTable(reply.getOffersList(), fiatCurrency)); out.println(formatOfferTable(reply.getOffersList(), fiatCurrency));
return; return;
} }
case createpaymentacct: { case createpaymentacct: {
if (nonOptionArgs.size() < 4) if (nonOptionArgs.size() < 5)
throw new IllegalArgumentException( throw new IllegalArgumentException(
"incorrect parameter count, expecting account name, account number, currency code"); "incorrect parameter count, expecting payment method id,"
+ " account name, account number, currency code");
var accountName = nonOptionArgs.get(1); var paymentMethodId = nonOptionArgs.get(1);
var accountNumber = nonOptionArgs.get(2); var accountName = nonOptionArgs.get(2);
var fiatCurrencyCode = nonOptionArgs.get(3); var accountNumber = nonOptionArgs.get(3);
var currencyCode = nonOptionArgs.get(4);
var request = CreatePaymentAccountRequest.newBuilder() var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.setAccountName(accountName) .setAccountName(accountName)
.setAccountNumber(accountNumber) .setAccountNumber(accountNumber)
.setFiatCurrencyCode(fiatCurrencyCode).build(); .setCurrencyCode(currencyCode).build();
paymentAccountsService.createPaymentAccount(request); paymentAccountsService.createPaymentAccount(request);
out.println(format("payment account %s saved", accountName)); out.printf("payment account %s saved", accountName);
return; return;
} }
case getpaymentaccts: { case getpaymentaccts: {
@ -232,7 +239,8 @@ public class CliMain {
if (nonOptionArgs.size() < 2) if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified"); throw new IllegalArgumentException("no password specified");
var request = RemoveWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)).build(); var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(nonOptionArgs.get(1)).build();
walletsService.removeWalletPassword(request); walletsService.removeWalletPassword(request);
out.println("wallet decrypted"); out.println("wallet decrypted");
return; return;
@ -241,7 +249,8 @@ public class CliMain {
if (nonOptionArgs.size() < 2) if (nonOptionArgs.size() < 2)
throw new IllegalArgumentException("no password specified"); throw new IllegalArgumentException("no password specified");
var requestBuilder = SetWalletPasswordRequest.newBuilder().setPassword(nonOptionArgs.get(1)); var requestBuilder = SetWalletPasswordRequest.newBuilder()
.setPassword(nonOptionArgs.get(1));
var hasNewPassword = nonOptionArgs.size() == 3; var hasNewPassword = nonOptionArgs.size() == 3;
if (hasNewPassword) if (hasNewPassword)
requestBuilder.setNewPassword(nonOptionArgs.get(2)); requestBuilder.setNewPassword(nonOptionArgs.get(2));
@ -249,6 +258,19 @@ public class CliMain {
out.println("wallet encrypted" + (hasNewPassword ? " with new password" : "")); out.println("wallet encrypted" + (hasNewPassword ? " with new password" : ""));
return; return;
} }
case registerdisputeagent: {
if (nonOptionArgs.size() < 3)
throw new IllegalArgumentException(
"incorrect parameter count, expecting dispute agent type, registration key");
var disputeAgentType = nonOptionArgs.get(1);
var registrationKey = nonOptionArgs.get(2);
var requestBuilder = RegisterDisputeAgentRequest.newBuilder()
.setDisputeAgentType(disputeAgentType).setRegistrationKey(registrationKey);
disputeAgentsService.registerDisputeAgent(requestBuilder.build());
out.println(disputeAgentType + " registered");
return;
}
default: { default: {
throw new RuntimeException(format("unhandled method '%s'", method)); throw new RuntimeException(format("unhandled method '%s'", method));
} }
@ -260,6 +282,13 @@ public class CliMain {
} }
} }
private static Method getMethodFromCmd(String methodName) {
// TODO if we use const type for enum we need add some mapping. Even if we don't
// change now it is handy to have flexibility in case we change internal code
// and don't want to break user commands.
return Method.valueOf(methodName.toLowerCase());
}
private static void printHelp(OptionParser parser, PrintStream stream) { private static void printHelp(OptionParser parser, PrintStream stream) {
try { try {
stream.println("Bisq RPC Client"); stream.println("Bisq RPC Client");

View File

@ -139,8 +139,14 @@ public class CoreApi {
// PaymentAccounts // PaymentAccounts
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { public void createPaymentAccount(String paymentMethodId,
paymentAccountsService.createPaymentAccount(accountName, accountNumber, fiatCurrencyCode); String accountName,
String accountNumber,
String currencyCode) {
paymentAccountsService.createPaymentAccount(paymentMethodId,
accountName,
accountNumber,
currencyCode);
} }
public Set<PaymentAccount> getPaymentAccounts() { public Set<PaymentAccount> getPaymentAccounts() {

View File

@ -68,7 +68,7 @@ class CoreDisputeAgentsService {
this.languageCodes = Arrays.asList("de", "en", "es", "fr"); this.languageCodes = Arrays.asList("de", "en", "es", "fr");
} }
public void registerDisputeAgent(String disputeAgentType, String registrationKey) { void registerDisputeAgent(String disputeAgentType, String registrationKey) {
if (!p2PService.isBootstrapped()) if (!p2PService.isBootstrapped())
throw new IllegalStateException("p2p service is not bootstrapped yet"); throw new IllegalStateException("p2p service is not bootstrapped yet");

View File

@ -58,7 +58,7 @@ class CoreOffersService {
this.user = user; this.user = user;
} }
public List<Offer> getOffers(String direction, String fiatCurrencyCode) { List<Offer> getOffers(String direction, String fiatCurrencyCode) {
List<Offer> offers = offerBookService.getOffers().stream() List<Offer> offers = offerBookService.getOffers().stream()
.filter(o -> { .filter(o -> {
var offerOfWantedDirection = o.getDirection().name().equalsIgnoreCase(direction); var offerOfWantedDirection = o.getDirection().name().equalsIgnoreCase(direction);
@ -77,16 +77,16 @@ class CoreOffersService {
return offers; return offers;
} }
public void createOffer(String currencyCode, void createOffer(String currencyCode,
String directionAsString, String directionAsString,
long priceAsLong, long priceAsLong,
boolean useMarketBasedPrice, boolean useMarketBasedPrice,
double marketPriceMargin, double marketPriceMargin,
long amountAsLong, long amountAsLong,
long minAmountAsLong, long minAmountAsLong,
double buyerSecurityDeposit, double buyerSecurityDeposit,
String paymentAccountId, String paymentAccountId,
TransactionResultHandler resultHandler) { TransactionResultHandler resultHandler) {
String offerId = createOfferService.getRandomOfferId(); String offerId = createOfferService.getRandomOfferId();
OfferPayload.Direction direction = OfferPayload.Direction.valueOf(directionAsString); OfferPayload.Direction direction = OfferPayload.Direction.valueOf(directionAsString);
Price price = Price.valueOf(currencyCode, priceAsLong); Price price = Price.valueOf(currencyCode, priceAsLong);
@ -111,18 +111,18 @@ class CoreOffersService {
resultHandler); resultHandler);
} }
public void createOffer(String offerId, void createOffer(String offerId,
String currencyCode, String currencyCode,
OfferPayload.Direction direction, OfferPayload.Direction direction,
Price price, Price price,
boolean useMarketBasedPrice, boolean useMarketBasedPrice,
double marketPriceMargin, double marketPriceMargin,
Coin amount, Coin amount,
Coin minAmount, Coin minAmount,
double buyerSecurityDeposit, double buyerSecurityDeposit,
PaymentAccount paymentAccount, PaymentAccount paymentAccount,
boolean useSavingsWallet, boolean useSavingsWallet,
TransactionResultHandler resultHandler) { TransactionResultHandler resultHandler) {
Coin useDefaultTxFee = Coin.ZERO; Coin useDefaultTxFee = Coin.ZERO;
Offer offer = createOfferService.createAndGetOffer(offerId, Offer offer = createOfferService.createAndGetOffer(offerId,
direction, direction,

View File

@ -33,6 +33,9 @@ import java.util.Set;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.core.payment.payload.PaymentMethod.*;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j @Slf4j
class CorePaymentAccountsService { class CorePaymentAccountsService {
@ -49,17 +52,19 @@ class CorePaymentAccountsService {
this.user = user; this.user = user;
} }
public void createPaymentAccount(String accountName, String accountNumber, String fiatCurrencyCode) { void createPaymentAccount(String paymentMethodId,
// Create and persist a PerfectMoney dummy payment account. There is no guard String accountName,
// against creating accounts with duplicate names & numbers, only the uuid and String accountNumber,
// creation date are unique. String currencyCode) {
PaymentMethod dummyPaymentMethod = PaymentMethod.getDummyPaymentMethod(PaymentMethod.PERFECT_MONEY_ID);
PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(dummyPaymentMethod); PaymentAccount paymentAccount = getNewPaymentAccount(paymentMethodId,
paymentAccount.init(); accountName,
paymentAccount.setAccountName(accountName); accountNumber,
((PerfectMoneyAccount) paymentAccount).setAccountNr(accountNumber); currencyCode);
paymentAccount.setSingleTradeCurrency(new FiatCurrency(fiatCurrencyCode.toUpperCase()));
user.addPaymentAccount(paymentAccount); // TODO not sure if there is more to do at account creation.
// Need to check all the flow when its created from UI.
user.addPaymentAccountIfNotExists(paymentAccount);
// Don't do this on mainnet until thoroughly tested. // Don't do this on mainnet until thoroughly tested.
if (config.baseCurrencyNetwork.isRegtest()) if (config.baseCurrencyNetwork.isRegtest())
@ -68,7 +73,64 @@ class CorePaymentAccountsService {
log.info("Payment account {} saved", paymentAccount.getId()); log.info("Payment account {} saved", paymentAccount.getId());
} }
public Set<PaymentAccount> getPaymentAccounts() { Set<PaymentAccount> getPaymentAccounts() {
return user.getPaymentAccounts(); return user.getPaymentAccounts();
} }
private PaymentAccount getNewPaymentAccount(String paymentMethodId,
String accountName,
String accountNumber,
String currencyCode) {
PaymentAccount paymentAccount = null;
PaymentMethod paymentMethod = getPaymentMethodById(paymentMethodId);
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;
}
checkNotNull(paymentAccount,
"Could not create payment account with paymentMethodId "
+ paymentMethodId + ".");
return paymentAccount;
}
} }

View File

@ -71,7 +71,7 @@ class CoreWalletsService {
this.btcWalletService = btcWalletService; this.btcWalletService = btcWalletService;
} }
public long getAvailableBalance() { long getAvailableBalance() {
verifyWalletsAreAvailable(); verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked(); verifyEncryptedWalletIsUnlocked();
@ -82,40 +82,38 @@ class CoreWalletsService {
return balance.getValue(); return balance.getValue();
} }
public long getAddressBalance(String addressString) { long getAddressBalance(String addressString) {
Address address = getAddressEntry(addressString).getAddress(); Address address = getAddressEntry(addressString).getAddress();
return btcWalletService.getBalanceForAddress(address).value; return btcWalletService.getBalanceForAddress(address).value;
} }
public AddressBalanceInfo getAddressBalanceInfo(String addressString) { AddressBalanceInfo getAddressBalanceInfo(String addressString) {
var satoshiBalance = getAddressBalance(addressString); var satoshiBalance = getAddressBalance(addressString);
var numConfirmations = getNumConfirmationsForMostRecentTransaction(addressString); var numConfirmations = getNumConfirmationsForMostRecentTransaction(addressString);
return new AddressBalanceInfo(addressString, satoshiBalance, numConfirmations); return new AddressBalanceInfo(addressString, satoshiBalance, numConfirmations);
} }
public List<AddressBalanceInfo> getFundingAddresses() { List<AddressBalanceInfo> getFundingAddresses() {
verifyWalletsAreAvailable(); verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked(); verifyEncryptedWalletIsUnlocked();
// Create a new funding address if none exists. // Create a new funding address if none exists.
if (btcWalletService.getAvailableAddressEntries().size() == 0) if (btcWalletService.getAvailableAddressEntries().isEmpty())
btcWalletService.getFreshAddressEntry(); btcWalletService.getFreshAddressEntry();
List<String> addressStrings = List<String> addressStrings = btcWalletService
btcWalletService .getAvailableAddressEntries()
.getAvailableAddressEntries() .stream()
.stream() .map(AddressEntry::getAddressString)
.map(AddressEntry::getAddressString) .collect(Collectors.toList());
.collect(Collectors.toList());
// getAddressBalance is memoized, because we'll map it over addresses twice. // getAddressBalance is memoized, because we'll map it over addresses twice.
// To get the balances, we'll be using .getUnchecked, because we know that // To get the balances, we'll be using .getUnchecked, because we know that
// this::getAddressBalance cannot return null. // this::getAddressBalance cannot return null.
var balances = memoize(this::getAddressBalance); var balances = memoize(this::getAddressBalance);
boolean noAddressHasZeroBalance = boolean noAddressHasZeroBalance = addressStrings.stream()
addressStrings.stream() .allMatch(addressString -> balances.getUnchecked(addressString) != 0);
.allMatch(addressString -> balances.getUnchecked(addressString) != 0);
if (noAddressHasZeroBalance) { if (noAddressHasZeroBalance) {
var newZeroBalanceAddress = btcWalletService.getFreshAddressEntry(); var newZeroBalanceAddress = btcWalletService.getFreshAddressEntry();
@ -129,13 +127,13 @@ class CoreWalletsService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public int getNumConfirmationsForMostRecentTransaction(String addressString) { int getNumConfirmationsForMostRecentTransaction(String addressString) {
Address address = getAddressEntry(addressString).getAddress(); Address address = getAddressEntry(addressString).getAddress();
TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address); TransactionConfidence confidence = btcWalletService.getConfidenceForAddress(address);
return confidence == null ? 0 : confidence.getDepthInBlocks(); return confidence == null ? 0 : confidence.getDepthInBlocks();
} }
public void setWalletPassword(String password, String newPassword) { void setWalletPassword(String password, String newPassword) {
verifyWalletsAreAvailable(); verifyWalletsAreAvailable();
KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt();
@ -165,7 +163,7 @@ class CoreWalletsService {
walletsManager.backupWallets(); walletsManager.backupWallets();
} }
public void lockWallet() { void lockWallet() {
if (!walletsManager.areWalletsEncrypted()) if (!walletsManager.areWalletsEncrypted())
throw new IllegalStateException("wallet is not encrypted with a password"); throw new IllegalStateException("wallet is not encrypted with a password");
@ -175,7 +173,7 @@ class CoreWalletsService {
tempAesKey = null; tempAesKey = null;
} }
public void unlockWallet(String password, long timeout) { void unlockWallet(String password, long timeout) {
verifyWalletIsAvailableAndEncrypted(); verifyWalletIsAvailableAndEncrypted();
KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt();
@ -213,7 +211,7 @@ class CoreWalletsService {
// Provided for automated wallet protection method testing, despite the // Provided for automated wallet protection method testing, despite the
// security risks exposed by providing users the ability to decrypt their wallets. // security risks exposed by providing users the ability to decrypt their wallets.
public void removeWalletPassword(String password) { void removeWalletPassword(String password) {
verifyWalletIsAvailableAndEncrypted(); verifyWalletIsAvailableAndEncrypted();
KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt(); KeyCrypterScrypt keyCrypterScrypt = getKeyCrypterScrypt();

View File

@ -734,6 +734,9 @@ public abstract class WalletService {
return maybeAddTxToWallet(transaction.bitcoinSerialize(), wallet, source); return maybeAddTxToWallet(transaction.bitcoinSerialize(), wallet, source);
} }
public Address getAddress(String addressString) {
return Address.fromBase58(params, addressString);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// bisqWalletEventListener // bisqWalletEventListener

View File

@ -189,6 +189,12 @@ public class User implements PersistedDataHost {
// Collection operations // Collection operations
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void addPaymentAccountIfNotExists(PaymentAccount paymentAccount) {
if (!paymentAccountExists(paymentAccount)) {
addPaymentAccount(paymentAccount);
}
}
public void addPaymentAccount(PaymentAccount paymentAccount) { public void addPaymentAccount(PaymentAccount paymentAccount) {
paymentAccount.onAddToUser(); paymentAccount.onAddToUser();
@ -493,4 +499,8 @@ public class User implements PersistedDataHost {
public boolean isPaymentAccountImport() { public boolean isPaymentAccountImport() {
return isPaymentAccountImport; return isPaymentAccountImport;
} }
private boolean paymentAccountExists(PaymentAccount paymentAccount) {
return getPaymentAccountsAsObservable().stream().anyMatch(e -> e.equals(paymentAccount));
}
} }

View File

@ -23,6 +23,7 @@ import bisq.core.app.CoreModule;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.AppModule; import bisq.common.app.AppModule;
import bisq.common.handlers.ResultHandler;
import bisq.common.setup.CommonSetup; import bisq.common.setup.CommonSetup;
import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.common.util.concurrent.ThreadFactoryBuilder;
@ -39,13 +40,15 @@ import bisq.daemon.grpc.GrpcServer;
@Slf4j @Slf4j
public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.BisqSetupListener { public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.BisqSetupListener {
private GrpcServer grpcServer;
public static void main(String[] args) { public static void main(String[] args) {
new BisqDaemonMain().execute(args); new BisqDaemonMain().execute(args);
} }
/////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
// First synchronous execution tasks // First synchronous execution tasks
/////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
@Override @Override
protected void configUserThread() { protected void configUserThread() {
@ -70,9 +73,9 @@ public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.Bis
headlessApp.setGracefulShutDownHandler(this); headlessApp.setGracefulShutDownHandler(this);
} }
/////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks // We continue with a series of synchronous execution tasks
/////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
@Override @Override
protected AppModule getModule() { protected AppModule getModule() {
@ -91,7 +94,8 @@ public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.Bis
// We need to be in user thread! We mapped at launchApplication already... // We need to be in user thread! We mapped at launchApplication already...
headlessApp.startApplication(); headlessApp.startApplication();
// In headless mode we don't have an async behaviour so we trigger the setup by calling onApplicationStarted // In headless mode we don't have an async behaviour so we trigger the setup by
// calling onApplicationStarted.
onApplicationStarted(); onApplicationStarted();
} }
@ -99,7 +103,14 @@ public class BisqDaemonMain extends BisqHeadlessAppMain implements BisqSetup.Bis
protected void onApplicationStarted() { protected void onApplicationStarted() {
super.onApplicationStarted(); super.onApplicationStarted();
GrpcServer grpcServer = injector.getInstance(GrpcServer.class); grpcServer = injector.getInstance(GrpcServer.class);
grpcServer.start(); grpcServer.start();
} }
@Override
public void gracefulShutDown(ResultHandler resultHandler) {
super.gracefulShutDown(resultHandler);
grpcServer.shutdown();
}
} }

View File

@ -0,0 +1,37 @@
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.trade.statistics.TradeStatistics2;
import bisq.proto.grpc.GetTradeStatisticsGrpc;
import bisq.proto.grpc.GetTradeStatisticsReply;
import bisq.proto.grpc.GetTradeStatisticsRequest;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
import java.util.stream.Collectors;
public class GrpcGetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
private final CoreApi coreApi;
@Inject
public GrpcGetTradeStatisticsService(CoreApi coreApi) {
this.coreApi = coreApi;
}
@Override
public void getTradeStatistics(GetTradeStatisticsRequest req,
StreamObserver<GetTradeStatisticsReply> responseObserver) {
var tradeStatistics = coreApi.getTradeStatistics().stream()
.map(TradeStatistics2::toProtoTradeStatistics2)
.collect(Collectors.toList());
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}

View File

@ -52,7 +52,7 @@ class GrpcOffersService extends OffersGrpc.OffersImplBase {
// The client cannot see bisq.core.Offer or its fromProto method. // The client cannot see bisq.core.Offer or its fromProto method.
// We use the lighter weight OfferInfo proto wrapper instead, containing just // We use the lighter weight OfferInfo proto wrapper instead, containing just
// enough fields to view and create offers. // enough fields to view and create offers.
List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getFiatCurrencyCode()) List<OfferInfo> result = coreApi.getOffers(req.getDirection(), req.getCurrencyCode())
.stream().map(offer -> new OfferInfo.OfferInfoBuilder() .stream().map(offer -> new OfferInfo.OfferInfoBuilder()
.withId(offer.getId()) .withId(offer.getId())
.withDirection(offer.getDirection().name()) .withDirection(offer.getDirection().name())

View File

@ -45,7 +45,10 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
@Override @Override
public void createPaymentAccount(CreatePaymentAccountRequest req, public void createPaymentAccount(CreatePaymentAccountRequest req,
StreamObserver<CreatePaymentAccountReply> responseObserver) { StreamObserver<CreatePaymentAccountReply> responseObserver) {
coreApi.createPaymentAccount(req.getAccountName(), req.getAccountNumber(), req.getFiatCurrencyCode()); coreApi.createPaymentAccount(req.getPaymentMethodId(),
req.getAccountName(),
req.getAccountNumber(),
req.getCurrencyCode());
var reply = CreatePaymentAccountReply.newBuilder().build(); var reply = CreatePaymentAccountReply.newBuilder().build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
@ -54,10 +57,11 @@ class GrpcPaymentAccountsService extends PaymentAccountsGrpc.PaymentAccountsImpl
@Override @Override
public void getPaymentAccounts(GetPaymentAccountsRequest req, public void getPaymentAccounts(GetPaymentAccountsRequest req,
StreamObserver<GetPaymentAccountsReply> responseObserver) { StreamObserver<GetPaymentAccountsReply> responseObserver) {
var tradeStatistics = coreApi.getPaymentAccounts().stream() var paymentAccounts = coreApi.getPaymentAccounts().stream()
.map(PaymentAccount::toProtoMessage) .map(PaymentAccount::toProtoMessage)
.collect(Collectors.toList()); .collect(Collectors.toList());
var reply = GetPaymentAccountsReply.newBuilder().addAllPaymentAccounts(tradeStatistics).build(); var reply = GetPaymentAccountsReply.newBuilder()
.addAllPaymentAccounts(paymentAccounts).build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
} }

View File

@ -18,30 +18,21 @@
package bisq.daemon.grpc; package bisq.daemon.grpc;
import bisq.core.api.CoreApi; import bisq.core.api.CoreApi;
import bisq.core.trade.statistics.TradeStatistics2;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.proto.grpc.GetTradeStatisticsGrpc;
import bisq.proto.grpc.GetTradeStatisticsReply;
import bisq.proto.grpc.GetTradeStatisticsRequest;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionReply;
import bisq.proto.grpc.GetVersionRequest;
import io.grpc.Server; import io.grpc.Server;
import io.grpc.ServerBuilder; import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Singleton
@Slf4j @Slf4j
public class GrpcServer { public class GrpcServer {
@ -51,19 +42,22 @@ public class GrpcServer {
@Inject @Inject
public GrpcServer(Config config, public GrpcServer(Config config,
CoreApi coreApi, CoreApi coreApi,
PasswordAuthInterceptor passwordAuthInterceptor,
GrpcDisputeAgentsService disputeAgentsService, GrpcDisputeAgentsService disputeAgentsService,
GrpcOffersService offersService, GrpcOffersService offersService,
GrpcPaymentAccountsService paymentAccountsService, GrpcPaymentAccountsService paymentAccountsService,
GrpcVersionService versionService,
GrpcGetTradeStatisticsService tradeStatisticsService,
GrpcWalletsService walletsService) { GrpcWalletsService walletsService) {
this.coreApi = coreApi; this.coreApi = coreApi;
this.server = ServerBuilder.forPort(config.apiPort) this.server = ServerBuilder.forPort(config.apiPort)
.addService(disputeAgentsService) .addService(disputeAgentsService)
.addService(new GetVersionService())
.addService(new GetTradeStatisticsService())
.addService(offersService) .addService(offersService)
.addService(paymentAccountsService) .addService(paymentAccountsService)
.addService(tradeStatisticsService)
.addService(versionService)
.addService(walletsService) .addService(walletsService)
.intercept(new PasswordAuthInterceptor(config.apiPassword)) .intercept(passwordAuthInterceptor)
.build(); .build();
} }
@ -71,36 +65,14 @@ public class GrpcServer {
try { try {
server.start(); server.start();
log.info("listening on port {}", server.getPort()); log.info("listening on port {}", server.getPort());
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.shutdown();
log.info("shutdown complete");
}));
} catch (IOException ex) { } catch (IOException ex) {
throw new UncheckedIOException(ex); throw new UncheckedIOException(ex);
} }
} }
class GetVersionService extends GetVersionGrpc.GetVersionImplBase { public void shutdown() {
@Override log.info("Server shutdown started");
public void getVersion(GetVersionRequest req, StreamObserver<GetVersionReply> responseObserver) { server.shutdown();
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build(); log.info("Server shutdown complete");
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
class GetTradeStatisticsService extends GetTradeStatisticsGrpc.GetTradeStatisticsImplBase {
@Override
public void getTradeStatistics(GetTradeStatisticsRequest req,
StreamObserver<GetTradeStatisticsReply> responseObserver) {
var tradeStatistics = coreApi.getTradeStatistics().stream()
.map(TradeStatistics2::toProtoTradeStatistics2)
.collect(Collectors.toList());
var reply = GetTradeStatisticsReply.newBuilder().addAllTradeStatistics(tradeStatistics).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
} }
} }

View File

@ -0,0 +1,28 @@
package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.proto.grpc.GetVersionGrpc;
import bisq.proto.grpc.GetVersionReply;
import bisq.proto.grpc.GetVersionRequest;
import io.grpc.stub.StreamObserver;
import javax.inject.Inject;
public class GrpcVersionService extends GetVersionGrpc.GetVersionImplBase {
private final CoreApi coreApi;
@Inject
public GrpcVersionService(CoreApi coreApi) {
this.coreApi = coreApi;
}
@Override
public void getVersion(GetVersionRequest req, StreamObserver<GetVersionReply> responseObserver) {
var reply = GetVersionReply.newBuilder().setVersion(coreApi.getVersion()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}

View File

@ -54,11 +54,17 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
this.coreApi = coreApi; 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 @Override
public void getBalance(GetBalanceRequest req, StreamObserver<GetBalanceReply> responseObserver) { public void getBalance(GetBalanceRequest req, StreamObserver<GetBalanceReply> responseObserver) {
try { try {
long result = coreApi.getAvailableBalance(); long availableBalance = coreApi.getAvailableBalance();
var reply = GetBalanceReply.newBuilder().setBalance(result).build(); var reply = GetBalanceReply.newBuilder().setBalance(availableBalance).build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
} catch (IllegalStateException cause) { } catch (IllegalStateException cause) {
@ -72,8 +78,9 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
public void getAddressBalance(GetAddressBalanceRequest req, public void getAddressBalance(GetAddressBalanceRequest req,
StreamObserver<GetAddressBalanceReply> responseObserver) { StreamObserver<GetAddressBalanceReply> responseObserver) {
try { try {
AddressBalanceInfo result = coreApi.getAddressBalanceInfo(req.getAddress()); AddressBalanceInfo balanceInfo = coreApi.getAddressBalanceInfo(req.getAddress());
var reply = GetAddressBalanceReply.newBuilder().setAddressBalanceInfo(result.toProtoMessage()).build(); var reply = GetAddressBalanceReply.newBuilder()
.setAddressBalanceInfo(balanceInfo.toProtoMessage()).build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
} catch (IllegalStateException cause) { } catch (IllegalStateException cause) {
@ -87,10 +94,10 @@ class GrpcWalletsService extends WalletsGrpc.WalletsImplBase {
public void getFundingAddresses(GetFundingAddressesRequest req, public void getFundingAddresses(GetFundingAddressesRequest req,
StreamObserver<GetFundingAddressesReply> responseObserver) { StreamObserver<GetFundingAddressesReply> responseObserver) {
try { try {
List<AddressBalanceInfo> result = coreApi.getFundingAddresses(); List<AddressBalanceInfo> balanceInfo = coreApi.getFundingAddresses();
var reply = GetFundingAddressesReply.newBuilder() var reply = GetFundingAddressesReply.newBuilder()
.addAllAddressBalanceInfo( .addAllAddressBalanceInfo(
result.stream() balanceInfo.stream()
.map(AddressBalanceInfo::toProtoMessage) .map(AddressBalanceInfo::toProtoMessage)
.collect(Collectors.toList())) .collect(Collectors.toList()))
.build(); .build();

View File

@ -17,12 +17,16 @@
package bisq.daemon.grpc; package bisq.daemon.grpc;
import bisq.common.config.Config;
import io.grpc.Metadata; import io.grpc.Metadata;
import io.grpc.ServerCall; import io.grpc.ServerCall;
import io.grpc.ServerCallHandler; import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor; import io.grpc.ServerInterceptor;
import io.grpc.StatusRuntimeException; import io.grpc.StatusRuntimeException;
import javax.inject.Inject;
import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static io.grpc.Metadata.ASCII_STRING_MARSHALLER;
import static io.grpc.Metadata.Key; import static io.grpc.Metadata.Key;
import static io.grpc.Status.UNAUTHENTICATED; import static io.grpc.Status.UNAUTHENTICATED;
@ -36,12 +40,13 @@ import static java.lang.String.format;
*/ */
class PasswordAuthInterceptor implements ServerInterceptor { class PasswordAuthInterceptor implements ServerInterceptor {
public static final String PASSWORD_KEY = "password"; private static final String PASSWORD_KEY = "password";
private final String expectedPasswordValue; private final String expectedPasswordValue;
public PasswordAuthInterceptor(String expectedPasswordValue) { @Inject
this.expectedPasswordValue = expectedPasswordValue; public PasswordAuthInterceptor(Config config) {
this.expectedPasswordValue = config.apiPassword;
} }
@Override @Override

View File

@ -53,7 +53,7 @@ service Offers {
message GetOffersRequest { message GetOffersRequest {
string direction = 1; string direction = 1;
string fiatCurrencyCode = 2; string currencyCode = 2;
} }
message GetOffersReply { message GetOffersReply {
@ -61,7 +61,7 @@ message GetOffersReply {
} }
message CreateOfferRequest { message CreateOfferRequest {
string currencyCode = 1; // TODO switch order w/ direction field in next PR string currencyCode = 1;
string direction = 2; string direction = 2;
uint64 price = 3; uint64 price = 3;
bool useMarketBasedPrice = 4; bool useMarketBasedPrice = 4;
@ -107,9 +107,11 @@ service PaymentAccounts {
} }
message CreatePaymentAccountRequest { message CreatePaymentAccountRequest {
string accountName = 1; string paymentMethodId = 1;
string accountNumber = 2; string accountName = 2;
string fiatCurrencyCode = 3; string accountNumber = 3;
// TODO Support all currencies. Maybe add a repeated and if only one is used its a singletonList.
string currencyCode = 4;
} }
message CreatePaymentAccountReply { message CreatePaymentAccountReply {
@ -123,7 +125,7 @@ message GetPaymentAccountsReply {
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// TradeStatistics // GetTradeStatistics
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
service GetTradeStatistics { service GetTradeStatistics {