Merge pull request #5357 from ghubstan/02-api-trade-contract-details

Provide more offer & contract detail to CLI
This commit is contained in:
sqrrm 2021-04-02 18:44:26 +02:00 committed by GitHub
commit b6f9231af9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1272 additions and 210 deletions

View File

@ -238,65 +238,35 @@ gettradedetail() {
istradedepositpublished() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $9}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
fi
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
commandalert $? "Could not parse istradedepositpublished from trade detail."
echo "$ANSWER"
}
istradedepositconfirmed() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $10}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}')
fi
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $11}')
commandalert $? "Could not parse istradedepositconfirmed from trade detail."
echo "$ANSWER"
}
istradepaymentsent() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $12}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
fi
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
commandalert $? "Could not parse istradepaymentsent from trade detail."
echo "$ANSWER"
}
istradepaymentreceived() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $13}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
fi
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
commandalert $? "Could not parse istradepaymentreceived from trade detail."
echo "$ANSWER"
}
istradepayoutpublished() {
TRADE_DETAIL="$1"
MAKER_OR_TAKER="$2"
if [ "$MAKER_OR_TAKER" = "MAKER" ]
then
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $14}')
else
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}')
fi
ANSWER=$(echo "$TRADE_DETAIL" | awk '{print $15}')
commandalert $? "Could not parse istradepayoutpublished from trade detail."
echo "$ANSWER"
}
@ -321,7 +291,7 @@ waitfortradedepositpublished() {
TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT")
exitoncommandalert $?
IS_TRADE_DEPOSIT_PUBLISHED=$(istradedepositpublished "$TRADE_DETAIL" "TAKER")
IS_TRADE_DEPOSIT_PUBLISHED=$(istradedepositpublished "$TRADE_DETAIL")
exitoncommandalert $?
printdate "BOB $BOB_ROLE: Has taker's trade deposit been published? $IS_TRADE_DEPOSIT_PUBLISHED"
@ -356,7 +326,7 @@ waitfortradedepositconfirmed() {
TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT")
exitoncommandalert $?
IS_TRADE_DEPOSIT_CONFIRMED=$(istradedepositconfirmed "$TRADE_DETAIL" "TAKER")
IS_TRADE_DEPOSIT_CONFIRMED=$(istradedepositconfirmed "$TRADE_DETAIL")
exitoncommandalert $?
printdate "BOB $BOB_ROLE: Has taker's trade deposit been confirmed? $IS_TRADE_DEPOSIT_CONFIRMED"
printbreak
@ -379,7 +349,6 @@ waitfortradepaymentsent() {
PORT="$1"
SELLER="$2"
OFFER_ID="$3"
MAKER_OR_TAKER="$4"
DONE=0
while : ; do
if [ "$DONE" -ne 0 ]; then
@ -397,7 +366,7 @@ waitfortradepaymentsent() {
TRADE_DETAIL=$(gettradedetail "$GETTRADE_CMD_OUTPUT")
exitoncommandalert $?
IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL" "$MAKER_OR_TAKER")
IS_TRADE_PAYMENT_SENT=$(istradepaymentsent "$TRADE_DETAIL")
exitoncommandalert $?
printdate "$SELLER: Has buyer's fiat payment been initiated? $IS_TRADE_PAYMENT_SENT"
if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ]
@ -416,7 +385,6 @@ waitfortradepaymentreceived() {
PORT="$1"
SELLER="$2"
OFFER_ID="$3"
MAKER_OR_TAKER="$4"
DONE=0
while : ; do
if [ "$DONE" -ne 0 ]; then
@ -437,7 +405,7 @@ waitfortradepaymentreceived() {
# When the seller receives a 'payment sent' message, it is assumed funds (fiat) have already been deposited.
# In a real trade, there is usually a delay between receipt of a 'payment sent' message, and the funds deposit,
# but we do not need to simulate that in this regtest script.
IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL" "$MAKER_OR_TAKER")
IS_TRADE_PAYMENT_SENT=$(istradepaymentreceived "$TRADE_DETAIL")
exitoncommandalert $?
printdate "$SELLER: Has buyer's payment been transferred to seller's fiat account? $IS_TRADE_PAYMENT_SENT"
if [ "$IS_TRADE_PAYMENT_SENT" = "YES" ]
@ -544,10 +512,10 @@ executetrade() {
if [ "$DIRECTION" = "BUY" ]
then
# Bob waits for payment, polling status in taker specific trade detail.
waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" "TAKER"
waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID"
else
# Alice waits for payment, polling status in maker specific trade detail.
waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID" "MAKER"
waitfortradepaymentsent "$PAYEE_PORT" "$PAYEE" "$OFFER_ID"
fi
@ -557,10 +525,10 @@ executetrade() {
if [ "$DIRECTION" = "BUY" ]
then
# Alice waits for payment rcvd confirm from Bob, polling status in maker specific trade detail.
waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" "MAKER"
waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID"
else
# Bob waits for payment rcvd confirm from Alice, polling status in taker specific trade detail.
waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID" "TAKER"
waitfortradepaymentreceived "$PAYER_PORT" "$PAYER" "$OFFER_ID"
fi
# Generate some btc blocks

View File

@ -56,6 +56,7 @@ import static java.util.Collections.singletonList;
import bisq.cli.opts.ArgumentList;
import bisq.cli.opts.CancelOfferOptionParser;
import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser;
import bisq.cli.opts.CreateOfferOptionParser;
import bisq.cli.opts.CreatePaymentAcctOptionParser;
import bisq.cli.opts.GetAddressBalanceOptionParser;
@ -517,6 +518,24 @@ public class CliMain {
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
return;
}
case createcryptopaymentacct: {
var opts = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var accountName = opts.getAccountName();
var currencyCode = opts.getCurrencyCode();
var address = opts.getAddress();
var isTradeInstant = opts.getIsTradeInstant();
var paymentAccount = client.createCryptoCurrencyPaymentAccount(accountName,
currencyCode,
address,
isTradeInstant);
out.println("payment account saved");
out.println(formatPaymentAcctTbl(singletonList(paymentAccount)));
return;
}
case getpaymentaccts: {
if (new SimpleMethodOptionParser(args).parse().isForHelp()) {
out.println(client.getMethodHelp(method));
@ -676,7 +695,7 @@ public class CliMain {
stream.println();
parser.printHelpOn(stream);
stream.println();
String rowFormat = "%-24s%-52s%s%n";
String rowFormat = "%-25s%-52s%s%n";
stream.format(rowFormat, "Method", "Params", "Description");
stream.format(rowFormat, "------", "------", "------------");
stream.format(rowFormat, getversion.name(), "", "Get server version");
@ -727,7 +746,9 @@ public class CliMain {
stream.format(rowFormat, getmyoffers.name(), "--direction=<buy|sell> \\", "Get my current offers");
stream.format(rowFormat, "", "--currency-code=<currency-code>", "");
stream.println();
stream.format(rowFormat, takeoffer.name(), "--offer-id=<offer-id> [--fee-currency=<btc|bsq>]", "Take offer with id");
stream.format(rowFormat, takeoffer.name(), "--offer-id=<offer-id> \\", "Take offer with id");
stream.format(rowFormat, "", "--payment-account=<payment-account-id>", "");
stream.format(rowFormat, "", "[--fee-currency=<btc|bsq>]", "");
stream.println();
stream.format(rowFormat, gettrade.name(), "--trade-id=<trade-id> \\", "Get trade summary or full contract");
stream.format(rowFormat, "", "[--show-contract=<true|false>]", "");
@ -748,6 +769,11 @@ public class CliMain {
stream.println();
stream.format(rowFormat, createpaymentacct.name(), "--payment-account-form=<path>", "Create a new payment account");
stream.println();
stream.format(rowFormat, createcryptopaymentacct.name(), "--account-name=<name> \\", "Create a new cryptocurrency payment account");
stream.format(rowFormat, "", "--currency-code=<bsq> \\", "");
stream.format(rowFormat, "", "--address=<bsq-address>", "");
stream.format(rowFormat, "", "--trade-instant=<true|false>", "");
stream.println();
stream.format(rowFormat, getpaymentaccts.name(), "", "Get user payment accounts");
stream.println();
stream.format(rowFormat, lockwallet.name(), "", "Remove wallet password from memory, locking the wallet");

View File

@ -30,7 +30,7 @@ class ColumnHeaderConstants {
// expected max data string length is accounted for. In others, column header
// lengths are expected to be greater than any column value length.
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_AMOUNT = "BTC(min - max)";
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";
@ -49,7 +49,9 @@ class ColumnHeaderConstants {
static final String COL_HEADER_NAME = "Name";
static final String COL_HEADER_PAYMENT_METHOD = "Payment Method";
static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC";
static final String COL_HEADER_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s";
static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' ');
static final String COL_HEADER_TRADE_BSQ_BUYER_ADDRESS = "BSQ Buyer Address";
static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' ');
static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published";
@ -59,8 +61,9 @@ class ColumnHeaderConstants {
static final String COL_HEADER_TRADE_WITHDRAWN = "Withdrawn";
static final String COL_HEADER_TRADE_ROLE = "My Role";
static final String COL_HEADER_TRADE_SHORT_ID = "ID";
static final String COL_HEADER_TRADE_TX_FEE = "Tx Fee(%-3s)";
static final String COL_HEADER_TRADE_TAKER_FEE = "Taker Fee(%-3s)";
static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' ');
static final String COL_HEADER_TRADE_MAKER_FEE = padEnd("Maker Fee(%-3s)", 12, ' '); // "Maker Fee(%-3s)";
static final String COL_HEADER_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)";
static final String COL_HEADER_TX_ID = "Tx ID";
static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)";
@ -71,5 +74,6 @@ class ColumnHeaderConstants {
static final String COL_HEADER_TX_MEMO = "Memo";
static final String COL_HEADER_VOLUME = padEnd("%-3s(min - max)", 15, ' ');
static final String COL_HEADER_UUID = padEnd("ID", 52, ' ');
}

View File

@ -25,23 +25,25 @@ import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Locale;
import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
import static java.math.RoundingMode.UNNECESSARY;
@VisibleForTesting
public class CurrencyFormat {
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100000000);
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000);
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000");
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0");
static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00");
static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00");
static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01");
@ -55,6 +57,14 @@ public class CurrencyFormat {
return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR));
}
public static String formatBsqAmount(long bsqSats) {
// BSQ sats = trade.getOffer().getVolume()
NUMBER_FORMAT.setMinimumFractionDigits(2);
NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setRoundingMode(HALF_UP);
return SEND_BSQ_FORMAT.format((double) bsqSats / SATOSHI_DIVISOR.doubleValue());
}
public static String formatTxFeeRateInfo(TxFeeRateInfo txFeeRateInfo) {
if (txFeeRateInfo.getUseCustomTxFeeRate())
return format("custom tx fee rate: %s sats/byte, network rate: %s sats/byte",
@ -77,22 +87,44 @@ public class CurrencyFormat {
: formatOfferVolume(volume);
}
public static String formatCryptoCurrencyVolumeRange(long minVolume, long volume) {
return minVolume != volume
? formatCryptoCurrencyOfferVolume(minVolume) + " - " + formatCryptoCurrencyOfferVolume(volume)
: formatCryptoCurrencyOfferVolume(volume);
}
public static String formatMarketPrice(double price) {
NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setMaximumFractionDigits(4);
return NUMBER_FORMAT.format(price);
}
public static String formatOfferPrice(long price) {
NUMBER_FORMAT.setMaximumFractionDigits(4);
public static String formatPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10000);
NUMBER_FORMAT.setMaximumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10_000);
}
public static String formatCryptoCurrencyPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(8);
NUMBER_FORMAT.setMaximumFractionDigits(8);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue());
}
public static String formatOfferVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(0);
NUMBER_FORMAT.setMaximumFractionDigits(0);
NUMBER_FORMAT.setRoundingMode(RoundingMode.UNNECESSARY);
return NUMBER_FORMAT.format((double) volume / 10000);
NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) volume / 10_000);
}
public static String formatCryptoCurrencyOfferVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(2);
NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue());
}
public static long toSatoshis(String btc) {

View File

@ -0,0 +1,60 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli;
import bisq.proto.grpc.OfferInfo;
import java.util.List;
import java.util.function.Function;
import static bisq.cli.ColumnHeaderConstants.COL_HEADER_DIRECTION;
import static java.lang.String.format;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
class DirectionFormat {
static int getLongestDirectionColWidth(List<OfferInfo> offers) {
if (offers.isEmpty() || offers.get(0).getBaseCurrencyCode().equals("BTC"))
return COL_HEADER_DIRECTION.length();
else
return 18; // .e.g., "Sell BSQ (Buy BTC)".length()
}
static final Function<OfferInfo, String> directionFormat = (offer) -> {
String baseCurrencyCode = offer.getBaseCurrencyCode();
boolean isCryptoCurrencyOffer = !baseCurrencyCode.equals("BTC");
if (!isCryptoCurrencyOffer) {
return baseCurrencyCode;
} else {
// Return "Sell BSQ (Buy BTC)", or "Buy BSQ (Sell BTC)".
String direction = offer.getDirection();
String mirroredDirection = getMirroredDirection(direction);
Function<String, String> mixedCase = (word) -> word.charAt(0) + word.substring(1).toLowerCase();
return format("%s %s (%s %s)",
mixedCase.apply(mirroredDirection),
baseCurrencyCode,
mixedCase.apply(direction),
offer.getCounterCurrencyCode());
}
};
static String getMirroredDirection(String directionAsString) {
return directionAsString.equalsIgnoreCase(BUY.name()) ? SELL.name() : BUY.name();
}
}

View File

@ -24,10 +24,12 @@ import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
@ -60,6 +62,7 @@ import bisq.proto.grpc.TxFeeRateInfo;
import bisq.proto.grpc.TxInfo;
import bisq.proto.grpc.UnlockWalletRequest;
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.VerifyBsqSentToAddressRequest;
import bisq.proto.grpc.WithdrawFundsRequest;
import protobuf.PaymentAccount;
@ -67,11 +70,11 @@ import protobuf.PaymentMethod;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
@ -164,6 +167,14 @@ public final class GrpcClient {
return grpcStubs.walletsService.sendBtc(request).getTxInfo();
}
public boolean verifyBsqSentToAddress(String address, String amount) {
var request = VerifyBsqSentToAddressRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.build();
return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived();
}
public TxFeeRateInfo getTxFeeRate() {
var request = GetTxFeeRateRequest.newBuilder().build();
return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
@ -228,7 +239,6 @@ public final class GrpcClient {
makerFeeCurrencyCode);
}
// TODO make private, move to bottom of class
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
@ -283,6 +293,12 @@ public final class GrpcClient {
return grpcStubs.offersService.getOffers(request).getOffersList();
}
public List<OfferInfo> getBsqOffers(String direction) {
return getOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equals("BSQ"))
.collect(toList());
}
public List<OfferInfo> getOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getOffers(BUY.name(), currencyCode));
@ -295,6 +311,13 @@ public final class GrpcClient {
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getBsqOffers(BUY.name()));
offers.addAll(getBsqOffers(SELL.name()));
return sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
@ -303,6 +326,12 @@ public final class GrpcClient {
return grpcStubs.offersService.getMyOffers(request).getOffersList();
}
public List<OfferInfo> getMyBsqOffers(String direction) {
return getMyOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equals("BSQ"))
.collect(toList());
}
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
@ -315,6 +344,13 @@ public final class GrpcClient {
return sortOffersByDate(offers);
}
public List<OfferInfo> getMyBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyBsqOffers(BUY.name()));
offers.addAll(getMyBsqOffers(SELL.name()));
return sortOffersByDate(offers);
}
public OfferInfo getMostRecentOffer(String direction, String currencyCode) {
List<OfferInfo> offers = getOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? null : offers.get(offers.size() - 1);
@ -323,7 +359,7 @@ public final class GrpcClient {
public List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(Collectors.toList());
.collect(toList());
}
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
@ -340,7 +376,7 @@ public final class GrpcClient {
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getAvailabilityResultDescription());
throw new IllegalStateException(reply.getFailureReason().getDescription());
}
public TradeInfo getTrade(String tradeId) {
@ -404,6 +440,24 @@ public final class GrpcClient {
return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList();
}
public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode,
String address,
boolean tradeInstant) {
var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder()
.setAccountName(accountName)
.setCurrencyCode(currencyCode)
.setAddress(address)
.setTradeInstant(tradeInstant)
.build();
return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount();
}
public List<PaymentMethod> getCryptoPaymentMethods() {
var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList();
}
public void lockWallet() {
var request = LockWalletRequest.newBuilder().build();
grpcStubs.walletsService.lockWallet(request);

View File

@ -26,6 +26,7 @@ public enum Method {
confirmpaymentstarted,
createoffer,
createpaymentacct,
createcryptopaymentacct,
getaddressbalance,
getbalance,
getbtcprice,

View File

@ -36,7 +36,10 @@ import java.util.stream.Collectors;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.*;
import static bisq.cli.DirectionFormat.directionFormat;
import static bisq.cli.DirectionFormat.getLongestDirectionColWidth;
import static com.google.common.base.Strings.padEnd;
import static com.google.common.base.Strings.padStart;
import static java.lang.String.format;
import static java.util.Collections.max;
import static java.util.Comparator.comparing;
@ -114,61 +117,24 @@ public class TableFormat {
formatSatoshis(btcBalanceInfo.getLockedBalance()));
}
public 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
+ COL_HEADER_VOLUME + COL_HEADER_DELIMITER // includes %s -> fiatCurrency
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER
+ COL_HEADER_UUID.trim() + "%n";
String headerLine = format(headersFormat, fiatCurrency.toUpperCase(), fiatCurrency.toUpperCase());
String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" // left
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify to end of hdr
+ " %-" + (COL_HEADER_AMOUNT.length() - 1) + "s" // left justify
+ " %" + COL_HEADER_VOLUME.length() + "s" // right justify
+ " %-" + paymentMethodColWidth + "s" // left justify
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" // left justify
+ " %-" + COL_HEADER_UUID.length() + "s";
return headerLine
+ offerInfo.stream()
.map(o -> format(colDataFormat,
o.getDirection(),
formatOfferPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
public static String formatPaymentAcctTbl(List<PaymentAccount> paymentAccounts) {
// Some column values might be longer than header, so we need to calculate them.
int nameColWidth = getLengthOfLongestColumn(
int nameColWidth = getLongestColumnSize(
COL_HEADER_NAME.length(),
paymentAccounts.stream().map(PaymentAccount::getAccountName)
.collect(Collectors.toList()));
int paymentMethodColWidth = getLengthOfLongestColumn(
int paymentMethodColWidth = getLongestColumnSize(
COL_HEADER_PAYMENT_METHOD.length(),
paymentAccounts.stream().map(a -> a.getPaymentMethod().getId())
.collect(Collectors.toList()));
String headerLine = padEnd(COL_HEADER_NAME, nameColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CURRENCY + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_UUID + COL_HEADER_DELIMITER + "\n";
String colDataFormat = "%-" + nameColWidth + "s" // left justify
+ " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify
+ " %-" + paymentMethodColWidth + "s" // left justify
+ " %-" + COL_HEADER_UUID.length() + "s"; // left justify
String colDataFormat = "%-" + nameColWidth + "s" // left justify
+ " %-" + COL_HEADER_CURRENCY.length() + "s" // left justify
+ " %-" + paymentMethodColWidth + "s" // left justify
+ " %-" + COL_HEADER_UUID.length() + "s"; // left justify
return headerLine
+ paymentAccounts.stream()
.map(a -> format(colDataFormat,
@ -179,8 +145,126 @@ public class TableFormat {
.collect(Collectors.joining("\n"));
}
// Return length of the longest string value, or the header.len, whichever is greater.
private static int getLengthOfLongestColumn(int headerLength, List<String> strings) {
public static String formatOfferTable(List<OfferInfo> offers, String currencyCode) {
if (offers == null || offers.isEmpty())
throw new IllegalArgumentException(format("%s offers argument is empty", currencyCode.toLowerCase()));
String baseCurrencyCode = offers.get(0).getBaseCurrencyCode();
return baseCurrencyCode.equalsIgnoreCase("BTC")
? formatFiatOfferTable(offers, currencyCode)
: formatCryptoCurrencyOfferTable(offers, baseCurrencyCode);
}
private static String formatFiatOfferTable(List<OfferInfo> offers, String fiatCurrencyCode) {
// Some column values might be longer than header, so we need to calculate them.
int amountColWith = getLongestAmountColWidth(offers);
int volumeColWidth = getLongestVolumeColWidth(offers);
int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers);
String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrencyCode
+ padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER
// COL_HEADER_VOLUME includes %s -> fiatCurrencyCode
+ padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER
+ COL_HEADER_UUID.trim() + "%n";
String headerLine = format(headersFormat,
fiatCurrencyCode.toUpperCase(),
fiatCurrencyCode.toUpperCase());
String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s"
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s"
+ " %" + amountColWith + "s"
+ " %" + (volumeColWidth - 1) + "s"
+ " %-" + paymentMethodColWidth + "s"
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
+ " %-" + COL_HEADER_UUID.length() + "s";
return headerLine
+ offers.stream()
.map(o -> format(colDataFormat,
o.getDirection(),
formatPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
private static String formatCryptoCurrencyOfferTable(List<OfferInfo> offers, String cryptoCurrencyCode) {
// Some column values might be longer than header, so we need to calculate them.
int directionColWidth = getLongestDirectionColWidth(offers);
int amountColWith = getLongestAmountColWidth(offers);
int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers);
int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers);
// TODO use memoize function to avoid duplicate the formatting done above?
String headersFormat = padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode
+ padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER
// COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode
+ padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER
+ COL_HEADER_UUID.trim() + "%n";
String headerLine = format(headersFormat,
cryptoCurrencyCode.toUpperCase(),
cryptoCurrencyCode.toUpperCase());
String colDataFormat = "%-" + directionColWidth + "s"
+ "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s"
+ " %" + amountColWith + "s"
+ " %" + (volumeColWidth - 1) + "s"
+ " %-" + paymentMethodColWidth + "s"
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
+ " %-" + COL_HEADER_UUID.length() + "s";
return headerLine
+ offers.stream()
.map(o -> format(colDataFormat,
directionFormat.apply(o),
formatCryptoCurrencyPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
}
private static int getLongestPaymentMethodColWidth(List<OfferInfo> offers) {
return getLongestColumnSize(
COL_HEADER_PAYMENT_METHOD.length(),
offers.stream()
.map(OfferInfo::getPaymentMethodShortName)
.collect(Collectors.toList()));
}
private static int getLongestAmountColWidth(List<OfferInfo> offers) {
return getLongestColumnSize(
COL_HEADER_AMOUNT.length(),
offers.stream()
.map(o -> formatAmountRange(o.getMinAmount(), o.getAmount()))
.collect(Collectors.toList()));
}
private static int getLongestVolumeColWidth(List<OfferInfo> offers) {
// Pad this col width by 1 space.
return 1 + getLongestColumnSize(
COL_HEADER_VOLUME.length(),
offers.stream()
.map(o -> formatVolumeRange(o.getMinVolume(), o.getVolume()))
.collect(Collectors.toList()));
}
private static int getLongestCryptoCurrencyVolumeColWidth(List<OfferInfo> offers) {
// Pad this col width by 1 space.
return 1 + getLongestColumnSize(
COL_HEADER_VOLUME.length(),
offers.stream()
.map(o -> formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()))
.collect(Collectors.toList()));
}
// Return size of the longest string value, or the header.len, whichever is greater.
private static int getLongestColumnSize(int headerLength, List<String> strings) {
int longest = max(strings, comparing(String::length)).length();
return Math.max(longest, headerLength);
}

View File

@ -17,21 +17,27 @@
package bisq.cli;
import bisq.proto.grpc.ContractInfo;
import bisq.proto.grpc.TradeInfo;
import com.google.common.annotations.VisibleForTesting;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import static bisq.cli.ColumnHeaderConstants.*;
import static bisq.cli.CurrencyFormat.formatOfferPrice;
import static bisq.cli.CurrencyFormat.formatOfferVolume;
import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.cli.CurrencyFormat.*;
import static com.google.common.base.Strings.padEnd;
@VisibleForTesting
public class TradeFormat {
private static final String YES = "YES";
private static final String NO = "NO";
// TODO add String format(List<TradeInfo> trades)
@VisibleForTesting
public static String format(TradeInfo tradeInfo) {
// Some column values might be longer than header, so we need to calculate them.
@ -40,19 +46,31 @@ public class TradeFormat {
// We only show taker fee under its header when user is the taker.
boolean isTaker = tradeInfo.getRole().toLowerCase().contains("taker");
Supplier<String> takerFeeHeaderFormat = () -> isTaker ?
padEnd(COL_HEADER_TRADE_TAKER_FEE, 12, ' ') + COL_HEADER_DELIMITER
Supplier<String> makerFeeHeader = () -> !isTaker ?
COL_HEADER_TRADE_MAKER_FEE + COL_HEADER_DELIMITER
: "";
Supplier<String> makerFeeHeaderSpec = () -> !isTaker ?
"%" + (COL_HEADER_TRADE_MAKER_FEE.length() + 2) + "s"
: "";
Supplier<String> takerFeeHeader = () -> isTaker ?
"%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 1) + "s"
COL_HEADER_TRADE_TAKER_FEE + COL_HEADER_DELIMITER
: "";
Supplier<String> takerFeeHeaderSpec = () -> isTaker ?
"%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s"
: "";
boolean showBsqBuyerAddress = shouldShowBsqBuyerAddress(tradeInfo, isTaker);
Supplier<String> bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : "";
Supplier<String> bsqBuyerAddressHeaderSpec = () -> showBsqBuyerAddress ? "%s" : "";
String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> currencyCode
+ priceHeader.apply(tradeInfo) + COL_HEADER_DELIMITER // includes %s -> currencyCode
+ padEnd(COL_HEADER_TRADE_AMOUNT, 12, ' ') + COL_HEADER_DELIMITER
+ padEnd(COL_HEADER_TRADE_TX_FEE, 12, ' ') + COL_HEADER_DELIMITER
+ takerFeeHeaderFormat.get()
+ makerFeeHeader.get()
// maker or taker fee header, not both
+ takerFeeHeader.get()
+ COL_HEADER_TRADE_DEPOSIT_PUBLISHED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_DEPOSIT_CONFIRMED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_BUYER_COST + COL_HEADER_DELIMITER
@ -60,79 +78,144 @@ public class TradeFormat {
+ COL_HEADER_TRADE_PAYMENT_RECEIVED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER
+ COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER
+ bsqBuyerAddressHeader.get()
+ "%n";
String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode();
String baseCurrencyCode = tradeInfo.getOffer().getBaseCurrencyCode();
// The taker's output contains an extra taker tx fee column.
String headerLine = isTaker
? String.format(headersFormat,
/* COL_HEADER_PRICE */ counterCurrencyCode,
String headerLine = String.format(headersFormat,
/* COL_HEADER_PRICE */ priceHeaderCurrencyCode.apply(tradeInfo),
/* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode,
/* COL_HEADER_TRADE_TX_FEE */ baseCurrencyCode,
/* COL_HEADER_TRADE_TAKER_FEE */ baseCurrencyCode,
/* COL_HEADER_TRADE_(M||T)AKER_FEE */ makerTakerFeeHeaderCurrencyCode.apply(tradeInfo, isTaker),
/* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_SENT */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_RECEIVED */ counterCurrencyCode)
: String.format(headersFormat,
/* COL_HEADER_PRICE */ counterCurrencyCode,
/* COL_HEADER_TRADE_AMOUNT */ baseCurrencyCode,
/* COL_HEADER_TRADE_TX_FEE */ baseCurrencyCode,
/* COL_HEADER_TRADE_BUYER_COST */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_SENT */ counterCurrencyCode,
/* COL_HEADER_TRADE_PAYMENT_RECEIVED */ counterCurrencyCode);
/* COL_HEADER_TRADE_PAYMENT_SENT */ paymentStatusHeaderCurrencyCode.apply(tradeInfo),
/* COL_HEADER_TRADE_PAYMENT_RECEIVED */ paymentStatusHeaderCurrencyCode.apply(tradeInfo));
String colDataFormat = "%-" + shortIdColWidth + "s" // lt justify
+ " %-" + (roleColWidth + COL_HEADER_DELIMITER.length()) + "s" // left
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" // rt justify
+ "%" + (COL_HEADER_TRADE_AMOUNT.length() + 1) + "s" // rt justify
+ "%" + (COL_HEADER_TRADE_TX_FEE.length() + 1) + "s" // rt justify
+ takerFeeHeader.get() // rt justify
+ makerFeeHeaderSpec.get() // rt justify
// OR (one of them is an empty string)
+ takerFeeHeaderSpec.get() // rt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_DEPOSIT_CONFIRMED.length() + "s" // lt justify
+ "%" + (COL_HEADER_TRADE_BUYER_COST.length() + 1) + "s" // rt justify
+ " %-" + (COL_HEADER_TRADE_PAYMENT_SENT.length() - 1) + "s" // left
+ " %-" + (COL_HEADER_TRADE_PAYMENT_RECEIVED.length() - 1) + "s" // left
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // lt justify
+ " %-" + (COL_HEADER_TRADE_WITHDRAWN.length() + 2) + "s"
+ bsqBuyerAddressHeaderSpec.get();
return headerLine +
(isTaker
? formatTradeForTaker(colDataFormat, tradeInfo)
: formatTradeForMaker(colDataFormat, tradeInfo));
return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker, showBsqBuyerAddress);
}
private static String formatTradeForMaker(String format, TradeInfo tradeInfo) {
private static String formatTradeData(String format,
TradeInfo tradeInfo,
boolean isTaker,
boolean showBsqBuyerAddress) {
return String.format(format,
tradeInfo.getShortId(),
tradeInfo.getRole(),
formatOfferPrice(tradeInfo.getTradePrice()),
formatSatoshis(tradeInfo.getTradeAmountAsLong()),
formatSatoshis(tradeInfo.getTxFeeAsLong()),
tradeInfo.getIsDepositPublished() ? "YES" : "NO",
tradeInfo.getIsDepositConfirmed() ? "YES" : "NO",
formatOfferVolume(tradeInfo.getOffer().getVolume()),
tradeInfo.getIsFiatSent() ? "YES" : "NO",
tradeInfo.getIsFiatReceived() ? "YES" : "NO",
tradeInfo.getIsPayoutPublished() ? "YES" : "NO",
tradeInfo.getIsWithdrawn() ? "YES" : "NO");
priceFormat.apply(tradeInfo),
amountFormat.apply(tradeInfo),
makerTakerMinerTxFeeFormat.apply(tradeInfo, isTaker),
makerTakerFeeFormat.apply(tradeInfo, isTaker),
tradeInfo.getIsDepositPublished() ? YES : NO,
tradeInfo.getIsDepositConfirmed() ? YES : NO,
tradeCostFormat.apply(tradeInfo),
tradeInfo.getIsFiatSent() ? YES : NO,
tradeInfo.getIsFiatReceived() ? YES : NO,
tradeInfo.getIsPayoutPublished() ? YES : NO,
tradeInfo.getIsWithdrawn() ? YES : NO,
bsqReceiveAddress.apply(tradeInfo, showBsqBuyerAddress));
}
private static String formatTradeForTaker(String format, TradeInfo tradeInfo) {
return String.format(format,
tradeInfo.getShortId(),
tradeInfo.getRole(),
formatOfferPrice(tradeInfo.getTradePrice()),
formatSatoshis(tradeInfo.getTradeAmountAsLong()),
formatSatoshis(tradeInfo.getTxFeeAsLong()),
formatSatoshis(tradeInfo.getTakerFeeAsLong()),
tradeInfo.getIsDepositPublished() ? "YES" : "NO",
tradeInfo.getIsDepositConfirmed() ? "YES" : "NO",
formatOfferVolume(tradeInfo.getOffer().getVolume()),
tradeInfo.getIsFiatSent() ? "YES" : "NO",
tradeInfo.getIsFiatReceived() ? "YES" : "NO",
tradeInfo.getIsPayoutPublished() ? "YES" : "NO",
tradeInfo.getIsWithdrawn() ? "YES" : "NO");
private static final Function<TradeInfo, String> priceHeader = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? COL_HEADER_PRICE
: COL_HEADER_PRICE_OF_ALTCOIN;
private static final Function<TradeInfo, String> priceHeaderCurrencyCode = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
private static final BiFunction<TradeInfo, Boolean, String> makerTakerFeeHeaderCurrencyCode = (t, isTaker) -> {
if (isTaker) {
return t.getIsCurrencyForTakerFeeBtc() ? "BTC" : "BSQ";
} else {
return t.getOffer().getIsCurrencyForMakerFeeBtc() ? "BTC" : "BSQ";
}
};
private static final Function<TradeInfo, String> paymentStatusHeaderCurrencyCode = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? t.getOffer().getCounterCurrencyCode()
: t.getOffer().getBaseCurrencyCode();
private static final Function<TradeInfo, String> priceFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? formatPrice(t.getTradePrice())
: formatCryptoCurrencyPrice(t.getOffer().getPrice());
private static final Function<TradeInfo, String> amountFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? formatSatoshis(t.getTradeAmountAsLong())
: formatCryptoCurrencyOfferVolume(t.getOffer().getVolume());
private static final BiFunction<TradeInfo, Boolean, String> makerTakerMinerTxFeeFormat = (t, isTaker) -> {
if (isTaker) {
return formatSatoshis(t.getTxFeeAsLong());
} else {
return formatSatoshis(t.getOffer().getTxFee());
}
};
private static final BiFunction<TradeInfo, Boolean, String> makerTakerFeeFormat = (t, isTaker) -> {
if (isTaker) {
return t.getIsCurrencyForTakerFeeBtc()
? formatSatoshis(t.getTakerFeeAsLong())
: formatBsq(t.getTakerFeeAsLong());
} else {
return t.getOffer().getIsCurrencyForMakerFeeBtc()
? formatSatoshis(t.getOffer().getMakerFee())
: formatBsq(t.getOffer().getMakerFee());
}
};
private static final Function<TradeInfo, String> tradeCostFormat = (t) ->
t.getOffer().getBaseCurrencyCode().equals("BTC")
? formatOfferVolume(t.getOffer().getVolume())
: formatSatoshis(t.getTradeAmountAsLong());
private static final BiFunction<TradeInfo, Boolean, String> bsqReceiveAddress = (t, showBsqBuyerAddress) -> {
if (showBsqBuyerAddress) {
ContractInfo contract = t.getContract();
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
return isBuyerMakerAndSellerTaker // (is BTC buyer / maker)
? contract.getTakerPaymentAccountPayload().getAddress()
: contract.getMakerPaymentAccountPayload().getAddress();
} else {
return "";
}
};
private static boolean shouldShowBsqBuyerAddress(TradeInfo tradeInfo, boolean isTaker) {
if (tradeInfo.getOffer().getBaseCurrencyCode().equals("BTC")) {
return false;
} else {
ContractInfo contract = tradeInfo.getContract();
// Do not forget buyer and seller refer to BTC buyer and seller, not BSQ
// buyer and seller. If you are buying BSQ, you are the (BTC) seller.
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
if (isTaker) {
return !isBuyerMakerAndSellerTaker;
} else {
return isBuyerMakerAndSellerTaker;
}
}
}
}

View File

@ -113,6 +113,7 @@ public class ArgumentList {
return currentIndex < arguments.length;
}
@SuppressWarnings("UnusedReturnValue")
String next() {
return arguments[currentIndex++];
}

View File

@ -0,0 +1,85 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.cli.opts;
import joptsimple.OptionSpec;
import static bisq.cli.opts.OptLabel.OPT_ACCOUNT_NAME;
import static bisq.cli.opts.OptLabel.OPT_ADDRESS;
import static bisq.cli.opts.OptLabel.OPT_CURRENCY_CODE;
import static bisq.cli.opts.OptLabel.OPT_TRADE_INSTANT;
public class CreateCryptoCurrencyPaymentAcctOptionParser extends AbstractMethodOptionParser implements MethodOpts {
final OptionSpec<String> accountNameOpt = parser.accepts(OPT_ACCOUNT_NAME, "crypto currency account name")
.withRequiredArg();
final OptionSpec<String> currencyCodeOpt = parser.accepts(OPT_CURRENCY_CODE, "crypto currency code (bsq only)")
.withRequiredArg();
final OptionSpec<String> addressOpt = parser.accepts(OPT_ADDRESS, "bsq address")
.withRequiredArg();
final OptionSpec<Boolean> tradeInstantOpt = parser.accepts(OPT_TRADE_INSTANT, "create trade instant account")
.withOptionalArg()
.ofType(boolean.class)
.defaultsTo(Boolean.FALSE);
public CreateCryptoCurrencyPaymentAcctOptionParser(String[] args) {
super(args);
}
public CreateCryptoCurrencyPaymentAcctOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(accountNameOpt) || options.valueOf(accountNameOpt).isEmpty())
throw new IllegalArgumentException("no payment account name specified");
if (!options.has(currencyCodeOpt) || options.valueOf(currencyCodeOpt).isEmpty())
throw new IllegalArgumentException("no currency code specified");
if (!options.valueOf(currencyCodeOpt).equalsIgnoreCase("bsq"))
throw new IllegalArgumentException("api only supports bsq crypto currency payment accounts");
if (!options.has(addressOpt) || options.valueOf(addressOpt).isEmpty())
throw new IllegalArgumentException("no bsq address specified");
return this;
}
public String getAccountName() {
return options.valueOf(accountNameOpt);
}
public String getCurrencyCode() {
return options.valueOf(currencyCodeOpt);
}
public String getAddress() {
return options.valueOf(addressOpt);
}
public boolean getIsTradeInstant() {
return options.valueOf(tradeInstantOpt);
}
}

View File

@ -22,5 +22,4 @@ public interface MethodOpts {
MethodOpts parse();
boolean isForHelp();
}

View File

@ -21,6 +21,7 @@ package bisq.cli.opts;
* CLI opt label definitions.
*/
public class OptLabel {
public final static String OPT_ACCOUNT_NAME = "account-name";
public final static String OPT_ADDRESS = "address";
public final static String OPT_AMOUNT = "amount";
public final static String OPT_CURRENCY_CODE = "currency-code";
@ -43,6 +44,7 @@ public class OptLabel {
public final static String OPT_SECURITY_DEPOSIT = "security-deposit";
public final static String OPT_SHOW_CONTRACT = "show-contract";
public final static String OPT_TRADE_ID = "trade-id";
public final static String OPT_TRADE_INSTANT = "trade-instant";
public final static String OPT_TIMEOUT = "timeout";
public final static String OPT_TRANSACTION_ID = "transaction-id";
public final static String OPT_TX_FEE_RATE = "tx-fee-rate";

View File

@ -3,6 +3,7 @@ package bisq.cli.opt;
import org.junit.jupiter.api.Test;
import static bisq.cli.Method.canceloffer;
import static bisq.cli.Method.createcryptopaymentacct;
import static bisq.cli.Method.createoffer;
import static bisq.cli.Method.createpaymentacct;
import static bisq.cli.opts.OptLabel.*;
@ -12,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import bisq.cli.opts.CancelOfferOptionParser;
import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser;
import bisq.cli.opts.CreateOfferOptionParser;
import bisq.cli.opts.CreatePaymentAcctOptionParser;
@ -20,7 +22,7 @@ public class OptionParsersTest {
private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz";
// CancelOffer opt parsing tests
// canceloffer opt parser tests
@Test
public void testCancelOfferWithMissingOfferIdOptShouldThrowException() {
@ -67,7 +69,7 @@ public class OptionParsersTest {
new CancelOfferOptionParser(args).parse();
}
// CreateOffer opt parsing tests
// createoffer opt parser tests
@Test
public void testCreateOfferOptParserWithMissingPaymentAccountIdOptShouldThrowException() {
@ -139,7 +141,7 @@ public class OptionParsersTest {
assertEquals("25.0", parser.getSecurityDeposit());
}
// CreatePaymentAcct opt parser tests
// createpaymentacct opt parser tests
@Test
public void testCreatePaymentAcctOptParserWithMissingPaymentFormOptShouldThrowException() {
@ -177,4 +179,85 @@ public class OptionParsersTest {
assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found",
exception.getMessage());
}
// createcryptopaymentacct parser tests
@Test
public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAcctNameOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createcryptopaymentacct.name()
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse());
assertEquals("no payment account name specified", exception.getMessage());
}
@Test
public void testCreateCryptoCurrencyPaymentAcctOptionParserWithEmptyAcctNameOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createcryptopaymentacct.name(),
"--" + OPT_ACCOUNT_NAME
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse());
assertEquals("account-name requires an argument", exception.getMessage());
}
@Test
public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingCurrencyCodeOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createcryptopaymentacct.name(),
"--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse());
assertEquals("no currency code specified", exception.getMessage());
}
@Test
public void testCreateCryptoCurrencyPaymentAcctOptionParserWithInvalidCurrencyCodeOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createcryptopaymentacct.name(),
"--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account",
"--" + OPT_CURRENCY_CODE + "=" + "xmr"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse());
assertEquals("api only supports bsq crypto currency payment accounts", exception.getMessage());
}
@Test
public void testCreateCryptoCurrencyPaymentAcctOptionParserWithMissingAddressOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
createcryptopaymentacct.name(),
"--" + OPT_ACCOUNT_NAME + "=" + "bsq payment account",
"--" + OPT_CURRENCY_CODE + "=" + "bsq"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse());
assertEquals("no bsq address specified", exception.getMessage());
}
@Test
public void testCreateCryptoCurrencyPaymentAcctOptionParser() {
var acctName = "bsq payment account";
var currencyCode = "bsq";
var address = "B1nXyZ"; // address is validated on server
String[] args = new String[]{
PASSWORD_OPT,
createcryptopaymentacct.name(),
"--" + OPT_ACCOUNT_NAME + "=" + acctName,
"--" + OPT_CURRENCY_CODE + "=" + currencyCode,
"--" + OPT_ADDRESS + "=" + address
};
var parser = new CreateCryptoCurrencyPaymentAcctOptionParser(args).parse();
assertEquals(acctName, parser.getAccountName());
assertEquals(currencyCode, parser.getCurrencyCode());
assertEquals(address, parser.getAddress());
}
}

View File

@ -210,6 +210,20 @@ public class CoreApi {
return paymentAccountsService.getPaymentAccountFormAsString(paymentMethodId);
}
public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode,
String address,
boolean tradeInstant) {
return paymentAccountsService.createCryptoCurrencyPaymentAccount(accountName,
currencyCode,
address,
tradeInstant);
}
public List<PaymentMethod> getCryptoCurrencyPaymentMethods() {
return paymentAccountsService.getCryptoCurrencyPaymentMethods();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Prices
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -19,7 +19,11 @@ package bisq.core.api;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.api.model.PaymentAccountForm;
import bisq.core.locale.CryptoCurrency;
import bisq.core.payment.CryptoCurrencyAccount;
import bisq.core.payment.InstantCryptoCurrencyAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountFactory;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.user.User;
@ -30,30 +34,37 @@ import java.io.File;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CurrencyUtil.getCryptoCurrency;
import static java.lang.String.format;
@Singleton
@Slf4j
class CorePaymentAccountsService {
private final CoreWalletsService coreWalletsService;
private final AccountAgeWitnessService accountAgeWitnessService;
private final PaymentAccountForm paymentAccountForm;
private final User user;
@Inject
public CorePaymentAccountsService(AccountAgeWitnessService accountAgeWitnessService,
public CorePaymentAccountsService(CoreWalletsService coreWalletsService,
AccountAgeWitnessService accountAgeWitnessService,
PaymentAccountForm paymentAccountForm,
User user) {
this.coreWalletsService = coreWalletsService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.paymentAccountForm = paymentAccountForm;
this.user = user;
}
// Fiat Currency Accounts
PaymentAccount createPaymentAccount(String jsonString) {
PaymentAccount paymentAccount = paymentAccountForm.toPaymentAccount(jsonString);
verifyPaymentAccountHasRequiredFields(paymentAccount);
@ -86,6 +97,46 @@ class CorePaymentAccountsService {
return paymentAccountForm.getPaymentAccountForm(paymentMethodId);
}
// Crypto Currency Accounts
PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode,
String address,
boolean tradeInstant) {
String bsqCode = currencyCode.toUpperCase();
if (!bsqCode.equals("BSQ"))
throw new IllegalArgumentException("api does not currently support " + currencyCode + " accounts");
// Validate the BSQ address string but ignore the return value.
coreWalletsService.getValidBsqLegacyAddress(address);
var cryptoCurrencyAccount = tradeInstant
? (InstantCryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS_INSTANT)
: (CryptoCurrencyAccount) PaymentAccountFactory.getPaymentAccount(PaymentMethod.BLOCK_CHAINS);
cryptoCurrencyAccount.init();
cryptoCurrencyAccount.setAccountName(accountName);
cryptoCurrencyAccount.setAddress(address);
Optional<CryptoCurrency> cryptoCurrency = getCryptoCurrency(bsqCode);
cryptoCurrency.ifPresent(cryptoCurrencyAccount::setSingleTradeCurrency);
user.addPaymentAccount(cryptoCurrencyAccount);
accountAgeWitnessService.publishMyAccountAgeWitness(cryptoCurrencyAccount.getPaymentAccountPayload());
log.info("Saved crypto payment account with id {} and payment method {}.",
cryptoCurrencyAccount.getId(),
cryptoCurrencyAccount.getPaymentAccountPayload().getPaymentMethodId());
return cryptoCurrencyAccount;
}
// TODO Support all alt coin payment methods supported by UI.
// The getCryptoCurrencyPaymentMethods method below will be
// callable from the CLI when more are supported.
List<PaymentMethod> getCryptoCurrencyPaymentMethods() {
return PaymentMethod.getPaymentMethods().stream()
.filter(PaymentMethod::isAsset)
.sorted(Comparator.comparing(PaymentMethod::getId))
.collect(Collectors.toList());
}
private void verifyPaymentAccountHasRequiredFields(PaymentAccount paymentAccount) {
// Do checks here to make sure required fields are populated.
if (paymentAccount.isTransferwiseAccount() && paymentAccount.getTradeCurrencies().isEmpty())

View File

@ -52,8 +52,10 @@ import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import javax.inject.Inject;
@ -75,6 +77,7 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@ -145,6 +148,10 @@ class CoreWalletsService {
return tempAesKey;
}
NetworkParameters getNetworkParameters() {
return btcWalletService.getWallet().getContext().getParams();
}
BalancesInfo getBalances(String currencyCode) {
verifyWalletCurrencyCodeIsValid(currencyCode);
verifyWalletsAreAvailable();
@ -305,6 +312,41 @@ class CoreWalletsService {
}
}
boolean verifyBsqSentToAddress(String address, String amount) {
Address receiverAddress = getValidBsqLegacyAddress(address);
NetworkParameters networkParameters = getNetworkParameters();
Predicate<TransactionOutput> isTxOutputAddressMatch = (txOut) ->
txOut.getScriptPubKey().getToAddress(networkParameters).equals(receiverAddress);
Coin coinValue = parseToCoin(amount, bsqFormatter);
Predicate<TransactionOutput> isTxOutputValueMatch = (txOut) ->
txOut.getValue().longValue() == coinValue.longValue();
List<TransactionOutput> spendableBsqTxOutputs = bsqWalletService.getSpendableBsqTransactionOutputs();
log.info("Searching {} spendable tx outputs for matching address {} and value {}:",
spendableBsqTxOutputs.size(),
address,
coinValue.toPlainString());
long numMatches = 0;
for (TransactionOutput txOut : spendableBsqTxOutputs) {
if (isTxOutputAddressMatch.test(txOut) && isTxOutputValueMatch.test(txOut)) {
log.info("\t\tTx {} output has matching address {} and value {}.",
txOut.getParentTransaction().getTxId(),
address,
txOut.getValue().toPlainString());
numMatches++;
}
}
if (numMatches > 1) {
log.warn("{} tx outputs matched address {} and value {}, could be a"
+ " false positive BSQ payment verification result.",
numMatches,
address,
coinValue.toPlainString());
}
return numMatches > 0;
}
void getTxFeeRate(ResultHandler resultHandler) {
try {
@SuppressWarnings({"unchecked", "Convert2MethodRef"})
@ -511,6 +553,16 @@ class CoreWalletsService {
throw new IllegalStateException("server is not fully initialized");
}
// Returns a LegacyAddress for the string, or a RuntimeException if invalid.
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));
}
}
// Throws a RuntimeException if wallet currency code is not BSQ or BTC.
private void verifyWalletCurrencyCodeIsValid(String currencyCode) {
if (currencyCode == null || currencyCode.isEmpty())
@ -575,16 +627,6 @@ class CoreWalletsService {
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 transfer amount string, or a RuntimeException if invalid.
private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) {
Coin amountAsCoin = parseToCoin(amount, coinFormatter);

View File

@ -0,0 +1,126 @@
/*
* 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 java.util.function.Supplier;
import lombok.Getter;
import static bisq.core.api.model.PaymentAccountPayloadInfo.emptyPaymentAccountPayload;
/**
* A lightweight Trade Contract constructed from a trade's json contract.
* Many fields in the core Contract are ignored, but can be added as needed.
*/
@Getter
public class ContractInfo implements Payload {
private final String buyerNodeAddress;
private final String sellerNodeAddress;
private final String mediatorNodeAddress;
private final String refundAgentNodeAddress;
private final boolean isBuyerMakerAndSellerTaker;
private final String makerAccountId;
private final String takerAccountId;
private final PaymentAccountPayloadInfo makerPaymentAccountPayload;
private final PaymentAccountPayloadInfo takerPaymentAccountPayload;
private final String makerPayoutAddressString;
private final String takerPayoutAddressString;
private final long lockTime;
public ContractInfo(String buyerNodeAddress,
String sellerNodeAddress,
String mediatorNodeAddress,
String refundAgentNodeAddress,
boolean isBuyerMakerAndSellerTaker,
String makerAccountId,
String takerAccountId,
PaymentAccountPayloadInfo makerPaymentAccountPayload,
PaymentAccountPayloadInfo takerPaymentAccountPayload,
String makerPayoutAddressString,
String takerPayoutAddressString,
long lockTime) {
this.buyerNodeAddress = buyerNodeAddress;
this.sellerNodeAddress = sellerNodeAddress;
this.mediatorNodeAddress = mediatorNodeAddress;
this.refundAgentNodeAddress = refundAgentNodeAddress;
this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker;
this.makerAccountId = makerAccountId;
this.takerAccountId = takerAccountId;
this.makerPaymentAccountPayload = makerPaymentAccountPayload;
this.takerPaymentAccountPayload = takerPaymentAccountPayload;
this.makerPayoutAddressString = makerPayoutAddressString;
this.takerPayoutAddressString = takerPayoutAddressString;
this.lockTime = lockTime;
}
// For transmitting TradeInfo messages when no contract is available.
public static Supplier<ContractInfo> emptyContract = () ->
new ContractInfo("",
"",
"",
"",
false,
"",
"",
emptyPaymentAccountPayload.get(),
emptyPaymentAccountPayload.get(),
"",
"",
0);
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
public static ContractInfo fromProto(bisq.proto.grpc.ContractInfo proto) {
return new ContractInfo(proto.getBuyerNodeAddress(),
proto.getSellerNodeAddress(),
proto.getMediatorNodeAddress(),
proto.getRefundAgentNodeAddress(),
proto.getIsBuyerMakerAndSellerTaker(),
proto.getMakerAccountId(),
proto.getTakerAccountId(),
PaymentAccountPayloadInfo.fromProto(proto.getMakerPaymentAccountPayload()),
PaymentAccountPayloadInfo.fromProto(proto.getTakerPaymentAccountPayload()),
proto.getMakerPayoutAddressString(),
proto.getTakerPayoutAddressString(),
proto.getLockTime());
}
@Override
public bisq.proto.grpc.ContractInfo toProtoMessage() {
return bisq.proto.grpc.ContractInfo.newBuilder()
.setBuyerNodeAddress(buyerNodeAddress)
.setSellerNodeAddress(sellerNodeAddress)
.setMediatorNodeAddress(mediatorNodeAddress)
.setRefundAgentNodeAddress(refundAgentNodeAddress)
.setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker)
.setMakerAccountId(makerAccountId)
.setTakerAccountId(takerAccountId)
.setMakerPaymentAccountPayload(makerPaymentAccountPayload.toProtoMessage())
.setTakerPaymentAccountPayload(takerPaymentAccountPayload.toProtoMessage())
.setMakerPayoutAddressString(makerPayoutAddressString)
.setTakerPayoutAddressString(takerPayoutAddressString)
.setLockTime(lockTime)
.build();
}
}

View File

@ -45,7 +45,11 @@ public class OfferInfo implements Payload {
private final long minAmount;
private final long volume;
private final long minVolume;
private final long txFee;
private final long makerFee;
private final String offerFeePaymentTxId;
private final long buyerSecurityDeposit;
private final long sellerSecurityDeposit;
private final long triggerPrice;
private final boolean isCurrencyForMakerFeeBtc;
private final String paymentAccountId;
@ -58,6 +62,7 @@ public class OfferInfo implements Payload {
private final long date;
private final String state;
public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.id;
this.direction = builder.direction;
@ -68,7 +73,11 @@ public class OfferInfo implements Payload {
this.minAmount = builder.minAmount;
this.volume = builder.volume;
this.minVolume = builder.minVolume;
this.txFee = builder.txFee;
this.makerFee = builder.makerFee;
this.offerFeePaymentTxId = builder.offerFeePaymentTxId;
this.buyerSecurityDeposit = builder.buyerSecurityDeposit;
this.sellerSecurityDeposit = builder.sellerSecurityDeposit;
this.triggerPrice = builder.triggerPrice;
this.isCurrencyForMakerFeeBtc = builder.isCurrencyForMakerFeeBtc;
this.paymentAccountId = builder.paymentAccountId;
@ -78,6 +87,7 @@ public class OfferInfo implements Payload {
this.counterCurrencyCode = builder.counterCurrencyCode;
this.date = builder.date;
this.state = builder.state;
}
public static OfferInfo toOfferInfo(Offer offer) {
@ -90,8 +100,8 @@ public class OfferInfo implements Payload {
return getOfferInfoBuilder(offer).withTriggerPrice(triggerPrice).build();
}
private static OfferInfo.OfferInfoBuilder getOfferInfoBuilder(Offer offer) {
return new OfferInfo.OfferInfoBuilder()
private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) {
return new OfferInfoBuilder()
.withId(offer.getId())
.withDirection(offer.getDirection().name())
.withPrice(Objects.requireNonNull(offer.getPrice()).getValue())
@ -101,7 +111,11 @@ public class OfferInfo implements Payload {
.withMinAmount(offer.getMinAmount().value)
.withVolume(Objects.requireNonNull(offer.getVolume()).getValue())
.withMinVolume(Objects.requireNonNull(offer.getMinVolume()).getValue())
.withMakerFee(offer.getMakerFee().value)
.withTxFee(offer.getTxFee().value)
.withOfferFeePaymentTxId(offer.getOfferFeePaymentTxId())
.withBuyerSecurityDeposit(offer.getBuyerSecurityDeposit().value)
.withSellerSecurityDeposit(offer.getSellerSecurityDeposit().value)
.withIsCurrencyForMakerFeeBtc(offer.isCurrencyForMakerFeeBtc())
.withPaymentAccountId(offer.getMakerPaymentAccountId())
.withPaymentMethodId(offer.getPaymentMethod().getId())
@ -128,7 +142,11 @@ public class OfferInfo implements Payload {
.setMinAmount(minAmount)
.setVolume(volume)
.setMinVolume(minVolume)
.setMakerFee(makerFee)
.setTxFee(txFee)
.setOfferFeePaymentTxId(offerFeePaymentTxId)
.setBuyerSecurityDeposit(buyerSecurityDeposit)
.setSellerSecurityDeposit(sellerSecurityDeposit)
.setTriggerPrice(triggerPrice)
.setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc)
.setPaymentAccountId(paymentAccountId)
@ -143,7 +161,7 @@ public class OfferInfo implements Payload {
@SuppressWarnings("unused")
public static OfferInfo fromProto(bisq.proto.grpc.OfferInfo proto) {
return new OfferInfo.OfferInfoBuilder()
return new OfferInfoBuilder()
.withId(proto.getId())
.withDirection(proto.getDirection())
.withPrice(proto.getPrice())
@ -153,7 +171,11 @@ public class OfferInfo implements Payload {
.withMinAmount(proto.getMinAmount())
.withVolume(proto.getVolume())
.withMinVolume(proto.getMinVolume())
.withMakerFee(proto.getMakerFee())
.withTxFee(proto.getTxFee())
.withOfferFeePaymentTxId(proto.getOfferFeePaymentTxId())
.withBuyerSecurityDeposit(proto.getBuyerSecurityDeposit())
.withSellerSecurityDeposit(proto.getSellerSecurityDeposit())
.withTriggerPrice(proto.getTriggerPrice())
.withIsCurrencyForMakerFeeBtc(proto.getIsCurrencyForMakerFeeBtc())
.withPaymentAccountId(proto.getPaymentAccountId())
@ -182,7 +204,11 @@ public class OfferInfo implements Payload {
private long minAmount;
private long volume;
private long minVolume;
private long txFee;
private long makerFee;
private String offerFeePaymentTxId;
private long buyerSecurityDeposit;
private long sellerSecurityDeposit;
private long triggerPrice;
private boolean isCurrencyForMakerFeeBtc;
private String paymentAccountId;
@ -238,11 +264,31 @@ public class OfferInfo implements Payload {
return this;
}
public OfferInfoBuilder withTxFee(long txFee) {
this.txFee = txFee;
return this;
}
public OfferInfoBuilder withMakerFee(long makerFee) {
this.makerFee = makerFee;
return this;
}
public OfferInfoBuilder withOfferFeePaymentTxId(String offerFeePaymentTxId) {
this.offerFeePaymentTxId = offerFeePaymentTxId;
return this;
}
public OfferInfoBuilder withBuyerSecurityDeposit(long buyerSecurityDeposit) {
this.buyerSecurityDeposit = buyerSecurityDeposit;
return this;
}
public OfferInfoBuilder withSellerSecurityDeposit(long sellerSecurityDeposit) {
this.sellerSecurityDeposit = sellerSecurityDeposit;
return this;
}
public OfferInfoBuilder withTriggerPrice(long triggerPrice) {
this.triggerPrice = triggerPrice;
return this;

View File

@ -57,7 +57,7 @@ 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}
* and de-serialize edited json files into {@link PaymentAccount}
* instances.
* </p>
* <p>
@ -66,8 +66,8 @@ import static java.nio.charset.StandardCharsets.UTF_8;
* </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
* (1) Ask for a hal cash account form: Pass a {@link PaymentMethod#HAL_CASH_ID}
* to {@link PaymentAccountForm#getPaymentAccountForm(String)} to
* get the json Hal Cash payment account form:
* <pre>
* {
@ -98,8 +98,8 @@ import static java.nio.charset.StandardCharsets.UTF_8;
* </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)}
* {@link PaymentAccountForm#toPaymentAccount(File)}, or
* a json string to {@link PaymentAccountForm#toPaymentAccount(String)}
* and get a {@link bisq.core.payment.HalCashAccount} instance.
* <pre>
* PaymentAccount(

View File

@ -0,0 +1,81 @@
/*
* 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.payload.CryptoCurrencyAccountPayload;
import bisq.core.payment.payload.InstantCryptoCurrencyPayload;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.common.Payload;
import java.util.Optional;
import java.util.function.Supplier;
import lombok.Getter;
import javax.annotation.Nullable;
@Getter
public class PaymentAccountPayloadInfo implements Payload {
private final String id;
private final String paymentMethodId;
@Nullable
private final String address;
public PaymentAccountPayloadInfo(String id,
String paymentMethodId,
@Nullable String address) {
this.id = id;
this.paymentMethodId = paymentMethodId;
this.address = address;
}
public static PaymentAccountPayloadInfo toPaymentAccountPayloadInfo(PaymentAccountPayload paymentAccountPayload) {
Optional<String> address = Optional.empty();
if (paymentAccountPayload instanceof CryptoCurrencyAccountPayload)
address = Optional.of(((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress());
else if (paymentAccountPayload instanceof InstantCryptoCurrencyPayload)
address = Optional.of(((InstantCryptoCurrencyPayload) paymentAccountPayload).getAddress());
return new PaymentAccountPayloadInfo(paymentAccountPayload.getId(),
paymentAccountPayload.getPaymentMethodId(),
address.orElse(""));
}
// For transmitting TradeInfo messages when no contract & payloads are available.
public static Supplier<PaymentAccountPayloadInfo> emptyPaymentAccountPayload = () ->
new PaymentAccountPayloadInfo("", "", "");
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
public static PaymentAccountPayloadInfo fromProto(bisq.proto.grpc.PaymentAccountPayloadInfo proto) {
return new PaymentAccountPayloadInfo(proto.getId(), proto.getPaymentMethodId(), proto.getAddress());
}
@Override
public bisq.proto.grpc.PaymentAccountPayloadInfo toProtoMessage() {
return bisq.proto.grpc.PaymentAccountPayloadInfo.newBuilder()
.setId(id)
.setPaymentMethodId(paymentMethodId)
.setAddress(address != null ? address : "")
.build();
}
}

View File

@ -17,6 +17,7 @@
package bisq.core.api.model;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.common.Payload;
@ -27,6 +28,7 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import static bisq.core.api.model.OfferInfo.toOfferInfo;
import static bisq.core.api.model.PaymentAccountPayloadInfo.toPaymentAccountPayloadInfo;
@EqualsAndHashCode
@Getter
@ -60,6 +62,7 @@ public class TradeInfo implements Payload {
private final boolean isPayoutPublished;
private final boolean isWithdrawn;
private final String contractAsJson;
private final ContractInfo contract;
public TradeInfo(TradeInfoBuilder builder) {
this.offer = builder.offer;
@ -86,6 +89,7 @@ public class TradeInfo implements Payload {
this.isPayoutPublished = builder.isPayoutPublished;
this.isWithdrawn = builder.isWithdrawn;
this.contractAsJson = builder.contractAsJson;
this.contract = builder.contract;
}
public static TradeInfo toTradeInfo(Trade trade) {
@ -93,7 +97,26 @@ public class TradeInfo implements Payload {
}
public static TradeInfo toTradeInfo(Trade trade, String role) {
return new TradeInfo.TradeInfoBuilder()
ContractInfo contractInfo;
if (trade.getContract() != null) {
Contract contract = trade.getContract();
contractInfo = new ContractInfo(contract.getBuyerPayoutAddressString(),
contract.getSellerPayoutAddressString(),
contract.getMediatorNodeAddress().getFullAddress(),
contract.getRefundAgentNodeAddress().getFullAddress(),
contract.isBuyerMakerAndSellerTaker(),
contract.getMakerAccountId(),
contract.getTakerAccountId(),
toPaymentAccountPayloadInfo(contract.getMakerPaymentAccountPayload()),
toPaymentAccountPayloadInfo(contract.getTakerPaymentAccountPayload()),
contract.getMakerPayoutAddressString(),
contract.getTakerPayoutAddressString(),
contract.getLockTime());
} else {
contractInfo = ContractInfo.emptyContract.get();
}
return new TradeInfoBuilder()
.withOffer(toOfferInfo(trade.getOffer()))
.withTradeId(trade.getId())
.withShortId(trade.getShortId())
@ -120,6 +143,7 @@ public class TradeInfo implements Payload {
.withIsPayoutPublished(trade.isPayoutPublished())
.withIsWithdrawn(trade.isWithdrawn())
.withContractAsJson(trade.getContractAsJson())
.withContract(contractInfo)
.build();
}
@ -154,12 +178,38 @@ public class TradeInfo implements Payload {
.setIsPayoutPublished(isPayoutPublished)
.setIsWithdrawn(isWithdrawn)
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
.setContract(contract.toProtoMessage())
.build();
}
@SuppressWarnings({"unused", "SameReturnValue"})
public static TradeInfo fromProto(bisq.proto.grpc.TradeInfo proto) {
return null; // TODO
return new TradeInfoBuilder()
.withOffer(OfferInfo.fromProto(proto.getOffer()))
.withTradeId(proto.getTradeId())
.withShortId(proto.getShortId())
.withDate(proto.getDate())
.withRole(proto.getRole())
.withIsCurrencyForTakerFeeBtc(proto.getIsCurrencyForTakerFeeBtc())
.withTxFeeAsLong(proto.getTxFeeAsLong())
.withTakerFeeAsLong(proto.getTakerFeeAsLong())
.withTakerFeeTxId(proto.getTakerFeeTxId())
.withDepositTxId(proto.getDepositTxId())
.withPayoutTxId(proto.getPayoutTxId())
.withTradeAmountAsLong(proto.getTradeAmountAsLong())
.withTradePrice(proto.getTradePrice())
.withTradePeriodState(proto.getTradePeriodState())
.withState(proto.getState())
.withPhase(proto.getPhase())
.withTradingPeerNodeAddress(proto.getTradingPeerNodeAddress())
.withIsDepositPublished(proto.getIsDepositPublished())
.withIsDepositConfirmed(proto.getIsDepositConfirmed())
.withIsFiatSent(proto.getIsFiatSent())
.withIsFiatReceived(proto.getIsFiatReceived())
.withIsPayoutPublished(proto.getIsPayoutPublished())
.withIsWithdrawn(proto.getIsWithdrawn())
.withContractAsJson(proto.getContractAsJson())
.withContract((ContractInfo.fromProto(proto.getContract())))
.build();
}
/*
@ -193,6 +243,7 @@ public class TradeInfo implements Payload {
private boolean isPayoutPublished;
private boolean isWithdrawn;
private String contractAsJson;
private ContractInfo contract;
public TradeInfoBuilder withOffer(OfferInfo offer) {
this.offer = offer;
@ -314,6 +365,11 @@ public class TradeInfo implements Payload {
return this;
}
public TradeInfoBuilder withContract(ContractInfo contract) {
this.contract = contract;
return this;
}
public TradeInfo build() {
return new TradeInfo(this);
}
@ -346,6 +402,7 @@ public class TradeInfo implements Payload {
", isWithdrawn=" + isWithdrawn + "\n" +
", offer=" + offer + "\n" +
", contractAsJson=" + contractAsJson + "\n" +
", contract=" + contract + "\n" +
'}';
}
}

View File

@ -46,7 +46,7 @@ public class TxInfo implements Payload {
private final boolean isPending;
private final String memo;
public TxInfo(TxInfo.TxInfoBuilder builder) {
public TxInfo(TxInfoBuilder builder) {
this.txId = builder.txId;
this.inputSum = builder.inputSum;
this.outputSum = builder.outputSum;
@ -61,7 +61,7 @@ public class TxInfo implements Payload {
throw new IllegalStateException("server created a null transaction");
if (transaction.getFee() != null)
return new TxInfo.TxInfoBuilder()
return new TxInfoBuilder()
.withTxId(transaction.getTxId().toString())
.withInputSum(transaction.getInputSum().value)
.withOutputSum(transaction.getOutputSum().value)
@ -71,7 +71,7 @@ public class TxInfo implements Payload {
.withMemo(transaction.getMemo())
.build();
else
return new TxInfo.TxInfoBuilder()
return new TxInfoBuilder()
.withTxId(transaction.getTxId().toString())
.withInputSum(transaction.getInputSum().value)
.withOutputSum(transaction.getOutputSum().value)
@ -101,7 +101,7 @@ public class TxInfo implements Payload {
@SuppressWarnings("unused")
public static TxInfo fromProto(bisq.proto.grpc.TxInfo proto) {
return new TxInfo.TxInfoBuilder()
return new TxInfoBuilder()
.withTxId(proto.getTxId())
.withInputSum(proto.getInputSum())
.withOutputSum(proto.getOutputSum())
@ -121,37 +121,37 @@ public class TxInfo implements Payload {
private boolean isPending;
private String memo;
public TxInfo.TxInfoBuilder withTxId(String txId) {
public TxInfoBuilder withTxId(String txId) {
this.txId = txId;
return this;
}
public TxInfo.TxInfoBuilder withInputSum(long inputSum) {
public TxInfoBuilder withInputSum(long inputSum) {
this.inputSum = inputSum;
return this;
}
public TxInfo.TxInfoBuilder withOutputSum(long outputSum) {
public TxInfoBuilder withOutputSum(long outputSum) {
this.outputSum = outputSum;
return this;
}
public TxInfo.TxInfoBuilder withFee(long fee) {
public TxInfoBuilder withFee(long fee) {
this.fee = fee;
return this;
}
public TxInfo.TxInfoBuilder withSize(int size) {
public TxInfoBuilder withSize(int size) {
this.size = size;
return this;
}
public TxInfo.TxInfoBuilder withIsPending(boolean isPending) {
public TxInfoBuilder withIsPending(boolean isPending) {
this.isPending = isPending;
return this;
}
public TxInfo.TxInfoBuilder withMemo(String memo) {
public TxInfoBuilder withMemo(String memo) {
this.memo = memo;
return this;
}

View File

@ -0,0 +1,47 @@
createcryptopaymentacct
NAME
----
createcryptopaymentacct - create a cryptocurrency payment account
SYNOPSIS
--------
createcryptopaymentacct
--account-name=<account-name>
--currency-code=<bsq>
--address=<unused-bsq-address>
[--trade-instant=<true|false default=false>]
DESCRIPTION
-----------
Create an cryptocurrency (altcoin) payment account. Only BSQ payment accounts are currently supported.
OPTIONS
-------
--account-name
The name of the cryptocurrency payment account used to create and take altcoin offers.
--currency-code
The three letter code for the altcoin, e.g., BSQ.
--address
The altcoin address to be used receive cryptocurrency payment when selling BTC.
--trade-instant
True for creating an instant cryptocurrency payment account, false otherwise.
Default is false.
EXAMPLES
--------
To create a BSQ Altcoin payment account:
$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My BSQ Account" \
--currency-code=bsq \
--address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \
--trade-instant=false
To create a BSQ Instant Altcoin payment account:
$ ./bisq-cli --password=xyz --port=9998 createcryptopaymentacct --account-name="My Instant BSQ Account" \
--currency-code=bsq \
--address=Bn3PCQgRwhkrGnaMp1RYwt9tFwL51YELqne \
--trade-instant=true

View File

@ -19,6 +19,7 @@ package bisq.daemon.grpc;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.proto.grpc.AvailabilityResultWithDescription;
import bisq.proto.grpc.TakeOfferReply;
import protobuf.AvailabilityResult;
@ -77,7 +78,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
this.isErrorHandled = true;
log.error(errorMessage);
if (isTakeOfferError()) {
if (takeOfferWasCalled()) {
handleTakeOfferError(errorMessage);
} else {
exceptionHandler.handleErrorMessage(log,
@ -88,14 +89,20 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
}
private void handleTakeOfferError(String errorMessage) {
// Send the AvailabilityResult to the client instead of throwing an exception.
// The client should look at the grpc reply object's AvailabilityResult
// field if reply.hasTrade = false, and use it give the user a human readable msg.
// If the errorMessage originated from a UI purposed TaskRunner, it should
// contain an AvailabilityResult enum name. If it does, derive the
// AvailabilityResult enum from the errorMessage, wrap it in a new
// AvailabilityResultWithDescription enum, then send the
// AvailabilityResultWithDescription to the client instead of throwing
// an exception. The client should use the grpc reply object's
// AvailabilityResultWithDescription field if reply.hasTrade = false, and the
// client can decide to throw an exception with the client friendly error
// description, or take some other action based on the AvailabilityResult enum.
// (Some offer availability problems are not fatal, and retries are appropriate.)
try {
AvailabilityResult availabilityResultProto = getAvailabilityResult(errorMessage);
var failureReason = getAvailabilityResultWithDescription(errorMessage);
var reply = TakeOfferReply.newBuilder()
.setAvailabilityResult(availabilityResultProto)
.setAvailabilityResultDescription(getAvailabilityResultDescription(availabilityResultProto))
.setFailureReason(failureReason)
.build();
@SuppressWarnings("unchecked")
var takeOfferResponseObserver = (StreamObserver<TakeOfferReply>) responseObserver;
@ -109,6 +116,15 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
}
}
private AvailabilityResultWithDescription getAvailabilityResultWithDescription(String errorMessage) {
AvailabilityResult proto = getAvailabilityResult(errorMessage);
String description = getAvailabilityResultDescription(proto);
return AvailabilityResultWithDescription.newBuilder()
.setAvailabilityResult(proto)
.setDescription(description)
.build();
}
private AvailabilityResult getAvailabilityResult(String errorMessage) {
return stream(AvailabilityResult.values())
.filter((e) -> errorMessage.toUpperCase().contains(e.name()))
@ -121,7 +137,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
return bisq.core.offer.AvailabilityResult.fromProto(proto).description();
}
private boolean isTakeOfferError() {
private boolean takeOfferWasCalled() {
return fullMethodName.equals(getTakeOfferMethod().getFullMethodName());
}
}

View File

@ -21,8 +21,12 @@ import bisq.core.api.CoreApi;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountReply;
import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest;
import bisq.proto.grpc.CreatePaymentAccountReply;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsReply;
import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest;
import bisq.proto.grpc.GetPaymentAccountFormReply;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsReply;
@ -125,6 +129,40 @@ class GrpcPaymentAccountsService extends PaymentAccountsImplBase {
}
}
@Override
public void createCryptoCurrencyPaymentAccount(CreateCryptoCurrencyPaymentAccountRequest req,
StreamObserver<CreateCryptoCurrencyPaymentAccountReply> responseObserver) {
try {
PaymentAccount paymentAccount = coreApi.createCryptoCurrencyPaymentAccount(req.getAccountName(),
req.getCurrencyCode(),
req.getAddress(),
req.getTradeInstant());
var reply = CreateCryptoCurrencyPaymentAccountReply.newBuilder()
.setPaymentAccount(paymentAccount.toProtoMessage())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override
public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req,
StreamObserver<GetCryptoCurrencyPaymentMethodsReply> responseObserver) {
try {
var paymentMethods = coreApi.getCryptoCurrencyPaymentMethods().stream()
.map(PaymentMethod::toProtoMessage)
.collect(Collectors.toList());
var reply = GetCryptoCurrencyPaymentMethodsReply.newBuilder()
.addAllPaymentMethods(paymentMethods).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
final ServerInterceptor[] interceptors() {
Optional<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
return rateMeteringInterceptor.map(serverInterceptor ->

View File

@ -155,6 +155,15 @@ message OfferInfo {
string counterCurrencyCode = 17;
uint64 date = 18;
string state = 19;
uint64 sellerSecurityDeposit = 20;
string offerFeePaymentTxId = 21;
uint64 txFee = 22;
uint64 makerFee = 23;
}
message AvailabilityResultWithDescription {
AvailabilityResult availabilityResult = 1;
string description = 2;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -170,6 +179,10 @@ service PaymentAccounts {
}
rpc GetPaymentAccountForm (GetPaymentAccountFormRequest) returns (GetPaymentAccountFormReply) {
}
rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) {
}
rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) {
}
}
message CreatePaymentAccountRequest {
@ -202,6 +215,24 @@ message GetPaymentAccountFormReply {
string paymentAccountFormJson = 1;
}
message CreateCryptoCurrencyPaymentAccountRequest {
string accountName = 1;
string currencyCode = 2;
string address = 3;
bool tradeInstant = 4;
}
message CreateCryptoCurrencyPaymentAccountReply {
PaymentAccount paymentAccount = 1;
}
message GetCryptoCurrencyPaymentMethodsRequest {
}
message GetCryptoCurrencyPaymentMethodsReply {
repeated PaymentMethod paymentMethods = 1;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Price
///////////////////////////////////////////////////////////////////////////////////////////
@ -277,8 +308,7 @@ message TakeOfferRequest {
message TakeOfferReply {
TradeInfo trade = 1;
AvailabilityResult availabilityResult = 2;
string availabilityResultDescription = 3;
AvailabilityResultWithDescription failureReason = 2;
}
message ConfirmPaymentStartedRequest {
@ -344,6 +374,28 @@ message TradeInfo {
bool isPayoutPublished = 22;
bool isWithdrawn = 23;
string contractAsJson = 24;
ContractInfo contract = 25;
}
message ContractInfo {
string buyerNodeAddress = 1;
string sellerNodeAddress = 2;
string mediatorNodeAddress = 3;
string refundAgentNodeAddress = 4;
bool isBuyerMakerAndSellerTaker = 5;
string makerAccountId = 6;
string takerAccountId = 7;
PaymentAccountPayloadInfo makerPaymentAccountPayload = 8;
PaymentAccountPayloadInfo takerPaymentAccountPayload = 9;
string makerPayoutAddressString = 10;
string takerPayoutAddressString = 11;
uint64 lockTime = 12;
}
message PaymentAccountPayloadInfo {
string id = 1;
string paymentMethodId = 2;
string address = 3;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -382,6 +434,8 @@ service Wallets {
}
rpc SendBtc (SendBtcRequest) returns (SendBtcReply) {
}
rpc VerifyBsqSentToAddress (VerifyBsqSentToAddressRequest) returns (VerifyBsqSentToAddressReply) {
}
rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) {
}
rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) {
@ -446,6 +500,15 @@ message SendBtcReply {
TxInfo txInfo = 1;
}
message VerifyBsqSentToAddressRequest {
string address = 1;
string amount = 2;
}
message VerifyBsqSentToAddressReply {
bool isAmountReceived = 1;
}
message GetTxFeeRateRequest {
}
@ -558,4 +621,3 @@ message GetVersionRequest {
message GetVersionReply {
string version = 1;
}