Merge pull request #5666 from ghubstan/08-handle-extradata-in-editoffer

Adjust API 'editoffer' to PR 5651 (include extraData field when editing offer)
This commit is contained in:
sqrrm 2021-08-30 10:46:23 +02:00 committed by GitHub
commit 58e09c96ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 3655 additions and 562 deletions

View file

@ -408,8 +408,118 @@ The offer will be removed from other Bisq users' offer views, and paid transacti
### Editing an Existing Offer ### Editing an Existing Offer
Editing existing offers is not yet supported. You can cancel and re-create an offer, but paid transaction fees Offers you create can be edited in various ways:
for the canceled offer will be forfeited.
- Disable or re-enable an offer.
- Change an offer's price model and disable (or re-enable) it.
- Change a market price margin based offer to a fixed price offer.
- Change a market price margin based offer's price margin.
- Change, set, or remove a trigger price on a market price margin based offer.
- Change a market price margin based offer's price margin and trigger price.
- Change a market price margin based offer's price margin and remove its trigger price.
- Change a fixed price offer to a market price margin based offer.
- Change a fixed price offer's fixed price.
_Note: the API does not support editing an offer's payment account._
The subsections below contain examples related to specific use cases.
#### Enable and Disable Offer
Existing offers you create can be disabled (removed from offer book) and re-enabled (re-published to offer book).
To disable an offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--enable=false
```
To enable an offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--enable=true
```
#### Change Offer Pricing Model
The `editoffer` command can be used to change an existing market price margin based offer to a fixed price offer,
and vice-versa.
##### Change Market Price Margin Based to Fixed Price Offer
Suppose you used `createoffer` to create a market price margin based offer as follows:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=SELL \
--currency-code=JPY \
--amount=0.125 \
--market-price-margin=0.5 \
--security-deposit=15.0 \
--fee-currency=BSQ
```
To change the market price margin based offer to a fixed price offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--fixed-price=3960000.5555
```
##### Change Fixed Price Offer to Market Price Margin Based Offer
Suppose you used `createoffer` to create a fixed price offer as follows:
```
$ ./bisq-cli --password=xyz --port=9998 createoffer \
--payment-account=f3c1ec8b-9761-458d-b13d-9039c6892413 \
--direction=SELL \
--currency-code=JPY \
--amount=0.125 \
--fixed-price=3960000.0000 \
--security-deposit=15.0 \
--fee-currency=BSQ
```
To change the fixed price offer to a market price margin based offer:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.5
```
Alternatively, you can also set a trigger price on the re-published, market price margin based offer.
A trigger price on a SELL offer causes the offer to be automatically disabled when the market price
falls below the trigger price. In the `editoffer` example below, the SELL offer will be disabled when
the JPY market price falls below 3960000.0000.
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.5 \
--trigger-price=3960000.0000
```
On a BUY offer, a trigger price causes the BUY offer to be automatically disabled when the market price
rises above the trigger price.
_Note: Disabled offers never automatically re-enable; they can only be manually re-enabled via
`editoffer --offer-id=<id> --enable=true`._
#### Remove Trigger Price
To remove a trigger price on a market price margin based offer, set the trigger price to 0:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--trigger-price=0
```
#### Change Disabled Offer's Pricing Model and Enable It
You can use `editoffer` to simultaneously change an offer's price details and disable or re-enable it.
Suppose you have a disabled, fixed price offer, and want to change it to a market price margin based offer, set
a trigger price, and re-enable it:
```
./bisq-cli --password=xyz --port=9998 editoffer \
--offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.5 \
--trigger-price=3960000.0000 \
--enable=true
```
### Taking Offers ### Taking Offers

View file

@ -17,14 +17,13 @@
package bisq.apitest.method.offer; package bisq.apitest.method.offer;
import bisq.core.monetary.Altcoin;
import protobuf.PaymentAccount; import protobuf.PaymentAccount;
import org.bitcoinj.utils.Fiat;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.function.BiFunction;
import java.util.function.Function;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -37,10 +36,7 @@ import static bisq.apitest.config.BisqAppConfig.alicedaemon;
import static bisq.apitest.config.BisqAppConfig.arbdaemon; import static bisq.apitest.config.BisqAppConfig.arbdaemon;
import static bisq.apitest.config.BisqAppConfig.bobdaemon; import static bisq.apitest.config.BisqAppConfig.bobdaemon;
import static bisq.apitest.config.BisqAppConfig.seednode; import static bisq.apitest.config.BisqAppConfig.seednode;
import static bisq.common.util.MathUtils.roundDouble; import static bisq.common.util.MathUtils.exactMultiply;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static java.math.RoundingMode.HALF_UP;
@ -49,6 +45,10 @@ import bisq.apitest.method.MethodTest;
@Slf4j @Slf4j
public abstract class AbstractOfferTest extends MethodTest { public abstract class AbstractOfferTest extends MethodTest {
protected static final int ACTIVATE_OFFER = 1;
protected static final int DEACTIVATE_OFFER = 0;
protected static final long NO_TRIGGER_PRICE = 0;
@Setter @Setter
protected static boolean isLongRunningTest; protected static boolean isLongRunningTest;
@ -67,6 +67,35 @@ public abstract class AbstractOfferTest extends MethodTest {
} }
// Mkt Price Margin value of offer returned from server is scaled down by 10^-2.
protected final Function<Double, Double> scaledDownMktPriceMargin = (mktPriceMargin) ->
exactMultiply(mktPriceMargin, 0.01);
// Price value of fiat offer returned from server will be scaled up by 10^4.
protected final Function<BigDecimal, Long> scaledUpFiatOfferPrice = (price) -> {
BigDecimal factor = new BigDecimal(10).pow(4);
return price.multiply(factor).longValue();
};
// Price value of altcoin offer returned from server will be scaled up by 10^8.
protected final Function<String, Long> scaledUpAltcoinOfferPrice = (altcoinPriceAsString) -> {
BigDecimal factor = new BigDecimal(10).pow(8);
BigDecimal priceAsBigDecimal = new BigDecimal(altcoinPriceAsString);
return priceAsBigDecimal.multiply(factor).longValue();
};
protected final BiFunction<Double, Double, Long> calcPriceAsLong = (base, delta) -> {
var priceAsDouble = new BigDecimal(base).add(new BigDecimal(delta)).doubleValue();
return Double.valueOf(exactMultiply(priceAsDouble, 10_000)).longValue();
};
protected final BiFunction<Double, Double, String> calcPriceAsString = (base, delta) -> {
var priceAsBigDecimal = new BigDecimal(Double.toString(base))
.add(new BigDecimal(Double.toString(delta)));
return priceAsBigDecimal.toPlainString();
};
@SuppressWarnings("ConstantConditions")
public static void createBsqPaymentAccounts() { public static void createBsqPaymentAccounts() {
alicesBsqAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's BSQ Account", alicesBsqAcct = aliceClient.createCryptoCurrencyPaymentAccount("Alice's BSQ Account",
BSQ, BSQ,
@ -78,17 +107,6 @@ public abstract class AbstractOfferTest extends MethodTest {
false); false);
} }
protected double getScaledOfferPrice(double offerPrice, String currencyCode) {
int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT;
return scaleDownByPowerOf10(offerPrice, precision);
}
protected final double getPercentageDifference(double price1, double price2) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)
.doubleValue();
}
@AfterAll @AfterAll
public static void tearDown() { public static void tearDown() {
tearDownScaffold(); tearDownScaffold();

View file

@ -54,7 +54,8 @@ public class CancelOfferTest extends AbstractOfferTest {
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
paymentAccountId, paymentAccountId,
BSQ); BSQ,
NO_TRIGGER_PRICE);
}; };
@Test @Test

View file

@ -39,6 +39,7 @@ import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferPayload.Direction.SELL;
@ -70,6 +71,9 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
alicesBsqAcct.getId(), alicesBsqAcct.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("Sell BSQ (Buy BTC) OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); log.info("Sell BSQ (Buy BTC) OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
@ -86,6 +90,8 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
genBtcBlockAndWaitForOfferPreparation(); genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
@ -112,6 +118,9 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
alicesBsqAcct.getId(), alicesBsqAcct.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("SELL 20K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); log.info("SELL 20K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
@ -128,6 +137,8 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
genBtcBlockAndWaitForOfferPreparation(); genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
@ -154,6 +165,9 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
alicesBsqAcct.getId(), alicesBsqAcct.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("BUY 1-2K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); log.info("BUY 1-2K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
@ -170,6 +184,8 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
genBtcBlockAndWaitForOfferPreparation(); genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
@ -196,6 +212,9 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
alicesBsqAcct.getId(), alicesBsqAcct.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("SELL 5-10K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ)); log.info("SELL 5-10K BSQ OFFER:\n{}", formatOfferTable(singletonList(newOffer), BSQ));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
@ -212,6 +231,8 @@ public class CreateBSQOffersTest extends AbstractOfferTest {
genBtcBlockAndWaitForOfferPreparation(); genBtcBlockAndWaitForOfferPreparation();
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());

View file

@ -35,6 +35,7 @@ import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferPayload.Direction.SELL;
@ -58,6 +59,9 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
audAccount.getId(), audAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "AUD")); log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "AUD"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
@ -72,6 +76,8 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
@ -98,6 +104,9 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
usdAccount.getId(), usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "USD")); log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "USD"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
@ -112,6 +121,8 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());
@ -138,6 +149,9 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
eurAccount.getId(), eurAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE);
log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "EUR")); log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "EUR"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
@ -152,6 +166,8 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
assertFalse(newOffer.getIsCurrencyForMakerFeeBtc()); assertFalse(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertFalse(newOffer.getUseMarketBasedPrice()); assertFalse(newOffer.getUseMarketBasedPrice());

View file

@ -17,12 +17,18 @@
package bisq.apitest.method.offer; package bisq.apitest.method.offer;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.OfferInfo;
import org.bitcoinj.utils.Fiat;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.math.BigDecimal;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
@ -33,18 +39,23 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.cli.TableFormat.formatOfferTable; import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.common.util.MathUtils.roundDouble;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static java.lang.Math.abs; import static java.lang.Math.abs;
import static java.lang.String.format; import static java.lang.String.format;
import static java.math.RoundingMode.HALF_UP;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferPayload.Direction.BUY; import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL; import static protobuf.OfferPayload.Direction.SELL;
@SuppressWarnings("ConstantConditions")
@Disabled @Disabled
@Slf4j @Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ -68,8 +79,12 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(), usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd")); log.info("OFFER #1:\n{}", formatOfferTable(singletonList(newOffer), "usd"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
@ -83,6 +98,8 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
@ -109,8 +126,12 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
nzdAccount.getId(), nzdAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd")); log.info("OFFER #2:\n{}", formatOfferTable(singletonList(newOffer), "nzd"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
@ -124,6 +145,8 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(BUY.name(), newOffer.getDirection()); assertEquals(BUY.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
@ -150,8 +173,12 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
gbpAccount.getId(), gbpAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp")); log.info("OFFER #3:\n{}", formatOfferTable(singletonList(newOffer), "gbp"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
@ -165,6 +192,8 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
@ -191,8 +220,12 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
priceMarginPctInput, priceMarginPctInput,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
brlAccount.getId(), brlAccount.getId(),
MAKER_FEE_CURRENCY_CODE); MAKER_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl")); log.info("OFFER #4:\n{}", formatOfferTable(singletonList(newOffer), "brl"));
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
String newOfferId = newOffer.getId(); String newOfferId = newOffer.getId();
assertNotEquals("", newOfferId); assertNotEquals("", newOfferId);
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
@ -206,6 +239,8 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertTrue(newOffer.getIsCurrencyForMakerFeeBtc()); assertTrue(newOffer.getIsCurrencyForMakerFeeBtc());
newOffer = aliceClient.getMyOffer(newOfferId); newOffer = aliceClient.getMyOffer(newOfferId);
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(newOfferId, newOffer.getId()); assertEquals(newOfferId, newOffer.getId());
assertEquals(SELL.name(), newOffer.getDirection()); assertEquals(SELL.name(), newOffer.getDirection());
assertTrue(newOffer.getUseMarketBasedPrice()); assertTrue(newOffer.getUseMarketBasedPrice());
@ -220,6 +255,35 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput); assertCalculatedPriceIsCorrect(newOffer, priceMarginPctInput);
} }
@Test
@Order(5)
public void testCreateUSDBTCBuyOfferWithTriggerPrice() {
PaymentAccount usdAccount = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("usd");
BigDecimal mktPrice = new BigDecimal(Double.toString(mktPriceAsDouble));
BigDecimal triggerPrice = mktPrice.add(new BigDecimal("1000.9999"));
long triggerPriceAsLong = Price.parse("USD", triggerPrice.toString()).getValue();
var newOffer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"usd",
10_000_000L,
5_000_000L,
0.0,
getDefaultBuyerSecurityDepositAsPercent(),
usdAccount.getId(),
MAKER_FEE_CURRENCY_CODE,
triggerPriceAsLong);
assertTrue(newOffer.getIsMyOffer());
assertTrue(newOffer.getIsMyPendingOffer());
genBtcBlocksThenWait(1, 4000); // give time to add to offer book
newOffer = aliceClient.getMyOffer(newOffer.getId());
log.info("OFFER #5:\n{}", formatOfferTable(singletonList(newOffer), "usd"));
assertTrue(newOffer.getIsMyOffer());
assertFalse(newOffer.getIsMyPendingOffer());
assertEquals(triggerPriceAsLong, newOffer.getTriggerPrice());
}
private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) { private void assertCalculatedPriceIsCorrect(OfferInfo offer, double priceMarginPctInput) {
assertTrue(() -> { assertTrue(() -> {
String counterCurrencyCode = offer.getCounterCurrencyCode(); String counterCurrencyCode = offer.getCounterCurrencyCode();
@ -239,6 +303,17 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
}); });
} }
private double getPercentageDifference(double price1, double price2) {
return BigDecimal.valueOf(roundDouble((1 - (price1 / price2)), 5))
.setScale(4, HALF_UP)
.doubleValue();
}
private double getScaledOfferPrice(double offerPrice, String currencyCode) {
int precision = isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT;
return scaleDownByPowerOf10(offerPrice, precision);
}
private boolean isCalculatedPriceWithinErrorTolerance(double delta, private boolean isCalculatedPriceWithinErrorTolerance(double delta,
double expectedDiffPct, double expectedDiffPct,
double actualDiffPct, double actualDiffPct,

View file

@ -0,0 +1,644 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.offer;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.OfferInfo;
import io.grpc.StatusRuntimeException;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BSQ;
import static bisq.cli.TableFormat.formatOfferTable;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.proto.grpc.EditOfferRequest.EditType.*;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
@SuppressWarnings("ALL")
@Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EditOfferTest extends AbstractOfferTest {
// Some test fixtures to reduce duplication.
private static final Map<String, PaymentAccount> paymentAcctCache = new HashMap<>();
private static final String DOLLAR = "USD";
private static final String RUBLE = "RUB";
private static final long AMOUNT = 10000000L;
@Test
@Order(1)
public void testOfferDisableAndEnable() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("DE");
OfferInfo originalOffer = createMktPricedOfferForEdit(BUY.name(),
"EUR",
paymentAcct.getId(),
0.0,
NO_TRIGGER_PRICE);
log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR"));
assertFalse(originalOffer.getIsActivated()); // Not activated until prep is done.
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
originalOffer = aliceClient.getMyOffer(originalOffer.getId());
assertTrue(originalOffer.getIsActivated());
// Disable offer
aliceClient.editOfferActivationState(originalOffer.getId(), DEACTIVATE_OFFER);
genBtcBlocksThenWait(1, 1500); // Wait for offer book removal.
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR"));
assertFalse(editedOffer.getIsActivated());
// Re-enable offer
aliceClient.editOfferActivationState(editedOffer.getId(), ACTIVATE_OFFER);
genBtcBlocksThenWait(1, 1500); // Wait for offer book re-entry.
editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR"));
assertTrue(editedOffer.getIsActivated());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(2)
public void testEditTriggerPrice() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("FI");
OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
"EUR",
paymentAcct.getId(),
0.0,
NO_TRIGGER_PRICE);
log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
originalOffer = aliceClient.getMyOffer(originalOffer.getId());
assertEquals(0 /*no trigger price*/, originalOffer.getTriggerPrice());
// Edit the offer's trigger price, nothing else.
var mktPrice = aliceClient.getBtcPrice("EUR");
var delta = 5_000.00;
var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPrice, delta);
aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPriceAsLong);
sleep(2500); // Wait for offer book re-entry.
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED EUR OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "EUR"));
assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice());
assertTrue(editedOffer.getUseMarketBasedPrice());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(3)
public void testSetTriggerPriceToNegativeValueShouldThrowException() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("FI");
final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
"EUR",
paymentAcct.getId(),
0.0,
NO_TRIGGER_PRICE);
log.info("ORIGINAL EUR OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "EUR"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
// Edit the offer's trigger price, set to -1, check error.
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.editOfferTriggerPrice(originalOffer.getId(), -1L));
String expectedExceptionMessage =
format("UNKNOWN: programmer error: cannot set trigger price to a negative value in offer with id '%s'",
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(4)
public void testEditMktPriceMargin() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("US");
var originalMktPriceMargin = new BigDecimal("0.1").doubleValue();
OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
DOLLAR,
paymentAcct.getId(),
originalMktPriceMargin,
NO_TRIGGER_PRICE);
log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin());
// Edit the offer's price margin, nothing else.
var newMktPriceMargin = new BigDecimal("0.5").doubleValue();
aliceClient.editOfferPriceMargin(originalOffer.getId(), newMktPriceMargin);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD"));
assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(5)
public void testEditFixedPrice() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU");
double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE);
String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000);
OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(),
RUBLE,
paymentAcct.getId(),
fixedPriceAsString);
log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
// Edit the offer's fixed price, nothing else.
String editedFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 100_000.0000);
aliceClient.editOfferFixedPrice(originalOffer.getId(), editedFixedPriceAsString);
// Wait for edited offer to be removed from offer-book, edited, and re-published.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED RUB OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "RUB"));
var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString));
assertEquals(expectedNewFixedPrice, editedOffer.getPrice());
assertFalse(editedOffer.getUseMarketBasedPrice());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(6)
public void testEditFixedPriceAndDeactivation() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU");
double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE);
String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000);
OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(),
RUBLE,
paymentAcct.getId(),
fixedPriceAsString);
log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
// Edit the offer's fixed price and deactivate it.
String editedFixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 100_000.0000);
aliceClient.editOffer(originalOffer.getId(),
editedFixedPriceAsString,
originalOffer.getUseMarketBasedPrice(),
0.0,
NO_TRIGGER_PRICE,
DEACTIVATE_OFFER,
FIXED_PRICE_AND_ACTIVATION_STATE);
// Wait for edited offer to be removed from offer-book, edited, and re-published.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED RUB OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "RUB"));
var expectedNewFixedPrice = scaledUpFiatOfferPrice.apply(new BigDecimal(editedFixedPriceAsString));
assertEquals(expectedNewFixedPrice, editedOffer.getPrice());
assertFalse(editedOffer.getIsActivated());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(7)
public void testEditMktPriceMarginAndDeactivation() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("US");
var originalMktPriceMargin = new BigDecimal("0.0").doubleValue();
OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
DOLLAR,
paymentAcct.getId(),
originalMktPriceMargin,
0);
log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
originalOffer = aliceClient.getMyOffer(originalOffer.getId());
assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin());
// Edit the offer's price margin and trigger price, and deactivate it.
var newMktPriceMargin = new BigDecimal("1.50").doubleValue();
aliceClient.editOffer(originalOffer.getId(),
"0.00",
originalOffer.getUseMarketBasedPrice(),
newMktPriceMargin,
0,
DEACTIVATE_OFFER,
MKT_PRICE_MARGIN_AND_ACTIVATION_STATE);
// Wait for edited offer to be removed from offer-book, edited, and re-published.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD"));
assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin());
assertEquals(0, editedOffer.getTriggerPrice());
assertFalse(editedOffer.getIsActivated());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(8)
public void testEditMktPriceMarginAndTriggerPriceAndDeactivation() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("US");
var originalMktPriceMargin = new BigDecimal("0.0").doubleValue();
var mktPriceAsDouble = aliceClient.getBtcPrice(DOLLAR);
var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -5_000.0000);
OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
DOLLAR,
paymentAcct.getId(),
originalMktPriceMargin,
originalTriggerPriceAsLong);
log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
originalOffer = aliceClient.getMyOffer(originalOffer.getId());
assertEquals(scaledDownMktPriceMargin.apply(originalMktPriceMargin), originalOffer.getMarketPriceMargin());
assertEquals(originalTriggerPriceAsLong, originalOffer.getTriggerPrice());
// Edit the offer's price margin and trigger price, and deactivate it.
var newMktPriceMargin = new BigDecimal("0.1").doubleValue();
var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, -2_000.0000);
aliceClient.editOffer(originalOffer.getId(),
"0.00",
originalOffer.getUseMarketBasedPrice(),
newMktPriceMargin,
newTriggerPriceAsLong,
DEACTIVATE_OFFER,
MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE);
// Wait for edited offer to be removed from offer-book, edited, and re-published.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED USD OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "USD"));
assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin());
assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice());
assertFalse(editedOffer.getIsActivated());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(9)
public void testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("US");
var originalMktPriceMargin = new BigDecimal("0.0").doubleValue();
final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
DOLLAR,
paymentAcct.getId(),
originalMktPriceMargin,
NO_TRIGGER_PRICE);
log.info("ORIGINAL USD OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "USD"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
// Try to edit both the fixed price and mkt price margin.
var newMktPriceMargin = new BigDecimal("0.25").doubleValue();
var newFixedPrice = "50000.0000";
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.editOffer(originalOffer.getId(),
newFixedPrice,
originalOffer.getUseMarketBasedPrice(),
newMktPriceMargin,
NO_TRIGGER_PRICE,
ACTIVATE_OFFER,
MKT_PRICE_MARGIN_ONLY));
String expectedExceptionMessage =
format("UNKNOWN: programmer error: cannot set fixed price (%s) in"
+ " mkt price margin based offer with id '%s'",
newFixedPrice,
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(10)
public void testEditingTriggerPriceInFixedPriceOfferShouldThrowException() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("RU");
double mktPriceAsDouble = aliceClient.getBtcPrice(RUBLE);
String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 200_000.0000);
OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(),
RUBLE,
paymentAcct.getId(),
fixedPriceAsString);
log.info("ORIGINAL RUB OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "RUB"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
long newTriggerPrice = 1000000L;
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.editOfferTriggerPrice(originalOffer.getId(), newTriggerPrice));
String expectedExceptionMessage =
format("UNKNOWN: programmer error: cannot set a trigger price (%s) in"
+ " fixed price offer with id '%s'",
newTriggerPrice,
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(11)
public void testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("MX");
double mktPriceAsDouble = aliceClient.getBtcPrice("MXN");
String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00);
OfferInfo originalOffer = createFixedPricedOfferForEdit(BUY.name(),
"MXN",
paymentAcct.getId(),
fixedPriceAsString);
log.info("ORIGINAL MXN OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "MXN"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
// Change the offer to mkt price based and set a trigger price.
var newMktPriceMargin = new BigDecimal("0.05").doubleValue();
var delta = 200_000.0000; // trigger price on buy offer is 200K above mkt price
var newTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, delta);
aliceClient.editOffer(originalOffer.getId(),
"0.00",
true,
newMktPriceMargin,
newTriggerPriceAsLong,
ACTIVATE_OFFER,
MKT_PRICE_MARGIN_AND_TRIGGER_PRICE);
// Wait for edited offer to be removed from offer-book, edited, and re-published.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED MXN OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "MXN"));
assertTrue(editedOffer.getUseMarketBasedPrice());
assertEquals(scaledDownMktPriceMargin.apply(newMktPriceMargin), editedOffer.getMarketPriceMargin());
assertEquals(newTriggerPriceAsLong, editedOffer.getTriggerPrice());
assertTrue(editedOffer.getIsActivated());
doSanityCheck(originalOffer, editedOffer);
}
@Test
@Order(12)
public void testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt() {
PaymentAccount paymentAcct = getOrCreatePaymentAccount("GB");
double mktPriceAsDouble = aliceClient.getBtcPrice("GBP");
var originalMktPriceMargin = new BigDecimal("0.25").doubleValue();
var delta = 1_000.0000; // trigger price on sell offer is 1K below mkt price
var originalTriggerPriceAsLong = calcPriceAsLong.apply(mktPriceAsDouble, delta);
final OfferInfo originalOffer = createMktPricedOfferForEdit(SELL.name(),
"GBP",
paymentAcct.getId(),
originalMktPriceMargin,
originalTriggerPriceAsLong);
log.info("ORIGINAL GBP OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "GBP"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
String fixedPriceAsString = calcPriceAsString.apply(mktPriceAsDouble, 0.00);
aliceClient.editOffer(originalOffer.getId(),
fixedPriceAsString,
false,
0.00,
0,
DEACTIVATE_OFFER,
FIXED_PRICE_AND_ACTIVATION_STATE);
// Wait for edited offer to be removed from offer-book, edited, and re-published.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED GBP OFFER:\n{}", formatOfferTable(singletonList(editedOffer), "GBP"));
assertEquals(scaledUpFiatOfferPrice.apply(new BigDecimal(fixedPriceAsString)), editedOffer.getPrice());
assertFalse(editedOffer.getUseMarketBasedPrice());
assertEquals(0.00, editedOffer.getMarketPriceMargin());
assertEquals(0, editedOffer.getTriggerPrice());
assertFalse(editedOffer.getIsActivated());
}
@Test
@Order(13)
public void testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowException() {
createBsqPaymentAccounts();
OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(),
BSQ,
100_000_000L,
100_000_000L,
"0.00005", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ
getDefaultBuyerSecurityDepositAsPercent(),
alicesBsqAcct.getId(),
BSQ);
log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.editOffer(originalOffer.getId(),
"0.00",
true,
0.1,
0,
ACTIVATE_OFFER,
MKT_PRICE_MARGIN_ONLY));
String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or"
+ " trigger price on fixed price altcoin offer with id '%s'",
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(14)
public void testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException() {
createBsqPaymentAccounts();
OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(),
BSQ,
100_000_000L,
100_000_000L,
"0.00005", // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ
getDefaultBuyerSecurityDepositAsPercent(),
alicesBsqAcct.getId(),
BSQ);
log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
var newTriggerPriceAsLong = calcPriceAsLong.apply(0.00005, 0.00);
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.editOffer(originalOffer.getId(),
"0.00",
false,
0.1,
newTriggerPriceAsLong,
ACTIVATE_OFFER,
TRIGGER_PRICE_ONLY));
String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or"
+ " trigger price on fixed price altcoin offer with id '%s'",
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
}
@Test
@Order(15)
public void testEditFixedPriceOnBsqOffer() {
createBsqPaymentAccounts();
String fixedPriceAsString = "0.00005"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ
final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(),
BSQ,
100_000_000L,
100_000_000L,
fixedPriceAsString,
getDefaultBuyerSecurityDepositAsPercent(),
alicesBsqAcct.getId(),
BSQ);
log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
String newFixedPriceAsString = "0.00003111";
aliceClient.editOffer(originalOffer.getId(),
newFixedPriceAsString,
false,
0.0,
0,
ACTIVATE_OFFER,
FIXED_PRICE_ONLY);
// Wait for edited offer to be edited and removed from offer-book.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ));
assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice());
assertTrue(editedOffer.getIsActivated());
assertFalse(editedOffer.getUseMarketBasedPrice());
assertEquals(0.00, editedOffer.getMarketPriceMargin());
assertEquals(0, editedOffer.getTriggerPrice());
}
@Test
@Order(16)
public void testDisableBsqOffer() {
createBsqPaymentAccounts();
String fixedPriceAsString = "0.00005"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ
final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(),
BSQ,
100_000_000L,
100_000_000L,
fixedPriceAsString,
getDefaultBuyerSecurityDepositAsPercent(),
alicesBsqAcct.getId(),
BSQ);
log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
aliceClient.editOffer(originalOffer.getId(),
fixedPriceAsString,
false,
0.0,
0,
DEACTIVATE_OFFER,
ACTIVATION_STATE_ONLY);
// Wait for edited offer to be removed from offer-book.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ));
assertFalse(editedOffer.getIsActivated());
assertEquals(scaledUpAltcoinOfferPrice.apply(fixedPriceAsString), editedOffer.getPrice());
assertFalse(editedOffer.getUseMarketBasedPrice());
assertEquals(0.00, editedOffer.getMarketPriceMargin());
assertEquals(0, editedOffer.getTriggerPrice());
}
@Test
@Order(17)
public void testEditFixedPriceAndDisableBsqOffer() {
createBsqPaymentAccounts();
String fixedPriceAsString = "0.00005"; // FIXED PRICE IN BTC (satoshis) FOR 1 BSQ
final OfferInfo originalOffer = aliceClient.createFixedPricedOffer(BUY.name(),
BSQ,
100_000_000L,
100_000_000L,
fixedPriceAsString,
getDefaultBuyerSecurityDepositAsPercent(),
alicesBsqAcct.getId(),
BSQ);
log.info("ORIGINAL BSQ OFFER:\n{}", formatOfferTable(singletonList(originalOffer), "BSQ"));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
String newFixedPriceAsString = "0.000045";
aliceClient.editOffer(originalOffer.getId(),
newFixedPriceAsString,
false,
0.0,
0,
DEACTIVATE_OFFER,
FIXED_PRICE_AND_ACTIVATION_STATE);
// Wait for edited offer to be edited and removed from offer-book.
genBtcBlocksThenWait(1, 2500);
OfferInfo editedOffer = aliceClient.getMyOffer(originalOffer.getId());
log.info("EDITED BSQ OFFER:\n{}", formatOfferTable(singletonList(editedOffer), BSQ));
assertFalse(editedOffer.getIsActivated());
assertEquals(scaledUpAltcoinOfferPrice.apply(newFixedPriceAsString), editedOffer.getPrice());
assertFalse(editedOffer.getUseMarketBasedPrice());
assertEquals(0.00, editedOffer.getMarketPriceMargin());
assertEquals(0, editedOffer.getTriggerPrice());
}
private OfferInfo createMktPricedOfferForEdit(String direction,
String currencyCode,
String paymentAccountId,
double marketPriceMargin,
long triggerPrice) {
return aliceClient.createMarketBasedPricedOffer(direction,
currencyCode,
AMOUNT,
AMOUNT,
marketPriceMargin,
getDefaultBuyerSecurityDepositAsPercent(),
paymentAccountId,
BSQ,
triggerPrice);
}
private OfferInfo createFixedPricedOfferForEdit(String direction,
String currencyCode,
String paymentAccountId,
String priceAsString) {
return aliceClient.createFixedPricedOffer(direction,
currencyCode,
AMOUNT,
AMOUNT,
priceAsString,
getDefaultBuyerSecurityDepositAsPercent(),
paymentAccountId,
BSQ);
}
private void doSanityCheck(OfferInfo originalOffer, OfferInfo editedOffer) {
// Assert some of the immutable offer fields are unchanged.
assertEquals(originalOffer.getDirection(), editedOffer.getDirection());
assertEquals(originalOffer.getAmount(), editedOffer.getAmount());
assertEquals(originalOffer.getMinAmount(), editedOffer.getMinAmount());
assertEquals(originalOffer.getTxFee(), editedOffer.getTxFee());
assertEquals(originalOffer.getMakerFee(), editedOffer.getMakerFee());
assertEquals(originalOffer.getPaymentAccountId(), editedOffer.getPaymentAccountId());
assertEquals(originalOffer.getDate(), editedOffer.getDate());
if (originalOffer.getDirection().equals(BUY.name()))
assertEquals(originalOffer.getBuyerSecurityDeposit(), editedOffer.getBuyerSecurityDeposit());
else
assertEquals(originalOffer.getSellerSecurityDeposit(), editedOffer.getSellerSecurityDeposit());
}
private PaymentAccount getOrCreatePaymentAccount(String countryCode) {
if (paymentAcctCache.containsKey(countryCode)) {
return paymentAcctCache.get(countryCode);
} else {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, countryCode);
paymentAcctCache.put(countryCode, paymentAcct);
return paymentAcct;
}
}
@AfterAll
public static void clearPaymentAcctCache() {
paymentAcctCache.clear();
}
}

View file

@ -72,7 +72,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId(), alicesUsdAccount.getId(),
TRADE_FEE_CURRENCY_CODE); TRADE_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();
assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc());

View file

@ -103,7 +103,8 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
alicesPaymentAccount.getId(), alicesPaymentAccount.getId(),
TRADE_FEE_CURRENCY_CODE); TRADE_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();
assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc()); assertFalse(alicesOffer.getIsCurrencyForMakerFeeBtc());

View file

@ -75,7 +75,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
0.00, 0.00,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
alicesUsdAccount.getId(), alicesUsdAccount.getId(),
TRADE_FEE_CURRENCY_CODE); TRADE_FEE_CURRENCY_CODE,
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId(); var offerId = alicesOffer.getId();
assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc()); assertTrue(alicesOffer.getIsCurrencyForMakerFeeBtc());

View file

@ -0,0 +1,167 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.scenario;
import bisq.core.payment.PaymentAccount;
import bisq.proto.grpc.OfferInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.condition.EnabledIf;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.cli.CurrencyFormat.formatPrice;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static java.lang.System.getenv;
import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferPayload.Direction.BUY;
import static protobuf.OfferPayload.Direction.SELL;
import bisq.apitest.method.offer.AbstractOfferTest;
/**
* Used to verify trigger based, automatic offer deactivation works.
* Disabled by default.
* Set ENV or IDE-ENV LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true to run.
*/
@EnabledIf("envLongRunningTestEnabled")
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
private static final int MAX_ITERATIONS = 500;
@Test
@Order(1)
public void testSellOfferAutoDisable(final TestInfo testInfo) {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("USD");
long triggerPrice = calcPriceAsLong.apply(mktPriceAsDouble, -50.0000);
log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice));
OfferInfo offer = aliceClient.createMarketBasedPricedOffer(SELL.name(),
"USD",
1_000_000,
1_000_000,
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
paymentAcct.getId(),
BTC,
triggerPrice);
log.info("SELL offer {} created with margin based price {}.",
offer.getId(),
formatPrice(offer.getPrice()));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
offer = aliceClient.getMyOffer(offer.getId()); // Offer has trigger price now.
log.info("SELL offer should be automatically disabled when mkt price falls below {}.",
formatPrice(offer.getTriggerPrice()));
int numIterations = 0;
while (++numIterations < MAX_ITERATIONS) {
offer = aliceClient.getMyOffer(offer.getId());
var mktPrice = aliceClient.getBtcPrice("USD");
if (offer.getIsActivated()) {
log.info("Offer still enabled at mkt price {} > {} trigger price",
mktPrice,
formatPrice(offer.getTriggerPrice()));
sleep(1000 * 60); // 60s
} else {
log.info("Successful test completion after offer disabled at mkt price {} < {} trigger price.",
mktPrice,
formatPrice(offer.getTriggerPrice()));
break;
}
if (numIterations == MAX_ITERATIONS)
fail("Offer never disabled");
genBtcBlocksThenWait(1, 0);
}
}
@Test
@Order(2)
public void testBuyOfferAutoDisable(final TestInfo testInfo) {
PaymentAccount paymentAcct = createDummyF2FAccount(aliceClient, "US");
double mktPriceAsDouble = aliceClient.getBtcPrice("USD");
long triggerPrice = calcPriceAsLong.apply(mktPriceAsDouble, 50.0000);
log.info("Current USD mkt price = {} Trigger Price = {}", mktPriceAsDouble, formatPrice(triggerPrice));
OfferInfo offer = aliceClient.createMarketBasedPricedOffer(BUY.name(),
"USD",
1_000_000,
1_000_000,
0.00,
getDefaultBuyerSecurityDepositAsPercent(),
paymentAcct.getId(),
BTC,
triggerPrice);
log.info("BUY offer {} created with margin based price {}.",
offer.getId(),
formatPrice(offer.getPrice()));
genBtcBlocksThenWait(1, 2500); // Wait for offer book entry.
offer = aliceClient.getMyOffer(offer.getId()); // Offer has trigger price now.
log.info("BUY offer should be automatically disabled when mkt price rises above {}.",
formatPrice(offer.getTriggerPrice()));
int numIterations = 0;
while (++numIterations < MAX_ITERATIONS) {
offer = aliceClient.getMyOffer(offer.getId());
var mktPrice = aliceClient.getBtcPrice("USD");
if (offer.getIsActivated()) {
log.info("Offer still enabled at mkt price {} < {} trigger price",
mktPrice,
formatPrice(offer.getTriggerPrice()));
sleep(1000 * 60); // 60s
} else {
log.info("Successful test completion after offer disabled at mkt price {} > {} trigger price.",
mktPrice,
formatPrice(offer.getTriggerPrice()));
break;
}
if (numIterations == MAX_ITERATIONS)
fail("Offer never disabled");
genBtcBlocksThenWait(1, 0);
}
}
protected static boolean envLongRunningTestEnabled() {
String envName = "LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED";
String envX = getenv(envName);
if (envX != null) {
log.info("Enabled, found {}.", envName);
return true;
} else {
log.info("Skipped, no environment variable {} defined.", envName);
log.info("To enable on Mac OS or Linux:"
+ "\tIf running in terminal, export LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in bash shell."
+ "\tIf running in Intellij, set LONG_RUNNING_OFFER_DEACTIVATION_TEST_ENABLED=true in launcher's Environment variables field.");
return false;
}
}
}

View file

@ -32,6 +32,7 @@ import bisq.apitest.method.offer.CancelOfferTest;
import bisq.apitest.method.offer.CreateBSQOffersTest; import bisq.apitest.method.offer.CreateBSQOffersTest;
import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest; import bisq.apitest.method.offer.CreateOfferUsingFixedPriceTest;
import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest; import bisq.apitest.method.offer.CreateOfferUsingMarketPriceMarginTest;
import bisq.apitest.method.offer.EditOfferTest;
import bisq.apitest.method.offer.ValidateCreateOfferTest; import bisq.apitest.method.offer.ValidateCreateOfferTest;
@Slf4j @Slf4j
@ -71,11 +72,12 @@ public class OfferTest extends AbstractOfferTest {
test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin(); test.testCreateNZDBTCBuyOfferMinus2PctPriceMargin();
test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin(); test.testCreateGBPBTCSellOfferMinus1Point5PctPriceMargin();
test.testCreateBRLBTCSellOffer6Point55PctPriceMargin(); test.testCreateBRLBTCSellOffer6Point55PctPriceMargin();
test.testCreateUSDBTCBuyOfferWithTriggerPrice();
} }
@Test @Test
@Order(5) @Order(5)
public void testCreateBSQOffersTest() { public void testCreateBSQOffers() {
CreateBSQOffersTest test = new CreateBSQOffersTest(); CreateBSQOffersTest test = new CreateBSQOffersTest();
CreateBSQOffersTest.createBsqPaymentAccounts(); CreateBSQOffersTest.createBsqPaymentAccounts();
test.testCreateBuy1BTCFor20KBSQOffer(); test.testCreateBuy1BTCFor20KBSQOffer();
@ -85,4 +87,30 @@ public class OfferTest extends AbstractOfferTest {
test.testGetAllMyBsqOffers(); test.testGetAllMyBsqOffers();
test.testGetAvailableBsqOffers(); test.testGetAvailableBsqOffers();
} }
@Test
@Order(6)
public void testEditOffer() {
EditOfferTest test = new EditOfferTest();
// Edit fiat offer tests
test.testOfferDisableAndEnable();
test.testEditTriggerPrice();
test.testSetTriggerPriceToNegativeValueShouldThrowException();
test.testEditMktPriceMargin();
test.testEditFixedPrice();
test.testEditFixedPriceAndDeactivation();
test.testEditMktPriceMarginAndDeactivation();
test.testEditMktPriceMarginAndTriggerPriceAndDeactivation();
test.testEditingFixedPriceInMktPriceMarginBasedOfferShouldThrowException();
test.testEditingTriggerPriceInFixedPriceOfferShouldThrowException();
test.testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice();
test.testChangePriceMarginBasedOfferToFixedPriceOfferAndDeactivateIt();
test.testChangeFixedPriceOfferToPriceMarginBasedOfferWithTriggerPrice();
// Edit bsq offer tests
test.testChangeFixedPricedBsqOfferToPriceMarginBasedOfferShouldThrowException();
test.testEditTriggerPriceOnFixedPriceBsqOfferShouldThrowException();
test.testEditFixedPriceOnBsqOffer();
test.testDisableBsqOffer();
test.testEditFixedPriceAndDisableBsqOffer();
}
} }

View file

@ -39,11 +39,6 @@ import bisq.cli.GrpcClient;
/** /**
* Convenience GrpcClient wrapper for bots using gRPC services. * Convenience GrpcClient wrapper for bots using gRPC services.
*
* TODO Consider if the duplication smell is bad enough to force a BotClient user
* to use the GrpcClient instead (and delete this class). But right now, I think it is
* OK because moving some of the non-gRPC related methods to GrpcClient is even smellier.
*
*/ */
@SuppressWarnings({"JavaDoc", "unused"}) @SuppressWarnings({"JavaDoc", "unused"})
@Slf4j @Slf4j
@ -134,7 +129,8 @@ public class BotClient {
long minAmountInSatoshis, long minAmountInSatoshis,
double priceMarginAsPercent, double priceMarginAsPercent,
double securityDepositAsPercent, double securityDepositAsPercent,
String feeCurrency) { String feeCurrency,
long triggerPrice) {
return grpcClient.createMarketBasedPricedOffer(direction, return grpcClient.createMarketBasedPricedOffer(direction,
currencyCode, currencyCode,
amountInSatoshis, amountInSatoshis,
@ -142,7 +138,8 @@ public class BotClient {
priceMarginAsPercent, priceMarginAsPercent,
securityDepositAsPercent, securityDepositAsPercent,
paymentAccount.getId(), paymentAccount.getId(),
feeCurrency); feeCurrency,
triggerPrice);
} }
/** /**

View file

@ -33,7 +33,7 @@ import java.util.function.Supplier;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice; import static bisq.cli.CurrencyFormat.formatInternalFiatPrice;
import static bisq.cli.CurrencyFormat.formatSatoshis; import static bisq.cli.CurrencyFormat.formatSatoshis;
import static bisq.common.util.MathUtils.scaleDownByPowerOf10; import static bisq.common.util.MathUtils.scaleDownByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent; import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
@ -128,7 +128,8 @@ public class RandomOffer {
minAmount, minAmount,
priceMargin, priceMargin,
getDefaultBuyerSecurityDepositAsPercent(), getDefaultBuyerSecurityDepositAsPercent(),
feeCurrency); feeCurrency,
0 /*no trigger price*/);
} else { } else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount, this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
direction, direction,
@ -167,11 +168,11 @@ public class RandomOffer {
log.info(description); log.info(description);
if (useMarketBasedPrice) { if (useMarketBasedPrice) {
log.info("Offer Price Margin = {}%", priceMargin); log.info("Offer Price Margin = {}%", priceMargin);
log.info("Expected Offer Price = {} {}", formatMarketPrice(Double.parseDouble(fixedOfferPrice)), currencyCode); log.info("Expected Offer Price = {} {}", formatInternalFiatPrice(Double.parseDouble(fixedOfferPrice)), currencyCode);
} else { } else {
log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode); log.info("Fixed Offer Price = {} {}", fixedOfferPrice, currencyCode);
} }
log.info("Current Market Price = {} {}", formatMarketPrice(currentMarketPrice), currencyCode); log.info("Current Market Price = {} {}", formatInternalFiatPrice(currentMarketPrice), currencyCode);
} }
} }

View file

@ -39,10 +39,7 @@ import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CurrencyFormat.formatMarketPrice; import static bisq.cli.CurrencyFormat.*;
import static bisq.cli.CurrencyFormat.formatTxFeeRateInfo;
import static bisq.cli.CurrencyFormat.toSatoshis;
import static bisq.cli.CurrencyFormat.toSecurityDepositAsPct;
import static bisq.cli.Method.*; import static bisq.cli.Method.*;
import static bisq.cli.TableFormat.*; import static bisq.cli.TableFormat.*;
import static bisq.cli.opts.OptLabel.*; import static bisq.cli.opts.OptLabel.*;
@ -59,6 +56,7 @@ import bisq.cli.opts.CancelOfferOptionParser;
import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser; import bisq.cli.opts.CreateCryptoCurrencyPaymentAcctOptionParser;
import bisq.cli.opts.CreateOfferOptionParser; import bisq.cli.opts.CreateOfferOptionParser;
import bisq.cli.opts.CreatePaymentAcctOptionParser; import bisq.cli.opts.CreatePaymentAcctOptionParser;
import bisq.cli.opts.EditOfferOptionParser;
import bisq.cli.opts.GetAddressBalanceOptionParser; import bisq.cli.opts.GetAddressBalanceOptionParser;
import bisq.cli.opts.GetBTCMarketPriceOptionParser; import bisq.cli.opts.GetBTCMarketPriceOptionParser;
import bisq.cli.opts.GetBalanceOptionParser; import bisq.cli.opts.GetBalanceOptionParser;
@ -200,7 +198,7 @@ public class CliMain {
} }
var currencyCode = opts.getCurrencyCode(); var currencyCode = opts.getCurrencyCode();
var price = client.getBtcPrice(currencyCode); var price = client.getBtcPrice(currencyCode);
out.println(formatMarketPrice(price)); out.println(formatInternalFiatPrice(price));
return; return;
} }
case getfundingaddresses: { case getfundingaddresses: {
@ -337,6 +335,7 @@ public class CliMain {
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal(); var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal();
var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit()); var securityDeposit = toSecurityDepositAsPct(opts.getSecurityDeposit());
var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode(); var makerFeeCurrencyCode = opts.getMakerFeeCurrencyCode();
var triggerPrice = 0; // Cannot be defined until offer is in book.
var offer = client.createOffer(direction, var offer = client.createOffer(direction,
currencyCode, currencyCode,
amount, amount,
@ -346,10 +345,34 @@ public class CliMain {
marketPriceMargin.doubleValue(), marketPriceMargin.doubleValue(),
securityDeposit, securityDeposit,
paymentAcctId, paymentAcctId,
makerFeeCurrencyCode); makerFeeCurrencyCode,
triggerPrice);
out.println(formatOfferTable(singletonList(offer), currencyCode)); out.println(formatOfferTable(singletonList(offer), currencyCode));
return; return;
} }
case editoffer: {
var opts = new EditOfferOptionParser(args).parse();
if (opts.isForHelp()) {
out.println(client.getMethodHelp(method));
return;
}
var offerId = opts.getOfferId();
var fixedPrice = opts.getFixedPrice();
var isUsingMktPriceMargin = opts.isUsingMktPriceMargin();
var marketPriceMargin = opts.getMktPriceMarginAsBigDecimal();
var triggerPrice = toInternalFiatPrice(opts.getTriggerPriceAsBigDecimal());
var enable = opts.getEnableAsSignedInt();
var editOfferType = opts.getOfferEditType();
client.editOffer(offerId,
fixedPrice,
isUsingMktPriceMargin,
marketPriceMargin.doubleValue(),
triggerPrice,
enable,
editOfferType);
out.println("offer has been edited");
return;
}
case canceloffer: { case canceloffer: {
var opts = new CancelOfferOptionParser(args).parse(); var opts = new CancelOfferOptionParser(args).parse();
if (opts.isForHelp()) { if (opts.isForHelp()) {
@ -486,7 +509,7 @@ public class CliMain {
} }
var tradeId = opts.getTradeId(); var tradeId = opts.getTradeId();
var address = opts.getAddress(); var address = opts.getAddress();
// Multi-word memos must be double quoted. // Multi-word memos must be double-quoted.
var memo = opts.getMemo(); var memo = opts.getMemo();
client.withdrawFunds(tradeId, address, memo); client.withdrawFunds(tradeId, address, memo);
out.printf("trade %s funds sent to btc address %s%n", tradeId, address); out.printf("trade %s funds sent to btc address %s%n", tradeId, address);
@ -754,6 +777,13 @@ public class CliMain {
stream.format(rowFormat, "", "--fixed-price=<price> | --market-price=margin=<percent> \\", ""); stream.format(rowFormat, "", "--fixed-price=<price> | --market-price=margin=<percent> \\", "");
stream.format(rowFormat, "", "--security-deposit=<percent> \\", ""); stream.format(rowFormat, "", "--security-deposit=<percent> \\", "");
stream.format(rowFormat, "", "[--fee-currency=<bsq|btc>]", ""); stream.format(rowFormat, "", "[--fee-currency=<bsq|btc>]", "");
stream.format(rowFormat, "", "[--trigger-price=<price>]", "");
stream.println();
stream.format(rowFormat, editoffer.name(), "--offer-id=<offer-id> \\", "Edit offer with id");
stream.format(rowFormat, "", "[--fixed-price=<price>] \\", "");
stream.format(rowFormat, "", "[--market-price=margin=<percent>] \\", "");
stream.format(rowFormat, "", "[--trigger-price=<price>] \\", "");
stream.format(rowFormat, "", "[--enabled=<true|false>]", "");
stream.println(); stream.println();
stream.format(rowFormat, canceloffer.name(), "--offer-id=<offer-id>", "Cancel offer with id"); stream.format(rowFormat, canceloffer.name(), "--offer-id=<offer-id>", "Cancel offer with id");
stream.println(); stream.println();

View file

@ -46,6 +46,7 @@ class ColumnHeaderConstants {
static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' '); static final String COL_HEADER_CREATION_DATE = padEnd("Creation Date (UTC)", 20, ' ');
static final String COL_HEADER_CURRENCY = "Currency"; static final String COL_HEADER_CURRENCY = "Currency";
static final String COL_HEADER_DIRECTION = "Buy/Sell"; static final String COL_HEADER_DIRECTION = "Buy/Sell";
static final String COL_HEADER_ENABLED = "Enabled";
static final String COL_HEADER_NAME = "Name"; static final String COL_HEADER_NAME = "Name";
static final String COL_HEADER_PAYMENT_METHOD = "Payment Method"; 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 = "Price in %-3s for 1 BTC";
@ -64,7 +65,7 @@ class ColumnHeaderConstants {
static final String COL_HEADER_TRADE_TX_FEE = padEnd("Tx Fee(BTC)", 12, ' '); 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_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_TRADE_TAKER_FEE = padEnd("Taker Fee(%-3s)", 12, ' '); // "Taker Fee(%-3s)";
static final String COL_HEADER_TRIGGER_PRICE = "Trigger Price(%-3s)";
static final String COL_HEADER_TX_ID = "Tx ID"; static final String COL_HEADER_TX_ID = "Tx ID";
static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)"; static final String COL_HEADER_TX_INPUT_SUM = "Tx Inputs (BTC)";
static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)"; static final String COL_HEADER_TX_OUTPUT_SUM = "Tx Outputs (BTC)";

View file

@ -22,6 +22,7 @@ import bisq.proto.grpc.TxFeeRateInfo;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -35,15 +36,22 @@ import static java.math.RoundingMode.UNNECESSARY;
@VisibleForTesting @VisibleForTesting
public class CurrencyFormat { public class CurrencyFormat {
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(Locale.US); // Use the US locale for all DecimalFormat objects.
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US);
// Formats numbers in US locale, human friendly style.
private static final NumberFormat FRIENDLY_NUMBER_FORMAT = NumberFormat.getInstance(Locale.US);
// Formats numbers for internal use, i.e., grpc request parameters.
private static final DecimalFormat INTERNAL_FIAT_DECIMAL_FORMAT = new DecimalFormat("##############0.0000");
static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000); static final BigDecimal SATOSHI_DIVISOR = new BigDecimal(100_000_000);
static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000"); static final DecimalFormat BTC_FORMAT = new DecimalFormat("###,##0.00000000", DECIMAL_FORMAT_SYMBOLS);
static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0"); static final DecimalFormat BTC_TX_FEE_FORMAT = new DecimalFormat("###,###,##0", DECIMAL_FORMAT_SYMBOLS);
static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100); static final BigDecimal BSQ_SATOSHI_DIVISOR = new BigDecimal(100);
static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00"); static final DecimalFormat BSQ_FORMAT = new DecimalFormat("###,###,###,##0.00", DECIMAL_FORMAT_SYMBOLS);
static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00"); static final DecimalFormat SEND_BSQ_FORMAT = new DecimalFormat("###########0.00", DECIMAL_FORMAT_SYMBOLS);
static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01"); static final BigDecimal SECURITY_DEPOSIT_MULTIPLICAND = new BigDecimal("0.01");
@ -58,10 +66,9 @@ public class CurrencyFormat {
} }
public static String formatBsqAmount(long bsqSats) { public static String formatBsqAmount(long bsqSats) {
// BSQ sats = trade.getOffer().getVolume() FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(2);
NUMBER_FORMAT.setMinimumFractionDigits(2); FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setMaximumFractionDigits(2); FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP);
NUMBER_FORMAT.setRoundingMode(HALF_UP);
return SEND_BSQ_FORMAT.format((double) bsqSats / SATOSHI_DIVISOR.doubleValue()); return SEND_BSQ_FORMAT.format((double) bsqSats / SATOSHI_DIVISOR.doubleValue());
} }
@ -95,38 +102,48 @@ public class CurrencyFormat {
: formatCryptoCurrencyOfferVolume(volume); : formatCryptoCurrencyOfferVolume(volume);
} }
public static String formatMarketPrice(double price) { public static String formatInternalFiatPrice(BigDecimal price) {
NUMBER_FORMAT.setMinimumFractionDigits(4); INTERNAL_FIAT_DECIMAL_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setMaximumFractionDigits(4); INTERNAL_FIAT_DECIMAL_FORMAT.setMaximumFractionDigits(4);
return NUMBER_FORMAT.format(price); return INTERNAL_FIAT_DECIMAL_FORMAT.format(price);
}
public static String formatInternalFiatPrice(double price) {
FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(4);
FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(4);
return FRIENDLY_NUMBER_FORMAT.format(price);
} }
public static String formatPrice(long price) { public static String formatPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(4); FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(4);
NUMBER_FORMAT.setMaximumFractionDigits(4); FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(4);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY); FRIENDLY_NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / 10_000); return FRIENDLY_NUMBER_FORMAT.format((double) price / 10_000);
} }
public static String formatCryptoCurrencyPrice(long price) { public static String formatCryptoCurrencyPrice(long price) {
NUMBER_FORMAT.setMinimumFractionDigits(8); FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(8);
NUMBER_FORMAT.setMaximumFractionDigits(8); FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(8);
NUMBER_FORMAT.setRoundingMode(UNNECESSARY); FRIENDLY_NUMBER_FORMAT.setRoundingMode(UNNECESSARY);
return NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue()); return FRIENDLY_NUMBER_FORMAT.format((double) price / SATOSHI_DIVISOR.doubleValue());
} }
public static String formatOfferVolume(long volume) { public static String formatOfferVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(0); FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(0);
NUMBER_FORMAT.setMaximumFractionDigits(0); FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(0);
NUMBER_FORMAT.setRoundingMode(HALF_UP); FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) volume / 10_000); return FRIENDLY_NUMBER_FORMAT.format((double) volume / 10_000);
} }
public static String formatCryptoCurrencyOfferVolume(long volume) { public static String formatCryptoCurrencyOfferVolume(long volume) {
NUMBER_FORMAT.setMinimumFractionDigits(2); FRIENDLY_NUMBER_FORMAT.setMinimumFractionDigits(2);
NUMBER_FORMAT.setMaximumFractionDigits(2); FRIENDLY_NUMBER_FORMAT.setMaximumFractionDigits(2);
NUMBER_FORMAT.setRoundingMode(HALF_UP); FRIENDLY_NUMBER_FORMAT.setRoundingMode(HALF_UP);
return NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue()); return FRIENDLY_NUMBER_FORMAT.format((double) volume / SATOSHI_DIVISOR.doubleValue());
}
public static long toInternalFiatPrice(BigDecimal humanFriendlyFiatPrice) {
return humanFriendlyFiatPrice.multiply(new BigDecimal(10_000)).longValue();
} }
public static long toSatoshis(String btc) { public static long toSatoshis(String btc) {

View file

@ -21,63 +21,31 @@ import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo; import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo; import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo; 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.GetMethodHelpRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.GetVersionRequest; import bisq.proto.grpc.GetVersionRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.OfferInfo; import bisq.proto.grpc.OfferInfo;
import bisq.proto.grpc.RegisterDisputeAgentRequest; import bisq.proto.grpc.RegisterDisputeAgentRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
import bisq.proto.grpc.StopRequest; import bisq.proto.grpc.StopRequest;
import bisq.proto.grpc.TakeOfferReply; import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo; import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.TxFeeRateInfo; import bisq.proto.grpc.TxFeeRateInfo;
import bisq.proto.grpc.TxInfo; 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; import protobuf.PaymentAccount;
import protobuf.PaymentMethod; import protobuf.PaymentMethod;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.cli.CryptoCurrencyUtil.isSupportedCryptoCurrency; import static bisq.proto.grpc.EditOfferRequest.EditType;
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; import bisq.cli.request.OffersServiceRequest;
import bisq.cli.request.PaymentAccountsServiceRequest;
import bisq.cli.request.TradesServiceRequest;
import bisq.cli.request.WalletsServiceRequest;
@SuppressWarnings("ResultOfMethodCallIgnored") @SuppressWarnings("ResultOfMethodCallIgnored")
@ -85,9 +53,19 @@ import static protobuf.OfferPayload.Direction.SELL;
public final class GrpcClient { public final class GrpcClient {
private final GrpcStubs grpcStubs; private final GrpcStubs grpcStubs;
private final OffersServiceRequest offersServiceRequest;
private final TradesServiceRequest tradesServiceRequest;
private final WalletsServiceRequest walletsServiceRequest;
private final PaymentAccountsServiceRequest paymentAccountsServiceRequest;
public GrpcClient(String apiHost, int apiPort, String apiPassword) { public GrpcClient(String apiHost,
int apiPort,
String apiPassword) {
this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword); this.grpcStubs = new GrpcStubs(apiHost, apiPort, apiPassword);
this.offersServiceRequest = new OffersServiceRequest(grpcStubs);
this.tradesServiceRequest = new TradesServiceRequest(grpcStubs);
this.walletsServiceRequest = new WalletsServiceRequest(grpcStubs);
this.paymentAccountsServiceRequest = new PaymentAccountsServiceRequest(grpcStubs);
} }
public String getVersion() { public String getVersion() {
@ -96,108 +74,67 @@ public final class GrpcClient {
} }
public BalancesInfo getBalances() { public BalancesInfo getBalances() {
return getBalances(""); return walletsServiceRequest.getBalances();
} }
public BsqBalanceInfo getBsqBalances() { public BsqBalanceInfo getBsqBalances() {
return getBalances("BSQ").getBsq(); return walletsServiceRequest.getBsqBalances();
} }
public BtcBalanceInfo getBtcBalances() { public BtcBalanceInfo getBtcBalances() {
return getBalances("BTC").getBtc(); return walletsServiceRequest.getBtcBalances();
} }
public BalancesInfo getBalances(String currencyCode) { public BalancesInfo getBalances(String currencyCode) {
var request = GetBalancesRequest.newBuilder() return walletsServiceRequest.getBalances(currencyCode);
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.walletsService.getBalances(request).getBalances();
} }
public AddressBalanceInfo getAddressBalance(String address) { public AddressBalanceInfo getAddressBalance(String address) {
var request = GetAddressBalanceRequest.newBuilder() return walletsServiceRequest.getAddressBalance(address);
.setAddress(address).build();
return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo();
} }
public double getBtcPrice(String currencyCode) { public double getBtcPrice(String currencyCode) {
var request = MarketPriceRequest.newBuilder() return walletsServiceRequest.getBtcPrice(currencyCode);
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.priceService.getMarketPrice(request).getPrice();
} }
public List<AddressBalanceInfo> getFundingAddresses() { public List<AddressBalanceInfo> getFundingAddresses() {
var request = GetFundingAddressesRequest.newBuilder().build(); return walletsServiceRequest.getFundingAddresses();
return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList();
} }
public String getUnusedBsqAddress() { public String getUnusedBsqAddress() {
var request = GetUnusedBsqAddressRequest.newBuilder().build(); return walletsServiceRequest.getUnusedBsqAddress();
return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress();
} }
public String getUnusedBtcAddress() { public String getUnusedBtcAddress() {
var request = GetFundingAddressesRequest.newBuilder().build(); return walletsServiceRequest.getUnusedBtcAddress();
var addressBalances = grpcStubs.walletsService.getFundingAddresses(request)
.getAddressBalanceInfoList();
//noinspection OptionalGetWithoutIsPresent
return addressBalances.stream()
.filter(AddressBalanceInfo::getIsAddressUnused)
.findFirst()
.get()
.getAddress();
} }
public TxInfo sendBsq(String address, String amount, String txFeeRate) { public TxInfo sendBsq(String address, String amount, String txFeeRate) {
var request = SendBsqRequest.newBuilder() return walletsServiceRequest.sendBsq(address, amount, txFeeRate);
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
return grpcStubs.walletsService.sendBsq(request).getTxInfo();
} }
public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) { public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) {
var request = SendBtcRequest.newBuilder() return walletsServiceRequest.sendBtc(address, amount, txFeeRate, memo);
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
return grpcStubs.walletsService.sendBtc(request).getTxInfo();
} }
public boolean verifyBsqSentToAddress(String address, String amount) { public boolean verifyBsqSentToAddress(String address, String amount) {
var request = VerifyBsqSentToAddressRequest.newBuilder() return walletsServiceRequest.verifyBsqSentToAddress(address, amount);
.setAddress(address)
.setAmount(amount)
.build();
return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived();
} }
public TxFeeRateInfo getTxFeeRate() { public TxFeeRateInfo getTxFeeRate() {
var request = GetTxFeeRateRequest.newBuilder().build(); return walletsServiceRequest.getTxFeeRate();
return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
} }
public TxFeeRateInfo setTxFeeRate(long txFeeRate) { public TxFeeRateInfo setTxFeeRate(long txFeeRate) {
var request = SetTxFeeRatePreferenceRequest.newBuilder() return walletsServiceRequest.setTxFeeRate(txFeeRate);
.setTxFeeRatePreference(txFeeRate)
.build();
return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo();
} }
public TxFeeRateInfo unsetTxFeeRate() { public TxFeeRateInfo unsetTxFeeRate() {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build(); return walletsServiceRequest.unsetTxFeeRate();
return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo();
} }
public TxInfo getTransaction(String txId) { public TxInfo getTransaction(String txId) {
var request = GetTransactionRequest.newBuilder() return walletsServiceRequest.getTransaction(txId);
.setTxId(txId)
.build();
return grpcStubs.walletsService.getTransaction(request).getTxInfo();
} }
public OfferInfo createFixedPricedOffer(String direction, public OfferInfo createFixedPricedOffer(String direction,
@ -208,7 +145,7 @@ public final class GrpcClient {
double securityDeposit, double securityDeposit,
String paymentAcctId, String paymentAcctId,
String makerFeeCurrencyCode) { String makerFeeCurrencyCode) {
return createOffer(direction, return offersServiceRequest.createOffer(direction,
currencyCode, currencyCode,
amount, amount,
minAmount, minAmount,
@ -217,7 +154,8 @@ public final class GrpcClient {
0.00, 0.00,
securityDeposit, securityDeposit,
paymentAcctId, paymentAcctId,
makerFeeCurrencyCode); makerFeeCurrencyCode,
0 /* no trigger price */);
} }
public OfferInfo createMarketBasedPricedOffer(String direction, public OfferInfo createMarketBasedPricedOffer(String direction,
@ -227,8 +165,9 @@ public final class GrpcClient {
double marketPriceMargin, double marketPriceMargin,
double securityDeposit, double securityDeposit,
String paymentAcctId, String paymentAcctId,
String makerFeeCurrencyCode) { String makerFeeCurrencyCode,
return createOffer(direction, long triggerPrice) {
return offersServiceRequest.createOffer(direction,
currencyCode, currencyCode,
amount, amount,
minAmount, minAmount,
@ -237,7 +176,8 @@ public final class GrpcClient {
marketPriceMargin, marketPriceMargin,
securityDeposit, securityDeposit,
paymentAcctId, paymentAcctId,
makerFeeCurrencyCode); makerFeeCurrencyCode,
triggerPrice);
} }
public OfferInfo createOffer(String direction, public OfferInfo createOffer(String direction,
@ -249,253 +189,192 @@ public final class GrpcClient {
double marketPriceMargin, double marketPriceMargin,
double securityDeposit, double securityDeposit,
String paymentAcctId, String paymentAcctId,
String makerFeeCurrencyCode) { String makerFeeCurrencyCode,
var request = CreateOfferRequest.newBuilder() long triggerPrice) {
.setDirection(direction) return offersServiceRequest.createOffer(direction,
.setCurrencyCode(currencyCode) currencyCode,
.setAmount(amount) amount,
.setMinAmount(minAmount) minAmount,
.setUseMarketBasedPrice(useMarketBasedPrice) useMarketBasedPrice,
.setPrice(fixedPrice) fixedPrice,
.setMarketPriceMargin(marketPriceMargin) marketPriceMargin,
.setBuyerSecurityDeposit(securityDeposit) securityDeposit,
.setPaymentAccountId(paymentAcctId) paymentAcctId,
.setMakerFeeCurrencyCode(makerFeeCurrencyCode) makerFeeCurrencyCode,
.build(); triggerPrice);
return grpcStubs.offersService.createOffer(request).getOffer(); }
public void editOfferActivationState(String offerId, int enable) {
offersServiceRequest.editOfferActivationState(offerId, enable);
}
public void editOfferFixedPrice(String offerId, String priceAsString) {
offersServiceRequest.editOfferFixedPrice(offerId, priceAsString);
}
public void editOfferPriceMargin(String offerId, double marketPriceMargin) {
offersServiceRequest.editOfferPriceMargin(offerId, marketPriceMargin);
}
public void editOfferTriggerPrice(String offerId, long triggerPrice) {
offersServiceRequest.editOfferTriggerPrice(offerId, triggerPrice);
}
public void editOffer(String offerId,
String priceAsString,
boolean useMarketBasedPrice,
double marketPriceMargin,
long triggerPrice,
int enable,
EditType editType) {
// Take care when using this method directly:
// useMarketBasedPrice = true if margin based offer, false for fixed priced offer
// scaledPriceString fmt = ######.####
offersServiceRequest.editOffer(offerId,
priceAsString,
useMarketBasedPrice,
marketPriceMargin,
triggerPrice,
enable,
editType);
} }
public void cancelOffer(String offerId) { public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder() offersServiceRequest.cancelOffer(offerId);
.setId(offerId)
.build();
grpcStubs.offersService.cancelOffer(request);
} }
public OfferInfo getOffer(String offerId) { public OfferInfo getOffer(String offerId) {
var request = GetOfferRequest.newBuilder() return offersServiceRequest.getOffer(offerId);
.setId(offerId)
.build();
return grpcStubs.offersService.getOffer(request).getOffer();
} }
public OfferInfo getMyOffer(String offerId) { public OfferInfo getMyOffer(String offerId) {
var request = GetMyOfferRequest.newBuilder() return offersServiceRequest.getMyOffer(offerId);
.setId(offerId)
.build();
return grpcStubs.offersService.getMyOffer(request).getOffer();
} }
public List<OfferInfo> getOffers(String direction, String currencyCode) { public List<OfferInfo> getOffers(String direction, String currencyCode) {
if (isSupportedCryptoCurrency(currencyCode)) { return offersServiceRequest.getOffers(direction, currencyCode);
return getCryptoCurrencyOffers(direction, currencyCode);
} else {
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getOffers(request).getOffersList();
}
} }
public List<OfferInfo> getCryptoCurrencyOffers(String direction, String currencyCode) { public List<OfferInfo> getCryptoCurrencyOffers(String direction, String currencyCode) {
return getOffers(direction, "BTC").stream() return offersServiceRequest.getCryptoCurrencyOffers(direction, currencyCode);
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
} }
public List<OfferInfo> getOffersSortedByDate(String currencyCode) { public List<OfferInfo> getOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>(); return offersServiceRequest.getOffersSortedByDate(currencyCode);
offers.addAll(getOffers(BUY.name(), currencyCode));
offers.addAll(getOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
} }
public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) { public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
var offers = getOffers(direction, currencyCode); return offersServiceRequest.getOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
} }
public List<OfferInfo> getBsqOffersSortedByDate() { public List<OfferInfo> getBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>(); return offersServiceRequest.getBsqOffersSortedByDate();
offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ"));
offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ"));
return sortOffersByDate(offers);
} }
public List<OfferInfo> getMyOffers(String direction, String currencyCode) { public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
if (isSupportedCryptoCurrency(currencyCode)) { return offersServiceRequest.getMyOffers(direction, currencyCode);
return getMyCryptoCurrencyOffers(direction, currencyCode);
} else {
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getMyOffers(request).getOffersList();
}
} }
public List<OfferInfo> getMyCryptoCurrencyOffers(String direction, String currencyCode) { public List<OfferInfo> getMyCryptoCurrencyOffers(String direction, String currencyCode) {
return getMyOffers(direction, "BTC").stream() return offersServiceRequest.getMyCryptoCurrencyOffers(direction, currencyCode);
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
} }
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) { public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode); return offersServiceRequest.getMyOffersSortedByDate(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
} }
public List<OfferInfo> getMyOffersSortedByDate(String currencyCode) { public List<OfferInfo> getMyOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>(); return offersServiceRequest.getMyOffersSortedByDate(currencyCode);
offers.addAll(getMyOffers(BUY.name(), currencyCode));
offers.addAll(getMyOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
} }
public List<OfferInfo> getMyBsqOffersSortedByDate() { public List<OfferInfo> getMyBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>(); return offersServiceRequest.getMyBsqOffersSortedByDate();
offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ"));
offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ"));
return sortOffersByDate(offers);
} }
public OfferInfo getMostRecentOffer(String direction, String currencyCode) { public OfferInfo getMostRecentOffer(String direction, String currencyCode) {
List<OfferInfo> offers = getOffersSortedByDate(direction, currencyCode); return offersServiceRequest.getMostRecentOffer(direction, currencyCode);
return offers.isEmpty() ? null : offers.get(offers.size() - 1);
} }
public List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) { public List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream() return offersServiceRequest.sortOffersByDate(offerInfoList);
.sorted(comparing(OfferInfo::getDate))
.collect(toList());
} }
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var request = TakeOfferRequest.newBuilder() return tradesServiceRequest.getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode);
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
return grpcStubs.tradesService.takeOffer(request);
} }
public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) { public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode); return tradesServiceRequest.takeOffer(offerId, paymentAccountId, takerFeeCurrencyCode);
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getFailureReason().getDescription());
} }
public TradeInfo getTrade(String tradeId) { public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder() return tradesServiceRequest.getTrade(tradeId);
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
} }
public void confirmPaymentStarted(String tradeId) { public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder() tradesServiceRequest.confirmPaymentStarted(tradeId);
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentStarted(request);
} }
public void confirmPaymentReceived(String tradeId) { public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder() tradesServiceRequest.confirmPaymentReceived(tradeId);
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentReceived(request);
} }
public void keepFunds(String tradeId) { public void keepFunds(String tradeId) {
var request = KeepFundsRequest.newBuilder() tradesServiceRequest.keepFunds(tradeId);
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.keepFunds(request);
} }
public void withdrawFunds(String tradeId, String address, String memo) { public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder() tradesServiceRequest.withdrawFunds(tradeId, address, memo);
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
grpcStubs.tradesService.withdrawFunds(request);
} }
public List<PaymentMethod> getPaymentMethods() { public List<PaymentMethod> getPaymentMethods() {
var request = GetPaymentMethodsRequest.newBuilder().build(); return paymentAccountsServiceRequest.getPaymentMethods();
return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList();
} }
public String getPaymentAcctFormAsJson(String paymentMethodId) { public String getPaymentAcctFormAsJson(String paymentMethodId) {
var request = GetPaymentAccountFormRequest.newBuilder() return paymentAccountsServiceRequest.getPaymentAcctFormAsJson(paymentMethodId);
.setPaymentMethodId(paymentMethodId)
.build();
return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson();
} }
public PaymentAccount createPaymentAccount(String json) { public PaymentAccount createPaymentAccount(String json) {
var request = CreatePaymentAccountRequest.newBuilder() return paymentAccountsServiceRequest.createPaymentAccount(json);
.setPaymentAccountForm(json)
.build();
return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount();
} }
public List<PaymentAccount> getPaymentAccounts() { public List<PaymentAccount> getPaymentAccounts() {
var request = GetPaymentAccountsRequest.newBuilder().build(); return paymentAccountsServiceRequest.getPaymentAccounts();
return grpcStubs.paymentAccountsService.getPaymentAccounts(request).getPaymentAccountsList();
} }
public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName, public PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode, String currencyCode,
String address, String address,
boolean tradeInstant) { boolean tradeInstant) {
var request = CreateCryptoCurrencyPaymentAccountRequest.newBuilder() return paymentAccountsServiceRequest.createCryptoCurrencyPaymentAccount(accountName,
.setAccountName(accountName) currencyCode,
.setCurrencyCode(currencyCode) address,
.setAddress(address) tradeInstant);
.setTradeInstant(tradeInstant)
.build();
return grpcStubs.paymentAccountsService.createCryptoCurrencyPaymentAccount(request).getPaymentAccount();
} }
public List<PaymentMethod> getCryptoPaymentMethods() { public List<PaymentMethod> getCryptoPaymentMethods() {
var request = GetCryptoCurrencyPaymentMethodsRequest.newBuilder().build(); return paymentAccountsServiceRequest.getCryptoPaymentMethods();
return grpcStubs.paymentAccountsService.getCryptoCurrencyPaymentMethods(request).getPaymentMethodsList();
} }
public void lockWallet() { public void lockWallet() {
var request = LockWalletRequest.newBuilder().build(); walletsServiceRequest.lockWallet();
grpcStubs.walletsService.lockWallet(request);
} }
public void unlockWallet(String walletPassword, long timeout) { public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder() walletsServiceRequest.unlockWallet(walletPassword, timeout);
.setPassword(walletPassword)
.setTimeout(timeout).build();
grpcStubs.walletsService.unlockWallet(request);
} }
public void removeWalletPassword(String walletPassword) { public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder() walletsServiceRequest.removeWalletPassword(walletPassword);
.setPassword(walletPassword).build();
grpcStubs.walletsService.removeWalletPassword(request);
} }
public void setWalletPassword(String walletPassword) { public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder() walletsServiceRequest.setWalletPassword(walletPassword);
.setPassword(walletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
} }
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) { public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder() walletsServiceRequest.setWalletPassword(oldWalletPassword, newWalletPassword);
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
} }
public void registerDisputeAgent(String disputeAgentType, String registrationKey) { public void registerDisputeAgent(String disputeAgentType, String registrationKey) {

View file

@ -25,6 +25,7 @@ public enum Method {
confirmpaymentreceived, confirmpaymentreceived,
confirmpaymentstarted, confirmpaymentstarted,
createoffer, createoffer,
editoffer,
createpaymentacct, createpaymentacct,
createcryptopaymentacct, createcryptopaymentacct,
getaddressbalance, getaddressbalance,

View file

@ -147,58 +147,126 @@ public class TableFormat {
public static String formatOfferTable(List<OfferInfo> offers, String currencyCode) { public static String formatOfferTable(List<OfferInfo> offers, String currencyCode) {
if (offers == null || offers.isEmpty()) if (offers == null || offers.isEmpty())
throw new IllegalArgumentException(format("%s offers argument is empty", currencyCode.toLowerCase())); throw new IllegalArgumentException(format("%s offer list is empty", currencyCode.toLowerCase()));
String baseCurrencyCode = offers.get(0).getBaseCurrencyCode(); String baseCurrencyCode = offers.get(0).getBaseCurrencyCode();
boolean isMyOffer = offers.get(0).getIsMyOffer();
return baseCurrencyCode.equalsIgnoreCase("BTC") return baseCurrencyCode.equalsIgnoreCase("BTC")
? formatFiatOfferTable(offers, currencyCode) ? formatFiatOfferTable(offers, currencyCode, isMyOffer)
: formatCryptoCurrencyOfferTable(offers, baseCurrencyCode); : formatCryptoCurrencyOfferTable(offers, baseCurrencyCode, isMyOffer);
} }
private static String formatFiatOfferTable(List<OfferInfo> offers, String fiatCurrencyCode) { private static String formatFiatOfferTable(List<OfferInfo> offers,
String fiatCurrencyCode,
boolean isMyOffer) {
// Some column values might be longer than header, so we need to calculate them. // Some column values might be longer than header, so we need to calculate them.
int amountColWith = getLongestAmountColWidth(offers); int amountColWith = getLongestAmountColWidth(offers);
int volumeColWidth = getLongestVolumeColWidth(offers); int volumeColWidth = getLongestVolumeColWidth(offers);
int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers);
String headersFormat = COL_HEADER_DIRECTION + COL_HEADER_DELIMITER // "Enabled" and "Trigger Price" columns are displayed for my offers only.
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER // includes %s -> fiatCurrencyCode String enabledHeaderFormat = isMyOffer ?
COL_HEADER_ENABLED + COL_HEADER_DELIMITER
: "";
String triggerPriceHeaderFormat = isMyOffer ?
// COL_HEADER_TRIGGER_PRICE includes %s -> fiatCurrencyCode
COL_HEADER_TRIGGER_PRICE + COL_HEADER_DELIMITER
: "";
String headersFormat = enabledHeaderFormat
+ COL_HEADER_DIRECTION + COL_HEADER_DELIMITER
// COL_HEADER_PRICE includes %s -> fiatCurrencyCode
+ COL_HEADER_PRICE + COL_HEADER_DELIMITER
+ padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER
// COL_HEADER_VOLUME includes %s -> fiatCurrencyCode // COL_HEADER_VOLUME includes %s -> fiatCurrencyCode
+ padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER + padStart(COL_HEADER_VOLUME, volumeColWidth, ' ') + COL_HEADER_DELIMITER
+ triggerPriceHeaderFormat
+ padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER + padEnd(COL_HEADER_PAYMENT_METHOD, paymentMethodColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER + COL_HEADER_CREATION_DATE + COL_HEADER_DELIMITER
+ COL_HEADER_UUID.trim() + "%n"; + COL_HEADER_UUID.trim() + "%n";
String headerLine = format(headersFormat, String headerLine = format(headersFormat,
fiatCurrencyCode.toUpperCase(), fiatCurrencyCode.toUpperCase(),
fiatCurrencyCode.toUpperCase()); fiatCurrencyCode.toUpperCase(),
String colDataFormat = "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s" // COL_HEADER_TRIGGER_PRICE includes %s -> fiatCurrencyCode
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s" isMyOffer ? fiatCurrencyCode.toUpperCase() : "");
+ " %" + amountColWith + "s" String colDataFormat = getFiatOfferColDataFormat(isMyOffer,
+ " %" + (volumeColWidth - 1) + "s" amountColWith,
+ " %-" + paymentMethodColWidth + "s" volumeColWidth,
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" paymentMethodColWidth);
+ " %-" + COL_HEADER_UUID.length() + "s"; return formattedFiatOfferTable(offers, isMyOffer, headerLine, colDataFormat);
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) { private static String formattedFiatOfferTable(List<OfferInfo> offers,
boolean isMyOffer,
String headerLine,
String colDataFormat) {
if (isMyOffer) {
return headerLine
+ offers.stream()
.map(o -> format(colDataFormat,
formatEnabled(o),
o.getDirection(),
formatPrice(o.getPrice()),
formatAmountRange(o.getMinAmount(), o.getAmount()),
formatVolumeRange(o.getMinVolume(), o.getVolume()),
o.getTriggerPrice() == 0 ? "" : formatPrice(o.getTriggerPrice()),
o.getPaymentMethodShortName(),
formatTimestamp(o.getDate()),
o.getId()))
.collect(Collectors.joining("\n"));
} else {
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 getFiatOfferColDataFormat(boolean isMyOffer,
int amountColWith,
int volumeColWidth,
int paymentMethodColWidth) {
if (isMyOffer) {
return "%-" + (COL_HEADER_ENABLED.length() + COL_HEADER_DELIMITER.length()) + "s"
+ "%-" + (COL_HEADER_DIRECTION.length() + COL_HEADER_DELIMITER.length()) + "s"
+ "%" + (COL_HEADER_PRICE.length() - 1) + "s"
+ " %" + amountColWith + "s"
+ " %" + (volumeColWidth - 1) + "s"
+ " %" + (COL_HEADER_TRIGGER_PRICE.length() - 1) + "s"
+ " %-" + paymentMethodColWidth + "s"
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
+ " %-" + COL_HEADER_UUID.length() + "s";
} else {
return "%-" + (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";
}
}
private static String formatCryptoCurrencyOfferTable(List<OfferInfo> offers,
String cryptoCurrencyCode,
boolean isMyOffer) {
// Some column values might be longer than header, so we need to calculate them. // Some column values might be longer than header, so we need to calculate them.
int directionColWidth = getLongestDirectionColWidth(offers); int directionColWidth = getLongestDirectionColWidth(offers);
int amountColWith = getLongestAmountColWidth(offers); int amountColWith = getLongestAmountColWidth(offers);
int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers); int volumeColWidth = getLongestCryptoCurrencyVolumeColWidth(offers);
int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers); int paymentMethodColWidth = getLongestPaymentMethodColWidth(offers);
// "Enabled" column is displayed for my offers only.
String enabledHeaderFormat = isMyOffer ?
COL_HEADER_ENABLED + COL_HEADER_DELIMITER
: "";
// TODO use memoize function to avoid duplicate the formatting done above? // TODO use memoize function to avoid duplicate the formatting done above?
String headersFormat = padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER String headersFormat = enabledHeaderFormat
+ padEnd(COL_HEADER_DIRECTION, directionColWidth, ' ') + COL_HEADER_DELIMITER
+ COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode + COL_HEADER_PRICE_OF_ALTCOIN + COL_HEADER_DELIMITER // includes %s -> cryptoCurrencyCode
+ padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER + padStart(COL_HEADER_AMOUNT, amountColWith, ' ') + COL_HEADER_DELIMITER
// COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode // COL_HEADER_VOLUME includes %s -> cryptoCurrencyCode
@ -209,24 +277,59 @@ public class TableFormat {
String headerLine = format(headersFormat, String headerLine = format(headersFormat,
cryptoCurrencyCode.toUpperCase(), cryptoCurrencyCode.toUpperCase(),
cryptoCurrencyCode.toUpperCase()); cryptoCurrencyCode.toUpperCase());
String colDataFormat = "%-" + directionColWidth + "s" String colDataFormat;
+ "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s" if (isMyOffer) {
+ " %" + amountColWith + "s" colDataFormat = "%-" + (COL_HEADER_ENABLED.length() + COL_HEADER_DELIMITER.length()) + "s"
+ " %" + (volumeColWidth - 1) + "s" + "%-" + directionColWidth + "s"
+ " %-" + paymentMethodColWidth + "s" + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s"
+ " %-" + (COL_HEADER_CREATION_DATE.length()) + "s" + " %" + amountColWith + "s"
+ " %-" + COL_HEADER_UUID.length() + "s"; + " %" + (volumeColWidth - 1) + "s"
return headerLine + " %-" + paymentMethodColWidth + "s"
+ offers.stream() + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
.map(o -> format(colDataFormat, + " %-" + COL_HEADER_UUID.length() + "s";
directionFormat.apply(o), } else {
formatCryptoCurrencyPrice(o.getPrice()), colDataFormat = "%-" + directionColWidth + "s"
formatAmountRange(o.getMinAmount(), o.getAmount()), + "%" + (COL_HEADER_PRICE_OF_ALTCOIN.length() + 1) + "s"
formatCryptoCurrencyVolumeRange(o.getMinVolume(), o.getVolume()), + " %" + amountColWith + "s"
o.getPaymentMethodShortName(), + " %" + (volumeColWidth - 1) + "s"
formatTimestamp(o.getDate()), + " %-" + paymentMethodColWidth + "s"
o.getId())) + " %-" + (COL_HEADER_CREATION_DATE.length()) + "s"
.collect(Collectors.joining("\n")); + " %-" + COL_HEADER_UUID.length() + "s";
}
if (isMyOffer) {
return headerLine
+ offers.stream()
.map(o -> format(colDataFormat,
formatEnabled(o),
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"));
} else {
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 String formatEnabled(OfferInfo offerInfo) {
if (offerInfo.getIsMyOffer() && offerInfo.getIsMyPendingOffer())
return "PENDING";
else
return offerInfo.getIsActivated() ? "YES" : "NO";
} }
private static int getLongestPaymentMethodColWidth(List<OfferInfo> offers) { private static int getLongestPaymentMethodColWidth(List<OfferInfo> offers) {

View file

@ -24,6 +24,7 @@ import joptsimple.OptionSpec;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
import lombok.Getter; import lombok.Getter;
@ -64,6 +65,9 @@ abstract class AbstractMethodOptionParser implements MethodOpts {
return options.has(helpOpt); return options.has(helpOpt);
} }
protected final Predicate<OptionSpec<String>> valueNotSpecified = (opt) ->
!options.hasArgument(opt) || options.valueOf(opt).isEmpty();
private final Function<OptionException, String> cliExceptionMessageStyle = (ex) -> { private final Function<OptionException, String> cliExceptionMessageStyle = (ex) -> {
if (ex.getMessage() == null) if (ex.getMessage() == null)
return null; return null;

View file

@ -0,0 +1,281 @@
/*
* 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 bisq.proto.grpc.EditOfferRequest;
import joptsimple.OptionSpec;
import java.math.BigDecimal;
import static bisq.cli.opts.OptLabel.*;
import static bisq.proto.grpc.EditOfferRequest.EditType.*;
import static java.lang.String.format;
import org.checkerframework.checker.nullness.qual.Nullable;
public class EditOfferOptionParser extends AbstractMethodOptionParser implements MethodOpts {
static int OPT_ENABLE_ON = 1;
static int OPT_ENABLE_OFF = 0;
static int OPT_ENABLE_IGNORED = -1;
final OptionSpec<String> offerIdOpt = parser.accepts(OPT_OFFER_ID, "id of offer to cancel")
.withRequiredArg();
final OptionSpec<String> fixedPriceOpt = parser.accepts(OPT_FIXED_PRICE, "fixed btc price")
.withOptionalArg()
.defaultsTo("0");
final OptionSpec<String> mktPriceMarginOpt = parser.accepts(OPT_MKT_PRICE_MARGIN,
"market btc price margin (%)")
.withOptionalArg()
.defaultsTo("0.00");
final OptionSpec<String> triggerPriceOpt = parser.accepts(OPT_TRIGGER_PRICE,
"trigger price (applies to mkt price margin based offers)")
.withOptionalArg()
.defaultsTo("0");
// The 'enable' string opt is optional, and can be empty (meaning do not change
// activation state). For this reason, a boolean type is not used (can only be
// true or false).
final OptionSpec<String> enableOpt = parser.accepts(OPT_ENABLE,
"enable or disable offer")
.withOptionalArg()
.ofType(String.class);
private EditOfferRequest.EditType offerEditType;
public EditOfferOptionParser(String[] args) {
super(args);
}
public EditOfferOptionParser parse() {
super.parse();
// Short circuit opt validation if user just wants help.
if (options.has(helpOpt))
return this;
if (!options.has(offerIdOpt) || options.valueOf(offerIdOpt).isEmpty())
throw new IllegalArgumentException("no offer id specified");
boolean hasNoEditDetails = !options.has(fixedPriceOpt)
&& !options.has(mktPriceMarginOpt)
&& !options.has(triggerPriceOpt)
&& !options.has(enableOpt);
if (hasNoEditDetails)
throw new IllegalArgumentException("no edit details specified");
if (options.has(enableOpt)) {
if (valueNotSpecified.test(enableOpt))
throw new IllegalArgumentException("invalid enable value specified, must be true|false");
var enableOptValue = options.valueOf(enableOpt);
if (!enableOptValue.equalsIgnoreCase("true")
&& !enableOptValue.equalsIgnoreCase("false"))
throw new IllegalArgumentException("invalid enable value specified, must be true|false");
// A single enable opt is a valid opt combo.
boolean enableOptIsOnlyOpt = !options.has(fixedPriceOpt)
&& !options.has(mktPriceMarginOpt)
&& !options.has(triggerPriceOpt);
if (enableOptIsOnlyOpt) {
offerEditType = ACTIVATION_STATE_ONLY;
return this;
}
}
if (options.has(fixedPriceOpt)) {
if (valueNotSpecified.test(fixedPriceOpt))
throw new IllegalArgumentException("no fixed price specified");
String fixedPriceAsString = options.valueOf(fixedPriceOpt);
verifyStringIsValidDouble(fixedPriceAsString);
boolean fixedPriceOptIsOnlyOpt = !options.has(mktPriceMarginOpt)
&& !options.has(triggerPriceOpt)
&& !options.has(enableOpt);
if (fixedPriceOptIsOnlyOpt) {
offerEditType = FIXED_PRICE_ONLY;
return this;
}
boolean fixedPriceOptAndEnableOptAreOnlyOpts = options.has(enableOpt)
&& !options.has(mktPriceMarginOpt)
&& !options.has(triggerPriceOpt);
if (fixedPriceOptAndEnableOptAreOnlyOpts) {
offerEditType = FIXED_PRICE_AND_ACTIVATION_STATE;
return this;
}
}
if (options.has(mktPriceMarginOpt)) {
if (valueNotSpecified.test(mktPriceMarginOpt))
throw new IllegalArgumentException("no mkt price margin specified");
String priceMarginAsString = options.valueOf(mktPriceMarginOpt);
if (priceMarginAsString.isEmpty())
throw new IllegalArgumentException("no market price margin specified");
verifyStringIsValidDouble(priceMarginAsString);
boolean mktPriceMarginOptIsOnlyOpt = !options.has(triggerPriceOpt)
&& !options.has(fixedPriceOpt)
&& !options.has(enableOpt);
if (mktPriceMarginOptIsOnlyOpt) {
offerEditType = MKT_PRICE_MARGIN_ONLY;
return this;
}
boolean mktPriceMarginOptAndEnableOptAreOnlyOpts = options.has(enableOpt)
&& !options.has(triggerPriceOpt);
if (mktPriceMarginOptAndEnableOptAreOnlyOpts) {
offerEditType = MKT_PRICE_MARGIN_AND_ACTIVATION_STATE;
return this;
}
}
if (options.has(triggerPriceOpt)) {
if (valueNotSpecified.test(triggerPriceOpt))
throw new IllegalArgumentException("no trigger price specified");
String triggerPriceAsString = options.valueOf(fixedPriceOpt);
if (triggerPriceAsString.isEmpty())
throw new IllegalArgumentException("trigger price not specified");
verifyStringIsValidDouble(triggerPriceAsString);
boolean triggerPriceOptIsOnlyOpt = !options.has(mktPriceMarginOpt)
&& !options.has(fixedPriceOpt)
&& !options.has(enableOpt);
if (triggerPriceOptIsOnlyOpt) {
offerEditType = TRIGGER_PRICE_ONLY;
return this;
}
boolean triggerPriceOptAndEnableOptAreOnlyOpts = !options.has(mktPriceMarginOpt)
&& !options.has(fixedPriceOpt)
&& options.has(enableOpt);
if (triggerPriceOptAndEnableOptAreOnlyOpts) {
offerEditType = TRIGGER_PRICE_AND_ACTIVATION_STATE;
return this;
}
}
if (options.has(mktPriceMarginOpt) && options.has(fixedPriceOpt))
throw new IllegalArgumentException("cannot specify market price margin and fixed price");
if (options.has(fixedPriceOpt) && options.has(triggerPriceOpt))
throw new IllegalArgumentException("trigger price cannot be set on fixed price offers");
if (options.has(mktPriceMarginOpt) && options.has(triggerPriceOpt) && !options.has(enableOpt)) {
offerEditType = MKT_PRICE_MARGIN_AND_TRIGGER_PRICE;
return this;
}
if (options.has(mktPriceMarginOpt) && options.has(triggerPriceOpt) && options.has(enableOpt)) {
offerEditType = MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE;
return this;
}
return this;
}
public String getOfferId() {
return options.valueOf(offerIdOpt);
}
public String getFixedPrice() {
if (offerEditType.equals(FIXED_PRICE_ONLY) || offerEditType.equals(FIXED_PRICE_AND_ACTIVATION_STATE)) {
return options.has(fixedPriceOpt) ? options.valueOf(fixedPriceOpt) : "0";
} else {
return "0";
}
}
public String getTriggerPrice() {
if (offerEditType.equals(TRIGGER_PRICE_ONLY)
|| offerEditType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE)
|| offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE)
|| offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE)) {
return options.has(triggerPriceOpt) ? options.valueOf(triggerPriceOpt) : "0";
} else {
return "0";
}
}
public BigDecimal getTriggerPriceAsBigDecimal() {
return new BigDecimal(getTriggerPrice());
}
public String getMktPriceMargin() {
if (offerEditType.equals(MKT_PRICE_MARGIN_ONLY)
|| offerEditType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE)
|| offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE)
|| offerEditType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE)) {
return isUsingMktPriceMargin() ? options.valueOf(mktPriceMarginOpt) : "0.00";
} else {
return "0.00";
}
}
public BigDecimal getMktPriceMarginAsBigDecimal() {
return new BigDecimal(options.valueOf(mktPriceMarginOpt));
}
public boolean isUsingMktPriceMargin() {
return !offerEditType.equals(FIXED_PRICE_ONLY)
&& !offerEditType.equals(FIXED_PRICE_AND_ACTIVATION_STATE);
}
public int getEnableAsSignedInt() {
// Client sends sint32 in grpc request, not a bool that can only be true or false.
// If enable = -1, do not change activation state
// If enable = 0, set state = AVAILABLE
// If enable = 1, set state = DEACTIVATED
@Nullable
Boolean input = isEnable();
return input == null
? OPT_ENABLE_IGNORED
: input ? OPT_ENABLE_ON : OPT_ENABLE_OFF;
}
@Nullable
public Boolean isEnable() {
return options.has(enableOpt)
? Boolean.valueOf(options.valueOf(enableOpt))
: null;
}
public EditOfferRequest.EditType getOfferEditType() {
return offerEditType;
}
private void verifyStringIsValidDouble(String string) {
try {
Double.valueOf(string);
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(format("%s is not a number", string));
}
}
}

View file

@ -27,6 +27,7 @@ public class OptLabel {
public final static String OPT_CURRENCY_CODE = "currency-code"; public final static String OPT_CURRENCY_CODE = "currency-code";
public final static String OPT_DIRECTION = "direction"; public final static String OPT_DIRECTION = "direction";
public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type"; public final static String OPT_DISPUTE_AGENT_TYPE = "dispute-agent-type";
public final static String OPT_ENABLE = "enable";
public final static String OPT_FEE_CURRENCY = "fee-currency"; public final static String OPT_FEE_CURRENCY = "fee-currency";
public final static String OPT_FIXED_PRICE = "fixed-price"; public final static String OPT_FIXED_PRICE = "fixed-price";
public final static String OPT_HELP = "help"; public final static String OPT_HELP = "help";
@ -47,6 +48,7 @@ public class OptLabel {
public final static String OPT_TRADE_INSTANT = "trade-instant"; public final static String OPT_TRADE_INSTANT = "trade-instant";
public final static String OPT_TIMEOUT = "timeout"; public final static String OPT_TIMEOUT = "timeout";
public final static String OPT_TRANSACTION_ID = "transaction-id"; public final static String OPT_TRANSACTION_ID = "transaction-id";
public final static String OPT_TRIGGER_PRICE = "trigger-price";
public final static String OPT_TX_FEE_RATE = "tx-fee-rate"; public final static String OPT_TX_FEE_RATE = "tx-fee-rate";
public final static String OPT_WALLET_PASSWORD = "wallet-password"; public final static String OPT_WALLET_PASSWORD = "wallet-password";
public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password"; public final static String OPT_NEW_WALLET_PASSWORD = "new-wallet-password";

View file

@ -0,0 +1,319 @@
/*
* 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.request;
import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.EditOfferRequest;
import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersRequest;
import bisq.proto.grpc.GetOfferRequest;
import bisq.proto.grpc.GetOffersRequest;
import bisq.proto.grpc.OfferInfo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import static bisq.proto.grpc.EditOfferRequest.EditType.ACTIVATION_STATE_ONLY;
import static bisq.proto.grpc.EditOfferRequest.EditType.FIXED_PRICE_ONLY;
import static bisq.proto.grpc.EditOfferRequest.EditType.MKT_PRICE_MARGIN_ONLY;
import static bisq.proto.grpc.EditOfferRequest.EditType.TRIGGER_PRICE_ONLY;
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;
import bisq.cli.GrpcStubs;
public class OffersServiceRequest {
private final Function<Long, String> scaledPriceStringRequestFormat = (price) -> {
BigDecimal factor = new BigDecimal(10).pow(4);
//noinspection BigDecimalMethodWithoutRoundingCalled
return new BigDecimal(price).divide(factor).toPlainString();
};
private final GrpcStubs grpcStubs;
public OffersServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
public OfferInfo createFixedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
String fixedPrice,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
false,
fixedPrice,
0.00,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode,
0 /* no trigger price */);
}
public OfferInfo createMarketBasedPricedOffer(String direction,
String currencyCode,
long amount,
long minAmount,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode,
long triggerPrice) {
return createOffer(direction,
currencyCode,
amount,
minAmount,
true,
"0",
marketPriceMargin,
securityDeposit,
paymentAcctId,
makerFeeCurrencyCode,
triggerPrice);
}
public OfferInfo createOffer(String direction,
String currencyCode,
long amount,
long minAmount,
boolean useMarketBasedPrice,
String fixedPrice,
double marketPriceMargin,
double securityDeposit,
String paymentAcctId,
String makerFeeCurrencyCode,
long triggerPrice) {
var request = CreateOfferRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.setAmount(amount)
.setMinAmount(minAmount)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMargin(marketPriceMargin)
.setBuyerSecurityDeposit(securityDeposit)
.setPaymentAccountId(paymentAcctId)
.setMakerFeeCurrencyCode(makerFeeCurrencyCode)
.setTriggerPrice(triggerPrice)
.build();
return grpcStubs.offersService.createOffer(request).getOffer();
}
public void editOfferActivationState(String offerId, int enable) {
var offer = getMyOffer(offerId);
var scaledPriceString = offer.getUseMarketBasedPrice()
? "0.00"
: scaledPriceStringRequestFormat.apply(offer.getPrice());
editOffer(offerId,
scaledPriceString,
offer.getUseMarketBasedPrice(),
offer.getMarketPriceMargin(),
offer.getTriggerPrice(),
enable,
ACTIVATION_STATE_ONLY);
}
public void editOfferFixedPrice(String offerId, String rawPriceString) {
var offer = getMyOffer(offerId);
editOffer(offerId,
rawPriceString,
false,
offer.getMarketPriceMargin(),
offer.getTriggerPrice(),
offer.getIsActivated() ? 1 : 0,
FIXED_PRICE_ONLY);
}
public void editOfferPriceMargin(String offerId, double marketPriceMargin) {
var offer = getMyOffer(offerId);
editOffer(offerId,
"0.00",
true,
marketPriceMargin,
offer.getTriggerPrice(),
offer.getIsActivated() ? 1 : 0,
MKT_PRICE_MARGIN_ONLY);
}
public void editOfferTriggerPrice(String offerId, long triggerPrice) {
var offer = getMyOffer(offerId);
editOffer(offerId,
"0.00",
offer.getUseMarketBasedPrice(),
offer.getMarketPriceMargin(),
triggerPrice,
offer.getIsActivated() ? 1 : 0,
TRIGGER_PRICE_ONLY);
}
public void editOffer(String offerId,
String scaledPriceString,
boolean useMarketBasedPrice,
double marketPriceMargin,
long triggerPrice,
int enable,
EditOfferRequest.EditType editType) {
// Take care when using this method directly:
// useMarketBasedPrice = true if margin based offer, false for fixed priced offer
// scaledPriceString fmt = ######.####
var request = EditOfferRequest.newBuilder()
.setId(offerId)
.setPrice(scaledPriceString)
.setUseMarketBasedPrice(useMarketBasedPrice)
.setMarketPriceMargin(marketPriceMargin)
.setTriggerPrice(triggerPrice)
.setEnable(enable)
.setEditType(editType)
.build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.offersService.editOffer(request);
}
public void cancelOffer(String offerId) {
var request = CancelOfferRequest.newBuilder()
.setId(offerId)
.build();
//noinspection ResultOfMethodCallIgnored
grpcStubs.offersService.cancelOffer(request);
}
public OfferInfo getOffer(String offerId) {
var request = GetOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getOffer(request).getOffer();
}
public OfferInfo getMyOffer(String offerId) {
var request = GetMyOfferRequest.newBuilder()
.setId(offerId)
.build();
return grpcStubs.offersService.getMyOffer(request).getOffer();
}
public List<OfferInfo> getOffers(String direction, String currencyCode) {
if (isSupportedCryptoCurrency(currencyCode)) {
return getCryptoCurrencyOffers(direction, currencyCode);
} else {
var request = GetOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getOffers(request).getOffersList();
}
}
public List<OfferInfo> getCryptoCurrencyOffers(String direction, String currencyCode) {
return getOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
}
public List<OfferInfo> getOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getOffers(BUY.name(), currencyCode));
offers.addAll(getOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
}
public List<OfferInfo> getOffersSortedByDate(String direction, String currencyCode) {
var offers = getOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getCryptoCurrencyOffers(BUY.name(), "BSQ"));
offers.addAll(getCryptoCurrencyOffers(SELL.name(), "BSQ"));
return sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffers(String direction, String currencyCode) {
if (isSupportedCryptoCurrency(currencyCode)) {
return getMyCryptoCurrencyOffers(direction, currencyCode);
} else {
var request = GetMyOffersRequest.newBuilder()
.setDirection(direction)
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.offersService.getMyOffers(request).getOffersList();
}
}
public List<OfferInfo> getMyCryptoCurrencyOffers(String direction, String currencyCode) {
return getMyOffers(direction, "BTC").stream()
.filter(o -> o.getBaseCurrencyCode().equalsIgnoreCase(currencyCode))
.collect(toList());
}
public List<OfferInfo> getMyOffersSortedByDate(String direction, String currencyCode) {
var offers = getMyOffers(direction, currencyCode);
return offers.isEmpty() ? offers : sortOffersByDate(offers);
}
public List<OfferInfo> getMyOffersSortedByDate(String currencyCode) {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyOffers(BUY.name(), currencyCode));
offers.addAll(getMyOffers(SELL.name(), currencyCode));
return sortOffersByDate(offers);
}
public List<OfferInfo> getMyBsqOffersSortedByDate() {
ArrayList<OfferInfo> offers = new ArrayList<>();
offers.addAll(getMyCryptoCurrencyOffers(BUY.name(), "BSQ"));
offers.addAll(getMyCryptoCurrencyOffers(SELL.name(), "BSQ"));
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);
}
public List<OfferInfo> sortOffersByDate(List<OfferInfo> offerInfoList) {
return offerInfoList.stream()
.sorted(comparing(OfferInfo::getDate))
.collect(toList());
}
private static boolean isSupportedCryptoCurrency(String currencyCode) {
return getSupportedCryptoCurrencies().contains(currencyCode.toUpperCase());
}
private static List<String> getSupportedCryptoCurrencies() {
final List<String> result = new ArrayList<>();
result.add("BSQ");
result.sort(String::compareTo);
return result;
}
}

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.request;
import bisq.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest;
import bisq.proto.grpc.CreatePaymentAccountRequest;
import bisq.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest;
import bisq.proto.grpc.GetPaymentAccountFormRequest;
import bisq.proto.grpc.GetPaymentAccountsRequest;
import bisq.proto.grpc.GetPaymentMethodsRequest;
import protobuf.PaymentAccount;
import protobuf.PaymentMethod;
import java.util.List;
import bisq.cli.GrpcStubs;
public class PaymentAccountsServiceRequest {
private final GrpcStubs grpcStubs;
public PaymentAccountsServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
public List<PaymentMethod> getPaymentMethods() {
var request = GetPaymentMethodsRequest.newBuilder().build();
return grpcStubs.paymentAccountsService.getPaymentMethods(request).getPaymentMethodsList();
}
public String getPaymentAcctFormAsJson(String paymentMethodId) {
var request = GetPaymentAccountFormRequest.newBuilder()
.setPaymentMethodId(paymentMethodId)
.build();
return grpcStubs.paymentAccountsService.getPaymentAccountForm(request).getPaymentAccountFormJson();
}
public PaymentAccount createPaymentAccount(String json) {
var request = CreatePaymentAccountRequest.newBuilder()
.setPaymentAccountForm(json)
.build();
return grpcStubs.paymentAccountsService.createPaymentAccount(request).getPaymentAccount();
}
public List<PaymentAccount> getPaymentAccounts() {
var request = GetPaymentAccountsRequest.newBuilder().build();
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();
}
}

View file

@ -0,0 +1,94 @@
/*
* 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.request;
import bisq.proto.grpc.ConfirmPaymentReceivedRequest;
import bisq.proto.grpc.ConfirmPaymentStartedRequest;
import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.KeepFundsRequest;
import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.TradeInfo;
import bisq.proto.grpc.WithdrawFundsRequest;
import bisq.cli.GrpcStubs;
public class TradesServiceRequest {
private final GrpcStubs grpcStubs;
public TradesServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
public TakeOfferReply getTakeOfferReply(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var request = TakeOfferRequest.newBuilder()
.setOfferId(offerId)
.setPaymentAccountId(paymentAccountId)
.setTakerFeeCurrencyCode(takerFeeCurrencyCode)
.build();
return grpcStubs.tradesService.takeOffer(request);
}
public TradeInfo takeOffer(String offerId, String paymentAccountId, String takerFeeCurrencyCode) {
var reply = getTakeOfferReply(offerId, paymentAccountId, takerFeeCurrencyCode);
if (reply.hasTrade())
return reply.getTrade();
else
throw new IllegalStateException(reply.getFailureReason().getDescription());
}
public TradeInfo getTrade(String tradeId) {
var request = GetTradeRequest.newBuilder()
.setTradeId(tradeId)
.build();
return grpcStubs.tradesService.getTrade(request).getTrade();
}
public void confirmPaymentStarted(String tradeId) {
var request = ConfirmPaymentStartedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentStarted(request);
}
public void confirmPaymentReceived(String tradeId) {
var request = ConfirmPaymentReceivedRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.confirmPaymentReceived(request);
}
public void keepFunds(String tradeId) {
var request = KeepFundsRequest.newBuilder()
.setTradeId(tradeId)
.build();
grpcStubs.tradesService.keepFunds(request);
}
public void withdrawFunds(String tradeId, String address, String memo) {
var request = WithdrawFundsRequest.newBuilder()
.setTradeId(tradeId)
.setAddress(address)
.setMemo(memo)
.build();
grpcStubs.tradesService.withdrawFunds(request);
}
}

View file

@ -0,0 +1,192 @@
/*
* 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.request;
import bisq.proto.grpc.AddressBalanceInfo;
import bisq.proto.grpc.BalancesInfo;
import bisq.proto.grpc.BsqBalanceInfo;
import bisq.proto.grpc.BtcBalanceInfo;
import bisq.proto.grpc.GetAddressBalanceRequest;
import bisq.proto.grpc.GetBalancesRequest;
import bisq.proto.grpc.GetFundingAddressesRequest;
import bisq.proto.grpc.GetTransactionRequest;
import bisq.proto.grpc.GetTxFeeRateRequest;
import bisq.proto.grpc.GetUnusedBsqAddressRequest;
import bisq.proto.grpc.LockWalletRequest;
import bisq.proto.grpc.MarketPriceRequest;
import bisq.proto.grpc.RemoveWalletPasswordRequest;
import bisq.proto.grpc.SendBsqRequest;
import bisq.proto.grpc.SendBtcRequest;
import bisq.proto.grpc.SetTxFeeRatePreferenceRequest;
import bisq.proto.grpc.SetWalletPasswordRequest;
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 java.util.List;
import bisq.cli.GrpcStubs;
public class WalletsServiceRequest {
private final GrpcStubs grpcStubs;
public WalletsServiceRequest(GrpcStubs grpcStubs) {
this.grpcStubs = grpcStubs;
}
public BalancesInfo getBalances() {
return getBalances("");
}
public BsqBalanceInfo getBsqBalances() {
return getBalances("BSQ").getBsq();
}
public BtcBalanceInfo getBtcBalances() {
return getBalances("BTC").getBtc();
}
public BalancesInfo getBalances(String currencyCode) {
var request = GetBalancesRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.walletsService.getBalances(request).getBalances();
}
public AddressBalanceInfo getAddressBalance(String address) {
var request = GetAddressBalanceRequest.newBuilder()
.setAddress(address).build();
return grpcStubs.walletsService.getAddressBalance(request).getAddressBalanceInfo();
}
public double getBtcPrice(String currencyCode) {
var request = MarketPriceRequest.newBuilder()
.setCurrencyCode(currencyCode)
.build();
return grpcStubs.priceService.getMarketPrice(request).getPrice();
}
public List<AddressBalanceInfo> getFundingAddresses() {
var request = GetFundingAddressesRequest.newBuilder().build();
return grpcStubs.walletsService.getFundingAddresses(request).getAddressBalanceInfoList();
}
public String getUnusedBsqAddress() {
var request = GetUnusedBsqAddressRequest.newBuilder().build();
return grpcStubs.walletsService.getUnusedBsqAddress(request).getAddress();
}
public String getUnusedBtcAddress() {
var request = GetFundingAddressesRequest.newBuilder().build();
var addressBalances = grpcStubs.walletsService.getFundingAddresses(request)
.getAddressBalanceInfoList();
//noinspection OptionalGetWithoutIsPresent
return addressBalances.stream()
.filter(AddressBalanceInfo::getIsAddressUnused)
.findFirst()
.get()
.getAddress();
}
public TxInfo sendBsq(String address, String amount, String txFeeRate) {
var request = SendBsqRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.build();
return grpcStubs.walletsService.sendBsq(request).getTxInfo();
}
public TxInfo sendBtc(String address, String amount, String txFeeRate, String memo) {
var request = SendBtcRequest.newBuilder()
.setAddress(address)
.setAmount(amount)
.setTxFeeRate(txFeeRate)
.setMemo(memo)
.build();
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();
}
public TxFeeRateInfo setTxFeeRate(long txFeeRate) {
var request = SetTxFeeRatePreferenceRequest.newBuilder()
.setTxFeeRatePreference(txFeeRate)
.build();
return grpcStubs.walletsService.setTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxFeeRateInfo unsetTxFeeRate() {
var request = UnsetTxFeeRatePreferenceRequest.newBuilder().build();
return grpcStubs.walletsService.unsetTxFeeRatePreference(request).getTxFeeRateInfo();
}
public TxInfo getTransaction(String txId) {
var request = GetTransactionRequest.newBuilder()
.setTxId(txId)
.build();
return grpcStubs.walletsService.getTransaction(request).getTxInfo();
}
public void lockWallet() {
var request = LockWalletRequest.newBuilder().build();
grpcStubs.walletsService.lockWallet(request);
}
public void unlockWallet(String walletPassword, long timeout) {
var request = UnlockWalletRequest.newBuilder()
.setPassword(walletPassword)
.setTimeout(timeout).build();
grpcStubs.walletsService.unlockWallet(request);
}
public void removeWalletPassword(String walletPassword) {
var request = RemoveWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.removeWalletPassword(request);
}
public void setWalletPassword(String walletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(walletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
public void setWalletPassword(String oldWalletPassword, String newWalletPassword) {
var request = SetWalletPasswordRequest.newBuilder()
.setPassword(oldWalletPassword)
.setNewPassword(newWalletPassword).build();
grpcStubs.walletsService.setWalletPassword(request);
}
}

View file

@ -0,0 +1,346 @@
package bisq.cli.opts;
import org.junit.jupiter.api.Test;
import static bisq.cli.Method.editoffer;
import static bisq.cli.opts.EditOfferOptionParser.OPT_ENABLE_IGNORED;
import static bisq.cli.opts.EditOfferOptionParser.OPT_ENABLE_OFF;
import static bisq.cli.opts.EditOfferOptionParser.OPT_ENABLE_ON;
import static bisq.cli.opts.OptLabel.*;
import static bisq.proto.grpc.EditOfferRequest.EditType.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
// This opt parser test ahs the most thorough coverage,
// and is a reference for other opt parser tests.
public class EditOfferOptionParserTest {
private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz";
@Test
public void testEditOfferWithMissingOfferIdOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name()
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no offer id specified", exception.getMessage());
}
@Test
public void testEditOfferWithoutAnyOptsShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no edit details specified", exception.getMessage());
}
@Test
public void testEditOfferWithEmptyEnableOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_ENABLE + "=" // missing opt value
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("invalid enable value specified, must be true|false",
exception.getMessage());
}
@Test
public void testEditOfferWithMissingEnableValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_ENABLE // missing equals sign & opt value
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("invalid enable value specified, must be true|false",
exception.getMessage());
}
@Test
public void testEditOfferWithInvalidEnableValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_ENABLE + "=0"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("invalid enable value specified, must be true|false",
exception.getMessage());
}
@Test
public void testEditOfferWithMktPriceOptAndFixedPriceOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_MKT_PRICE_MARGIN + "=0.11",
"--" + OPT_FIXED_PRICE + "=50000.0000"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("cannot specify market price margin and fixed price",
exception.getMessage());
}
@Test
public void testEditOfferWithFixedPriceOptAndTriggerPriceOptShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_FIXED_PRICE + "=50000.0000",
"--" + OPT_TRIGGER_PRICE + "=51000.0000"
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("trigger price cannot be set on fixed price offers",
exception.getMessage());
}
@Test
public void testEditOfferActivationStateOnly() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_ENABLE + "=" + "true"
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(ACTIVATION_STATE_ONLY, parser.getOfferEditType());
assertEquals(OPT_ENABLE_ON, parser.getEnableAsSignedInt());
}
@Test
public void testEditOfferFixedPriceWithoutOptValueShouldThrowException1() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_FIXED_PRICE + "="
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no fixed price specified",
exception.getMessage());
}
@Test
public void testEditOfferFixedPriceWithoutOptValueShouldThrowException2() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_FIXED_PRICE
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no fixed price specified",
exception.getMessage());
}
@Test
public void testEditOfferFixedPriceOnly() {
String fixedPriceAsString = "50000.0000";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_FIXED_PRICE + "=" + fixedPriceAsString
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(FIXED_PRICE_ONLY, parser.getOfferEditType());
assertEquals(fixedPriceAsString, parser.getFixedPrice());
assertFalse(parser.isUsingMktPriceMargin());
assertEquals("0.00", parser.getMktPriceMargin());
assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt());
}
@Test
public void testEditOfferFixedPriceAndActivationStateOnly() {
String fixedPriceAsString = "50000.0000";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_FIXED_PRICE + "=" + fixedPriceAsString,
"--" + OPT_ENABLE + "=" + "false"
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(FIXED_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType());
assertEquals(fixedPriceAsString, parser.getFixedPrice());
assertFalse(parser.isUsingMktPriceMargin());
assertEquals("0.00", parser.getMktPriceMargin());
assertEquals(OPT_ENABLE_OFF, parser.getEnableAsSignedInt());
}
@Test
public void testEditOfferMktPriceMarginOnly() {
String mktPriceMarginAsString = "0.25";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(MKT_PRICE_MARGIN_ONLY, parser.getOfferEditType());
assertTrue(parser.isUsingMktPriceMargin());
assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin());
assertEquals("0", parser.getTriggerPrice());
assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt());
}
@Test
public void testEditOfferMktPriceMarginWithoutOptValueShouldThrowException() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_MKT_PRICE_MARGIN
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no mkt price margin specified",
exception.getMessage());
}
@Test
public void testEditOfferMktPriceMarginAndActivationStateOnly() {
String mktPriceMarginAsString = "0.15";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString,
"--" + OPT_ENABLE + "=" + "false"
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE, parser.getOfferEditType());
assertTrue(parser.isUsingMktPriceMargin());
assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin());
assertEquals("0", parser.getTriggerPrice());
assertEquals(OPT_ENABLE_OFF, parser.getEnableAsSignedInt());
}
@Test
public void testEditTriggerPriceOnly() {
String triggerPriceAsString = "50000.0000";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(TRIGGER_PRICE_ONLY, parser.getOfferEditType());
assertEquals(triggerPriceAsString, parser.getTriggerPrice());
assertTrue(parser.isUsingMktPriceMargin());
assertEquals("0.00", parser.getMktPriceMargin());
assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt());
}
@Test
public void testEditTriggerPriceWithoutOptValueShouldThrowException1() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_TRIGGER_PRICE + "="
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no trigger price specified",
exception.getMessage());
}
@Test
public void testEditTriggerPriceWithoutOptValueShouldThrowException2() {
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_TRIGGER_PRICE
};
Throwable exception = assertThrows(RuntimeException.class, () ->
new EditOfferOptionParser(args).parse());
assertEquals("no trigger price specified",
exception.getMessage());
}
@Test
public void testEditTriggerPriceAndActivationStateOnly() {
String triggerPriceAsString = "50000.0000";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString,
"--" + OPT_ENABLE + "=" + "true"
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(TRIGGER_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType());
assertEquals(triggerPriceAsString, parser.getTriggerPrice());
assertTrue(parser.isUsingMktPriceMargin());
assertEquals("0.00", parser.getMktPriceMargin());
assertEquals("0", parser.getFixedPrice());
assertEquals(OPT_ENABLE_ON, parser.getEnableAsSignedInt());
}
@Test
public void testEditMKtPriceMarginAndTriggerPrice() {
String mktPriceMarginAsString = "0.25";
String triggerPriceAsString = "50000.0000";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString,
"--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE, parser.getOfferEditType());
assertEquals(triggerPriceAsString, parser.getTriggerPrice());
assertTrue(parser.isUsingMktPriceMargin());
assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin());
assertEquals("0", parser.getFixedPrice());
assertEquals(OPT_ENABLE_IGNORED, parser.getEnableAsSignedInt());
}
@Test
public void testEditMKtPriceMarginAndTriggerPriceAndEnableState() {
String mktPriceMarginAsString = "0.25";
String triggerPriceAsString = "50000.0000";
String[] args = new String[]{
PASSWORD_OPT,
editoffer.name(),
"--" + OPT_OFFER_ID + "=" + "ABC-OFFER-ID",
"--" + OPT_MKT_PRICE_MARGIN + "=" + mktPriceMarginAsString,
"--" + OPT_TRIGGER_PRICE + "=" + triggerPriceAsString,
"--" + OPT_ENABLE + "=" + "FALSE"
};
EditOfferOptionParser parser = new EditOfferOptionParser(args).parse();
assertEquals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE, parser.getOfferEditType());
assertEquals(triggerPriceAsString, parser.getTriggerPrice());
assertTrue(parser.isUsingMktPriceMargin());
assertEquals(mktPriceMarginAsString, parser.getMktPriceMargin());
assertEquals("0", parser.getFixedPrice());
assertEquals(OPT_ENABLE_OFF, parser.getEnableAsSignedInt());
}
}

View file

@ -1,4 +1,4 @@
package bisq.cli.opt; package bisq.cli.opts;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -11,13 +11,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; 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;
public class OptionParsersTest { public class OptionParsersTest {
private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz"; private static final String PASSWORD_OPT = "--" + OPT_PASSWORD + "=" + "xyz";
@ -178,7 +171,7 @@ public class OptionParsersTest {
new CreatePaymentAcctOptionParser(args).parse()); new CreatePaymentAcctOptionParser(args).parse());
if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0) if (System.getProperty("os.name").toLowerCase().indexOf("win") >= 0)
assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found", assertEquals("json payment account form '\\tmp\\milkyway\\solarsystem\\mars' could not be found",
exception.getMessage()); exception.getMessage());
else else
assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found", assertEquals("json payment account form '/tmp/milkyway/solarsystem/mars' could not be found",
exception.getMessage()); exception.getMessage());

View file

@ -21,9 +21,7 @@ import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo; import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.TxFeeRateInfo; import bisq.core.api.model.TxFeeRateInfo;
import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.monetary.Price;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
@ -36,7 +34,6 @@ import bisq.common.config.Config;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction;
import javax.inject.Inject; import javax.inject.Inject;
@ -52,6 +49,8 @@ import java.util.function.Consumer;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.proto.grpc.EditOfferRequest.EditType;
/** /**
* Provides high level interface to functionality of core Bisq features. * Provides high level interface to functionality of core Bisq features.
* E.g. useful for different APIs to access data of different domains of Bisq. * E.g. useful for different APIs to access data of different domains of Bisq.
@ -122,7 +121,7 @@ public class CoreApi {
return coreOffersService.getOffer(id); return coreOffersService.getOffer(id);
} }
public Offer getMyOffer(String id) { public OpenOffer getMyOffer(String id) {
return coreOffersService.getMyOffer(id); return coreOffersService.getMyOffer(id);
} }
@ -130,14 +129,10 @@ public class CoreApi {
return coreOffersService.getOffers(direction, currencyCode); return coreOffersService.getOffers(direction, currencyCode);
} }
public List<Offer> getMyOffers(String direction, String currencyCode) { public List<OpenOffer> getMyOffers(String direction, String currencyCode) {
return coreOffersService.getMyOffers(direction, currencyCode); return coreOffersService.getMyOffers(direction, currencyCode);
} }
public OpenOffer getMyOpenOffer(String id) {
return coreOffersService.getMyOpenOffer(id);
}
public void createAnPlaceOffer(String currencyCode, public void createAnPlaceOffer(String currencyCode,
String directionAsString, String directionAsString,
String priceAsString, String priceAsString,
@ -164,32 +159,30 @@ public class CoreApi {
resultHandler); resultHandler);
} }
public Offer editOffer(String offerId, public void editOffer(String offerId,
String currencyCode, String priceAsString,
OfferPayload.Direction direction, boolean useMarketBasedPrice,
Price price, double marketPriceMargin,
boolean useMarketBasedPrice, long triggerPrice,
double marketPriceMargin, int enable,
Coin amount, EditType editType) {
Coin minAmount, coreOffersService.editOffer(offerId,
double buyerSecurityDeposit, priceAsString,
PaymentAccount paymentAccount) {
return coreOffersService.editOffer(offerId,
currencyCode,
direction,
price,
useMarketBasedPrice, useMarketBasedPrice,
marketPriceMargin, marketPriceMargin,
amount, triggerPrice,
minAmount, enable,
buyerSecurityDeposit, editType);
paymentAccount);
} }
public void cancelOffer(String id) { public void cancelOffer(String id) {
coreOffersService.cancelOffer(id); coreOffersService.cancelOffer(id);
} }
public boolean isMyOffer(String id) {
return coreOffersService.isMyOffer(id);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// PaymentAccounts // PaymentAccounts
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -20,13 +20,16 @@ package bisq.core.api;
import bisq.core.monetary.Altcoin; import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.offer.CreateOfferService; import bisq.core.offer.CreateOfferService;
import bisq.core.offer.MutableOfferPayloadFields;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferBookService; import bisq.core.offer.OfferBookService;
import bisq.core.offer.OfferFilter; import bisq.core.offer.OfferFilter;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil; import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.user.User; import bisq.core.user.User;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
@ -42,6 +45,7 @@ import java.math.BigDecimal;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -52,9 +56,14 @@ import static bisq.common.util.MathUtils.exactMultiply;
import static bisq.common.util.MathUtils.roundDoubleToLong; import static bisq.common.util.MathUtils.roundDoubleToLong;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10; import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency; import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static bisq.core.offer.Offer.State;
import static bisq.core.offer.OfferPayload.Direction; import static bisq.core.offer.OfferPayload.Direction;
import static bisq.core.offer.OfferPayload.Direction.BUY; import static bisq.core.offer.OfferPayload.Direction.BUY;
import static bisq.core.offer.OpenOffer.State.AVAILABLE;
import static bisq.core.offer.OpenOffer.State.DEACTIVATED;
import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer; import static bisq.core.payment.PaymentAccountUtil.isPaymentAccountValidForOffer;
import static bisq.proto.grpc.EditOfferRequest.EditType;
import static bisq.proto.grpc.EditOfferRequest.EditType.*;
import static java.lang.String.format; import static java.lang.String.format;
import static java.util.Comparator.comparing; import static java.util.Comparator.comparing;
@ -62,8 +71,11 @@ import static java.util.Comparator.comparing;
@Slf4j @Slf4j
class CoreOffersService { class CoreOffersService {
private final Supplier<Comparator<Offer>> priceComparator = () -> comparing(Offer::getPrice); private final Supplier<Comparator<Offer>> priceComparator = () ->
private final Supplier<Comparator<Offer>> reversePriceComparator = () -> comparing(Offer::getPrice).reversed(); comparing(Offer::getPrice);
private final Supplier<Comparator<OpenOffer>> openOfferPriceComparator = () ->
comparing(openOffer -> openOffer.getOffer().getPrice());
private final CoreContext coreContext; private final CoreContext coreContext;
private final KeyRing keyRing; private final KeyRing keyRing;
@ -76,6 +88,7 @@ class CoreOffersService {
private final OfferFilter offerFilter; private final OfferFilter offerFilter;
private final OpenOfferManager openOfferManager; private final OpenOfferManager openOfferManager;
private final OfferUtil offerUtil; private final OfferUtil offerUtil;
private final PriceFeedService priceFeedService;
private final User user; private final User user;
@Inject @Inject
@ -87,6 +100,7 @@ class CoreOffersService {
OfferFilter offerFilter, OfferFilter offerFilter,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
OfferUtil offerUtil, OfferUtil offerUtil,
PriceFeedService priceFeedService,
User user) { User user) {
this.coreContext = coreContext; this.coreContext = coreContext;
this.keyRing = keyRing; this.keyRing = keyRing;
@ -96,6 +110,7 @@ class CoreOffersService {
this.offerFilter = offerFilter; this.offerFilter = offerFilter;
this.openOfferManager = openOfferManager; this.openOfferManager = openOfferManager;
this.offerUtil = offerUtil; this.offerUtil = offerUtil;
this.priceFeedService = priceFeedService;
this.user = user; this.user = user;
} }
@ -108,10 +123,10 @@ class CoreOffersService {
new IllegalStateException(format("offer with id '%s' not found", id))); new IllegalStateException(format("offer with id '%s' not found", id)));
} }
Offer getMyOffer(String id) { OpenOffer getMyOffer(String id) {
return offerBookService.getOffers().stream() return openOfferManager.getObservableList().stream()
.filter(o -> o.getId().equals(id)) .filter(o -> o.getId().equals(id))
.filter(o -> o.isMyOffer(keyRing)) .filter(o -> o.getOffer().isMyOffer(keyRing))
.findAny().orElseThrow(() -> .findAny().orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id))); new IllegalStateException(format("offer with id '%s' not found", id)));
} }
@ -125,11 +140,11 @@ class CoreOffersService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
List<Offer> getMyOffers(String direction, String currencyCode) { List<OpenOffer> getMyOffers(String direction, String currencyCode) {
return offerBookService.getOffers().stream() return openOfferManager.getObservableList().stream()
.filter(o -> o.isMyOffer(keyRing)) .filter(o -> o.getOffer().isMyOffer(keyRing))
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode)) .filter(o -> offerMatchesDirectionAndCurrency(o.getOffer(), direction, currencyCode))
.sorted(priceComparator(direction)) .sorted(openOfferPriceComparator(direction))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
@ -137,7 +152,13 @@ class CoreOffersService {
return openOfferManager.getOpenOfferById(id) return openOfferManager.getOpenOfferById(id)
.filter(open -> open.getOffer().isMyOffer(keyRing)) .filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() -> .orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id))); new IllegalStateException(format("offer with id '%s' not found", id)));
}
boolean isMyOffer(String id) {
return openOfferManager.getOpenOfferById(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.isPresent();
} }
// Create and place new offer. // Create and place new offer.
@ -193,47 +214,67 @@ class CoreOffersService {
} }
// Edit a placed offer. // Edit a placed offer.
Offer editOffer(String offerId, void editOffer(String offerId,
String currencyCode, String editedPriceAsString,
Direction direction, boolean editedUseMarketBasedPrice,
Price price, double editedMarketPriceMargin,
boolean useMarketBasedPrice, long editedTriggerPrice,
double marketPriceMargin, int editedEnable,
Coin amount, EditType editType) {
Coin minAmount, OpenOffer openOffer = getMyOpenOffer(offerId);
double buyerSecurityDeposit, new EditOfferValidator(openOffer,
PaymentAccount paymentAccount) { editedPriceAsString,
Coin useDefaultTxFee = Coin.ZERO; editedUseMarketBasedPrice,
return createOfferService.createAndGetOffer(offerId, editedMarketPriceMargin,
direction, editedTriggerPrice,
currencyCode.toUpperCase(), editedEnable,
amount, editType).validate();
minAmount, log.info("Validated 'editoffer' params offerId={}"
price, + "\n\teditedPriceAsString={}"
useDefaultTxFee, + "\n\teditedUseMarketBasedPrice={}"
useMarketBasedPrice, + "\n\teditedMarketPriceMargin={}"
exactMultiply(marketPriceMargin, 0.01), + "\n\teditedTriggerPrice={}"
buyerSecurityDeposit, + "\n\teditedEnable={}"
paymentAccount); + "\n\teditType={}",
offerId,
editedPriceAsString,
editedUseMarketBasedPrice,
editedMarketPriceMargin,
editedTriggerPrice,
editedEnable,
editType);
OpenOffer.State currentOfferState = openOffer.getState();
// Client sent (sint32) editedEnable, not a bool (with default=false).
// If editedEnable = -1, do not change current state
// If editedEnable = 0, set state = AVAILABLE
// If editedEnable = 1, set state = DEACTIVATED
OpenOffer.State newOfferState = editedEnable < 0
? currentOfferState
: editedEnable > 0 ? AVAILABLE : DEACTIVATED;
OfferPayload editedPayload = getMergedOfferPayload(openOffer,
editedPriceAsString,
editedMarketPriceMargin,
editType);
Offer editedOffer = new Offer(editedPayload);
priceFeedService.setCurrencyCode(openOffer.getOffer().getOfferPayload().getCurrencyCode());
editedOffer.setPriceFeedService(priceFeedService);
editedOffer.setState(State.AVAILABLE);
openOfferManager.editOpenOfferStart(openOffer,
() -> log.info("EditOpenOfferStart: offer {}", openOffer.getId()),
log::error);
openOfferManager.editOpenOfferPublish(editedOffer,
editedTriggerPrice,
newOfferState,
() -> log.info("EditOpenOfferPublish: offer {}", openOffer.getId()),
log::error);
} }
void cancelOffer(String id) { void cancelOffer(String id) {
Offer offer = getMyOffer(id); OpenOffer openOffer = getMyOffer(id);
openOfferManager.removeOffer(offer, openOfferManager.removeOffer(openOffer.getOffer(),
() -> { () -> {
}, },
errorMessage -> { log::error);
throw new IllegalStateException(errorMessage);
});
}
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",
offer.getOfferPayload().getCounterCurrencyCode(),
paymentAccount.getId());
throw new IllegalStateException(error);
}
} }
private void placeOffer(Offer offer, private void placeOffer(Offer offer,
@ -252,6 +293,55 @@ class CoreOffersService {
throw new IllegalStateException(offer.getErrorMessage()); throw new IllegalStateException(offer.getErrorMessage());
} }
private OfferPayload getMergedOfferPayload(OpenOffer openOffer,
String editedPriceAsString,
double editedMarketPriceMargin,
EditType editType) {
// API supports editing (1) price, OR (2) marketPriceMargin & useMarketBasedPrice
// OfferPayload fields. API does not support editing payment acct or currency
// code fields. Note: triggerPrice isDeactivated fields are in OpenOffer, not
// in OfferPayload.
Offer offer = openOffer.getOffer();
String currencyCode = offer.getOfferPayload().getCurrencyCode();
boolean isEditingPrice = editType.equals(FIXED_PRICE_ONLY) || editType.equals(FIXED_PRICE_AND_ACTIVATION_STATE);
Price editedPrice;
if (isEditingPrice) {
editedPrice = Price.valueOf(currencyCode, priceStringToLong(editedPriceAsString, currencyCode));
} else {
editedPrice = offer.getPrice();
}
boolean isUsingMktPriceMargin = editType.equals(MKT_PRICE_MARGIN_ONLY)
|| editType.equals(MKT_PRICE_MARGIN_AND_ACTIVATION_STATE)
|| editType.equals(TRIGGER_PRICE_ONLY)
|| editType.equals(TRIGGER_PRICE_AND_ACTIVATION_STATE)
|| editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE)
|| editType.equals(MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE);
MutableOfferPayloadFields mutableOfferPayloadFields = new MutableOfferPayloadFields(
Objects.requireNonNull(editedPrice).getValue(),
isUsingMktPriceMargin ? exactMultiply(editedMarketPriceMargin, 0.01) : 0.00,
isUsingMktPriceMargin,
offer.getOfferPayload().getBaseCurrencyCode(),
offer.getOfferPayload().getCounterCurrencyCode(),
offer.getPaymentMethod().getId(),
offer.getMakerPaymentAccountId(),
offer.getOfferPayload().getCountryCode(),
offer.getOfferPayload().getAcceptedCountryCodes(),
offer.getOfferPayload().getBankId(),
offer.getOfferPayload().getAcceptedBankIds(),
offer.getOfferPayload().getExtraDataMap());
log.info("Merging OfferPayload with {}", mutableOfferPayloadFields);
return offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields);
}
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",
offer.getOfferPayload().getCounterCurrencyCode(),
paymentAccount.getId());
throw new IllegalStateException(error);
}
}
private boolean offerMatchesDirectionAndCurrency(Offer offer, private boolean offerMatchesDirectionAndCurrency(Offer offer,
String direction, String direction,
String currencyCode) { String currencyCode) {
@ -261,11 +351,19 @@ class CoreOffersService {
return offerOfWantedDirection && offerInWantedCurrency; return offerOfWantedDirection && offerInWantedCurrency;
} }
private Comparator<OpenOffer> openOfferPriceComparator(String direction) {
// A buyer probably wants to see sell orders in price ascending order.
// A seller probably wants to see buy orders in price descending order.
return direction.equalsIgnoreCase(BUY.name())
? openOfferPriceComparator.get().reversed()
: openOfferPriceComparator.get();
}
private Comparator<Offer> priceComparator(String direction) { private Comparator<Offer> priceComparator(String direction) {
// A buyer probably wants to see sell orders in price ascending order. // A buyer probably wants to see sell orders in price ascending order.
// A seller probably wants to see buy orders in price descending order. // A seller probably wants to see buy orders in price descending order.
return direction.equalsIgnoreCase(BUY.name()) return direction.equalsIgnoreCase(BUY.name())
? reversePriceComparator.get() ? priceComparator.get().reversed()
: priceComparator.get(); : priceComparator.get();
} }

View file

@ -0,0 +1,141 @@
package bisq.core.api;
import bisq.core.offer.OpenOffer;
import bisq.proto.grpc.EditOfferRequest;
import java.math.BigDecimal;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.locale.CurrencyUtil.isCryptoCurrency;
import static java.lang.String.format;
@Slf4j
class EditOfferValidator {
private final OpenOffer currentlyOpenOffer;
private final String editedPriceAsString;
private final boolean editedUseMarketBasedPrice;
private final double editedMarketPriceMargin;
private final long editedTriggerPrice;
private final int editedEnable;
private final EditOfferRequest.EditType editType;
private final boolean isZeroEditedFixedPriceString;
private final boolean isZeroEditedTriggerPrice;
EditOfferValidator(OpenOffer currentlyOpenOffer,
String editedPriceAsString,
boolean editedUseMarketBasedPrice,
double editedMarketPriceMargin,
long editedTriggerPrice,
int editedEnable,
EditOfferRequest.EditType editType) {
this.currentlyOpenOffer = currentlyOpenOffer;
this.editedPriceAsString = editedPriceAsString;
this.editedUseMarketBasedPrice = editedUseMarketBasedPrice;
this.editedMarketPriceMargin = editedMarketPriceMargin;
this.editedTriggerPrice = editedTriggerPrice;
this.editedEnable = editedEnable;
this.editType = editType;
this.isZeroEditedFixedPriceString = new BigDecimal(editedPriceAsString).doubleValue() == 0;
this.isZeroEditedTriggerPrice = editedTriggerPrice == 0;
}
void validate() {
log.info("Verifying 'editoffer' params OK for editType {}", editType);
switch (editType) {
case ACTIVATION_STATE_ONLY: {
validateEditedActivationState();
break;
}
case FIXED_PRICE_ONLY:
case FIXED_PRICE_AND_ACTIVATION_STATE: {
validateEditedFixedPrice();
break;
}
case MKT_PRICE_MARGIN_ONLY:
case MKT_PRICE_MARGIN_AND_ACTIVATION_STATE:
case TRIGGER_PRICE_ONLY:
case TRIGGER_PRICE_AND_ACTIVATION_STATE:
case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE:
case MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE: {
checkNotAltcoinOffer();
validateEditedTriggerPrice();
validateEditedMarketPriceMargin();
break;
}
default:
break;
}
}
private void validateEditedActivationState() {
if (editedEnable < 0)
throw new IllegalStateException(
format("programmer error: the 'enable' request parameter does not"
+ " indicate activation state of offer with id '%s' should be changed.",
currentlyOpenOffer.getId()));
}
private void validateEditedFixedPrice() {
if (currentlyOpenOffer.getOffer().isUseMarketBasedPrice())
log.info("Attempting to change mkt price margin based offer with id '{}' to fixed price offer.",
currentlyOpenOffer.getId());
if (editedUseMarketBasedPrice)
throw new IllegalStateException(
format("programmer error: cannot change fixed price (%s)"
+ " in mkt price based offer with id '%s'",
editedMarketPriceMargin,
currentlyOpenOffer.getId()));
if (!isZeroEditedTriggerPrice)
throw new IllegalStateException(
format("programmer error: cannot change trigger price (%s)"
+ " in offer with id '%s' when changing fixed price",
editedTriggerPrice,
currentlyOpenOffer.getId()));
}
private void validateEditedMarketPriceMargin() {
if (!currentlyOpenOffer.getOffer().isUseMarketBasedPrice())
log.info("Attempting to change fixed price offer with id '{}' to mkt price margin based offer.",
currentlyOpenOffer.getId());
if (!isZeroEditedFixedPriceString)
throw new IllegalStateException(
format("programmer error: cannot set fixed price (%s)"
+ " in mkt price margin based offer with id '%s'",
editedPriceAsString,
currentlyOpenOffer.getId()));
}
private void validateEditedTriggerPrice() {
if (!currentlyOpenOffer.getOffer().isUseMarketBasedPrice()
&& !editedUseMarketBasedPrice
&& !isZeroEditedTriggerPrice)
throw new IllegalStateException(
format("programmer error: cannot set a trigger price (%s)"
+ " in fixed price offer with id '%s'",
editedTriggerPrice,
currentlyOpenOffer.getId()));
if (editedTriggerPrice < 0)
throw new IllegalStateException(
format("programmer error: cannot set trigger price to a negative value"
+ " in offer with id '%s'",
currentlyOpenOffer.getId()));
}
private void checkNotAltcoinOffer() {
if (isCryptoCurrency(currentlyOpenOffer.getOffer().getCurrencyCode())) {
throw new IllegalStateException(
format("cannot set mkt price margin or trigger price on fixed price altcoin offer with id '%s'",
currentlyOpenOffer.getId()));
}
}
}

View file

@ -18,6 +18,7 @@
package bisq.core.api.model; package bisq.core.api.model;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OpenOffer;
import bisq.common.Payload; import bisq.common.Payload;
@ -61,7 +62,9 @@ public class OfferInfo implements Payload {
private final String counterCurrencyCode; private final String counterCurrencyCode;
private final long date; private final long date;
private final String state; private final String state;
private final boolean isActivated;
private boolean isMyOffer; // Not final -- may be re-set after instantiation.
private final boolean isMyPendingOffer;
public OfferInfo(OfferInfoBuilder builder) { public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.id; this.id = builder.id;
@ -87,20 +90,41 @@ public class OfferInfo implements Payload {
this.counterCurrencyCode = builder.counterCurrencyCode; this.counterCurrencyCode = builder.counterCurrencyCode;
this.date = builder.date; this.date = builder.date;
this.state = builder.state; this.state = builder.state;
this.isActivated = builder.isActivated;
this.isMyOffer = builder.isMyOffer;
this.isMyPendingOffer = builder.isMyPendingOffer;
}
// Allow isMyOffer to be set on a new offer's OfferInfo instance.
public void setIsMyOffer(boolean isMyOffer) {
this.isMyOffer = isMyOffer;
} }
public static OfferInfo toOfferInfo(Offer offer) { public static OfferInfo toOfferInfo(Offer offer) {
return getOfferInfoBuilder(offer).build(); // Assume the offer is not mine, but isMyOffer can be reset to true, i.e., when
// calling TradeInfo toTradeInfo(Trade trade, String role, boolean isMyOffer);
return getOfferInfoBuilder(offer, false).build();
} }
public static OfferInfo toOfferInfo(Offer offer, long triggerPrice) { public static OfferInfo toPendingOfferInfo(Offer myNewOffer) {
// The Offer does not have a triggerPrice attribute, so we get // Use this to build an OfferInfo instance when a new OpenOffer is being
// the base OfferInfoBuilder, then add the OpenOffer's triggerPrice. // prepared, and no valid OpenOffer state (AVAILABLE, DEACTIVATED) exists.
return getOfferInfoBuilder(offer).withTriggerPrice(triggerPrice).build(); // It is needed for the CLI's 'createoffer' output, which has a boolean 'ENABLED'
// column that will show a PENDING value when this.isMyPendingOffer = true.
return getOfferInfoBuilder(myNewOffer, true)
.withIsMyPendingOffer(true)
.build();
} }
private static OfferInfoBuilder getOfferInfoBuilder(Offer offer) { public static OfferInfo toOfferInfo(OpenOffer openOffer) {
// An OpenOffer is always my offer.
return getOfferInfoBuilder(openOffer.getOffer(), true)
.withTriggerPrice(openOffer.getTriggerPrice())
.withIsActivated(!openOffer.isDeactivated())
.build();
}
private static OfferInfoBuilder getOfferInfoBuilder(Offer offer, boolean isMyOffer) {
return new OfferInfoBuilder() return new OfferInfoBuilder()
.withId(offer.getId()) .withId(offer.getId())
.withDirection(offer.getDirection().name()) .withDirection(offer.getDirection().name())
@ -123,7 +147,8 @@ public class OfferInfo implements Payload {
.withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode()) .withBaseCurrencyCode(offer.getOfferPayload().getBaseCurrencyCode())
.withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode()) .withCounterCurrencyCode(offer.getOfferPayload().getCounterCurrencyCode())
.withDate(offer.getDate().getTime()) .withDate(offer.getDate().getTime())
.withState(offer.getState().name()); .withState(offer.getState().name())
.withIsMyOffer(isMyOffer);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -156,6 +181,9 @@ public class OfferInfo implements Payload {
.setCounterCurrencyCode(counterCurrencyCode) .setCounterCurrencyCode(counterCurrencyCode)
.setDate(date) .setDate(date)
.setState(state) .setState(state)
.setIsActivated(isActivated)
.setIsMyOffer(isMyOffer)
.setIsMyPendingOffer(isMyPendingOffer)
.build(); .build();
} }
@ -185,6 +213,9 @@ public class OfferInfo implements Payload {
.withCounterCurrencyCode(proto.getCounterCurrencyCode()) .withCounterCurrencyCode(proto.getCounterCurrencyCode())
.withDate(proto.getDate()) .withDate(proto.getDate())
.withState(proto.getState()) .withState(proto.getState())
.withIsActivated(proto.getIsActivated())
.withIsMyOffer(proto.getIsMyOffer())
.withIsMyPendingOffer(proto.getIsMyPendingOffer())
.build(); .build();
} }
@ -218,6 +249,9 @@ public class OfferInfo implements Payload {
private String counterCurrencyCode; private String counterCurrencyCode;
private long date; private long date;
private String state; private String state;
private boolean isActivated;
private boolean isMyOffer;
private boolean isMyPendingOffer;
public OfferInfoBuilder withId(String id) { public OfferInfoBuilder withId(String id) {
this.id = id; this.id = id;
@ -334,6 +368,21 @@ public class OfferInfo implements Payload {
return this; return this;
} }
public OfferInfoBuilder withIsActivated(boolean isActivated) {
this.isActivated = isActivated;
return this;
}
public OfferInfoBuilder withIsMyOffer(boolean isMyOffer) {
this.isMyOffer = isMyOffer;
return this;
}
public OfferInfoBuilder withIsMyPendingOffer(boolean isMyPendingOffer) {
this.isMyPendingOffer = isMyPendingOffer;
return this;
}
public OfferInfo build() { public OfferInfo build() {
return new OfferInfo(this); return new OfferInfo(this);
} }

View file

@ -92,11 +92,11 @@ public class TradeInfo implements Payload {
this.contract = builder.contract; this.contract = builder.contract;
} }
public static TradeInfo toTradeInfo(Trade trade) { public static TradeInfo toNewTradeInfo(Trade trade) {
return toTradeInfo(trade, null); return toTradeInfo(trade, null, false);
} }
public static TradeInfo toTradeInfo(Trade trade, String role) { public static TradeInfo toTradeInfo(Trade trade, String role, boolean isMyOffer) {
ContractInfo contractInfo; ContractInfo contractInfo;
if (trade.getContract() != null) { if (trade.getContract() != null) {
Contract contract = trade.getContract(); Contract contract = trade.getContract();
@ -116,8 +116,10 @@ public class TradeInfo implements Payload {
contractInfo = ContractInfo.emptyContract.get(); contractInfo = ContractInfo.emptyContract.get();
} }
OfferInfo offerInfo = toOfferInfo(trade.getOffer());
offerInfo.setIsMyOffer(isMyOffer);
return new TradeInfoBuilder() return new TradeInfoBuilder()
.withOffer(toOfferInfo(trade.getOffer())) .withOffer(offerInfo)
.withTradeId(trade.getId()) .withTradeId(trade.getId())
.withShortId(trade.getShortId()) .withShortId(trade.getShortId())
.withDate(trade.getDate().getTime()) .withDate(trade.getDate().getTime())

View file

@ -32,6 +32,8 @@ import bisq.core.provider.price.PriceFeedService;
import bisq.core.user.User; import bisq.core.user.User;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import bisq.common.util.MathUtils; import bisq.common.util.MathUtils;
@ -72,12 +74,12 @@ public class MarketAlerts {
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() {
@Override @Override
public void onAdded(Offer offer) { public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
onOfferAdded(offer); onOfferAdded(offer);
} }
@Override @Override
public void onRemoved(Offer offer) { public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
} }
}); });
applyFilterOnAllOffers(); applyFilterOnAllOffers();

View file

@ -0,0 +1,95 @@
/*
* 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.offer;
import java.util.List;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.annotation.Nullable;
/**
* The set of editable OfferPayload fields.
*/
@Getter
@Setter
@ToString
public final class MutableOfferPayloadFields {
private final long price;
private final double marketPriceMargin;
private final boolean useMarketBasedPrice;
private final String baseCurrencyCode;
private final String counterCurrencyCode;
private final String paymentMethodId;
private final String makerPaymentAccountId;
@Nullable
private final String countryCode;
@Nullable
private final List<String> acceptedCountryCodes;
@Nullable
private final String bankId;
@Nullable
private final List<String> acceptedBankIds;
@Nullable
private final Map<String, String> extraDataMap;
public MutableOfferPayloadFields(OfferPayload offerPayload) {
this(offerPayload.getPrice(),
offerPayload.getMarketPriceMargin(),
offerPayload.isUseMarketBasedPrice(),
offerPayload.getBaseCurrencyCode(),
offerPayload.getCounterCurrencyCode(),
offerPayload.getPaymentMethodId(),
offerPayload.getMakerPaymentAccountId(),
offerPayload.getCountryCode(),
offerPayload.getAcceptedCountryCodes(),
offerPayload.getBankId(),
offerPayload.getAcceptedBankIds(),
offerPayload.getExtraDataMap());
}
public MutableOfferPayloadFields(long price,
double marketPriceMargin,
boolean useMarketBasedPrice,
String baseCurrencyCode,
String counterCurrencyCode,
String paymentMethodId,
String makerPaymentAccountId,
@Nullable String countryCode,
@Nullable List<String> acceptedCountryCodes,
@Nullable String bankId,
@Nullable List<String> acceptedBankIds,
@Nullable Map<String, String> extraDataMap) {
this.price = price;
this.marketPriceMargin = marketPriceMargin;
this.useMarketBasedPrice = useMarketBasedPrice;
this.baseCurrencyCode = baseCurrencyCode;
this.counterCurrencyCode = counterCurrencyCode;
this.paymentMethodId = paymentMethodId;
this.makerPaymentAccountId = makerPaymentAccountId;
this.countryCode = countryCode;
this.acceptedCountryCodes = acceptedCountryCodes;
this.bankId = bankId;
this.acceptedBankIds = acceptedBankIds;
this.extraDataMap = extraDataMap;
}
}

View file

@ -24,6 +24,7 @@ import bisq.core.provider.price.PriceFeedService;
import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.HashMapChangedListener;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.payload.ProtectedStorageEntry; import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
import bisq.common.UserThread; import bisq.common.UserThread;
@ -44,22 +45,23 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import static bisq.network.p2p.storage.P2PDataStorage.get32ByteHashAsByteArray;
/** /**
* Handles storage and retrieval of offers. * Handles storage and retrieval of offers.
* Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer). * Uses an invalidation flag to only request the full offer map in case there was a change (anyone has added or removed an offer).
*/ */
@Slf4j
public class OfferBookService { public class OfferBookService {
private static final Logger log = LoggerFactory.getLogger(OfferBookService.class);
public interface OfferBookChangedListener { public interface OfferBookChangedListener {
void onAdded(Offer offer); void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload);
void onRemoved(Offer offer); void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload);
} }
private final P2PService p2PService; private final P2PService p2PService;
@ -92,7 +94,8 @@ public class OfferBookService {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
listener.onAdded(offer); P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(offerPayload);
listener.onAdded(offer, hashOfPayload);
} }
})); }));
} }
@ -104,7 +107,8 @@ public class OfferBookService {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
listener.onRemoved(offer); P2PDataStorage.ByteArray hashOfPayload = get32ByteHashAsByteArray(offerPayload);
listener.onRemoved(offer, hashOfPayload);
} }
})); }));
} }
@ -116,12 +120,12 @@ public class OfferBookService {
public void onUpdatedDataReceived() { public void onUpdatedDataReceived() {
addOfferBookChangedListener(new OfferBookChangedListener() { addOfferBookChangedListener(new OfferBookChangedListener() {
@Override @Override
public void onAdded(Offer offer) { public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
doDumpStatistics(); doDumpStatistics();
} }
@Override @Override
public void onRemoved(Offer offer) { public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
doDumpStatistics(); doDumpStatistics();
} }
}); });

View file

@ -364,6 +364,53 @@ public class OfferUtil {
Res.get("offerbook.warning.paymentMethodBanned")); Res.get("offerbook.warning.paymentMethodBanned"));
} }
// Returns an edited payload: a merge of the original offerPayload and
// editedOfferPayload fields. Mutable fields are sourced from
// mutableOfferPayloadFields param, e.g., payment account details, price, etc.
// Immutable fields are sourced from the original openOffer param.
public OfferPayload getMergedOfferPayload(OpenOffer openOffer,
MutableOfferPayloadFields mutableOfferPayloadFields) {
OfferPayload originalOfferPayload = openOffer.getOffer().getOfferPayload();
return new OfferPayload(originalOfferPayload.getId(),
originalOfferPayload.getDate(),
originalOfferPayload.getOwnerNodeAddress(),
originalOfferPayload.getPubKeyRing(),
originalOfferPayload.getDirection(),
mutableOfferPayloadFields.getPrice(),
mutableOfferPayloadFields.getMarketPriceMargin(),
mutableOfferPayloadFields.isUseMarketBasedPrice(),
originalOfferPayload.getAmount(),
originalOfferPayload.getMinAmount(),
mutableOfferPayloadFields.getBaseCurrencyCode(),
mutableOfferPayloadFields.getCounterCurrencyCode(),
originalOfferPayload.getArbitratorNodeAddresses(),
originalOfferPayload.getMediatorNodeAddresses(),
mutableOfferPayloadFields.getPaymentMethodId(),
mutableOfferPayloadFields.getMakerPaymentAccountId(),
originalOfferPayload.getOfferFeePaymentTxId(),
mutableOfferPayloadFields.getCountryCode(),
mutableOfferPayloadFields.getAcceptedCountryCodes(),
mutableOfferPayloadFields.getBankId(),
mutableOfferPayloadFields.getAcceptedBankIds(),
originalOfferPayload.getVersionNr(),
originalOfferPayload.getBlockHeightAtOfferCreation(),
originalOfferPayload.getTxFee(),
originalOfferPayload.getMakerFee(),
originalOfferPayload.isCurrencyForMakerFeeBtc(),
originalOfferPayload.getBuyerSecurityDeposit(),
originalOfferPayload.getSellerSecurityDeposit(),
originalOfferPayload.getMaxTradeLimit(),
originalOfferPayload.getMaxTradePeriod(),
originalOfferPayload.isUseAutoClose(),
originalOfferPayload.isUseReOpenAfterAutoClose(),
originalOfferPayload.getLowerClosePrice(),
originalOfferPayload.getUpperClosePrice(),
originalOfferPayload.isPrivateOffer(),
originalOfferPayload.getHashOfChallenge(),
mutableOfferPayloadFields.getExtraDataMap(),
originalOfferPayload.getProtocolVersion());
}
private Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee, private Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee,
boolean isCurrencyForMakerFeeBtc, boolean isCurrencyForMakerFeeBtc,
String userCurrencyCode, String userCurrencyCode,

View file

@ -0,0 +1,95 @@
editoffer
NAME
----
editoffer - edit an existing offer to buy or sell BTC
SYNOPSIS
--------
editoffer
--offer-id=<offer-id>
[--market-price-margin=<percent>]
[--trigger-price=<btc-price>]
[--fixed-price=<btc-price>]
[--enabled=<true|false>]
DESCRIPTION
-----------
Edit an existing offer. Offers can be changed in the following ways:
Change a fixed-price offer to a market-price-margin based offer.
Change a market-price-margin based offer to a fixed-price offer.
Change a market-price-margin.
Change a fixed-price.
Define, change, or remove a market-price-margin based offer's trigger price.
Disable an enabled offer.
Enable a disabled offer.
OPTIONS
-------
--offer-id
The ID of the buy or sell offer to edit.
--market-price-margin
Changes the % above or below market BTC price, e.g., 1.00 (1%).
A --fixed-price offer can be changed to a --market-price-margin offer with this option.
The --market-price-margin and --trigger-price options can be used in the same editoffer command.
The --market-price-margin and --fixed-price options cannot be used in the same editoffer command.
--fixed-price
Changes the fixed BTC price in fiat used to buy or sell BTC, e.g., 34000 (USD).
A --market-price-margin offer can be changed to a --fixed-price offer with this option.
The --fixed-price and --market-price-margin options cannot be used in the same editoffer command.
--trigger-price
Sets the market price for triggering the de-activation of an offer, or defines trigger-price on an
offer that did not have a trigger-price when it was created.
A buy BTC offer is de-activated when the market price rises above the trigger-price.
A sell BTC offer is de-activated when the market price falls below the trigger-price.
Only applies to market-price-margin based offers; a fixed-price offer's trigger-price is ignored.
The --fixed-price and --trigger-price options cannot be used in the same editoffer command.
--enabled
If true, enables a disabled offer. Does nothing if offer is already enabled.
If false, disabled an enabled offer. Does nothing if offer is already disabled.
EXAMPLES
--------
To change a fixed-price offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea
to a 0.10% market-price-margin based offer:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.10
To change a market-price-margin based offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea
to a fixed-price offer:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--fixed-price=50000.0000
To set or change the trigger-price on a market-price-margin
based offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--trigger-price=50000.0000
To remove a trigger-price on a market-price-margin
based offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--trigger-price=0
To change the market-price-margin and trigger-price on an
offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.05 \
--trigger-price=50000.0000
To disable an offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--enable=false
To enable a disabled offer with ID 83e8b2e2-51b6-4f39-a748-3ebd29c22aea,
and change it from a fixed-price offer to a 0.50% market-price-margin based offer,
and set the trigger-price to 50000.0000:
$ ./bisq-cli --password=xyz --port=9998 editoffer --offer-id=83e8b2e2-51b6-4f39-a748-3ebd29c22aea \
--market-price-margin=0.50 \
--trigger-price=50000.0000 \
--enable=true

View file

@ -26,6 +26,8 @@ import bisq.proto.grpc.CancelOfferReply;
import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.CreateOfferReply; import bisq.proto.grpc.CreateOfferReply;
import bisq.proto.grpc.CreateOfferRequest; import bisq.proto.grpc.CreateOfferRequest;
import bisq.proto.grpc.EditOfferReply;
import bisq.proto.grpc.EditOfferRequest;
import bisq.proto.grpc.GetMyOfferReply; import bisq.proto.grpc.GetMyOfferReply;
import bisq.proto.grpc.GetMyOfferRequest; import bisq.proto.grpc.GetMyOfferRequest;
import bisq.proto.grpc.GetMyOffersReply; import bisq.proto.grpc.GetMyOffersReply;
@ -48,6 +50,7 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.core.api.model.OfferInfo.toOfferInfo; import static bisq.core.api.model.OfferInfo.toOfferInfo;
import static bisq.core.api.model.OfferInfo.toPendingOfferInfo;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.OffersGrpc.*; import static bisq.proto.grpc.OffersGrpc.*;
import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.MINUTES;
@ -89,10 +92,9 @@ class GrpcOffersService extends OffersImplBase {
public void getMyOffer(GetMyOfferRequest req, public void getMyOffer(GetMyOfferRequest req,
StreamObserver<GetMyOfferReply> responseObserver) { StreamObserver<GetMyOfferReply> responseObserver) {
try { try {
Offer offer = coreApi.getMyOffer(req.getId()); OpenOffer openOffer = coreApi.getMyOffer(req.getId());
OpenOffer openOffer = coreApi.getMyOpenOffer(req.getId());
var reply = GetMyOfferReply.newBuilder() var reply = GetMyOfferReply.newBuilder()
.setOffer(toOfferInfo(offer, openOffer.getTriggerPrice()).toProtoMessage()) .setOffer(toOfferInfo(openOffer).toProtoMessage())
.build(); .build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
@ -125,7 +127,8 @@ class GrpcOffersService extends OffersImplBase {
StreamObserver<GetMyOffersReply> responseObserver) { StreamObserver<GetMyOffersReply> responseObserver) {
try { try {
List<OfferInfo> result = coreApi.getMyOffers(req.getDirection(), req.getCurrencyCode()) List<OfferInfo> result = coreApi.getMyOffers(req.getDirection(), req.getCurrencyCode())
.stream().map(OfferInfo::toOfferInfo) .stream()
.map(OfferInfo::toOfferInfo)
.collect(Collectors.toList()); .collect(Collectors.toList());
var reply = GetMyOffersReply.newBuilder() var reply = GetMyOffersReply.newBuilder()
.addAllOffers(result.stream() .addAllOffers(result.stream()
@ -158,7 +161,7 @@ class GrpcOffersService extends OffersImplBase {
offer -> { offer -> {
// This result handling consumer's accept operation will return // This result handling consumer's accept operation will return
// the new offer to the gRPC client after async placement is done. // the new offer to the gRPC client after async placement is done.
OfferInfo offerInfo = toOfferInfo(offer); OfferInfo offerInfo = toPendingOfferInfo(offer);
CreateOfferReply reply = CreateOfferReply.newBuilder() CreateOfferReply reply = CreateOfferReply.newBuilder()
.setOffer(offerInfo.toProtoMessage()) .setOffer(offerInfo.toProtoMessage())
.build(); .build();
@ -170,6 +173,25 @@ class GrpcOffersService extends OffersImplBase {
} }
} }
@Override
public void editOffer(EditOfferRequest req,
StreamObserver<EditOfferReply> responseObserver) {
try {
coreApi.editOffer(req.getId(),
req.getPrice(),
req.getUseMarketBasedPrice(),
req.getMarketPriceMargin(),
req.getTriggerPrice(),
req.getEnable(),
req.getEditType());
var reply = EditOfferReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override @Override
public void cancelOffer(CancelOfferRequest req, public void cancelOffer(CancelOfferRequest req,
StreamObserver<CancelOfferReply> responseObserver) { StreamObserver<CancelOfferReply> responseObserver) {
@ -198,6 +220,7 @@ class GrpcOffersService extends OffersImplBase {
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getCreateOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
put(getEditOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
}} }}
))); )));

View file

@ -44,6 +44,7 @@ import java.util.Optional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static bisq.core.api.model.TradeInfo.toNewTradeInfo;
import static bisq.core.api.model.TradeInfo.toTradeInfo; import static bisq.core.api.model.TradeInfo.toTradeInfo;
import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor;
import static bisq.proto.grpc.TradesGrpc.*; import static bisq.proto.grpc.TradesGrpc.*;
@ -72,9 +73,10 @@ class GrpcTradesService extends TradesImplBase {
StreamObserver<GetTradeReply> responseObserver) { StreamObserver<GetTradeReply> responseObserver) {
try { try {
Trade trade = coreApi.getTrade(req.getTradeId()); Trade trade = coreApi.getTrade(req.getTradeId());
boolean isMyOffer = coreApi.isMyOffer(trade.getOffer().getId());
String role = coreApi.getTradeRole(req.getTradeId()); String role = coreApi.getTradeRole(req.getTradeId());
var reply = GetTradeReply.newBuilder() var reply = GetTradeReply.newBuilder()
.setTrade(toTradeInfo(trade, role).toProtoMessage()) .setTrade(toTradeInfo(trade, role, isMyOffer).toProtoMessage())
.build(); .build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
@ -99,7 +101,7 @@ class GrpcTradesService extends TradesImplBase {
req.getPaymentAccountId(), req.getPaymentAccountId(),
req.getTakerFeeCurrencyCode(), req.getTakerFeeCurrencyCode(),
trade -> { trade -> {
TradeInfo tradeInfo = toTradeInfo(trade); TradeInfo tradeInfo = toNewTradeInfo(trade);
var reply = TakeOfferReply.newBuilder() var reply = TakeOfferReply.newBuilder()
.setTrade(tradeInfo.toProtoMessage()) .setTrade(tradeInfo.toProtoMessage())
.build(); .build();

View file

@ -21,8 +21,8 @@ import bisq.core.filter.FilterManager;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferBookService; import bisq.core.offer.OfferBookService;
import bisq.core.offer.OfferRestrictions; import bisq.core.offer.OfferRestrictions;
import bisq.core.trade.TradeManager;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.utils.Utils; import bisq.network.utils.Utils;
import javax.inject.Inject; import javax.inject.Inject;
@ -32,6 +32,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -61,13 +62,14 @@ public class OfferBook {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Inject @Inject
OfferBook(OfferBookService offerBookService, TradeManager tradeManager, FilterManager filterManager) { OfferBook(OfferBookService offerBookService, FilterManager filterManager) {
this.offerBookService = offerBookService; this.offerBookService = offerBookService;
this.filterManager = filterManager; this.filterManager = filterManager;
offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() { offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() {
@Override @Override
public void onAdded(Offer offer) { public void onAdded(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
printOfferBookListItems("Before onAdded");
// We get onAdded called every time a new ProtectedStorageEntry is received. // We get onAdded called every time a new ProtectedStorageEntry is received.
// Mostly it is the same OfferPayload but the ProtectedStorageEntry is different. // Mostly it is the same OfferPayload but the ProtectedStorageEntry is different.
// We filter here to only add new offers if the same offer (using equals) was not already added and it // We filter here to only add new offers if the same offer (using equals) was not already added and it
@ -83,44 +85,111 @@ public class OfferBook {
return; return;
} }
boolean hasSameOffer = offerBookListItems.stream() // Use offer.equals(offer) to see if the OfferBook list contains an exact
.anyMatch(item -> item.getOffer().equals(offer)); // match -- offer.equals(offer) includes comparisons of payload, state
// and errorMessage.
boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer));
if (!hasSameOffer) { if (!hasSameOffer) {
OfferBookListItem offerBookListItem = new OfferBookListItem(offer); OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer, hashOfPayload);
// We don't use the contains method as the equals method in Offer takes state and errorMessage into account. removeDuplicateItem(newOfferBookListItem);
// If we have an offer with same ID we remove it and add the new offer as it might have a changed state. offerBookListItems.add(newOfferBookListItem); // Add replacement.
Optional<OfferBookListItem> candidateWithSameId = offerBookListItems.stream() if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
.filter(item -> item.getOffer().getId().equals(offer.getId())) log.debug("onAdded: Added new offer {}\n"
.findAny(); + "\twith newItem.payloadHash: {}",
if (candidateWithSameId.isPresent()) { offer.getId(),
log.warn("We had an old offer in the list with the same Offer ID. We remove the old one. " + newOfferBookListItem.hashOfPayload == null ? "null" : newOfferBookListItem.hashOfPayload.getHex());
"old offerBookListItem={}, new offerBookListItem={}", candidateWithSameId.get(), offerBookListItem);
offerBookListItems.remove(candidateWithSameId.get());
} }
offerBookListItems.add(offerBookListItem);
} else { } else {
log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId());
} }
printOfferBookListItems("After onAdded");
} }
@Override @Override
public void onRemoved(Offer offer) { public void onRemoved(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
removeOffer(offer, tradeManager); printOfferBookListItems("Before onRemoved");
removeOffer(offer, hashOfPayload);
printOfferBookListItems("After onRemoved");
} }
}); });
} }
public void removeOffer(Offer offer, TradeManager tradeManager) { private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) {
String offerId = newOfferBookListItem.getOffer().getId();
// We need to remove any view items with a matching offerId before
// a newOfferBookListItem is added to the view.
List<OfferBookListItem> duplicateItems = offerBookListItems.stream()
.filter(item -> item.getOffer().getId().equals(offerId))
.collect(Collectors.toList());
duplicateItems.forEach(oldOfferItem -> {
offerBookListItems.remove(oldOfferItem);
if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
log.debug("onAdded: Removed old offer {}\n"
+ "\twith payload hash {} from list.\n"
+ "\tThis may make a subsequent onRemoved( {} ) call redundant.",
offerId,
oldOfferItem.getHashOfPayload() == null ? "null" : oldOfferItem.getHashOfPayload().getHex(),
oldOfferItem.getOffer().getId());
}
});
}
public void removeOffer(Offer offer, P2PDataStorage.ByteArray hashOfPayload) {
// Update state in case that that offer is used in the take offer screen, so it gets updated correctly // Update state in case that that offer is used in the take offer screen, so it gets updated correctly
offer.setState(Offer.State.REMOVED); offer.setState(Offer.State.REMOVED);
offer.cancelAvailabilityRequest(); offer.cancelAvailabilityRequest();
// We don't use the contains method as the equals method in Offer takes state and errorMessage into account.
Optional<OfferBookListItem> candidateToRemove = offerBookListItems.stream() if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
.filter(item -> item.getOffer().getId().equals(offer.getId())) log.debug("onRemoved: id = {}\n"
+ "\twith payload-hash = {}",
offer.getId(),
hashOfPayload == null ? "null" : hashOfPayload.getHex());
}
// Find the removal candidate in the OfferBook list with matching offerId and payload-hash.
Optional<OfferBookListItem> candidateWithMatchingPayloadHash = offerBookListItems.stream()
.filter(item -> item.getOffer().getId().equals(offer.getId()) && (
item.hashOfPayload == null
|| item.hashOfPayload.equals(hashOfPayload))
)
.findAny(); .findAny();
candidateToRemove.ifPresent(offerBookListItems::remove);
if (!candidateWithMatchingPayloadHash.isPresent()) {
if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
log.debug("UI view list does not contain offer with id {} and payload-hash {}",
offer.getId(),
hashOfPayload == null ? "null" : hashOfPayload.getHex());
}
return;
}
OfferBookListItem candidate = candidateWithMatchingPayloadHash.get();
// Remove the candidate only if the candidate's offer payload is null (after list
// is populated by 'fillOfferBookListItems()'), or the hash matches the onRemoved
// hashOfPayload parameter. We may receive add/remove messages out of order
// (from api's 'editoffer'), and use the offer payload hash to ensure we do not
// remove an edited offer immediately after it was added.
if ((candidate.getHashOfPayload() == null || candidate.getHashOfPayload().equals(hashOfPayload))) {
// The payload-hash test passed, remove the candidate and print reason.
offerBookListItems.remove(candidate);
if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
log.debug("Candidate.payload-hash: {} is null or == onRemoved.payload-hash: {} ?"
+ " Yes, removed old offer",
candidate.hashOfPayload == null ? "null" : candidate.hashOfPayload.getHex(),
hashOfPayload == null ? "null" : hashOfPayload.getHex());
}
} else {
if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
// Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash.
// Print reason for not removing candidate.
log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?"
+ " No, old offer not removed",
candidate.hashOfPayload == null ? "null" : candidate.hashOfPayload.getHex(),
hashOfPayload == null ? "null" : hashOfPayload.getHex());
}
}
} }
public ObservableList<OfferBookListItem> getOfferBookListItems() { public ObservableList<OfferBookListItem> getOfferBookListItems() {
@ -133,16 +202,28 @@ public class OfferBook {
// Investigate why.... // Investigate why....
offerBookListItems.clear(); offerBookListItems.clear();
offerBookListItems.addAll(offerBookService.getOffers().stream() offerBookListItems.addAll(offerBookService.getOffers().stream()
.filter(o -> !filterManager.isOfferIdBanned(o.getId())) .filter(o -> isOfferAllowed(o))
.filter(o -> !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(o.getMakerNodeAddress().getHostName()))
.map(OfferBookListItem::new) .map(OfferBookListItem::new)
.collect(Collectors.toList())); .collect(Collectors.toList()));
log.debug("offerBookListItems.size {}", offerBookListItems.size()); log.debug("offerBookListItems.size {}", offerBookListItems.size());
fillOfferCountMaps(); fillOfferCountMaps();
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); log.error("Error at fillOfferBookListItems: " + t);
log.error("Error at fillOfferBookListItems: " + t.toString()); }
}
public void printOfferBookListItems(String msg) {
if (log.isDebugEnabled()) {
if (offerBookListItems.size() == 0) {
log.debug("{} -> OfferBookListItems: none", msg);
return;
}
StringBuilder stringBuilder = new StringBuilder(msg + " -> ").append("OfferBookListItems:").append("\n");
offerBookListItems.forEach(i -> stringBuilder.append("\t").append(i.toString()).append("\n"));
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
log.debug(stringBuilder.toString());
} }
} }
@ -154,6 +235,13 @@ public class OfferBook {
return sellOfferCountMap; return sellOfferCountMap;
} }
private boolean isOfferAllowed(Offer offer) {
boolean isBanned = filterManager.isOfferIdBanned(offer.getId());
boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate()
|| Utils.isV3Address(offer.getMakerNodeAddress().getHostName());
return !isBanned && isV3NodeAddressCompliant;
}
private void fillOfferCountMaps() { private void fillOfferCountMaps() {
buyOfferCountMap.clear(); buyOfferCountMap.clear();
sellOfferCountMap.clear(); sellOfferCountMap.clear();

View file

@ -27,6 +27,8 @@ import bisq.core.locale.Res;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
import bisq.network.p2p.storage.P2PDataStorage;
import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.GlyphIcons;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
@ -40,18 +42,41 @@ import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@Slf4j import javax.annotation.Nullable;
@Slf4j
public class OfferBookListItem { public class OfferBookListItem {
@Getter @Getter
private final Offer offer; private final Offer offer;
/**
* The protected storage (offer) payload hash helps prevent edited offers from being
* mistakenly removed from a UI user's OfferBook list if the API's 'editoffer'
* command results in onRemoved(offer) being called after onAdded(offer) on peers.
* (Checking the offer-id is not enough.) This msg order problem does not happen
* when the UI edits an offer because the remove/add msgs are always sent in separate
* envelope bundles. It can happen when the API is used to edit an offer because
* the remove/add msgs are received in the same envelope bundle, then processed in
* unpredictable order.
*
* A null value indicates the item's payload hash has not been set by onAdded or
* onRemoved since the most recent OfferBook view refresh.
*/
@Nullable
@Getter
P2PDataStorage.ByteArray hashOfPayload;
// We cache the data once created for performance reasons. AccountAgeWitnessService calls can // We cache the data once created for performance reasons. AccountAgeWitnessService calls can
// be a bit expensive. // be a bit expensive.
private WitnessAgeData witnessAgeData; private WitnessAgeData witnessAgeData;
public OfferBookListItem(Offer offer) { public OfferBookListItem(Offer offer) {
this(offer, null);
}
public OfferBookListItem(Offer offer, @Nullable P2PDataStorage.ByteArray hashOfPayload) {
this.offer = offer; this.offer = offer;
this.hashOfPayload = hashOfPayload;
} }
public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService,
@ -95,6 +120,15 @@ public class OfferBookListItem {
return witnessAgeData; return witnessAgeData;
} }
@Override
public String toString() {
return "OfferBookListItem{" +
"offerId=" + offer.getId() +
", hashOfPayload=" + (hashOfPayload == null ? "null" : hashOfPayload.getHex()) +
", witnessAgeData=" + (witnessAgeData == null ? "null" : witnessAgeData.displayString) +
'}';
}
@Value @Value
public static class WitnessAgeData implements Comparable<WitnessAgeData> { public static class WitnessAgeData implements Comparable<WitnessAgeData> {
String displayString; String displayString;

View file

@ -298,7 +298,7 @@ class TakeOfferDataModel extends OfferDataModel {
// only local effect. Other trader might see the offer for a few seconds // only local effect. Other trader might see the offer for a few seconds
// still (but cannot take it). // still (but cannot take it).
if (removeOffer) { if (removeOffer) {
offerBook.removeOffer(checkNotNull(offer), tradeManager); offerBook.removeOffer(checkNotNull(offer), null);
} }
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());

View file

@ -28,6 +28,7 @@ import bisq.core.btc.wallet.Restrictions;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.TradeCurrency; import bisq.core.locale.TradeCurrency;
import bisq.core.offer.CreateOfferService; import bisq.core.offer.CreateOfferService;
import bisq.core.offer.MutableOfferPayloadFields;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil; import bisq.core.offer.OfferUtil;
@ -182,54 +183,12 @@ class EditOfferDataModel extends MutableOfferDataModel {
} }
public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// editedPayload is a merge of the original offerPayload and newOfferPayload MutableOfferPayloadFields mutableOfferPayloadFields =
// fields which are editable are merged in from newOfferPayload (such as payment account details) new MutableOfferPayloadFields(createAndGetOffer().getOfferPayload());
// fields which cannot change (most importantly BTC amount) are sourced from the original offerPayload final OfferPayload editedPayload = offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields);
final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload();
final OfferPayload newOfferPayload = createAndGetOffer().getOfferPayload();
final OfferPayload editedPayload = new OfferPayload(offerPayload.getId(),
offerPayload.getDate(),
offerPayload.getOwnerNodeAddress(),
offerPayload.getPubKeyRing(),
offerPayload.getDirection(),
newOfferPayload.getPrice(),
newOfferPayload.getMarketPriceMargin(),
newOfferPayload.isUseMarketBasedPrice(),
offerPayload.getAmount(),
offerPayload.getMinAmount(),
newOfferPayload.getBaseCurrencyCode(),
newOfferPayload.getCounterCurrencyCode(),
offerPayload.getArbitratorNodeAddresses(),
offerPayload.getMediatorNodeAddresses(),
newOfferPayload.getPaymentMethodId(),
newOfferPayload.getMakerPaymentAccountId(),
offerPayload.getOfferFeePaymentTxId(),
newOfferPayload.getCountryCode(),
newOfferPayload.getAcceptedCountryCodes(),
newOfferPayload.getBankId(),
newOfferPayload.getAcceptedBankIds(),
offerPayload.getVersionNr(),
offerPayload.getBlockHeightAtOfferCreation(),
offerPayload.getTxFee(),
offerPayload.getMakerFee(),
offerPayload.isCurrencyForMakerFeeBtc(),
offerPayload.getBuyerSecurityDeposit(),
offerPayload.getSellerSecurityDeposit(),
offerPayload.getMaxTradeLimit(),
offerPayload.getMaxTradePeriod(),
offerPayload.isUseAutoClose(),
offerPayload.isUseReOpenAfterAutoClose(),
offerPayload.getLowerClosePrice(),
offerPayload.getUpperClosePrice(),
offerPayload.isPrivateOffer(),
offerPayload.getHashOfChallenge(),
newOfferPayload.getExtraDataMap(),
offerPayload.getProtocolVersion());
final Offer editedOffer = new Offer(editedPayload); final Offer editedOffer = new Offer(editedPayload);
editedOffer.setPriceFeedService(priceFeedService); editedOffer.setPriceFeedService(priceFeedService);
editedOffer.setState(Offer.State.AVAILABLE); editedOffer.setState(Offer.State.AVAILABLE);
openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> {
openOffer = null; openOffer = null;
resultHandler.handleResult(); resultHandler.handleResult();

View file

@ -9,5 +9,4 @@
<root level="TRACE"> <root level="TRACE">
<appender-ref ref="CONSOLE_APPENDER"/> <appender-ref ref="CONSOLE_APPENDER"/>
</root> </root>
</configuration> </configuration>

View file

@ -72,6 +72,8 @@ service Offers {
} }
rpc CreateOffer (CreateOfferRequest) returns (CreateOfferReply) { rpc CreateOffer (CreateOfferRequest) returns (CreateOfferReply) {
} }
rpc EditOffer (EditOfferRequest) returns (EditOfferReply) {
}
rpc CancelOffer (CancelOfferRequest) returns (CancelOfferReply) { rpc CancelOffer (CancelOfferRequest) returns (CancelOfferReply) {
} }
} }
@ -128,6 +130,35 @@ message CreateOfferReply {
OfferInfo offer = 1; OfferInfo offer = 1;
} }
message EditOfferRequest {
string id = 1;
string price = 2;
bool useMarketBasedPrice = 3;
double marketPriceMargin = 4;
uint64 triggerPrice = 5;
// Send a signed int, not a bool (with default=false).
// -1 = do not change activation state
// 0 = disable
// 1 = enable
sint32 enable = 6;
// The EditType constricts what offer details can be modified and simplifies param validation.
enum EditType {
ACTIVATION_STATE_ONLY = 0;
FIXED_PRICE_ONLY = 1;
FIXED_PRICE_AND_ACTIVATION_STATE = 2;
MKT_PRICE_MARGIN_ONLY = 3;
MKT_PRICE_MARGIN_AND_ACTIVATION_STATE = 4;
TRIGGER_PRICE_ONLY = 5;
TRIGGER_PRICE_AND_ACTIVATION_STATE = 6;
MKT_PRICE_MARGIN_AND_TRIGGER_PRICE = 7;
MKT_PRICE_MARGIN_AND_TRIGGER_PRICE_AND_ACTIVATION_STATE = 8;
}
EditType editType = 7;
}
message EditOfferReply {
}
message CancelOfferRequest { message CancelOfferRequest {
string id = 1; string id = 1;
} }
@ -159,6 +190,9 @@ message OfferInfo {
string offerFeePaymentTxId = 21; string offerFeePaymentTxId = 21;
uint64 txFee = 22; uint64 txFee = 22;
uint64 makerFee = 23; uint64 makerFee = 23;
bool isActivated = 24;
bool isMyOffer = 25;
bool isMyPendingOffer = 26;
} }
message AvailabilityResultWithDescription { message AvailabilityResultWithDescription {