defaultSecurityDepositPct = () -> {
+ var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
if (defaultPct.precision() != 2)
throw new IllegalStateException(format(
"Unexpected decimal precision, expected 2 but actual is %d%n."
diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java
index a7776dd683..1867605507 100644
--- a/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/offer/CancelOfferTest.java
@@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
0.00,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
paymentAccountId,
NO_TRIGGER_PRICE);
};
diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java
index 38d83f696d..49c01d5ae4 100644
--- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingFixedPriceTest.java
@@ -49,7 +49,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
"36000",
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
audAccount.getId());
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@@ -97,7 +97,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
"30000.1234",
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
usdAccount.getId());
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@@ -145,7 +145,7 @@ public class CreateOfferUsingFixedPriceTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
"29500.1234",
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
eurAccount.getId());
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java
index cc6f53acdc..f4dff640c1 100644
--- a/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java
@@ -66,7 +66,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
usdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #1:\n{}", toOfferTable.apply(newOffer));
@@ -114,7 +114,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
10_000_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
nzdAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #2:\n{}", toOfferTable.apply(newOffer));
@@ -162,7 +162,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
gbpAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #3:\n{}", toOfferTable.apply(newOffer));
@@ -210,7 +210,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
brlAccount.getId(),
NO_TRIGGER_PRICE);
log.debug("Offer #4:\n{}", toOfferTable.apply(newOffer));
@@ -259,7 +259,7 @@ public class CreateOfferUsingMarketPriceMarginTest extends AbstractOfferTest {
10_000_000L,
5_000_000L,
0.0,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
usdAccount.getId(),
triggerPrice);
assertTrue(newOffer.getIsMyOffer());
diff --git a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java
index 4b7032c86f..8571c0c59d 100644
--- a/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/offer/CreateXMROffersTest.java
@@ -62,7 +62,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
75_000_000L,
"0.005", // FIXED PRICE IN BTC FOR 1 XMR
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@@ -108,7 +108,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
50_000_000L,
"0.005", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
assertTrue(newOffer.getIsMyOffer());
@@ -156,7 +156,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
75_000_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(),
triggerPrice);
log.debug("Pending Sell XMR (Buy BTC) offer:\n{}", toOfferTable.apply(newOffer));
@@ -211,7 +211,7 @@ public class CreateXMROffersTest extends AbstractOfferTest {
100_000_000L,
50_000_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Buy XMR (Sell BTC) offer:\n{}", toOfferTable.apply(newOffer));
diff --git a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java
index f299801c5a..e8745c1b2c 100644
--- a/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/offer/ValidateCreateOfferTest.java
@@ -47,7 +47,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
100000000000L, // exceeds amount limit
100000000000L,
"10000.0000",
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
usdAccount.getId()));
assertEquals("UNKNOWN: An error occurred at task: ValidateOffer", exception.getMessage());
}
@@ -63,7 +63,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
"40000.0000",
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
chfAccount.getId()));
String expectedError = format("UNKNOWN: cannot create EUR offer with payment account %s", chfAccount.getId());
assertEquals(expectedError, exception.getMessage());
@@ -80,7 +80,7 @@ public class ValidateCreateOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
"63000.0000",
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
audAccount.getId()));
String expectedError = format("UNKNOWN: cannot create CAD offer with payment account %s", audAccount.getId());
assertEquals(expectedError, exception.getMessage());
diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java
index fee6e798ba..abbec38ff6 100644
--- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferTest.java
@@ -52,7 +52,7 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java
index 10be976c8b..3199a6b053 100644
--- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyBTCOfferWithNationalBankAcctTest.java
@@ -96,7 +96,7 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
1_000_000L,
1_000_000L, // min-amount = amount
0.00,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesPaymentAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java
index 40289e1d50..f61203400b 100644
--- a/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeBuyXMROfferTest.java
@@ -65,7 +65,7 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
15_000_000L,
7_500_000L,
"0.00455500", // FIXED PRICE IN BTC (satoshis) FOR 1 XMR
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesXmrAcct.getId());
log.debug("Alice's BUY XMR (SELL BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
genBtcBlocksThenWait(1, 5000);
diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java
index a425759717..4f43c21cd7 100644
--- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellBTCOfferTest.java
@@ -58,7 +58,7 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
12_500_000L,
12_500_000L, // min-amount = amount
0.00,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesUsdAccount.getId(),
NO_TRIGGER_PRICE);
var offerId = alicesOffer.getId();
diff --git a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java
index 68daa64050..9a769b5f04 100644
--- a/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java
+++ b/apitest/src/test/java/haveno/apitest/method/trade/TakeSellXMROfferTest.java
@@ -71,7 +71,7 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
20_000_000L,
10_500_000L,
priceMarginPctInput,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
alicesXmrAcct.getId(),
NO_TRIGGER_PRICE);
log.debug("Alice's SELL XMR (BUY BTC) Offer:\n{}", new TableBuilder(OFFER_TBL, alicesOffer).build());
diff --git a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java
index 13b72ff79f..356b77ef8a 100644
--- a/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java
+++ b/apitest/src/test/java/haveno/apitest/scenario/LongRunningOfferDeactivationTest.java
@@ -57,7 +57,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
1_000_000,
1_000_000,
0.00,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("SELL offer {} created with margin based price {}.",
@@ -103,7 +103,7 @@ public class LongRunningOfferDeactivationTest extends AbstractOfferTest {
1_000_000,
1_000_000,
0.00,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
paymentAcct.getId(),
triggerPrice);
log.info("BUY offer {} created with margin based price {}.",
diff --git a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java
index eebaec8b04..4e8b86afac 100644
--- a/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java
+++ b/apitest/src/test/java/haveno/apitest/scenario/bot/RandomOffer.java
@@ -28,7 +28,7 @@ import java.text.DecimalFormat;
import java.util.Objects;
import java.util.function.Supplier;
-import static haveno.apitest.method.offer.AbstractOfferTest.defaultBuyerSecurityDepositPct;
+import static haveno.apitest.method.offer.AbstractOfferTest.defaultSecurityDepositPct;
import static haveno.cli.CurrencyFormat.formatInternalFiatPrice;
import static haveno.cli.CurrencyFormat.formatSatoshis;
import static haveno.common.util.MathUtils.scaleDownByPowerOf10;
@@ -119,7 +119,7 @@ public class RandomOffer {
amount,
minAmount,
priceMargin,
- defaultBuyerSecurityDepositPct.get(),
+ defaultSecurityDepositPct.get(),
"0" /*no trigger price*/);
} else {
this.offer = botClient.createOfferAtFixedPrice(paymentAccount,
@@ -128,7 +128,7 @@ public class RandomOffer {
amount,
minAmount,
fixedOfferPrice,
- defaultBuyerSecurityDepositPct.get());
+ defaultSecurityDepositPct.get());
}
this.id = offer.getId();
return this;
diff --git a/assets/src/main/java/haveno/asset/Trc20Token.java b/assets/src/main/java/haveno/asset/Trc20Token.java
new file mode 100644
index 0000000000..3cffa34455
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/Trc20Token.java
@@ -0,0 +1,29 @@
+/*
+ * This file is part of Haveno.
+ *
+ * Haveno 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.
+ *
+ * Haveno 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 Haveno. If not, see .
+ */
+
+package haveno.asset;
+
+/**
+ * Abstract base class for Tron-based {@link Token}s that implement the
+ * TRC-20 Token Standard.
+ */
+public abstract class Trc20Token extends Token {
+
+ public Trc20Token(String name, String tickerSymbol) {
+ super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}"));
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/package-info.java b/assets/src/main/java/haveno/asset/package-info.java
index a50b986115..6eeaf4095f 100644
--- a/assets/src/main/java/haveno/asset/package-info.java
+++ b/assets/src/main/java/haveno/asset/package-info.java
@@ -21,7 +21,7 @@
* {@link haveno.asset.Token} and {@link haveno.asset.Erc20Token}, as well as concrete
* implementations of each, such as {@link haveno.asset.coins.Bitcoin} itself, cryptos like
* {@link haveno.asset.coins.Litecoin} and {@link haveno.asset.coins.Ether} and tokens like
- * {@link haveno.asset.tokens.DaiStablecoin}.
+ * {@link haveno.asset.tokens.DaiStablecoinERC20}.
*
* The purpose of this package is to provide everything necessary for registering
* ("listing") new assets and managing / accessing those assets within, e.g. the Haveno
diff --git a/assets/src/main/java/haveno/asset/tokens/USDCoin.java b/assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java
similarity index 85%
rename from assets/src/main/java/haveno/asset/tokens/USDCoin.java
rename to assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java
index b3e5c121e5..8c1e84e871 100644
--- a/assets/src/main/java/haveno/asset/tokens/USDCoin.java
+++ b/assets/src/main/java/haveno/asset/tokens/DaiStablecoinERC20.java
@@ -19,9 +19,9 @@ package haveno.asset.tokens;
import haveno.asset.Erc20Token;
-public class USDCoin extends Erc20Token {
+public class DaiStablecoinERC20 extends Erc20Token {
- public USDCoin() {
- super("USD Coin", "USDC");
+ public DaiStablecoinERC20() {
+ super("Dai Stablecoin", "DAI-ERC20");
}
}
diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
new file mode 100644
index 0000000000..1afb7ff1f2
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDERC20.java
@@ -0,0 +1,11 @@
+package haveno.asset.tokens;
+
+import haveno.asset.Erc20Token;
+
+public class TetherUSDERC20 extends Erc20Token {
+ public TetherUSDERC20() {
+ // If you add a new USDT variant or want to change this ticker symbol you should also look here:
+ // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
+ super("Tether USD (ERC20)", "USDT-ERC20");
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
new file mode 100644
index 0000000000..c5669d126a
--- /dev/null
+++ b/assets/src/main/java/haveno/asset/tokens/TetherUSDTRC20.java
@@ -0,0 +1,11 @@
+package haveno.asset.tokens;
+
+import haveno.asset.Trc20Token;
+
+public class TetherUSDTRC20 extends Trc20Token {
+ public TetherUSDTRC20() {
+ // If you add a new USDT variant or want to change this ticker symbol you should also look here:
+ // core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
+ super("Tether USD (TRC20)", "USDT-TRC20");
+ }
+}
diff --git a/assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
similarity index 85%
rename from assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java
rename to assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
index e9cc01f74f..a65c021df9 100644
--- a/assets/src/main/java/haveno/asset/tokens/DaiStablecoin.java
+++ b/assets/src/main/java/haveno/asset/tokens/USDCoinERC20.java
@@ -19,9 +19,9 @@ package haveno.asset.tokens;
import haveno.asset.Erc20Token;
-public class DaiStablecoin extends Erc20Token {
+public class USDCoinERC20 extends Erc20Token {
- public DaiStablecoin() {
- super("Dai Stablecoin", "DAI");
+ public USDCoinERC20() {
+ super("USD Coin (ERC20)", "USDC-ERC20");
}
}
diff --git a/assets/src/main/resources/META-INF/services/haveno.asset.Asset b/assets/src/main/resources/META-INF/services/haveno.asset.Asset
index 7108c28830..80b9cd036d 100644
--- a/assets/src/main/resources/META-INF/services/haveno.asset.Asset
+++ b/assets/src/main/resources/META-INF/services/haveno.asset.Asset
@@ -7,3 +7,7 @@ haveno.asset.coins.BitcoinCash
haveno.asset.coins.Ether
haveno.asset.coins.Litecoin
haveno.asset.coins.Monero
+haveno.asset.tokens.TetherUSDERC20
+haveno.asset.tokens.TetherUSDTRC20
+haveno.asset.tokens.USDCoinERC20
+haveno.asset.tokens.DaiStablecoinERC20
\ No newline at end of file
diff --git a/assets/src/test/java/haveno/asset/coins/BitcoinTest.java b/assets/src/test/java/haveno/asset/coins/BitcoinTest.java
index ad3bcee2c8..e90c25b6f7 100644
--- a/assets/src/test/java/haveno/asset/coins/BitcoinTest.java
+++ b/assets/src/test/java/haveno/asset/coins/BitcoinTest.java
@@ -32,6 +32,7 @@ public class BitcoinTest extends AbstractAssetTest {
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
assertValidAddress("1111111111111111111114oLvT2");
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
+ assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw");
}
@Test
diff --git a/assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java b/assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java
new file mode 100644
index 0000000000..fd7d97ce7d
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/TetherUSDERC20Test.java
@@ -0,0 +1,43 @@
+/*
+ * 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 .
+ */
+
+package haveno.asset.coins;
+
+import haveno.asset.AbstractAssetTest;
+import haveno.asset.tokens.TetherUSDERC20;
+
+import org.junit.jupiter.api.Test;
+
+ public class TetherUSDERC20Test extends AbstractAssetTest {
+
+ public TetherUSDERC20Test() {
+ super(new TetherUSDERC20());
+ }
+
+ @Test
+ public void testValidAddresses() {
+ assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226");
+ assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226");
+ }
+
+ @Test
+ public void testInvalidAddresses() {
+ assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
+ assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
+ assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
+ }
+ }
\ No newline at end of file
diff --git a/assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java b/assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java
new file mode 100644
index 0000000000..7fef554c75
--- /dev/null
+++ b/assets/src/test/java/haveno/asset/coins/TetherUSDTRC20Test.java
@@ -0,0 +1,42 @@
+/*
+ * This file is part of Haveno.
+ *
+ * Haveno 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.
+ *
+ * Haveno 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 Haveno. If not, see .
+ */
+
+package haveno.asset.coins;
+
+import haveno.asset.AbstractAssetTest;
+import haveno.asset.tokens.TetherUSDTRC20;
+
+import org.junit.jupiter.api.Test;
+
+ public class TetherUSDTRC20Test extends AbstractAssetTest {
+
+ public TetherUSDTRC20Test() {
+ super(new TetherUSDTRC20());
+ }
+
+ @Test
+ public void testValidAddresses() {
+ assertValidAddress("TVnmu3E6DYVL4bpAoZnPNEPVUrgC7eSWaX");
+ }
+
+ @Test
+ public void testInvalidAddresses() {
+ assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
+ assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
+ assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
+ }
+ }
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index ce796bc2d9..0c8a4412f2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -49,7 +49,7 @@ configure(subprojects) {
gsonVersion = '2.8.5'
guavaVersion = '32.1.1-jre'
guiceVersion = '7.0.0'
- moneroJavaVersion = '0.8.33'
+ moneroJavaVersion = '0.8.36'
httpclient5Version = '5.0'
hamcrestVersion = '2.2'
httpclientVersion = '4.5.12'
@@ -71,7 +71,7 @@ configure(subprojects) {
loggingVersion = '1.2'
lombokVersion = '1.18.30'
mockitoVersion = '5.10.0'
- netlayerVersion = 'e2ce2a142c' // Tor browser version 13.0.15 and tor binary version: 0.4.8.11
+ netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
protobufVersion = '3.19.1'
protocVersion = protobufVersion
pushyVersion = '0.13.2'
@@ -457,14 +457,14 @@ configure(project(':core')) {
doLast {
// get monero binaries download url
Map moneroBinaries = [
- 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-x86_64.tar.gz',
- 'linux-x86_64-sha256' : '0810808292fd5ad595a46a7fcc8ecb28d251d80f8d75c0e7a7d51afbeb413b68',
- 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-linux-aarch64.tar.gz',
- 'linux-aarch64-sha256' : '61222ee8e2021aaf59ab8813543afc5548f484190ee9360bc9cfa8fdf21cc1de',
- 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-mac.tar.gz',
- 'mac-sha256' : '5debb8d8d8dd63809e8351368a11aa85c47987f1a8a8f2dcca343e60bcff3287',
- 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release4/monero-bins-haveno-windows.zip',
- 'windows-sha256' : 'd7c14f029db37ae2a8bc6b74c35f572283257df5fbcc8cc97b704d1a97be9888'
+ 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz',
+ 'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208',
+ 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz',
+ 'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c',
+ 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz',
+ 'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8',
+ 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip',
+ 'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d'
]
String osKey
@@ -506,16 +506,16 @@ configure(project(':core')) {
} else {
ext.extractArchiveTarGz(moneroArchiveFile, localnetDir)
}
+ }
- // add the current platform's monero dependencies into the resources folder for installation
- copy {
- from "${monerodFile}"
- into "${project(':core').projectDir}/src/main/resources/bin"
- }
- copy {
- from "${moneroRpcFile}"
- into "${project(':core').projectDir}/src/main/resources/bin"
- }
+ // add the current platform's monero dependencies into the resources folder for installation
+ copy {
+ from "${monerodFile}"
+ into "${project(':core').projectDir}/src/main/resources/bin"
+ }
+ copy {
+ from "${moneroRpcFile}"
+ into "${project(':core').projectDir}/src/main/resources/bin"
}
}
@@ -610,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
- version = '1.0.12-SNAPSHOT'
+ version = '1.1.1-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,
diff --git a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java
index eaa0cac150..2fcb3426d1 100644
--- a/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java
+++ b/cli/src/main/java/haveno/cli/request/OffersServiceRequest.java
@@ -81,7 +81,7 @@ public class OffersServiceRequest {
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMarginPct(marketPriceMarginPct)
- .setBuyerSecurityDepositPct(securityDepositPct)
+ .setSecurityDepositPct(securityDepositPct)
.setPaymentAccountId(paymentAcctId)
.setTriggerPrice(triggerPrice)
.build();
diff --git a/common/src/main/java/haveno/common/ClockWatcher.java b/common/src/main/java/haveno/common/ClockWatcher.java
index 7bd8454c78..ba77ca38ab 100644
--- a/common/src/main/java/haveno/common/ClockWatcher.java
+++ b/common/src/main/java/haveno/common/ClockWatcher.java
@@ -69,7 +69,7 @@ public class ClockWatcher {
listeners.forEach(listener -> listener.onMissedSecondTick(missedMs));
if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) {
- log.info("We have been in standby mode for {} sec", missedMs / 1000);
+ log.warn("We have been in standby mode for {} sec", missedMs / 1000);
listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs));
}
}
diff --git a/common/src/main/java/haveno/common/app/Capabilities.java b/common/src/main/java/haveno/common/app/Capabilities.java
index 39266a25c6..5bb3bd0bc4 100644
--- a/common/src/main/java/haveno/common/app/Capabilities.java
+++ b/common/src/main/java/haveno/common/app/Capabilities.java
@@ -59,8 +59,10 @@ public class Capabilities {
}
public Capabilities(Collection capabilities) {
- synchronized (this.capabilities) {
- this.capabilities.addAll(capabilities);
+ synchronized (capabilities) {
+ synchronized (this.capabilities) {
+ this.capabilities.addAll(capabilities);
+ }
}
}
@@ -73,9 +75,11 @@ public class Capabilities {
}
public void set(Collection capabilities) {
- synchronized (this.capabilities) {
- this.capabilities.clear();
- this.capabilities.addAll(capabilities);
+ synchronized (capabilities) {
+ synchronized (this.capabilities) {
+ this.capabilities.clear();
+ this.capabilities.addAll(capabilities);
+ }
}
}
@@ -87,15 +91,19 @@ public class Capabilities {
public void addAll(Capabilities capabilities) {
if (capabilities != null) {
- synchronized (this.capabilities) {
- this.capabilities.addAll(capabilities.capabilities);
+ synchronized (capabilities.capabilities) {
+ synchronized (this.capabilities) {
+ this.capabilities.addAll(capabilities.capabilities);
+ }
}
}
}
public boolean containsAll(final Set requiredItems) {
- synchronized (this.capabilities) {
- return capabilities.containsAll(requiredItems);
+ synchronized(requiredItems) {
+ synchronized (this.capabilities) {
+ return capabilities.containsAll(requiredItems);
+ }
}
}
@@ -129,7 +137,9 @@ public class Capabilities {
* @return int list of Capability ordinals
*/
public static List toIntList(Capabilities capabilities) {
- return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
+ synchronized (capabilities.capabilities) {
+ return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
+ }
}
/**
@@ -139,11 +149,13 @@ public class Capabilities {
* @return a {@link Capabilities} object
*/
public static Capabilities fromIntList(List capabilities) {
- return new Capabilities(capabilities.stream()
- .filter(integer -> integer < Capability.values().length)
- .filter(integer -> integer >= 0)
- .map(integer -> Capability.values()[integer])
- .collect(Collectors.toSet()));
+ synchronized (capabilities) {
+ return new Capabilities(capabilities.stream()
+ .filter(integer -> integer < Capability.values().length)
+ .filter(integer -> integer >= 0)
+ .map(integer -> Capability.values()[integer])
+ .collect(Collectors.toSet()));
+ }
}
/**
@@ -181,7 +193,9 @@ public class Capabilities {
}
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
- return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
+ synchronized (capabilities.capabilities) {
+ return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
+ }
}
@Override
@@ -211,8 +225,10 @@ public class Capabilities {
// Neither would support removal of past capabilities, a use case we never had so far and which might have
// backward compatibility issues, so we should treat capabilities as an append-only data structure.
public int findHighestCapability(Capabilities capabilities) {
- return (int) capabilities.capabilities.stream()
- .mapToLong(e -> (long) e.ordinal())
- .sum();
+ synchronized (capabilities.capabilities) {
+ return (int) capabilities.capabilities.stream()
+ .mapToLong(e -> (long) e.ordinal())
+ .sum();
+ }
}
}
diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java
index 6708f17d79..d7be891aea 100644
--- a/common/src/main/java/haveno/common/app/Version.java
+++ b/common/src/main/java/haveno/common/app/Version.java
@@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version {
// The application versions
// We use semantic versioning with major, minor and patch
- public static final String VERSION = "1.0.12";
+ public static final String VERSION = "1.1.1";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
@@ -72,6 +72,25 @@ public class Version {
return false;
}
+ public static int compare(String version1, String version2) {
+ if (version1.equals(version2))
+ return 0;
+ else if (getMajorVersion(version1) > getMajorVersion(version2))
+ return 1;
+ else if (getMajorVersion(version1) < getMajorVersion(version2))
+ return -1;
+ else if (getMinorVersion(version1) > getMinorVersion(version2))
+ return 1;
+ else if (getMinorVersion(version1) < getMinorVersion(version2))
+ return -1;
+ else if (getPatchVersion(version1) > getPatchVersion(version2))
+ return 1;
+ else if (getPatchVersion(version1) < getPatchVersion(version2))
+ return -1;
+ else
+ return 0;
+ }
+
private static int getSubVersion(String version, int index) {
final String[] split = version.split("\\.");
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
@@ -91,8 +110,9 @@ public class Version {
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Haveno app.
- // VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
- public static final int TRADE_PROTOCOL_VERSION = 1;
+ // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
+ // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
+ public static final int TRADE_PROTOCOL_VERSION = 2;
private static String p2pMessageVersion;
public static String getP2PMessageVersion() {
diff --git a/common/src/main/java/haveno/common/config/Config.java b/common/src/main/java/haveno/common/config/Config.java
index 92359c76f9..b162e211b4 100644
--- a/common/src/main/java/haveno/common/config/Config.java
+++ b/common/src/main/java/haveno/common/config/Config.java
@@ -117,6 +117,8 @@ public class Config {
public static final String BTC_FEE_INFO = "bitcoinFeeInfo";
public static final String BYPASS_MEMPOOL_VALIDATION = "bypassMempoolValidation";
public static final String PASSWORD_REQUIRED = "passwordRequired";
+ public static final String UPDATE_XMR_BINARIES = "updateXmrBinaries";
+ public static final String XMR_BLOCKCHAIN_PATH = "xmrBlockchainPath";
// Default values for certain options
public static final int UNSPECIFIED_PORT = -1;
@@ -204,6 +206,8 @@ public class Config {
public final boolean republishMailboxEntries;
public final boolean bypassMempoolValidation;
public final boolean passwordRequired;
+ public final boolean updateXmrBinaries;
+ public final String xmrBlockchainPath;
// Properties derived from options but not exposed as options themselves
public final File torDir;
@@ -621,6 +625,20 @@ public class Config {
.ofType(boolean.class)
.defaultsTo(false);
+ ArgumentAcceptingOptionSpec updateXmrBinariesOpt =
+ parser.accepts(UPDATE_XMR_BINARIES,
+ "Update Monero binaries if applicable")
+ .withRequiredArg()
+ .ofType(boolean.class)
+ .defaultsTo(true);
+
+ ArgumentAcceptingOptionSpec xmrBlockchainPathOpt =
+ parser.accepts(XMR_BLOCKCHAIN_PATH,
+ "Path to Monero blockchain when using local Monero node")
+ .withRequiredArg()
+ .ofType(String.class)
+ .defaultsTo("");
+
try {
CompositeOptionSet options = new CompositeOptionSet();
@@ -733,6 +751,8 @@ public class Config {
this.republishMailboxEntries = options.valueOf(republishMailboxEntriesOpt);
this.bypassMempoolValidation = options.valueOf(bypassMempoolValidationOpt);
this.passwordRequired = options.valueOf(passwordRequiredOpt);
+ this.updateXmrBinaries = options.valueOf(updateXmrBinariesOpt);
+ this.xmrBlockchainPath = options.valueOf(xmrBlockchainPathOpt);
} catch (OptionException ex) {
throw new ConfigException("problem parsing option '%s': %s",
ex.options().get(0),
@@ -742,11 +762,11 @@ public class Config {
}
// Create all appDataDir subdirectories and assign to their respective properties
- File btcNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
- this.keyStorageDir = mkdir(btcNetworkDir, "keys");
- this.storageDir = mkdir(btcNetworkDir, "db");
- this.torDir = mkdir(btcNetworkDir, "tor");
- this.walletDir = mkdir(btcNetworkDir, "wallet");
+ File xmrNetworkDir = mkdir(appDataDir, baseCurrencyNetwork.name().toLowerCase());
+ this.keyStorageDir = mkdir(xmrNetworkDir, "keys");
+ this.storageDir = mkdir(xmrNetworkDir, "db");
+ this.torDir = mkdir(xmrNetworkDir, "tor");
+ this.walletDir = mkdir(xmrNetworkDir, "wallet");
// Assign values to special-case static fields
APP_DATA_DIR_VALUE = appDataDir;
diff --git a/common/src/main/java/haveno/common/crypto/KeyRing.java b/common/src/main/java/haveno/common/crypto/KeyRing.java
index 89552c7c74..580b3b285a 100644
--- a/common/src/main/java/haveno/common/crypto/KeyRing.java
+++ b/common/src/main/java/haveno/common/crypto/KeyRing.java
@@ -110,7 +110,7 @@ public final class KeyRing {
* @param password The password to unlock the keys or to generate new keys, nullable.
*/
public void generateKeys(String password) {
- if (isUnlocked()) throw new Error("Current keyring must be closed to generate new keys");
+ if (isUnlocked()) throw new IllegalStateException("Current keyring must be closed to generate new keys");
symmetricKey = Encryption.generateSecretKey(256);
signatureKeyPair = Sig.generateKeyPair();
encryptionKeyPair = Encryption.generateKeyPair();
diff --git a/common/src/main/java/haveno/common/crypto/KeyStorage.java b/common/src/main/java/haveno/common/crypto/KeyStorage.java
index 05280b8d4a..5edfb89575 100644
--- a/common/src/main/java/haveno/common/crypto/KeyStorage.java
+++ b/common/src/main/java/haveno/common/crypto/KeyStorage.java
@@ -243,6 +243,11 @@ public class KeyStorage {
//noinspection ResultOfMethodCallIgnored
storageDir.mkdirs();
+ // password must be ascii
+ if (password != null && !password.matches("\\p{ASCII}*")) {
+ throw new IllegalArgumentException("Password must be ASCII.");
+ }
+
var oldPasswordChars = oldPassword == null ? new char[0] : oldPassword.toCharArray();
var passwordChars = password == null ? new char[0] : password.toCharArray();
try {
diff --git a/common/src/main/java/haveno/common/file/FileUtil.java b/common/src/main/java/haveno/common/file/FileUtil.java
index 27058f3025..ca533cc0d2 100644
--- a/common/src/main/java/haveno/common/file/FileUtil.java
+++ b/common/src/main/java/haveno/common/file/FileUtil.java
@@ -32,6 +32,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
@@ -74,17 +75,22 @@ public class FileUtil {
}
}
- public static File getLatestBackupFile(File dir, String fileName) {
+ public static List getBackupFiles(File dir, String fileName) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
- if (!backupDir.exists()) return null;
+ if (!backupDir.exists()) return new ArrayList();
String dirName = "backups_" + fileName;
if (dirName.contains(".")) dirName = dirName.replace(".", "_");
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
- if (!backupFileDir.exists()) return null;
+ if (!backupFileDir.exists()) return new ArrayList();
File[] files = backupFileDir.listFiles();
- if (files == null || files.length == 0) return null;
- Arrays.sort(files, Comparator.comparing(File::getName));
- return files[files.length - 1];
+ return Arrays.asList(files);
+ }
+
+ public static File getLatestBackupFile(File dir, String fileName) {
+ List files = getBackupFiles(dir, fileName);
+ if (files.isEmpty()) return null;
+ files.sort(Comparator.comparing(File::getName));
+ return files.get(files.size() - 1);
}
public static void deleteRollingBackup(File dir, String fileName) {
diff --git a/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java b/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java
index 6e336c0036..d9af624c67 100644
--- a/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java
+++ b/common/src/main/java/haveno/common/util/SingleThreadExecutorUtils.java
@@ -11,8 +11,8 @@
* 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 .
+ * You should have received a copy of the GNU Affero General Public
+ * License along with Bisq. If not, see .
*/
package haveno.common.util;
@@ -25,38 +25,67 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
+/**
+ * Utility class for creating single-threaded executors.
+ */
public class SingleThreadExecutorUtils {
+
+ private SingleThreadExecutorUtils() {
+ // Prevent instantiation
+ }
+
public static ExecutorService getSingleThreadExecutor(Class> aClass) {
- String name = aClass.getSimpleName();
- return getSingleThreadExecutor(name);
+ validateClass(aClass);
+ return getSingleThreadExecutor(aClass.getSimpleName());
}
public static ExecutorService getNonDaemonSingleThreadExecutor(Class> aClass) {
- String name = aClass.getSimpleName();
- return getSingleThreadExecutor(name, false);
+ validateClass(aClass);
+ return getSingleThreadExecutor(aClass.getSimpleName(), false);
}
public static ExecutorService getSingleThreadExecutor(String name) {
+ validateName(name);
return getSingleThreadExecutor(name, true);
}
public static ListeningExecutorService getSingleThreadListeningExecutor(String name) {
+ validateName(name);
return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name));
}
public static ExecutorService getSingleThreadExecutor(ThreadFactory threadFactory) {
+ validateThreadFactory(threadFactory);
return Executors.newSingleThreadExecutor(threadFactory);
}
private static ExecutorService getSingleThreadExecutor(String name, boolean isDaemonThread) {
- final ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
+ ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
return Executors.newSingleThreadExecutor(threadFactory);
}
private static ThreadFactory getThreadFactory(String name, boolean isDaemonThread) {
return new ThreadFactoryBuilder()
- .setNameFormat(name)
+ .setNameFormat(name + "-%d")
.setDaemon(isDaemonThread)
.build();
}
+
+ private static void validateClass(Class> aClass) {
+ if (aClass == null) {
+ throw new IllegalArgumentException("Class must not be null.");
+ }
+ }
+
+ private static void validateName(String name) {
+ if (name == null || name.isEmpty()) {
+ throw new IllegalArgumentException("Name must not be null or empty.");
+ }
+ }
+
+ private static void validateThreadFactory(ThreadFactory threadFactory) {
+ if (threadFactory == null) {
+ throw new IllegalArgumentException("ThreadFactory must not be null.");
+ }
+ }
}
diff --git a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java
index b4ba7b58a8..f86a0c2bb2 100644
--- a/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java
+++ b/core/src/main/java/haveno/core/account/sign/SignedWitnessService.java
@@ -335,12 +335,13 @@ public class SignedWitnessService {
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
- if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) {
+ String pubKeyHex = Utilities.encodeToHex(key.getPubKey());
+ if (arbitratorManager.isPublicKeyInList(pubKeyHex)) {
key.verifyMessage(message, signatureBase64);
verifySignatureWithECKeyResultCache.put(hash, true);
return true;
} else {
- log.warn("Provided EC key is not in list of valid arbitrators.");
+ log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex);
verifySignatureWithECKeyResultCache.put(hash, false);
return false;
}
diff --git a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java
index deab92eb50..978b6dd715 100644
--- a/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java
+++ b/core/src/main/java/haveno/core/account/witness/AccountAgeWitnessService.java
@@ -40,6 +40,7 @@ import haveno.core.offer.OfferDirection;
import haveno.core.offer.OfferRestrictions;
import haveno.core.payment.ChargeBackRisk;
import haveno.core.payment.PaymentAccount;
+import haveno.core.payment.TradeLimits;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.support.dispute.Dispute;
@@ -498,10 +499,15 @@ public class AccountAgeWitnessService {
return getAccountAge(getMyWitness(paymentAccountPayload), new Date());
}
- public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction) {
+ public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferDirection direction, boolean buyerAsTakerWithoutDeposit) {
if (paymentAccount == null)
return 0;
+ if (buyerAsTakerWithoutDeposit) {
+ TradeLimits tradeLimits = new TradeLimits();
+ return tradeLimits.getMaxTradeLimitBuyerAsTakerWithoutDeposit().longValueExact();
+ }
+
AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload());
BigInteger maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimit(currencyCode);
if (hasTradeLimitException(accountAgeWitness)) {
@@ -737,14 +743,13 @@ public class AccountAgeWitnessService {
}
public Optional traderSignAndPublishPeersAccountAgeWitness(Trade trade) {
- AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
- BigInteger tradeAmount = trade.getAmount();
checkNotNull(trade.getTradePeer().getPubKeyRing(), "Peer must have a keyring");
PublicKey peersPubKey = trade.getTradePeer().getPubKeyRing().getSignaturePubKey();
- checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}",
- trade.toString());
- checkNotNull(tradeAmount, "Trade amount must not be null");
checkNotNull(peersPubKey, "Peers pub key must not be null");
+ AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
+ checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade " + trade.toString());
+ BigInteger tradeAmount = trade.getAmount();
+ checkNotNull(tradeAmount, "Trade amount must not be null");
try {
return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey);
diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java
index 14ec0f6d08..e8e83978eb 100644
--- a/core/src/main/java/haveno/core/api/CoreApi.java
+++ b/core/src/main/java/haveno/core/api/CoreApi.java
@@ -239,8 +239,8 @@ public class CoreApi {
xmrConnectionService.stopCheckingConnection();
}
- public MoneroRpcConnection getBestAvailableXmrConnection() {
- return xmrConnectionService.getBestAvailableConnection();
+ public MoneroRpcConnection getBestXmrConnection() {
+ return xmrConnectionService.getBestConnection();
}
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
@@ -413,18 +413,22 @@ public class CoreApi {
}
public void postOffer(String currencyCode,
- String directionAsString,
- String priceAsString,
- boolean useMarketBasedPrice,
- double marketPriceMargin,
- long amountAsLong,
- long minAmountAsLong,
- double buyerSecurityDeposit,
- String triggerPriceAsString,
- boolean reserveExactAmount,
- String paymentAccountId,
- Consumer resultHandler,
- ErrorMessageHandler errorMessageHandler) {
+ String directionAsString,
+ String priceAsString,
+ boolean useMarketBasedPrice,
+ double marketPriceMargin,
+ long amountAsLong,
+ long minAmountAsLong,
+ double securityDepositPct,
+ String triggerPriceAsString,
+ boolean reserveExactAmount,
+ String paymentAccountId,
+ boolean isPrivateOffer,
+ boolean buyerAsTakerWithoutDeposit,
+ String extraInfo,
+ String sourceOfferId,
+ Consumer resultHandler,
+ ErrorMessageHandler errorMessageHandler) {
coreOffersService.postOffer(currencyCode,
directionAsString,
priceAsString,
@@ -432,10 +436,14 @@ public class CoreApi {
marketPriceMargin,
amountAsLong,
minAmountAsLong,
- buyerSecurityDeposit,
+ securityDepositPct,
triggerPriceAsString,
reserveExactAmount,
paymentAccountId,
+ isPrivateOffer,
+ buyerAsTakerWithoutDeposit,
+ extraInfo,
+ sourceOfferId,
resultHandler,
errorMessageHandler);
}
@@ -448,8 +456,11 @@ public class CoreApi {
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
- double buyerSecurityDeposit,
- PaymentAccount paymentAccount) {
+ double securityDepositPct,
+ PaymentAccount paymentAccount,
+ boolean isPrivateOffer,
+ boolean buyerAsTakerWithoutDeposit,
+ String extraInfo) {
return coreOffersService.editOffer(offerId,
currencyCode,
direction,
@@ -458,8 +469,11 @@ public class CoreApi {
marketPriceMargin,
amount,
minAmount,
- buyerSecurityDeposit,
- paymentAccount);
+ securityDepositPct,
+ paymentAccount,
+ isPrivateOffer,
+ buyerAsTakerWithoutDeposit,
+ extraInfo);
}
public void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@@ -535,9 +549,11 @@ public class CoreApi {
public void takeOffer(String offerId,
String paymentAccountId,
long amountAsLong,
+ String challenge,
Consumer resultHandler,
ErrorMessageHandler errorMessageHandler) {
Offer offer = coreOffersService.getOffer(offerId);
+ offer.setChallenge(challenge);
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
}
diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java
index f193287edc..f4bb4c803d 100644
--- a/core/src/main/java/haveno/core/api/CoreDisputesService.java
+++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java
@@ -62,11 +62,12 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CoreDisputesService {
- public enum DisputePayout {
+ // TODO: persist in DisputeResult?
+ public enum PayoutSuggestion {
BUYER_GETS_TRADE_AMOUNT,
- BUYER_GETS_ALL, // used in desktop
+ BUYER_GETS_ALL,
SELLER_GETS_TRADE_AMOUNT,
- SELLER_GETS_ALL, // used in desktop
+ SELLER_GETS_ALL,
CUSTOM
}
@@ -172,17 +173,17 @@ public class CoreDisputesService {
// create dispute result
var closeDate = new Date();
var winnerDisputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
- DisputePayout payout;
+ PayoutSuggestion payoutSuggestion;
if (customWinnerAmount > 0) {
- payout = DisputePayout.CUSTOM;
+ payoutSuggestion = PayoutSuggestion.CUSTOM;
} else if (winner == DisputeResult.Winner.BUYER) {
- payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
+ payoutSuggestion = PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT;
} else if (winner == DisputeResult.Winner.SELLER) {
- payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
+ payoutSuggestion = PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT;
} else {
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
}
- applyPayoutAmountsToDisputeResult(payout, winningDispute, winnerDisputeResult, customWinnerAmount);
+ applyPayoutAmountsToDisputeResult(payoutSuggestion, winningDispute, winnerDisputeResult, customWinnerAmount);
// close winning dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, winnerDisputeResult, () -> {
@@ -227,26 +228,26 @@ public class CoreDisputesService {
* Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer
* receives the remaining amount minus the mining fee.
*/
- public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
+ public void applyPayoutAmountsToDisputeResult(PayoutSuggestion payoutSuggestion, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) {
Contract contract = dispute.getContract();
Trade trade = tradeManager.getTrade(dispute.getTradeId());
BigInteger buyerSecurityDeposit = trade.getBuyer().getSecurityDeposit();
BigInteger sellerSecurityDeposit = trade.getSeller().getSecurityDeposit();
BigInteger tradeAmount = contract.getTradeAmount();
disputeResult.setSubtractFeeFrom(DisputeResult.SubtractFeeFrom.BUYER_AND_SELLER);
- if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) {
+ if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit));
disputeResult.setSellerPayoutAmountBeforeCost(sellerSecurityDeposit);
- } else if (payout == DisputePayout.BUYER_GETS_ALL) {
+ } else if (payoutSuggestion == PayoutSuggestion.BUYER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser? (see post v1.1.7)
disputeResult.setSellerPayoutAmountBeforeCost(BigInteger.ZERO);
- } else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) {
+ } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_TRADE_AMOUNT) {
disputeResult.setBuyerPayoutAmountBeforeCost(buyerSecurityDeposit);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit));
- } else if (payout == DisputePayout.SELLER_GETS_ALL) {
+ } else if (payoutSuggestion == PayoutSuggestion.SELLER_GETS_ALL) {
disputeResult.setBuyerPayoutAmountBeforeCost(BigInteger.ZERO);
disputeResult.setSellerPayoutAmountBeforeCost(tradeAmount.add(sellerSecurityDeposit).add(buyerSecurityDeposit));
- } else if (payout == DisputePayout.CUSTOM) {
+ } else if (payoutSuggestion == PayoutSuggestion.CUSTOM) {
if (customWinnerAmount > trade.getWallet().getBalance().longValueExact()) throw new RuntimeException("Winner payout is more than the trade wallet's balance");
long loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).subtract(BigInteger.valueOf(customWinnerAmount)).longValueExact();
if (loserAmount < 0) throw new RuntimeException("Loser payout cannot be negative");
diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java
index 5bdd2f1605..3ee7e047f1 100644
--- a/core/src/main/java/haveno/core/api/CoreOffersService.java
+++ b/core/src/main/java/haveno/core/api/CoreOffersService.java
@@ -43,6 +43,7 @@ import static haveno.common.util.MathUtils.exactMultiply;
import static haveno.common.util.MathUtils.roundDoubleToLong;
import static haveno.common.util.MathUtils.scaleUpByPowerOf10;
import haveno.core.locale.CurrencyUtil;
+import haveno.core.locale.Res;
import haveno.core.monetary.CryptoMoney;
import haveno.core.monetary.Price;
import haveno.core.monetary.TraditionalMoney;
@@ -66,9 +67,7 @@ import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import static java.util.Comparator.comparing;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -124,7 +123,6 @@ public class CoreOffersService {
return result.isValid() || result == Result.HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER;
})
.collect(Collectors.toList());
- offers.removeAll(getOffersWithDuplicateKeyImages(offers));
return offers;
}
@@ -143,12 +141,9 @@ public class CoreOffersService {
}
List getMyOffers() {
- List offers = openOfferManager.getOpenOffers().stream()
+ return openOfferManager.getOpenOffers().stream()
.filter(o -> o.getOffer().isMyOffer(keyRing))
.collect(Collectors.toList());
- Set offersWithDuplicateKeyImages = getOffersWithDuplicateKeyImages(offers.stream().map(OpenOffer::getOffer).collect(Collectors.toList())); // TODO: this is hacky way of filtering offers with duplicate key images
- Set offerIdsWithDuplicateKeyImages = offersWithDuplicateKeyImages.stream().map(Offer::getId).collect(Collectors.toSet());
- return offers.stream().filter(o -> !offerIdsWithDuplicateKeyImages.contains(o.getId())).collect(Collectors.toList());
};
List getMyOffers(String direction, String currencyCode) {
@@ -159,7 +154,7 @@ public class CoreOffersService {
}
OpenOffer getMyOffer(String id) {
- return openOfferManager.getOpenOfferById(id)
+ return openOfferManager.getOpenOffer(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id)));
@@ -172,19 +167,38 @@ public class CoreOffersService {
double marketPriceMargin,
long amountAsLong,
long minAmountAsLong,
- double securityDeposit,
+ double securityDepositPct,
String triggerPriceAsString,
boolean reserveExactAmount,
String paymentAccountId,
+ boolean isPrivateOffer,
+ boolean buyerAsTakerWithoutDeposit,
+ String extraInfo,
+ String sourceOfferId,
Consumer resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
- if (paymentAccount == null)
- throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
+ if (paymentAccount == null) throw new IllegalArgumentException(format("payment account with id %s not found", paymentAccountId));
+ // clone offer if sourceOfferId given
+ if (!sourceOfferId.isEmpty()) {
+ cloneOffer(sourceOfferId,
+ currencyCode,
+ priceAsString,
+ useMarketBasedPrice,
+ marketPriceMargin,
+ triggerPriceAsString,
+ paymentAccountId,
+ extraInfo,
+ resultHandler,
+ errorMessageHandler);
+ return;
+ }
+
+ // create new offer
String upperCaseCurrencyCode = currencyCode.toUpperCase();
String offerId = createOfferService.getRandomOfferId();
OfferDirection direction = OfferDirection.valueOf(directionAsString.toUpperCase());
@@ -199,22 +213,78 @@ public class CoreOffersService {
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
- securityDeposit,
- paymentAccount);
+ securityDepositPct,
+ paymentAccount,
+ isPrivateOffer,
+ buyerAsTakerWithoutDeposit,
+ extraInfo);
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
- // We don't support atm funding from external wallet to keep it simple.
- boolean useSavingsWallet = true;
- //noinspection ConstantConditions
placeOffer(offer,
triggerPriceAsString,
- useSavingsWallet,
+ true,
reserveExactAmount,
+ null,
transaction -> resultHandler.accept(offer),
errorMessageHandler);
}
+ private void cloneOffer(String sourceOfferId,
+ String currencyCode,
+ String priceAsString,
+ boolean useMarketBasedPrice,
+ double marketPriceMargin,
+ String triggerPriceAsString,
+ String paymentAccountId,
+ String extraInfo,
+ Consumer resultHandler,
+ ErrorMessageHandler errorMessageHandler) {
+
+ // get source offer
+ OpenOffer sourceOpenOffer = getMyOffer(sourceOfferId);
+ Offer sourceOffer = sourceOpenOffer.getOffer();
+
+ // get trade currency (default source currency)
+ if (currencyCode.isEmpty()) currencyCode = sourceOffer.getOfferPayload().getBaseCurrencyCode();
+ if (currencyCode.equalsIgnoreCase(Res.getBaseCurrencyCode())) currencyCode = sourceOffer.getOfferPayload().getCounterCurrencyCode();
+ String upperCaseCurrencyCode = currencyCode.toUpperCase();
+
+ // get price (default source price)
+ Price price = useMarketBasedPrice ? null : priceAsString.isEmpty() ? sourceOffer.isUseMarketBasedPrice() ? null : sourceOffer.getPrice() : Price.parse(upperCaseCurrencyCode, priceAsString);
+ if (price == null) useMarketBasedPrice = true;
+
+ // get payment account
+ if (paymentAccountId.isEmpty()) paymentAccountId = sourceOffer.getOfferPayload().getMakerPaymentAccountId();
+ PaymentAccount paymentAccount = user.getPaymentAccount(paymentAccountId);
+ if (paymentAccount == null) throw new IllegalArgumentException(format("payment acRcount with id %s not found", paymentAccountId));
+
+ // get extra info
+ if (extraInfo.isEmpty()) extraInfo = sourceOffer.getOfferPayload().getExtraInfo();
+
+ // create cloned offer
+ Offer offer = createOfferService.createClonedOffer(sourceOffer,
+ upperCaseCurrencyCode,
+ price,
+ useMarketBasedPrice,
+ exactMultiply(marketPriceMargin, 0.01),
+ paymentAccount,
+ extraInfo);
+
+ // verify cloned offer
+ verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
+
+ // place offer
+ placeOffer(offer,
+ triggerPriceAsString,
+ true,
+ false, // ignored when cloning
+ sourceOfferId,
+ transaction -> resultHandler.accept(offer),
+ errorMessageHandler);
+ }
+
+ // TODO: this implementation is missing; implement.
Offer editOffer(String offerId,
String currencyCode,
OfferDirection direction,
@@ -223,8 +293,11 @@ public class CoreOffersService {
double marketPriceMargin,
BigInteger amount,
BigInteger minAmount,
- double buyerSecurityDeposit,
- PaymentAccount paymentAccount) {
+ double securityDepositPct,
+ PaymentAccount paymentAccount,
+ boolean isPrivateOffer,
+ boolean buyerAsTakerWithoutDeposit,
+ String extraInfo) {
return createOfferService.createAndGetOffer(offerId,
direction,
currencyCode.toUpperCase(),
@@ -233,8 +306,11 @@ public class CoreOffersService {
price,
useMarketBasedPrice,
exactMultiply(marketPriceMargin, 0.01),
- buyerSecurityDeposit,
- paymentAccount);
+ securityDepositPct,
+ paymentAccount,
+ isPrivateOffer,
+ buyerAsTakerWithoutDeposit,
+ extraInfo);
}
void cancelOffer(String id, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
@@ -244,26 +320,6 @@ public class CoreOffersService {
// -------------------------- PRIVATE HELPERS -----------------------------
- private Set getOffersWithDuplicateKeyImages(List offers) {
- Set duplicateFundedOffers = new HashSet();
- Set seenKeyImages = new HashSet();
- for (Offer offer : offers) {
- if (offer.getOfferPayload().getReserveTxKeyImages() == null) continue;
- for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
- if (!seenKeyImages.add(keyImage)) {
- for (Offer offer2 : offers) {
- if (offer == offer2) continue;
- if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
- log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
- duplicateFundedOffers.add(offer2);
- }
- }
- }
- }
- }
- return duplicateFundedOffers;
- }
-
private void verifyPaymentAccountIsValidForNewOffer(Offer offer, PaymentAccount paymentAccount) {
if (!isPaymentAccountValidForOffer(offer, paymentAccount)) {
String error = format("cannot create %s offer with payment account %s",
@@ -277,6 +333,7 @@ public class CoreOffersService {
String triggerPriceAsString,
boolean useSavingsWallet,
boolean reserveExactAmount,
+ String sourceOfferId,
Consumer resultHandler,
ErrorMessageHandler errorMessageHandler) {
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, offer.getCurrencyCode());
@@ -285,6 +342,7 @@ public class CoreOffersService {
triggerPriceAsLong,
reserveExactAmount,
true,
+ sourceOfferId,
resultHandler::accept,
errorMessageHandler);
}
diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java
index dcf1ef0e74..b506a00ac1 100644
--- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java
+++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java
@@ -64,9 +64,14 @@ class CorePaymentAccountsService {
}
PaymentAccount createPaymentAccount(PaymentAccountForm form) {
+ validateFormFields(form);
PaymentAccount paymentAccount = form.toPaymentAccount();
setSelectedTradeCurrency(paymentAccount); // TODO: selected trade currency is function of offer, not payment account payload
verifyPaymentAccountHasRequiredFields(paymentAccount);
+ if (paymentAccount instanceof CryptoCurrencyAccount) {
+ CryptoCurrencyAccount cryptoAccount = (CryptoCurrencyAccount) paymentAccount;
+ verifyCryptoCurrencyAddress(cryptoAccount.getSingleTradeCurrency().getCode(), cryptoAccount.getAddress());
+ }
user.addPaymentAccountIfNotExists(paymentAccount);
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
log.info("Saved payment account with id {} and payment method {}.",
@@ -166,6 +171,12 @@ class CorePaymentAccountsService {
.collect(Collectors.toList());
}
+ private void validateFormFields(PaymentAccountForm form) {
+ for (PaymentAccountFormField field : form.getFields()) {
+ validateFormField(form, field.getId(), field.getValue());
+ }
+ }
+
void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
// get payment method id
diff --git a/core/src/main/java/haveno/core/api/CorePriceService.java b/core/src/main/java/haveno/core/api/CorePriceService.java
index 453236c5bc..ddd194ebab 100644
--- a/core/src/main/java/haveno/core/api/CorePriceService.java
+++ b/core/src/main/java/haveno/core/api/CorePriceService.java
@@ -72,7 +72,7 @@ class CorePriceService {
* @return Price per 1 XMR in the given currency (traditional or crypto)
*/
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
- var marketPrice = priceFeedService.requestAllPrices().get(currencyCode);
+ var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
if (marketPrice == null) {
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
}
diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java
index 431ab9a652..5e53ff64e4 100644
--- a/core/src/main/java/haveno/core/api/CoreTradesService.java
+++ b/core/src/main/java/haveno/core/api/CoreTradesService.java
@@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage;
import haveno.core.support.traderchat.TradeChatSession;
import haveno.core.support.traderchat.TraderChatManager;
import haveno.core.trade.ClosedTradableManager;
-import haveno.core.trade.Tradable;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.TradeUtil;
@@ -55,9 +54,6 @@ import haveno.core.trade.protocol.BuyerProtocol;
import haveno.core.trade.protocol.SellerProtocol;
import haveno.core.user.User;
import haveno.core.util.coin.CoinUtil;
-import haveno.core.util.validation.BtcAddressValidator;
-import haveno.core.xmr.model.AddressEntry;
-import static haveno.core.xmr.model.AddressEntry.Context.TRADE_PAYOUT;
import haveno.core.xmr.wallet.BtcWalletService;
import static java.lang.String.format;
import java.math.BigInteger;
@@ -68,7 +64,6 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
-import org.bitcoinj.core.Coin;
@Singleton
@Slf4j
@@ -84,7 +79,6 @@ class CoreTradesService {
private final TakeOfferModel takeOfferModel;
private final TradeManager tradeManager;
private final TraderChatManager traderChatManager;
- private final TradeUtil tradeUtil;
private final OfferUtil offerUtil;
private final User user;
@@ -106,7 +100,6 @@ class CoreTradesService {
this.takeOfferModel = takeOfferModel;
this.tradeManager = tradeManager;
this.traderChatManager = traderChatManager;
- this.tradeUtil = tradeUtil;
this.offerUtil = offerUtil;
this.user = user;
}
@@ -132,7 +125,7 @@ class CoreTradesService {
// adjust amount for fixed-price offer (based on TakeOfferViewModel)
String currencyCode = offer.getCurrencyCode();
OfferDirection direction = offer.getOfferPayload().getDirection();
- long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
+ long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, offer.hasBuyerAsTakerWithoutDeposit());
if (offer.getPrice() != null) {
if (PaymentMethod.isRoundedForAtmCash(paymentAccount.getPaymentMethod().getId())) {
amount = CoinUtil.getRoundedAtmCashAmount(amount, offer.getPrice(), maxTradeLimit);
@@ -206,7 +199,7 @@ class CoreTradesService {
String getTradeRole(String tradeId) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
- return tradeUtil.getRole(getTrade(tradeId));
+ return TradeUtil.getRole(getTrade(tradeId));
}
Trade getTrade(String tradeId) {
@@ -223,8 +216,7 @@ class CoreTradesService {
}
private Optional getClosedTrade(String tradeId) {
- Optional tradable = closedTradableManager.getTradeById(tradeId);
- return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value);
+ return closedTradableManager.getTradeById(tradeId);
}
List getTrades() {
@@ -267,40 +259,9 @@ class CoreTradesService {
return tradeManager.getTradeProtocol(trade) instanceof BuyerProtocol;
}
- private Coin getEstimatedTxFee(String fromAddress, String toAddress, Coin amount) {
- // TODO This and identical logic should be refactored into TradeUtil.
- try {
- return btcWalletService.getFeeEstimationTransaction(fromAddress,
- toAddress,
- amount,
- TRADE_PAYOUT).getFee();
- } catch (Exception ex) {
- log.error("", ex);
- throw new IllegalStateException(format("could not estimate tx fee: %s", ex.getMessage()));
- }
- }
-
// Throws a RuntimeException trade is already closed.
private void verifyTradeIsNotClosed(String tradeId) {
if (getClosedTrade(tradeId).isPresent())
throw new IllegalArgumentException(format("trade '%s' is already closed", tradeId));
}
-
- // Throws a RuntimeException if address is not valid.
- private void verifyIsValidBTCAddress(String address) {
- try {
- new BtcAddressValidator().validate(address);
- } catch (Throwable t) {
- log.error("", t);
- throw new IllegalArgumentException(format("'%s' is not a valid btc address", address));
- }
- }
-
- // Throws a RuntimeException if address has a zero balance.
- private void verifyFundsNotWithdrawn(AddressEntry fromAddressEntry) {
- Coin fromAddressBalance = btcWalletService.getBalanceForAddress(fromAddressEntry.getAddress());
- if (fromAddressBalance.isZero())
- throw new IllegalStateException(format("funds already withdrawn from address '%s'",
- fromAddressEntry.getAddressString()));
- }
}
diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java
index 75be338503..53eba276a0 100644
--- a/core/src/main/java/haveno/core/api/XmrConnectionService.java
+++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java
@@ -32,6 +32,7 @@ import haveno.core.xmr.nodes.XmrNodes.XmrNode;
import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.setup.DownloadListener;
import haveno.core.xmr.setup.WalletsSetup;
+import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.P2PServiceListener;
@@ -40,7 +41,6 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.stream.Collectors;
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -64,7 +64,6 @@ import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemonRpc;
import monero.daemon.model.MoneroDaemonInfo;
-import monero.daemon.model.MoneroPeer;
@Slf4j
@Singleton
@@ -73,6 +72,14 @@ public final class XmrConnectionService {
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
+ private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
+ private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
+
+ public enum XmrConnectionFallbackType {
+ LOCAL,
+ CUSTOM,
+ PROVIDED
+ }
private final Object lock = new Object();
private final Object pollLock = new Object();
@@ -85,12 +92,14 @@ public final class XmrConnectionService {
private final XmrLocalNode xmrLocalNode;
private final MoneroConnectionManager connectionManager;
private final EncryptedConnectionList connectionList;
- private final ObjectProperty> peers = new SimpleObjectProperty<>();
+ private final ObjectProperty> connections = new SimpleObjectProperty<>();
+ private final IntegerProperty numConnections = new SimpleIntegerProperty(0);
private final ObjectProperty connectionProperty = new SimpleObjectProperty<>();
- private final IntegerProperty numPeers = new SimpleIntegerProperty(0);
private final LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener();
@Getter
+ private final ObjectProperty connectionServiceFallbackType = new SimpleObjectProperty<>();
+ @Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0);
private Socks5ProxyProvider socks5ProxyProvider;
@@ -101,6 +110,7 @@ public final class XmrConnectionService {
private Boolean isConnected = false;
@Getter
private MoneroDaemonInfo lastInfo;
+ private Long lastFallbackInvocation;
private Long lastLogPollErrorTimestamp;
private long lastLogDaemonNotSyncedTimestamp;
private Long syncStartHeight;
@@ -109,6 +119,7 @@ public final class XmrConnectionService {
@Getter
private boolean isShutDownStarted;
private List listeners = new ArrayList<>();
+ private XmrKeyImagePoller keyImagePoller;
// connection switching
private static final int EXCLUDE_CONNECTION_SECONDS = 180;
@@ -117,6 +128,9 @@ public final class XmrConnectionService {
private int numRequestsLastMinute;
private long lastSwitchTimestamp;
private Set excludedConnections = new HashSet<>();
+ private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
+ private boolean fallbackApplied;
+ private boolean usedSyncingLocalNodeBeforeStartup;
@Inject
public XmrConnectionService(P2PService p2PService,
@@ -144,7 +158,13 @@ public final class XmrConnectionService {
p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override
public void onTorNodeReady() {
- initialize();
+ ThreadUtils.submitToPool(() -> {
+ try {
+ initialize();
+ } catch (Exception e) {
+ log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
+ }
+ });
}
@Override
public void onHiddenServicePublished() {}
@@ -250,18 +270,29 @@ public final class XmrConnectionService {
updatePolling();
}
- public MoneroRpcConnection getBestAvailableConnection() {
- accountService.checkAccountOpen();
- List ignoredConnections = new ArrayList();
- addLocalNodeIfIgnored(ignoredConnections);
- return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
+ public MoneroRpcConnection getBestConnection() {
+ return getBestConnection(new ArrayList());
}
- private MoneroRpcConnection getBestAvailableConnection(Collection ignoredConnections) {
+ private MoneroRpcConnection getBestConnection(Collection ignoredConnections) {
accountService.checkAccountOpen();
+
+ // user needs to authorize fallback on startup after using locally synced node
+ if (fallbackRequiredBeforeConnectionSwitch()) {
+ log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
+ return null;
+ }
+
+ // get best connection
Set ignoredConnectionsSet = new HashSet<>(ignoredConnections);
addLocalNodeIfIgnored(ignoredConnectionsSet);
- return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0]));
+ MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections
+ if (bestConnection == null && connectionManager.getConnections().size() == 1 && !ignoredConnectionsSet.contains(connectionManager.getConnections().get(0))) bestConnection = connectionManager.getConnections().get(0);
+ return bestConnection;
+ }
+
+ private boolean fallbackRequiredBeforeConnectionSwitch() {
+ return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
}
private void addLocalNodeIfIgnored(Collection ignoredConnections) {
@@ -273,7 +304,7 @@ public final class XmrConnectionService {
log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled");
return;
}
- MoneroRpcConnection bestConnection = getBestAvailableConnection();
+ MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection);
}
@@ -324,7 +355,7 @@ public final class XmrConnectionService {
if (currentConnection != null) excludedConnections.add(currentConnection);
// get connection to switch to
- MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections);
+ MoneroRpcConnection bestConnection = getBestConnection(excludedConnections);
// remove from excluded connections after period
UserThread.runAfter(() -> {
@@ -332,7 +363,7 @@ public final class XmrConnectionService {
}, EXCLUDE_CONNECTION_SECONDS);
// return if no connection to switch to
- if (bestConnection == null) {
+ if (bestConnection == null || !Boolean.TRUE.equals(bestConnection.isConnected())) {
log.warn("No connection to switch to");
return false;
}
@@ -388,14 +419,25 @@ public final class XmrConnectionService {
return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced
}
- // ----------------------------- APP METHODS ------------------------------
-
- public ReadOnlyIntegerProperty numPeersProperty() {
- return numPeers;
+ public XmrKeyImagePoller getKeyImagePoller() {
+ synchronized (lock) {
+ if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller();
+ return keyImagePoller;
+ }
}
- public ReadOnlyObjectProperty> peerConnectionsProperty() {
- return peers;
+ private long getKeyImageRefreshPeriodMs() {
+ return isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
+ }
+
+ // ----------------------------- APP METHODS ------------------------------
+
+ public ReadOnlyIntegerProperty numConnectionsProperty() {
+ return numConnections;
+ }
+
+ public ReadOnlyObjectProperty> connectionsProperty() {
+ return connections;
}
public ReadOnlyObjectProperty connectionProperty() {
@@ -403,7 +445,7 @@ public final class XmrConnectionService {
}
public boolean hasSufficientPeersForBroadcast() {
- return numPeers.get() >= getMinBroadcastConnections();
+ return numConnections.get() >= getMinBroadcastConnections();
}
public LongProperty chainHeightProperty() {
@@ -426,6 +468,24 @@ public final class XmrConnectionService {
return numUpdates;
}
+ public void fallbackToBestConnection() {
+ if (isShutDownStarted) return;
+ fallbackApplied = true;
+ if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
+ log.warn("Falling back to public nodes");
+ preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
+ initializeConnections();
+ } else {
+ log.warn("Falling back to provided nodes");
+ preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
+ initializeConnections();
+ if (getConnection() == null) {
+ log.warn("No provided nodes available, falling back to public nodes");
+ fallbackToBestConnection();
+ }
+ }
+ }
+
// ------------------------------- HELPERS --------------------------------
private void doneDownload() {
@@ -460,6 +520,13 @@ public final class XmrConnectionService {
private void initialize() {
+ // initialize key image poller
+ getKeyImagePoller();
+ new Thread(() -> {
+ HavenoUtils.waitFor(20000);
+ keyImagePoller.poll(); // TODO: keep or remove first poll?s
+ }).start();
+
// initialize connections
initializeConnections();
@@ -526,8 +593,13 @@ public final class XmrConnectionService {
// update connection
if (isConnected) {
setConnection(connection.getUri());
+
+ // reset error connecting to local node
+ if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
+ connectionServiceFallbackType.set(null);
+ }
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
- MoneroRpcConnection bestConnection = getBestAvailableConnection();
+ MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
}
}
@@ -535,7 +607,7 @@ public final class XmrConnectionService {
}
// restore connections
- if ("".equals(config.xmrNode)) {
+ if (!isFixedConnection()) {
// load previous or default connections
if (coreContext.isApiUser()) {
@@ -547,8 +619,10 @@ public final class XmrConnectionService {
// add default connections
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
if (node.hasClearNetAddress()) {
- MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
- if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
+ if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
+ MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
+ if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
+ }
}
if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@@ -560,8 +634,10 @@ public final class XmrConnectionService {
// add default connections
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.hasClearNetAddress()) {
- MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
- addConnection(connection);
+ if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
+ MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
+ addConnection(connection);
+ }
}
if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@@ -571,15 +647,17 @@ public final class XmrConnectionService {
}
// restore last connection
- if (isFixedConnection()) {
- if (getConnections().size() != 1) throw new IllegalStateException("Expected connection list to have 1 fixed connection but was: " + getConnections().size());
- connectionManager.setConnection(getConnections().get(0));
- } else if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
+ if (connectionList.getCurrentConnectionUri().isPresent() && connectionManager.hasConnection(connectionList.getCurrentConnectionUri().get())) {
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get())) {
connectionManager.setConnection(connectionList.getCurrentConnectionUri().get());
}
}
+ // set if last node was locally syncing
+ if (!isInitialized) {
+ usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain();
+ }
+
// set connection proxies
log.info("TOR proxy URI: " + getProxyUri());
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
@@ -590,12 +668,9 @@ public final class XmrConnectionService {
if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui
- // start local node if applicable
- maybeStartLocalNode();
-
// update connection
- if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
- MoneroRpcConnection bestConnection = getBestAvailableConnection();
+ if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
+ MoneroRpcConnection bestConnection = getBestConnection();
if (bestConnection != null) setConnection(bestConnection);
}
} else if (!isInitialized) {
@@ -605,9 +680,6 @@ public final class XmrConnectionService {
MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1);
if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri());
connectionManager.setConnection(connection);
-
- // start local node if applicable
- maybeStartLocalNode();
}
// register connection listener
@@ -616,30 +688,26 @@ public final class XmrConnectionService {
}
// notify initial connection
+ lastRefreshPeriodMs = getRefreshPeriodMs();
onConnectionChanged(connectionManager.getConnection());
}
- private void maybeStartLocalNode() {
-
- // skip if seed node
- if (HavenoUtils.isSeedNode()) return;
-
- // start local node if offline and used as last connection
- if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
- try {
- log.info("Starting local node");
- xmrLocalNode.start();
- } catch (Exception e) {
- log.error("Unable to start local monero node, error={}\n", e.getMessage(), e);
- }
+ public void startLocalNode() throws Exception {
+
+ // cannot start local node as seed node
+ if (HavenoUtils.isSeedNode()) {
+ throw new RuntimeException("Cannot start local node on seed node");
}
+
+ // start local node
+ log.info("Starting local node");
+ xmrLocalNode.start();
}
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted || !accountService.isAccountOpen()) return;
if (currentConnection == null) {
- log.warn("Setting daemon connection to null");
- Thread.dumpStack();
+ log.warn("Setting daemon connection to null", new Throwable("Stack trace"));
}
synchronized (lock) {
if (currentConnection == null) {
@@ -660,6 +728,10 @@ public final class XmrConnectionService {
numUpdates.set(numUpdates.get() + 1);
});
}
+
+ // update key image poller
+ keyImagePoller.setDaemon(getDaemon());
+ keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
// update polling
doPollDaemon();
@@ -709,25 +781,31 @@ public final class XmrConnectionService {
try {
// poll daemon
- if (daemon == null) switchToBestConnection();
- if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
+ if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
try {
+ if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = daemon.getInfo();
} catch (Exception e) {
// skip handling if shutting down
if (isShutDownStarted) return;
- // fallback to provided or public nodes if custom connection fails on startup
- if (lastInfo == null && "".equals(config.xmrNode) && preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM) {
- if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
- log.warn("Failed to fetch daemon info from custom node on startup, falling back to public nodes: " + e.getMessage());
- preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
- } else {
- log.warn("Failed to fetch daemon info from custom node on startup, falling back to provided nodes: " + e.getMessage());
- preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
+ // invoke fallback handling on startup error
+ boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
+ if (lastInfo == null && canFallback) {
+ if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
+ lastFallbackInvocation = System.currentTimeMillis();
+ if (usedSyncingLocalNodeBeforeStartup) {
+ log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
+ connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
+ } else if (isProvidedConnections()) {
+ log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage());
+ connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
+ } else {
+ log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
+ connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
+ }
}
- initializeConnections();
return;
}
@@ -740,11 +818,13 @@ public final class XmrConnectionService {
// switch to best connection
switchToBestConnection();
+ if (daemon == null) throw new RuntimeException("No connection to Monero daemon after error handling");
lastInfo = daemon.getInfo(); // caught internally if still fails
}
// connected to daemon
isConnected = true;
+ connectionServiceFallbackType.set(null);
// determine if blockchain is syncing locally
boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0
@@ -782,16 +862,15 @@ public final class XmrConnectionService {
downloadListener.progress(percent, blocksLeft, null);
}
- // set peer connections
- // TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections
- // try {
- // peers.set(getOnlinePeers());
- // } catch (Exception err) {
- // // TODO: peers unknown due to restricted RPC call
- // }
- // numPeers.set(peers.get().size());
- numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
- peers.set(new ArrayList());
+ // set available connections
+ List availableConnections = new ArrayList<>();
+ for (MoneroRpcConnection connection : connectionManager.getConnections()) {
+ if (Boolean.TRUE.equals(connection.isOnline()) && Boolean.TRUE.equals(connection.isAuthenticated())) {
+ availableConnections.add(connection);
+ }
+ }
+ connections.set(availableConnections);
+ numConnections.set(availableConnections.size());
// notify update
numUpdates.set(numUpdates.get() + 1);
@@ -821,13 +900,15 @@ public final class XmrConnectionService {
}
}
- private List getOnlinePeers() {
- return daemon.getPeers().stream()
- .filter(peer -> peer.isOnline())
- .collect(Collectors.toList());
+ private boolean isFixedConnection() {
+ return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
}
- private boolean isFixedConnection() {
- return !"".equals(config.xmrNode) || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
+ private boolean isCustomConnections() {
+ return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
+ }
+
+ private boolean isProvidedConnections() {
+ return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
}
}
diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java
index cd5ed266f1..0928340d25 100644
--- a/core/src/main/java/haveno/core/api/XmrLocalNode.java
+++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java
@@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.user.Preferences;
import haveno.core.xmr.XmrNodeSettings;
import haveno.core.xmr.nodes.XmrNodes;
+import haveno.core.xmr.nodes.XmrNodes.XmrNode;
+import haveno.core.xmr.nodes.XmrNodesSetupPreferences;
import haveno.core.xmr.wallet.XmrWalletService;
import java.io.File;
@@ -55,6 +57,7 @@ public class XmrLocalNode {
private MoneroConnectionManager connectionManager;
private final Config config;
private final Preferences preferences;
+ private final XmrNodes xmrNodes;
private final List listeners = new ArrayList<>();
// required arguments
@@ -69,9 +72,12 @@ public class XmrLocalNode {
}
@Inject
- public XmrLocalNode(Config config, Preferences preferences) {
+ public XmrLocalNode(Config config,
+ Preferences preferences,
+ XmrNodes xmrNodes) {
this.config = config;
this.preferences = preferences;
+ this.xmrNodes = xmrNodes;
this.daemon = new MoneroDaemonRpc(getUri());
// initialize connection manager to listen to local connection
@@ -101,7 +107,20 @@ public class XmrLocalNode {
* Returns whether Haveno should ignore a local Monero node even if it is usable.
*/
public boolean shouldBeIgnored() {
- return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
+ if (config.ignoreLocalXmrNode) return true;
+
+ // ignore if fixed connection is not local
+ if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode);
+
+ // check if local node is within configuration
+ boolean hasConfiguredLocalNode = false;
+ for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
+ if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
+ hasConfiguredLocalNode = true;
+ break;
+ }
+ }
+ return !hasConfiguredLocalNode;
}
public void addListener(XmrLocalNodeListener listener) {
@@ -120,7 +139,11 @@ public class XmrLocalNode {
}
public boolean equalsUri(String uri) {
- return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
+ try {
+ return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort();
+ } catch (Exception e) {
+ return false;
+ }
}
/**
@@ -166,11 +189,18 @@ public class XmrLocalNode {
var args = new ArrayList<>(MONEROD_ARGS);
- var dataDir = settings.getBlockchainPath();
- if (dataDir == null || dataDir.isEmpty()) {
- dataDir = MONEROD_DATADIR;
+ var dataDir = "";
+ if (config.xmrBlockchainPath == null || config.xmrBlockchainPath.isEmpty()) {
+ dataDir = settings.getBlockchainPath();
+ if (dataDir == null || dataDir.isEmpty()) {
+ dataDir = MONEROD_DATADIR;
+ }
+ } else {
+ dataDir = config.xmrBlockchainPath; // startup config overrides settings
+ }
+ if (dataDir != null && !dataDir.isEmpty()) {
+ args.add("--data-dir=" + dataDir);
}
- if (dataDir != null) args.add("--data-dir=" + dataDir);
var bootstrapUrl = settings.getBootstrapUrl();
if (bootstrapUrl != null && !bootstrapUrl.isEmpty()) {
diff --git a/core/src/main/java/haveno/core/api/model/OfferInfo.java b/core/src/main/java/haveno/core/api/model/OfferInfo.java
index b489aaa8bb..76de24401a 100644
--- a/core/src/main/java/haveno/core/api/model/OfferInfo.java
+++ b/core/src/main/java/haveno/core/api/model/OfferInfo.java
@@ -78,6 +78,9 @@ public class OfferInfo implements Payload {
@Nullable
private final String splitOutputTxHash;
private final long splitOutputTxFee;
+ private final boolean isPrivateOffer;
+ private final String challenge;
+ private final String extraInfo;
public OfferInfo(OfferInfoBuilder builder) {
this.id = builder.getId();
@@ -111,6 +114,9 @@ public class OfferInfo implements Payload {
this.arbitratorSigner = builder.getArbitratorSigner();
this.splitOutputTxHash = builder.getSplitOutputTxHash();
this.splitOutputTxFee = builder.getSplitOutputTxFee();
+ this.isPrivateOffer = builder.isPrivateOffer();
+ this.challenge = builder.getChallenge();
+ this.extraInfo = builder.getExtraInfo();
}
public static OfferInfo toOfferInfo(Offer offer) {
@@ -137,6 +143,7 @@ public class OfferInfo implements Payload {
.withIsActivated(isActivated)
.withSplitOutputTxHash(openOffer.getSplitOutputTxHash())
.withSplitOutputTxFee(openOffer.getSplitOutputTxFee())
+ .withChallenge(openOffer.getChallenge())
.build();
}
@@ -177,7 +184,10 @@ public class OfferInfo implements Payload {
.withPubKeyRing(offer.getOfferPayload().getPubKeyRing().toString())
.withVersionNumber(offer.getOfferPayload().getVersionNr())
.withProtocolVersion(offer.getOfferPayload().getProtocolVersion())
- .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress());
+ .withArbitratorSigner(offer.getOfferPayload().getArbitratorSigner() == null ? null : offer.getOfferPayload().getArbitratorSigner().getFullAddress())
+ .withIsPrivateOffer(offer.isPrivateOffer())
+ .withChallenge(offer.getChallenge())
+ .withExtraInfo(offer.getCombinedExtraInfo());
}
///////////////////////////////////////////////////////////////////////////////////////////
@@ -215,9 +225,12 @@ public class OfferInfo implements Payload {
.setPubKeyRing(pubKeyRing)
.setVersionNr(versionNumber)
.setProtocolVersion(protocolVersion)
- .setSplitOutputTxFee(splitOutputTxFee);
+ .setSplitOutputTxFee(splitOutputTxFee)
+ .setIsPrivateOffer(isPrivateOffer);
Optional.ofNullable(arbitratorSigner).ifPresent(builder::setArbitratorSigner);
Optional.ofNullable(splitOutputTxHash).ifPresent(builder::setSplitOutputTxHash);
+ Optional.ofNullable(challenge).ifPresent(builder::setChallenge);
+ Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return builder.build();
}
@@ -255,6 +268,9 @@ public class OfferInfo implements Payload {
.withArbitratorSigner(proto.getArbitratorSigner())
.withSplitOutputTxHash(proto.getSplitOutputTxHash())
.withSplitOutputTxFee(proto.getSplitOutputTxFee())
+ .withIsPrivateOffer(proto.getIsPrivateOffer())
+ .withChallenge(proto.getChallenge())
+ .withExtraInfo(proto.getExtraInfo())
.build();
}
}
diff --git a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java
index 6650265fbb..6b1b494047 100644
--- a/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java
+++ b/core/src/main/java/haveno/core/api/model/PaymentAccountForm.java
@@ -77,7 +77,8 @@ public final class PaymentAccountForm implements PersistablePayload {
AUSTRALIA_PAYID,
CASH_APP,
PAYPAL,
- VENMO;
+ VENMO,
+ PAYSAFE;
public static PaymentAccountForm.FormId fromProto(protobuf.PaymentAccountForm.FormId formId) {
return ProtoUtil.enumFromProto(PaymentAccountForm.FormId.class, formId.name());
diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java
index fa94fd27f2..8df26368ba 100644
--- a/core/src/main/java/haveno/core/api/model/TradeInfo.java
+++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java
@@ -172,14 +172,14 @@ public class TradeInfo implements Payload {
.withAmount(trade.getAmount().longValueExact())
.withMakerFee(trade.getMakerFee().longValueExact())
.withTakerFee(trade.getTakerFee().longValueExact())
- .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit() == null ? -1 : trade.getBuyer().getSecurityDeposit().longValueExact())
- .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit() == null ? -1 : trade.getSeller().getSecurityDeposit().longValueExact())
- .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee() == null ? -1 : trade.getBuyer().getDepositTxFee().longValueExact())
- .withSellerDepositTxFee(trade.getSeller().getDepositTxFee() == null ? -1 : trade.getSeller().getDepositTxFee().longValueExact())
- .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee() == null ? -1 : trade.getBuyer().getPayoutTxFee().longValueExact())
- .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee() == null ? -1 : trade.getSeller().getPayoutTxFee().longValueExact())
- .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount() == null ? -1 : trade.getBuyer().getPayoutAmount().longValueExact())
- .withSellerPayoutAmount(trade.getSeller().getPayoutAmount() == null ? -1 : trade.getSeller().getPayoutAmount().longValueExact())
+ .withBuyerSecurityDeposit(trade.getBuyer().getSecurityDeposit().longValueExact())
+ .withSellerSecurityDeposit(trade.getSeller().getSecurityDeposit().longValueExact())
+ .withBuyerDepositTxFee(trade.getBuyer().getDepositTxFee().longValueExact())
+ .withSellerDepositTxFee(trade.getSeller().getDepositTxFee().longValueExact())
+ .withBuyerPayoutTxFee(trade.getBuyer().getPayoutTxFee().longValueExact())
+ .withSellerPayoutTxFee(trade.getSeller().getPayoutTxFee().longValueExact())
+ .withBuyerPayoutAmount(trade.getBuyer().getPayoutAmount().longValueExact())
+ .withSellerPayoutAmount(trade.getSeller().getPayoutAmount().longValueExact())
.withTotalTxFee(trade.getTotalTxFee().longValueExact())
.withPrice(toPreciseTradePrice.apply(trade))
.withVolume(toRoundedVolume.apply(trade))
diff --git a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java
index 35d532f67f..23e403fcd2 100644
--- a/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java
+++ b/core/src/main/java/haveno/core/api/model/builder/OfferInfoBuilder.java
@@ -63,6 +63,9 @@ public final class OfferInfoBuilder {
private String arbitratorSigner;
private String splitOutputTxHash;
private long splitOutputTxFee;
+ private boolean isPrivateOffer;
+ private String challenge;
+ private String extraInfo;
public OfferInfoBuilder withId(String id) {
this.id = id;
@@ -234,6 +237,21 @@ public final class OfferInfoBuilder {
return this;
}
+ public OfferInfoBuilder withIsPrivateOffer(boolean isPrivateOffer) {
+ this.isPrivateOffer = isPrivateOffer;
+ return this;
+ }
+
+ public OfferInfoBuilder withChallenge(String challenge) {
+ this.challenge = challenge;
+ return this;
+ }
+
+ public OfferInfoBuilder withExtraInfo(String extraInfo) {
+ this.extraInfo = extraInfo;
+ return this;
+ }
+
public OfferInfo build() {
return new OfferInfo(this);
}
diff --git a/core/src/main/java/haveno/core/app/AppStartupState.java b/core/src/main/java/haveno/core/app/AppStartupState.java
index 85da2af9d4..89fe61576a 100644
--- a/core/src/main/java/haveno/core/app/AppStartupState.java
+++ b/core/src/main/java/haveno/core/app/AppStartupState.java
@@ -73,7 +73,7 @@ public class AppStartupState {
isWalletSynced.set(true);
});
- xmrConnectionService.numPeersProperty().addListener((observable, oldValue, newValue) -> {
+ xmrConnectionService.numConnectionsProperty().addListener((observable, oldValue, newValue) -> {
if (xmrConnectionService.hasSufficientPeersForBroadcast())
hasSufficientPeersForBroadcast.set(true);
});
diff --git a/core/src/main/java/haveno/core/app/DomainInitialisation.java b/core/src/main/java/haveno/core/app/DomainInitialisation.java
index 646d80d9dd..220fb05040 100644
--- a/core/src/main/java/haveno/core/app/DomainInitialisation.java
+++ b/core/src/main/java/haveno/core/app/DomainInitialisation.java
@@ -178,6 +178,9 @@ public class DomainInitialisation {
closedTradableManager.onAllServicesInitialized();
failedTradesManager.onAllServicesInitialized();
+ filterManager.setFilterWarningHandler(filterWarningHandler);
+ filterManager.onAllServicesInitialized();
+
openOfferManager.onAllServicesInitialized();
balances.onAllServicesInitialized();
@@ -199,10 +202,6 @@ public class DomainInitialisation {
priceFeedService.setCurrencyCodeOnInit();
priceFeedService.startRequestingPrices();
- filterManager.setFilterWarningHandler(filterWarningHandler);
- filterManager.onAllServicesInitialized();
-
-
mobileNotificationService.onAllServicesInitialized();
myOfferTakenEvents.onAllServicesInitialized();
tradeEvents.onAllServicesInitialized();
diff --git a/core/src/main/java/haveno/core/app/HavenoExecutable.java b/core/src/main/java/haveno/core/app/HavenoExecutable.java
index 5f2d14622b..8f2998eb0d 100644
--- a/core/src/main/java/haveno/core/app/HavenoExecutable.java
+++ b/core/src/main/java/haveno/core/app/HavenoExecutable.java
@@ -100,7 +100,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
protected AppModule module;
protected Config config;
@Getter
- protected boolean isShutdownInProgress;
+ protected boolean isShutDownStarted;
private boolean isReadOnly;
private Thread keepRunningThread;
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
@@ -330,12 +330,12 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
- // ignore if shut down in progress
- if (isShutdownInProgress) {
- log.info("Ignoring call to gracefulShutDown, already in progress");
+ // ignore if shut down started
+ if (isShutDownStarted) {
+ log.info("Ignoring call to gracefulShutDown, already started");
return;
}
- isShutdownInProgress = true;
+ isShutDownStarted = true;
ResultHandler resultHandler;
if (shutdownCompletedHandler != null) {
@@ -357,45 +357,46 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
// notify trade protocols and wallets to prepare for shut down before shutting down
Set tasks = new HashSet();
+ tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
- tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
try {
ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout
} catch (Exception e) {
log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e);
}
- injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown();
// shut down open offer manager
- log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
+ log.info("Shutting down OpenOfferManager");
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
- // shut down offer book service
- injector.getInstance(OfferBookService.class).shutDown();
+ // listen for shut down of wallets setup
+ injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
- // shut down p2p service
- injector.getInstance(P2PService.class).shutDown(() -> {
+ // shut down p2p service
+ log.info("Shutting down P2P service");
+ injector.getInstance(P2PService.class).shutDown(() -> {
- // shut down monero wallets and connections
- log.info("Shutting down wallet and connection services");
- injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
-
// done shutting down
log.info("Graceful shutdown completed. Exiting now.");
module.close(injector);
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
});
- injector.getInstance(BtcWalletService.class).shutDown();
- injector.getInstance(XmrWalletService.class).shutDown();
- injector.getInstance(XmrConnectionService.class).shutDown();
- injector.getInstance(WalletsSetup.class).shutDown();
});
+
+ // shut down trade and wallet services
+ log.info("Shutting down trade and wallet services");
+ injector.getInstance(OfferBookService.class).shutDown();
+ injector.getInstance(TradeManager.class).shutDown();
+ injector.getInstance(BtcWalletService.class).shutDown();
+ injector.getInstance(XmrWalletService.class).shutDown();
+ injector.getInstance(XmrConnectionService.class).shutDown();
+ injector.getInstance(WalletsSetup.class).shutDown();
});
} catch (Throwable t) {
log.error("App shutdown failed with exception: {}\n", t.getMessage(), t);
diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
index 7235efce7b..0bdac1abc1 100644
--- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
+++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
@@ -75,6 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
acceptedHandler.run();
});
+ havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show));
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
@@ -85,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler"));
havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg));
havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
- havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler"));
+ havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler"));
havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));
diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java
index d80ca807cc..192e3870b7 100644
--- a/core/src/main/java/haveno/core/app/HavenoSetup.java
+++ b/core/src/main/java/haveno/core/app/HavenoSetup.java
@@ -55,6 +55,7 @@ import haveno.core.alert.PrivateNotificationManager;
import haveno.core.alert.PrivateNotificationPayload;
import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService;
+import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType;
import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager;
@@ -158,6 +159,9 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler;
@Setter
@Nullable
+ private Consumer displayMoneroConnectionFallbackHandler;
+ @Setter
+ @Nullable
private Consumer displayTorNetworkSettingsHandler;
@Setter
@Nullable
@@ -173,7 +177,7 @@ public class HavenoSetup {
private Consumer displayPrivateNotificationHandler;
@Setter
@Nullable
- private Runnable showPopupIfInvalidBtcConfigHandler;
+ private Runnable showPopupIfInvalidXmrConfigHandler;
@Setter
@Nullable
private Consumer> revolutAccountsUpdateHandler;
@@ -366,7 +370,7 @@ public class HavenoSetup {
// install monerod
File monerodFile = new File(XmrLocalNode.MONEROD_PATH);
String monerodResourcePath = "bin/" + XmrLocalNode.MONEROD_NAME;
- if (!monerodFile.exists() || !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile)) {
+ if (!monerodFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(monerodResourcePath, monerodFile))) {
log.info("Installing monerod");
monerodFile.getParentFile().mkdirs();
FileUtil.resourceToFile("bin/" + XmrLocalNode.MONEROD_NAME, monerodFile);
@@ -376,7 +380,7 @@ public class HavenoSetup {
// install monero-wallet-rpc
File moneroWalletRpcFile = new File(XmrWalletService.MONERO_WALLET_RPC_PATH);
String moneroWalletRpcResourcePath = "bin/" + XmrWalletService.MONERO_WALLET_RPC_NAME;
- if (!moneroWalletRpcFile.exists() || !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile)) {
+ if (!moneroWalletRpcFile.exists() || (config.updateXmrBinaries && !FileUtil.resourceEqualToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile))) {
log.info("Installing monero-wallet-rpc");
moneroWalletRpcFile.getParentFile().mkdirs();
FileUtil.resourceToFile(moneroWalletRpcResourcePath, moneroWalletRpcFile);
@@ -426,6 +430,12 @@ public class HavenoSetup {
getXmrDaemonSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
+ // listen for fallback handling
+ getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
+ if (displayMoneroConnectionFallbackHandler == null) return;
+ displayMoneroConnectionFallbackHandler.accept(newValue);
+ });
+
log.info("Init P2P network");
havenoSetupListeners.forEach(HavenoSetupListener::onInitP2pNetwork);
p2pNetworkReady = p2PNetworkSetup.init(this::initWallet, displayTorNetworkSettingsHandler);
@@ -452,7 +462,7 @@ public class HavenoSetup {
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler,
- showPopupIfInvalidBtcConfigHandler,
+ showPopupIfInvalidXmrConfigHandler,
() -> {},
() -> {});
}
@@ -725,6 +735,10 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg();
}
+ public ObjectProperty getConnectionServiceFallbackType() {
+ return xmrConnectionService.getConnectionServiceFallbackType();
+ }
+
public StringProperty getTopErrorMsg() {
return topErrorMsg;
}
diff --git a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java
index 4e90faeb55..69609a4bba 100644
--- a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java
+++ b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java
@@ -87,7 +87,7 @@ public class P2PNetworkSetup {
BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty();
p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(),
- xmrConnectionService.numPeersProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
+ xmrConnectionService.numConnectionsProperty(), hiddenServicePublished, initialP2PNetworkDataReceived,
(state, warning, numP2pPeers, numXmrPeers, hiddenService, dataReceived) -> {
String result;
int p2pPeers = (int) numP2pPeers;
diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java
index 1f7946eac7..7d37372afd 100644
--- a/core/src/main/java/haveno/core/app/WalletAppSetup.java
+++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java
@@ -117,10 +117,10 @@ public class WalletAppSetup {
void init(@Nullable Consumer chainFileLockedExceptionHandler,
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
- @Nullable Runnable showPopupIfInvalidBtcConfigHandler,
+ @Nullable Runnable showPopupIfInvalidXmrConfigHandler,
Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) {
- log.info("Initialize WalletAppSetup with monero-java version {}", MoneroUtils.getVersion());
+ log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion());
ObjectProperty walletServiceException = new SimpleObjectProperty<>();
xmrInfoBinding = EasyBind.combine(
@@ -199,8 +199,8 @@ public class WalletAppSetup {
walletInitializedHandler.run();
},
exception -> {
- if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) {
- showPopupIfInvalidBtcConfigHandler.run();
+ if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
+ showPopupIfInvalidXmrConfigHandler.run();
} else {
walletServiceException.set(exception);
}
diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java
index 725ccd877c..3184d9ba11 100644
--- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java
+++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java
@@ -105,21 +105,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
public void gracefulShutDown(ResultHandler resultHandler) {
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
- // ignore if shut down in progress
- if (isShutdownInProgress) {
- log.info("Ignoring call to gracefulShutDown, already in progress");
+ // ignore if shut down started
+ if (isShutDownStarted) {
+ log.info("Ignoring call to gracefulShutDown, already started");
return;
}
- isShutdownInProgress = true;
+ isShutDownStarted = true;
try {
if (injector != null) {
// notify trade protocols and wallets to prepare for shut down
Set tasks = new HashSet();
+ tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
tasks.add(() -> injector.getInstance(XmrConnectionService.class).onShutDownStarted());
- tasks.add(() -> injector.getInstance(TradeManager.class).onShutDownStarted());
try {
ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout
} catch (Exception e) {
@@ -127,25 +127,21 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
}
JsonFileManager.shutDownAllInstances();
- injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown();
// shut down open offer manager
- log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
+ log.info("Shutting down OpenOfferManager");
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
- // shut down offer book service
- injector.getInstance(OfferBookService.class).shutDown();
+ // listen for shut down of wallets setup
+ injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
- // shut down p2p service
- injector.getInstance(P2PService.class).shutDown(() -> {
-
- // shut down monero wallets and connections
- log.info("Shutting down wallet and connection services");
- injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
+ // shut down p2p service
+ log.info("Shutting down P2P service");
+ injector.getInstance(P2PService.class).shutDown(() -> {
module.close(injector);
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
@@ -155,18 +151,23 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
});
});
- injector.getInstance(BtcWalletService.class).shutDown();
- injector.getInstance(XmrWalletService.class).shutDown();
- injector.getInstance(XmrConnectionService.class).shutDown();
- injector.getInstance(WalletsSetup.class).shutDown();
});
+
+ // shut down trade and wallet services
+ log.info("Shutting down trade and wallet services");
+ injector.getInstance(OfferBookService.class).shutDown();
+ injector.getInstance(TradeManager.class).shutDown();
+ injector.getInstance(BtcWalletService.class).shutDown();
+ injector.getInstance(XmrWalletService.class).shutDown();
+ injector.getInstance(XmrConnectionService.class).shutDown();
+ injector.getInstance(WalletsSetup.class).shutDown();
});
// we wait max 5 sec.
UserThread.runAfter(() -> {
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
resultHandler.handleResult();
- log.info("Graceful shutdown caused a timeout. Exiting now.");
+ log.warn("Graceful shutdown caused a timeout. Exiting now.");
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
});
}, 5);
diff --git a/core/src/main/java/haveno/core/filter/FilterManager.java b/core/src/main/java/haveno/core/filter/FilterManager.java
index cb7e0e9b21..2cebb66f3a 100644
--- a/core/src/main/java/haveno/core/filter/FilterManager.java
+++ b/core/src/main/java/haveno/core/filter/FilterManager.java
@@ -406,6 +406,10 @@ public class FilterManager {
.anyMatch(e -> e.equals(address));
}
+ public String getDisableTradeBelowVersion() {
+ return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion();
+ }
+
public boolean requireUpdateToNewVersionForTrading() {
if (getFilter() == null) {
return false;
diff --git a/core/src/main/java/haveno/core/locale/CryptoCurrency.java b/core/src/main/java/haveno/core/locale/CryptoCurrency.java
index bbc5b4a4be..6c46c9d2b3 100644
--- a/core/src/main/java/haveno/core/locale/CryptoCurrency.java
+++ b/core/src/main/java/haveno/core/locale/CryptoCurrency.java
@@ -19,10 +19,8 @@ package haveno.core.locale;
import com.google.protobuf.Message;
-import lombok.EqualsAndHashCode;
import lombok.Getter;
-@EqualsAndHashCode(callSuper = true)
public final class CryptoCurrency extends TradeCurrency {
// http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618
private final static String PREFIX = "✦ ";
diff --git a/core/src/main/java/haveno/core/locale/CurrencyUtil.java b/core/src/main/java/haveno/core/locale/CurrencyUtil.java
index b482ea4f28..6ed42b6234 100644
--- a/core/src/main/java/haveno/core/locale/CurrencyUtil.java
+++ b/core/src/main/java/haveno/core/locale/CurrencyUtil.java
@@ -73,14 +73,6 @@ public class CurrencyUtil {
private static String baseCurrencyCode = "XMR";
- private static List getTraditionalNonFiatCurrencies() {
- return Arrays.asList(
- new TraditionalCurrency("XAG", "Silver"),
- new TraditionalCurrency("XAU", "Gold"),
- new TraditionalCurrency("XGB", "Goldback")
- );
- }
-
// Calls to isTraditionalCurrency and isCryptoCurrency are very frequent so we use a cache of the results.
// The main improvement was already achieved with using memoize for the source maps, but
// the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50%
@@ -124,6 +116,14 @@ public class CurrencyUtil {
return new ArrayList<>(traditionalCurrencyMapSupplier.get().values());
}
+ public static List getTraditionalNonFiatCurrencies() {
+ return Arrays.asList(
+ new TraditionalCurrency("XAG", "Silver"),
+ new TraditionalCurrency("XAU", "Gold"),
+ new TraditionalCurrency("XGB", "Goldback")
+ );
+ }
+
public static Collection getAllSortedTraditionalCurrencies(Comparator comparator) {
return (List) getAllSortedTraditionalCurrencies().stream()
.sorted(comparator)
@@ -200,6 +200,10 @@ public class CurrencyUtil {
result.add(new CryptoCurrency("BCH", "Bitcoin Cash"));
result.add(new CryptoCurrency("ETH", "Ether"));
result.add(new CryptoCurrency("LTC", "Litecoin"));
+ result.add(new CryptoCurrency("DAI-ERC20", "Dai Stablecoin (ERC20)"));
+ result.add(new CryptoCurrency("USDT-ERC20", "Tether USD (ERC20)"));
+ result.add(new CryptoCurrency("USDT-TRC20", "Tether USD (TRC20)"));
+ result.add(new CryptoCurrency("USDC-ERC20", "USD Coin (ERC20)"));
result.sort(TradeCurrency::compareTo);
return result;
}
@@ -295,6 +299,9 @@ public class CurrencyUtil {
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
}
+ if (isCryptoCurrencyCodeBase(currencyCode)) {
+ return true;
+ }
boolean isCryptoCurrency;
if (currencyCode == null) {
@@ -321,6 +328,21 @@ public class CurrencyUtil {
return isCryptoCurrency;
}
+ private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
+ if (currencyCode == null) return false;
+ currencyCode = currencyCode.toUpperCase();
+ return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
+ }
+
+ public static String getCurrencyCodeBase(String currencyCode) {
+ if (currencyCode == null) return null;
+ currencyCode = currencyCode.toUpperCase();
+ if (currencyCode.contains("USDT")) return "USDT";
+ if (currencyCode.contains("USDC")) return "USDC";
+ if (currencyCode.contains("DAI")) return "DAI";
+ return currencyCode;
+ }
+
public static Optional getCryptoCurrency(String currencyCode) {
return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode));
}
diff --git a/core/src/main/java/haveno/core/locale/TradeCurrency.java b/core/src/main/java/haveno/core/locale/TradeCurrency.java
index a9da96ea16..b60abf5e88 100644
--- a/core/src/main/java/haveno/core/locale/TradeCurrency.java
+++ b/core/src/main/java/haveno/core/locale/TradeCurrency.java
@@ -19,19 +19,16 @@ package haveno.core.locale;
import haveno.common.proto.ProtobufferRuntimeException;
import haveno.common.proto.persistable.PersistablePayload;
-import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
-@EqualsAndHashCode
@ToString
@Getter
@Slf4j
public abstract class TradeCurrency implements PersistablePayload, Comparable {
protected final String code;
- @EqualsAndHashCode.Exclude
protected final String name;
public TradeCurrency(String code, String name) {
@@ -82,4 +79,23 @@ public abstract class TradeCurrency implements PersistablePayload, Comparable acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
-
- // reserved for future use cases
- // Use null values if not set
- boolean isPrivateOffer = false;
+ boolean hasBuyerAsTakerWithoutDeposit = !isBuyerMaker && isPrivateOffer && buyerAsTakerWithoutDeposit;
+ long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction, hasBuyerAsTakerWithoutDeposit);
boolean useAutoClose = false;
boolean useReOpenAfterAutoClose = false;
long lowerClosePrice = 0;
long upperClosePrice = 0;
- String hashOfChallenge = null;
- Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
- currencyCode,
- direction);
+ Map extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction);
offerUtil.validateOfferData(
- securityDepositAsDouble,
+ securityDepositPct,
paymentAccount,
currencyCode);
@@ -189,11 +204,11 @@ public class CreateOfferService {
useMarketBasedPriceValue,
amountAsLong,
minAmountAsLong,
- HavenoUtils.MAKER_FEE_PCT,
- HavenoUtils.TAKER_FEE_PCT,
+ hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT,
+ hasBuyerAsTakerWithoutDeposit ? 0d : HavenoUtils.TAKER_FEE_PCT,
HavenoUtils.PENALTY_FEE_PCT,
- securityDepositAsDouble,
- securityDepositAsDouble,
+ hasBuyerAsTakerWithoutDeposit ? 0d : securityDepositPct, // buyer as taker security deposit is optional for private offers
+ securityDepositPct,
baseCurrencyCode,
counterCurrencyCode,
paymentAccount.getPaymentMethod().getId(),
@@ -211,44 +226,110 @@ public class CreateOfferService {
upperClosePrice,
lowerClosePrice,
isPrivateOffer,
- hashOfChallenge,
+ challengeHash,
extraDataMap,
Version.TRADE_PROTOCOL_VERSION,
null,
null,
- null);
+ null,
+ extraInfo);
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
+ offer.setChallenge(challenge);
return offer;
}
- public BigInteger getReservedFundsForOffer(OfferDirection direction,
- BigInteger amount,
- double buyerSecurityDeposit,
- double sellerSecurityDeposit) {
+ // TODO: add trigger price?
+ public Offer createClonedOffer(Offer sourceOffer,
+ String currencyCode,
+ Price fixedPrice,
+ boolean useMarketBasedPrice,
+ double marketPriceMargin,
+ PaymentAccount paymentAccount,
+ String extraInfo) {
+ log.info("Cloning offer with sourceId={}, " +
+ "currencyCode={}, " +
+ "fixedPrice={}, " +
+ "useMarketBasedPrice={}, " +
+ "marketPriceMargin={}, " +
+ "extraInfo={}",
+ sourceOffer.getId(),
+ currencyCode,
+ fixedPrice == null ? null : fixedPrice.getValue(),
+ useMarketBasedPrice,
+ marketPriceMargin,
+ extraInfo);
- BigInteger reservedFundsForOffer = getSecurityDeposit(direction,
- amount,
- buyerSecurityDeposit,
- sellerSecurityDeposit);
- if (!offerUtil.isBuyOffer(direction))
- reservedFundsForOffer = reservedFundsForOffer.add(amount);
+ OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload();
+ String newOfferId = OfferUtil.getRandomOfferId();
+ Offer editedOffer = createAndGetOffer(newOfferId,
+ sourceOfferPayload.getDirection(),
+ currencyCode,
+ BigInteger.valueOf(sourceOfferPayload.getAmount()),
+ BigInteger.valueOf(sourceOfferPayload.getMinAmount()),
+ fixedPrice,
+ useMarketBasedPrice,
+ marketPriceMargin,
+ sourceOfferPayload.getSellerSecurityDepositPct(),
+ paymentAccount,
+ sourceOfferPayload.isPrivateOffer(),
+ sourceOfferPayload.isBuyerAsTakerWithoutDeposit(),
+ extraInfo);
- return reservedFundsForOffer;
- }
-
- public BigInteger getSecurityDeposit(OfferDirection direction,
- BigInteger amount,
- double buyerSecurityDeposit,
- double sellerSecurityDeposit) {
- return offerUtil.isBuyOffer(direction) ?
- getBuyerSecurityDeposit(amount, buyerSecurityDeposit) :
- getSellerSecurityDeposit(amount, sellerSecurityDeposit);
- }
-
- public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
- return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
- Restrictions.getSellerSecurityDepositAsPercent();
+ // generate one-time challenge for private offer
+ String challenge = null;
+ String challengeHash = null;
+ if (sourceOfferPayload.isPrivateOffer()) {
+ challenge = HavenoUtils.generateChallenge();
+ challengeHash = HavenoUtils.getChallengeHash(challenge);
+ }
+
+ OfferPayload editedOfferPayload = editedOffer.getOfferPayload();
+ long date = new Date().getTime();
+ OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
+ date,
+ sourceOfferPayload.getOwnerNodeAddress(),
+ sourceOfferPayload.getPubKeyRing(),
+ sourceOfferPayload.getDirection(),
+ editedOfferPayload.getPrice(),
+ editedOfferPayload.getMarketPriceMarginPct(),
+ editedOfferPayload.isUseMarketBasedPrice(),
+ sourceOfferPayload.getAmount(),
+ sourceOfferPayload.getMinAmount(),
+ sourceOfferPayload.getMakerFeePct(),
+ sourceOfferPayload.getTakerFeePct(),
+ sourceOfferPayload.getPenaltyFeePct(),
+ sourceOfferPayload.getBuyerSecurityDepositPct(),
+ sourceOfferPayload.getSellerSecurityDepositPct(),
+ editedOfferPayload.getBaseCurrencyCode(),
+ editedOfferPayload.getCounterCurrencyCode(),
+ editedOfferPayload.getPaymentMethodId(),
+ editedOfferPayload.getMakerPaymentAccountId(),
+ editedOfferPayload.getCountryCode(),
+ editedOfferPayload.getAcceptedCountryCodes(),
+ editedOfferPayload.getBankId(),
+ editedOfferPayload.getAcceptedBankIds(),
+ editedOfferPayload.getVersionNr(),
+ sourceOfferPayload.getBlockHeightAtOfferCreation(),
+ editedOfferPayload.getMaxTradeLimit(),
+ editedOfferPayload.getMaxTradePeriod(),
+ sourceOfferPayload.isUseAutoClose(),
+ sourceOfferPayload.isUseReOpenAfterAutoClose(),
+ sourceOfferPayload.getLowerClosePrice(),
+ sourceOfferPayload.getUpperClosePrice(),
+ sourceOfferPayload.isPrivateOffer(),
+ challengeHash,
+ editedOfferPayload.getExtraDataMap(),
+ sourceOfferPayload.getProtocolVersion(),
+ null,
+ null,
+ sourceOfferPayload.getReserveTxKeyImages(),
+ editedOfferPayload.getExtraInfo());
+ Offer clonedOffer = new Offer(clonedOfferPayload);
+ clonedOffer.setPriceFeedService(priceFeedService);
+ clonedOffer.setChallenge(challenge);
+ clonedOffer.setState(Offer.State.AVAILABLE);
+ return clonedOffer;
}
///////////////////////////////////////////////////////////////////////////////////////////
@@ -259,26 +340,4 @@ public class CreateOfferService {
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
}
-
- private BigInteger getBuyerSecurityDeposit(BigInteger amount, double buyerSecurityDeposit) {
- BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(buyerSecurityDeposit, amount);
- return getBoundedBuyerSecurityDeposit(percentOfAmount);
- }
-
- private BigInteger getSellerSecurityDeposit(BigInteger amount, double sellerSecurityDeposit) {
- BigInteger percentOfAmount = CoinUtil.getPercentOfAmount(sellerSecurityDeposit, amount);
- return getBoundedSellerSecurityDeposit(percentOfAmount);
- }
-
- private BigInteger getBoundedBuyerSecurityDeposit(BigInteger value) {
- // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
- // MinBuyerSecurityDeposit from Restrictions.
- return Restrictions.getMinBuyerSecurityDeposit().max(value);
- }
-
- private BigInteger getBoundedSellerSecurityDeposit(BigInteger value) {
- // We need to ensure that for small amount values we don't get a too low BTC amount. We limit it with using the
- // MinSellerSecurityDeposit from Restrictions.
- return Restrictions.getMinSellerSecurityDeposit().max(value);
- }
}
diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java
index 675fbeb550..8df8511b3a 100644
--- a/core/src/main/java/haveno/core/offer/Offer.java
+++ b/core/src/main/java/haveno/core/offer/Offer.java
@@ -18,7 +18,6 @@
package haveno.core.offer;
import haveno.common.ThreadUtils;
-import haveno.common.UserThread;
import haveno.common.crypto.KeyRing;
import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.ErrorMessageHandler;
@@ -115,6 +114,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
@Setter
transient private boolean isReservedFundsSpent;
+ @JsonExclude
+ @Getter
+ @Setter
+ @Nullable
+ transient private String challenge;
+
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@@ -275,7 +280,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
}
public void setErrorMessage(String errorMessage) {
- UserThread.await(() -> errorMessageProperty.set(errorMessage));
+ errorMessageProperty.set(errorMessage);
}
@@ -337,6 +342,18 @@ public class Offer implements NetworkPayload, PersistablePayload {
return offerPayload.getSellerSecurityDepositPct();
}
+ public boolean isPrivateOffer() {
+ return offerPayload.isPrivateOffer();
+ }
+
+ public String getChallengeHash() {
+ return offerPayload.getChallengeHash();
+ }
+
+ public boolean hasBuyerAsTakerWithoutDeposit() {
+ return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
+ }
+
public BigInteger getMaxTradeLimit() {
return BigInteger.valueOf(offerPayload.getMaxTradeLimit());
}
@@ -403,7 +420,23 @@ public class Offer implements NetworkPayload, PersistablePayload {
return "";
}
- public String getExtraInfo() {
+ public String getCombinedExtraInfo() {
+ StringBuilder sb = new StringBuilder();
+ if (getOfferExtraInfo() != null && !getOfferExtraInfo().isEmpty()) {
+ sb.append(getOfferExtraInfo());
+ }
+ if (getPaymentAccountExtraInfo() != null && !getPaymentAccountExtraInfo().isEmpty()) {
+ if (sb.length() > 0) sb.append("\n\n");
+ sb.append(getPaymentAccountExtraInfo());
+ }
+ return sb.toString();
+ }
+
+ public String getOfferExtraInfo() {
+ return offerPayload.getExtraInfo();
+ }
+
+ public String getPaymentAccountExtraInfo() {
if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.F2F_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.F2F_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAY_BY_MAIL_EXTRA_INFO))
@@ -414,6 +447,8 @@ public class Offer implements NetworkPayload, PersistablePayload {
return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO))
return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO);
+ else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASH_AT_ATM_EXTRA_INFO))
+ return getExtraDataMap().get(OfferPayload.CASH_AT_ATM_EXTRA_INFO);
else
return "";
}
diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java
index 7698aeb1ca..50981a8fa6 100644
--- a/core/src/main/java/haveno/core/offer/OfferBookService.java
+++ b/core/src/main/java/haveno/core/offer/OfferBookService.java
@@ -36,6 +36,9 @@ package haveno.core.offer;
import com.google.inject.Inject;
import com.google.inject.name.Named;
+
+import haveno.common.ThreadUtils;
+import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.file.JsonFileManager;
@@ -45,45 +48,51 @@ import haveno.core.api.XmrConnectionService;
import haveno.core.filter.FilterManager;
import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService;
-import haveno.core.trade.HavenoUtils;
import haveno.core.util.JsonUtil;
+import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrKeyImageListener;
-import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.HashMapChangedListener;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
+import haveno.network.utils.Utils;
+import lombok.extern.slf4j.Slf4j;
+
import java.io.File;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import monero.daemon.model.MoneroKeyImageSpentStatus;
/**
- * 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).
+ * Handles validation and announcement of offers added or removed.
*/
+@Slf4j
public class OfferBookService {
+ private final static long INVALID_OFFERS_TIMEOUT = 5 * 60 * 1000; // 5 minutes
+
private final P2PService p2PService;
private final PriceFeedService priceFeedService;
private final List offerBookChangedListeners = new LinkedList<>();
private final FilterManager filterManager;
private final JsonFileManager jsonFileManager;
private final XmrConnectionService xmrConnectionService;
-
- // poll key images of offers
- private XmrKeyImagePoller keyImagePoller;
- private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
- private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
+ private final List validOffers = new ArrayList();
+ private final List invalidOffers = new ArrayList();
+ private final Map invalidOfferTimers = new HashMap<>();
public interface OfferBookChangedListener {
void onAdded(Offer offer);
-
void onRemoved(Offer offer);
}
@@ -104,51 +113,59 @@ public class OfferBookService {
this.xmrConnectionService = xmrConnectionService;
jsonFileManager = new JsonFileManager(storageDir);
- // listen for connection changes to monerod
- xmrConnectionService.addConnectionListener((connection) -> {
- maybeInitializeKeyImagePoller();
- keyImagePoller.setDaemon(xmrConnectionService.getDaemon());
- keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
- });
-
// listen for offers
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
@Override
public void onAdded(Collection protectedStorageEntries) {
- UserThread.execute(() -> {
+ ThreadUtils.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
- maybeInitializeKeyImagePoller();
- keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
- setReservedFundsSpent(offer);
- synchronized (offerBookChangedListeners) {
- offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
+ synchronized (validOffers) {
+ try {
+ validateOfferPayload(offerPayload);
+ replaceValidOffer(offer);
+ announceOfferAdded(offer);
+ } catch (IllegalArgumentException e) {
+ // ignore illegal offers
+ } catch (RuntimeException e) {
+ replaceInvalidOffer(offer); // offer can become valid later
+ }
}
}
});
- });
+ }, OfferBookService.class.getSimpleName());
}
@Override
public void onRemoved(Collection protectedStorageEntries) {
- UserThread.execute(() -> {
+ ThreadUtils.execute(() -> {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
- maybeInitializeKeyImagePoller();
- keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
+ removeValidOffer(offerPayload.getId());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
- setReservedFundsSpent(offer);
- synchronized (offerBookChangedListeners) {
- offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
+ announceOfferRemoved(offer);
+
+ // check if invalid offers are now valid
+ synchronized (invalidOffers) {
+ for (Offer invalidOffer : new ArrayList(invalidOffers)) {
+ try {
+ validateOfferPayload(invalidOffer.getOfferPayload());
+ removeInvalidOffer(invalidOffer.getId());
+ replaceValidOffer(invalidOffer);
+ announceOfferAdded(invalidOffer);
+ } catch (Exception e) {
+ // ignore
+ }
+ }
}
}
});
- });
+ }, OfferBookService.class.getSimpleName());
}
});
@@ -171,6 +188,16 @@ public class OfferBookService {
}
});
}
+
+ // listen for changes to key images
+ xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() {
+ @Override
+ public void onSpentStatusChanged(Map spentStatuses) {
+ for (String keyImage : spentStatuses.keySet()) {
+ updateAffectedOffers(keyImage);
+ }
+ }
+ });
}
@@ -178,6 +205,10 @@ public class OfferBookService {
// API
///////////////////////////////////////////////////////////////////////////////////////////
+ public boolean hasOffer(String offerId) {
+ return hasValidOffer(offerId);
+ }
+
public void addOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (filterManager.requireUpdateToNewVersionForTrading()) {
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
@@ -233,16 +264,9 @@ public class OfferBookService {
}
public List getOffers() {
- return p2PService.getDataMap().values().stream()
- .filter(data -> data.getProtectedStoragePayload() instanceof OfferPayload)
- .map(data -> {
- OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
- Offer offer = new Offer(offerPayload);
- offer.setPriceFeedService(priceFeedService);
- setReservedFundsSpent(offer);
- return offer;
- })
- .collect(Collectors.toList());
+ synchronized (validOffers) {
+ return new ArrayList<>(validOffers);
+ }
}
public List getOffersByCurrency(String direction, String currencyCode) {
@@ -266,7 +290,7 @@ public class OfferBookService {
}
public void shutDown() {
- if (keyImagePoller != null) keyImagePoller.clearKeyImages();
+ xmrConnectionService.getKeyImagePoller().removeKeyImages(OfferBookService.class.getName());
}
@@ -274,37 +298,131 @@ public class OfferBookService {
// Private
///////////////////////////////////////////////////////////////////////////////////////////
- private synchronized void maybeInitializeKeyImagePoller() {
- if (keyImagePoller != null) return;
- keyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs());
+ private void announceOfferAdded(Offer offer) {
+ xmrConnectionService.getKeyImagePoller().addKeyImages(offer.getOfferPayload().getReserveTxKeyImages(), OfferBookService.class.getSimpleName());
+ updateReservedFundsSpentStatus(offer);
+ synchronized (offerBookChangedListeners) {
+ offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
+ }
+ }
- // handle when key images spent
- keyImagePoller.addListener(new XmrKeyImageListener() {
- @Override
- public void onSpentStatusChanged(Map spentStatuses) {
- UserThread.execute(() -> {
- for (String keyImage : spentStatuses.keySet()) {
- updateAffectedOffers(keyImage);
- }
- });
+ private void announceOfferRemoved(Offer offer) {
+ updateReservedFundsSpentStatus(offer);
+ removeKeyImages(offer);
+ synchronized (offerBookChangedListeners) {
+ offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
+ }
+ }
+
+ private boolean hasValidOffer(String offerId) {
+ for (Offer offer : getOffers()) {
+ if (offer.getId().equals(offerId)) {
+ return true;
}
- });
-
- // first poll after 20s
- // TODO: remove?
- new Thread(() -> {
- HavenoUtils.waitFor(20000);
- keyImagePoller.poll();
- }).start();
+ }
+ return false;
+ }
+
+ private void replaceValidOffer(Offer offer) {
+ synchronized (validOffers) {
+ removeValidOffer(offer.getId());
+ validOffers.add(offer);
+ }
}
- private long getKeyImageRefreshPeriodMs() {
- return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
+ private void replaceInvalidOffer(Offer offer) {
+ synchronized (invalidOffers) {
+ removeInvalidOffer(offer.getId());
+ invalidOffers.add(offer);
+
+ // remove invalid offer after timeout
+ synchronized (invalidOfferTimers) {
+ Timer timer = invalidOfferTimers.get(offer.getId());
+ if (timer != null) timer.stop();
+ timer = UserThread.runAfter(() -> {
+ removeInvalidOffer(offer.getId());
+ }, INVALID_OFFERS_TIMEOUT);
+ invalidOfferTimers.put(offer.getId(), timer);
+ }
+ }
}
+ private void removeValidOffer(String offerId) {
+ synchronized (validOffers) {
+ validOffers.removeIf(offer -> offer.getId().equals(offerId));
+ }
+ }
+
+ private void removeInvalidOffer(String offerId) {
+ synchronized (invalidOffers) {
+ invalidOffers.removeIf(offer -> offer.getId().equals(offerId));
+
+ // remove timeout
+ synchronized (invalidOfferTimers) {
+ Timer timer = invalidOfferTimers.get(offerId);
+ if (timer != null) timer.stop();
+ invalidOfferTimers.remove(offerId);
+ }
+ }
+ }
+
+ private void validateOfferPayload(OfferPayload offerPayload) {
+
+ // validate offer is not banned
+ if (filterManager.isOfferIdBanned(offerPayload.getId())) {
+ throw new IllegalArgumentException("Offer is banned with offerId=" + offerPayload.getId());
+ }
+
+ // validate v3 node address compliance
+ boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate() || Utils.isV3Address(offerPayload.getOwnerNodeAddress().getHostName());
+ if (!isV3NodeAddressCompliant) {
+ throw new IllegalArgumentException("Offer with non-V3 node address is not allowed with offerId=" + offerPayload.getId());
+ }
+
+ // validate against existing offers
+ synchronized (validOffers) {
+ int numOffersWithSharedKeyImages = 0;
+ for (Offer offer : validOffers) {
+
+ // validate that no offer has overlapping but different key images
+ if (!offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
+ !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) {
+ throw new RuntimeException("Offer with overlapping key images already exists with offerId=" + offer.getId());
+ }
+
+ // validate that no offer has same key images, payment method, and currency
+ if (!offer.getId().equals(offerPayload.getId()) &&
+ offer.getOfferPayload().getReserveTxKeyImages().equals(offerPayload.getReserveTxKeyImages()) &&
+ offer.getOfferPayload().getPaymentMethodId().equals(offerPayload.getPaymentMethodId()) &&
+ offer.getOfferPayload().getBaseCurrencyCode().equals(offerPayload.getBaseCurrencyCode()) &&
+ offer.getOfferPayload().getCounterCurrencyCode().equals(offerPayload.getCounterCurrencyCode())) {
+ throw new RuntimeException("Offer with same key images, payment method, and currency already exists with offerId=" + offer.getId());
+ }
+
+ // count offers with same key images
+ if (!offer.getId().equals(offerPayload.getId()) && !Collections.disjoint(offer.getOfferPayload().getReserveTxKeyImages(), offerPayload.getReserveTxKeyImages())) numOffersWithSharedKeyImages = Math.max(2, numOffersWithSharedKeyImages + 1);
+ }
+
+ // validate max offers with same key images
+ if (numOffersWithSharedKeyImages > Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) throw new RuntimeException("More than " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " offers exist with same same key images as new offerId=" + offerPayload.getId());
+ }
+ }
+
+ private void removeKeyImages(Offer offer) {
+ Set unsharedKeyImages = new HashSet<>(offer.getOfferPayload().getReserveTxKeyImages());
+ synchronized (validOffers) {
+ for (Offer validOffer : validOffers) {
+ if (validOffer.getId().equals(offer.getId())) continue;
+ unsharedKeyImages.removeAll(validOffer.getOfferPayload().getReserveTxKeyImages());
+ }
+ }
+ xmrConnectionService.getKeyImagePoller().removeKeyImages(unsharedKeyImages, OfferBookService.class.getSimpleName());
+ }
+
private void updateAffectedOffers(String keyImage) {
for (Offer offer : getOffers()) {
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
+ updateReservedFundsSpentStatus(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
listener.onRemoved(offer);
@@ -315,10 +433,9 @@ public class OfferBookService {
}
}
- private void setReservedFundsSpent(Offer offer) {
- if (keyImagePoller == null) return;
+ private void updateReservedFundsSpentStatus(Offer offer) {
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
- if (Boolean.TRUE.equals(keyImagePoller.isSpent(keyImage))) {
+ if (Boolean.TRUE.equals(xmrConnectionService.getKeyImagePoller().isSpent(keyImage))) {
offer.setReservedFundsSpent(true);
}
}
diff --git a/core/src/main/java/haveno/core/offer/OfferFilterService.java b/core/src/main/java/haveno/core/offer/OfferFilterService.java
index 51ac8cdee7..e64a1ee6eb 100644
--- a/core/src/main/java/haveno/core/offer/OfferFilterService.java
+++ b/core/src/main/java/haveno/core/offer/OfferFilterService.java
@@ -201,7 +201,7 @@ public class OfferFilterService {
accountAgeWitnessService);
long myTradeLimit = accountOptional
.map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount,
- offer.getCurrencyCode(), offer.getMirroredDirection()))
+ offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()))
.orElse(0L);
long offerMinAmount = offer.getMinAmount().longValueExact();
log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ",
diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java
index fa05685dee..1db2cca940 100644
--- a/core/src/main/java/haveno/core/offer/OfferPayload.java
+++ b/core/src/main/java/haveno/core/offer/OfferPayload.java
@@ -102,6 +102,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo";
public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo";
+ public static final String CASH_AT_ATM_EXTRA_INFO = "cashAtAtmExtraInfo";
// Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
@@ -156,7 +157,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// Reserved for possible future use to support private trades where the taker needs to have an accessKey
private final boolean isPrivateOffer;
@Nullable
- private final String hashOfChallenge;
+ private final String challengeHash;
+ @Nullable
+ private final String extraInfo;
///////////////////////////////////////////////////////////////////////////////////////////
@@ -195,12 +198,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
long lowerClosePrice,
long upperClosePrice,
boolean isPrivateOffer,
- @Nullable String hashOfChallenge,
+ @Nullable String challengeHash,
@Nullable Map extraDataMap,
int protocolVersion,
@Nullable NodeAddress arbitratorSigner,
@Nullable byte[] arbitratorSignature,
- @Nullable List reserveTxKeyImages) {
+ @Nullable List reserveTxKeyImages,
+ @Nullable String extraInfo) {
this.id = id;
this.date = date;
this.ownerNodeAddress = ownerNodeAddress;
@@ -238,7 +242,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
this.lowerClosePrice = lowerClosePrice;
this.upperClosePrice = upperClosePrice;
this.isPrivateOffer = isPrivateOffer;
- this.hashOfChallenge = hashOfChallenge;
+ this.challengeHash = challengeHash;
+ this.extraInfo = extraInfo;
}
public byte[] getHash() {
@@ -284,12 +289,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
lowerClosePrice,
upperClosePrice,
isPrivateOffer,
- hashOfChallenge,
+ challengeHash,
extraDataMap,
protocolVersion,
arbitratorSigner,
null,
- reserveTxKeyImages
+ reserveTxKeyImages,
+ null
);
return signee.getHash();
@@ -328,12 +334,21 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
public BigInteger getBuyerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getBuyerSecurityDepositPct());
- return Restrictions.getMinBuyerSecurityDeposit().max(securityDepositUnadjusted);
+ boolean isBuyerTaker = getDirection() == OfferDirection.SELL;
+ if (isPrivateOffer() && isBuyerTaker) {
+ return securityDepositUnadjusted;
+ } else {
+ return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
+ }
}
public BigInteger getSellerSecurityDepositForTradeAmount(BigInteger tradeAmount) {
BigInteger securityDepositUnadjusted = HavenoUtils.multiply(tradeAmount, getSellerSecurityDepositPct());
- return Restrictions.getMinSellerSecurityDeposit().max(securityDepositUnadjusted);
+ return Restrictions.getMinSecurityDeposit().max(securityDepositUnadjusted);
+ }
+
+ public boolean isBuyerAsTakerWithoutDeposit() {
+ return getDirection() == OfferDirection.SELL && getBuyerSecurityDepositPct() == 0;
}
///////////////////////////////////////////////////////////////////////////////////////////
@@ -376,11 +391,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
Optional.ofNullable(bankId).ifPresent(builder::setBankId);
Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds);
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
- Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
+ Optional.ofNullable(challengeHash).ifPresent(builder::setChallengeHash);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
Optional.ofNullable(arbitratorSigner).ifPresent(e -> builder.setArbitratorSigner(arbitratorSigner.toProtoMessage()));
Optional.ofNullable(arbitratorSignature).ifPresent(e -> builder.setArbitratorSignature(ByteString.copyFrom(e)));
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
+ Optional.ofNullable(extraInfo).ifPresent(builder::setExtraInfo);
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
}
@@ -392,7 +408,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
List reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
- String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : proto.getExtraDataMap();
@@ -428,12 +443,13 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
proto.getLowerClosePrice(),
proto.getUpperClosePrice(),
proto.getIsPrivateOffer(),
- hashOfChallenge,
+ ProtoUtil.stringOrNullFromProto(proto.getChallengeHash()),
extraDataMapMap,
proto.getProtocolVersion(),
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
- reserveTxKeyImages);
+ reserveTxKeyImages,
+ ProtoUtil.stringOrNullFromProto(proto.getExtraInfo()));
}
@Override
@@ -475,14 +491,15 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
",\r\n lowerClosePrice=" + lowerClosePrice +
",\r\n upperClosePrice=" + upperClosePrice +
",\r\n isPrivateOffer=" + isPrivateOffer +
- ",\r\n hashOfChallenge='" + hashOfChallenge + '\'' +
+ ",\r\n challengeHash='" + challengeHash +
",\r\n arbitratorSigner=" + arbitratorSigner +
",\r\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
+ ",\r\n extraInfo='" + extraInfo +
"\r\n} ";
}
// For backward compatibility we need to ensure same order for json fields as with 1.7.5. and earlier versions.
- // The json is used for the hash in the contract and change of oder would cause a different hash and
+ // The json is used for the hash in the contract and change of order would cause a different hash and
// therefore a failure during trade.
public static class JsonSerializer implements com.google.gson.JsonSerializer {
@Override
@@ -519,6 +536,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
object.add("protocolVersion", context.serialize(offerPayload.getProtocolVersion()));
object.add("arbitratorSigner", context.serialize(offerPayload.getArbitratorSigner()));
object.add("arbitratorSignature", context.serialize(offerPayload.getArbitratorSignature()));
+ object.add("extraInfo", context.serialize(offerPayload.getExtraInfo()));
+ // reserveTxKeyImages and challengeHash are purposely excluded because they are not relevant to existing trades and would break existing contracts
return object;
}
}
diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java
index 72593ab5e7..2e2644630a 100644
--- a/core/src/main/java/haveno/core/offer/OfferUtil.java
+++ b/core/src/main/java/haveno/core/offer/OfferUtil.java
@@ -37,6 +37,7 @@ import haveno.core.monetary.Volume;
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CAPABILITIES;
+import static haveno.core.offer.OfferPayload.CASH_AT_ATM_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.F2F_CITY;
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO;
@@ -48,6 +49,7 @@ import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.CashAppAccount;
+import haveno.core.payment.CashAtAtmAccount;
import haveno.core.payment.F2FAccount;
import haveno.core.payment.PayByMailAccount;
import haveno.core.payment.PayPalAccount;
@@ -58,8 +60,8 @@ import haveno.core.trade.statistics.ReferralIdService;
import haveno.core.user.AutoConfirmSettings;
import haveno.core.user.Preferences;
import haveno.core.util.coin.CoinFormatter;
-import static haveno.core.xmr.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent;
-import static haveno.core.xmr.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent;
+import static haveno.core.xmr.wallet.Restrictions.getMaxSecurityDepositAsPercent;
+import static haveno.core.xmr.wallet.Restrictions.getMinSecurityDepositAsPercent;
import haveno.network.p2p.P2PService;
import java.math.BigInteger;
import java.util.HashMap;
@@ -120,9 +122,10 @@ public class OfferUtil {
public long getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode,
- OfferDirection direction) {
+ OfferDirection direction,
+ boolean buyerAsTakerWithoutDeposit) {
return paymentAccount != null
- ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction)
+ ? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction, buyerAsTakerWithoutDeposit)
: 0;
}
@@ -216,6 +219,10 @@ public class OfferUtil {
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
}
+ if (paymentAccount instanceof CashAtAtmAccount) {
+ extraDataMap.put(CASH_AT_ATM_EXTRA_INFO, ((CashAtAtmAccount) paymentAccount).getExtraInfo());
+ }
+
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
if (currencyCode.equals("XMR") && direction == OfferDirection.SELL) {
@@ -228,16 +235,16 @@ public class OfferUtil {
return extraDataMap.isEmpty() ? null : extraDataMap;
}
- public void validateOfferData(double buyerSecurityDeposit,
+ public void validateOfferData(double securityDeposit,
PaymentAccount paymentAccount,
String currencyCode) {
checkNotNull(p2PService.getAddress(), "Address must not be null");
- checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(),
+ checkArgument(securityDeposit <= getMaxSecurityDepositAsPercent(),
"securityDeposit must not exceed " +
- getMaxBuyerSecurityDepositAsPercent());
- checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
+ getMaxSecurityDepositAsPercent());
+ checkArgument(securityDeposit >= getMinSecurityDepositAsPercent(),
"securityDeposit must not be less than " +
- getMinBuyerSecurityDepositAsPercent() + " but was " + buyerSecurityDeposit);
+ getMinSecurityDepositAsPercent() + " but was " + securityDeposit);
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java
index b0df3e0e35..f493b1b584 100644
--- a/core/src/main/java/haveno/core/offer/OpenOffer.java
+++ b/core/src/main/java/haveno/core/offer/OpenOffer.java
@@ -48,6 +48,7 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
+import java.util.UUID;
@EqualsAndHashCode
public final class OpenOffer implements Tradable {
@@ -96,6 +97,9 @@ public final class OpenOffer implements Tradable {
@Getter
private String reserveTxKey;
@Getter
+ @Setter
+ private String challenge;
+ @Getter
private final long triggerPrice;
@Getter
@Setter
@@ -107,6 +111,12 @@ public final class OpenOffer implements Tradable {
@Getter
@Setter
transient int numProcessingAttempts = 0;
+ @Getter
+ @Setter
+ private boolean deactivatedByTrigger;
+ @Getter
+ @Setter
+ private String groupId;
public OpenOffer(Offer offer) {
this(offer, 0, false);
@@ -120,6 +130,8 @@ public final class OpenOffer implements Tradable {
this.offer = offer;
this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount;
+ this.challenge = offer.getChallenge();
+ this.groupId = UUID.randomUUID().toString();
state = State.PENDING;
}
@@ -137,6 +149,9 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = openOffer.reserveTxHash;
this.reserveTxHex = openOffer.reserveTxHex;
this.reserveTxKey = openOffer.reserveTxKey;
+ this.challenge = openOffer.challenge;
+ this.deactivatedByTrigger = openOffer.deactivatedByTrigger;
+ this.groupId = openOffer.groupId;
}
///////////////////////////////////////////////////////////////////////////////////////////
@@ -153,7 +168,10 @@ public final class OpenOffer implements Tradable {
long splitOutputTxFee,
@Nullable String reserveTxHash,
@Nullable String reserveTxHex,
- @Nullable String reserveTxKey) {
+ @Nullable String reserveTxKey,
+ @Nullable String challenge,
+ boolean deactivatedByTrigger,
+ @Nullable String groupId) {
this.offer = offer;
this.state = state;
this.triggerPrice = triggerPrice;
@@ -164,6 +182,10 @@ public final class OpenOffer implements Tradable {
this.reserveTxHash = reserveTxHash;
this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey;
+ this.challenge = challenge;
+ this.deactivatedByTrigger = deactivatedByTrigger;
+ if (groupId == null) groupId = UUID.randomUUID().toString(); // initialize groupId if not set (added in v1.0.19)
+ this.groupId = groupId;
// reset reserved state to available
if (this.state == State.RESERVED) setState(State.AVAILABLE);
@@ -176,7 +198,8 @@ public final class OpenOffer implements Tradable {
.setTriggerPrice(triggerPrice)
.setState(protobuf.OpenOffer.State.valueOf(state.name()))
.setSplitOutputTxFee(splitOutputTxFee)
- .setReserveExactAmount(reserveExactAmount);
+ .setReserveExactAmount(reserveExactAmount)
+ .setDeactivatedByTrigger(deactivatedByTrigger);
Optional.ofNullable(scheduledAmount).ifPresent(e -> builder.setScheduledAmount(scheduledAmount));
Optional.ofNullable(scheduledTxHashes).ifPresent(e -> builder.addAllScheduledTxHashes(scheduledTxHashes));
@@ -184,6 +207,8 @@ public final class OpenOffer implements Tradable {
Optional.ofNullable(reserveTxHash).ifPresent(e -> builder.setReserveTxHash(reserveTxHash));
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
+ Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
+ Optional.ofNullable(groupId).ifPresent(e -> builder.setGroupId(groupId));
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
}
@@ -199,7 +224,10 @@ public final class OpenOffer implements Tradable {
proto.getSplitOutputTxFee(),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
- ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()));
+ ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
+ ProtoUtil.stringOrNullFromProto(proto.getChallenge()),
+ proto.getDeactivatedByTrigger(),
+ ProtoUtil.stringOrNullFromProto(proto.getGroupId()));
return openOffer;
}
@@ -226,6 +254,14 @@ public final class OpenOffer implements Tradable {
public void setState(State state) {
this.state = state;
stateProperty.set(state);
+ if (state == State.AVAILABLE) {
+ deactivatedByTrigger = false;
+ }
+ }
+
+ public void deactivate(boolean deactivatedByTrigger) {
+ this.deactivatedByTrigger = deactivatedByTrigger;
+ setState(State.DEACTIVATED);
}
public ReadOnlyObjectProperty stateProperty() {
@@ -257,6 +293,7 @@ public final class OpenOffer implements Tradable {
",\n reserveExactAmount=" + reserveExactAmount +
",\n scheduledAmount=" + scheduledAmount +
",\n splitOutputTxFee=" + splitOutputTxFee +
+ ",\n groupId=" + groupId +
"\n}";
}
}
diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
index ffed498bc9..a928494c1d 100644
--- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java
+++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java
@@ -35,6 +35,8 @@
package haveno.core.offer;
import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
@@ -55,13 +57,14 @@ import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService;
import haveno.core.exceptions.TradePriceOutOfToleranceException;
import haveno.core.filter.FilterManager;
-import haveno.core.offer.OfferBookService.OfferBookChangedListener;
+import haveno.core.locale.Res;
import haveno.core.offer.messages.OfferAvailabilityRequest;
import haveno.core.offer.messages.OfferAvailabilityResponse;
import haveno.core.offer.messages.SignOfferRequest;
import haveno.core.offer.messages.SignOfferResponse;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.offer.placeoffer.PlaceOfferProtocol;
+import haveno.core.offer.placeoffer.tasks.ValidateOffer;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
@@ -78,6 +81,7 @@ import haveno.core.util.JsonUtil;
import haveno.core.util.Validator;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.BtcWalletService;
+import haveno.core.xmr.wallet.Restrictions;
import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
import haveno.core.xmr.wallet.TradeWalletService;
@@ -113,7 +117,6 @@ import lombok.Getter;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx;
-import monero.wallet.model.MoneroIncomingTransfer;
import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTransferQuery;
@@ -133,7 +136,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30;
private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30);
private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2;
- private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts
+ private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process offer only on republish cycle after this many attempts
+ private static final long SHUTDOWN_TIMEOUT_MS = 60000;
+ private static final String OPEN_OFFER_GROUP_KEY_IMAGE_ID = OpenOffer.class.getSimpleName();
+ private static final String SIGNED_OFFER_KEY_IMAGE_GROUP_ID = SignedOffer.class.getSimpleName();
private final CoreContext coreContext;
private final KeyRing keyRing;
@@ -167,12 +173,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
@Getter
private final AccountAgeWitnessService accountAgeWitnessService;
- // poll key images of signed offers
- private XmrKeyImagePoller signedOfferKeyImagePoller;
- private static final long SHUTDOWN_TIMEOUT_MS = 60000;
- private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
- private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
-
private Object processOffersLock = new Object(); // lock for processing offers
@@ -225,27 +225,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.persistenceManager.initialize(openOffers, "OpenOffers", PersistenceManager.Source.PRIVATE);
this.signedOfferPersistenceManager.initialize(signedOffers, "SignedOffers", PersistenceManager.Source.PRIVATE); // arbitrator stores reserve tx for signed offers
-
- // listen for connection changes to monerod
- xmrConnectionService.addConnectionListener((connection) -> maybeInitializeKeyImagePoller());
-
- // close open offer if reserved funds spent
- offerBookService.addOfferBookChangedListener(new OfferBookChangedListener() {
- @Override
- public void onAdded(Offer offer) {
-
- // cancel offer if reserved funds spent
- Optional openOfferOptional = getOpenOfferById(offer.getId());
- if (openOfferOptional.isPresent() && openOfferOptional.get().getState() != OpenOffer.State.RESERVED && offer.isReservedFundsSpent()) {
- log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", offer.getId(), openOfferOptional.get().getState());
- cancelOpenOffer(openOfferOptional.get(), null, null);
- }
- }
- @Override
- public void onRemoved(Offer offer) {
- // nothing to do
- }
- });
}
@Override
@@ -266,34 +245,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
completeHandler);
}
- private synchronized void maybeInitializeKeyImagePoller() {
- if (signedOfferKeyImagePoller != null) return;
- signedOfferKeyImagePoller = new XmrKeyImagePoller(xmrConnectionService.getDaemon(), getKeyImageRefreshPeriodMs());
-
- // handle when key images confirmed spent
- signedOfferKeyImagePoller.addListener(new XmrKeyImageListener() {
- @Override
- public void onSpentStatusChanged(Map spentStatuses) {
- for (Entry entry : spentStatuses.entrySet()) {
- if (entry.getValue() == MoneroKeyImageSpentStatus.CONFIRMED) {
- removeSignedOffers(entry.getKey());
- }
- }
- }
- });
-
- // first poll in 5s
- // TODO: remove?
- new Thread(() -> {
- HavenoUtils.waitFor(5000);
- signedOfferKeyImagePoller.poll();
- }).start();
- }
-
- private long getKeyImageRefreshPeriodMs() {
- return xmrConnectionService.isConnectionLocalHost() ? KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL : KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE;
- }
-
public void onAllServicesInitialized() {
p2PService.addDecryptedDirectMessageListener(this);
@@ -312,7 +263,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
private void cleanUpAddressEntries() {
- Set openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet());
+ Set openOffersIdSet;
+ synchronized (openOffers.getList()) {
+ openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet());
+ }
xmrWalletService.getAddressEntriesForOpenOffer().stream()
.filter(e -> !openOffersIdSet.contains(e.getOfferId()))
.forEach(e -> {
@@ -328,7 +282,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
stopped = true;
p2PService.getPeerManager().removeListener(this);
p2PService.removeDecryptedDirectMessageListener(this);
- if (signedOfferKeyImagePoller != null) signedOfferKeyImagePoller.clearKeyImages();
+ xmrConnectionService.getKeyImagePoller().removeKeyImages(OPEN_OFFER_GROUP_KEY_IMAGE_ID);
+ xmrConnectionService.getKeyImagePoller().removeKeyImages(SIGNED_OFFER_KEY_IMAGE_GROUP_ID);
stopPeriodicRefreshOffersTimer();
stopPeriodicRepublishOffersTimer();
@@ -342,7 +297,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ThreadUtils.execute(() -> {
// remove offers from offer book
- synchronized (openOffers) {
+ synchronized (openOffers.getList()) {
openOffers.forEach(openOffer -> {
if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
offerBookService.removeOfferAtShutDown(openOffer.getOffer().getOfferPayload());
@@ -383,20 +338,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
removeOpenOffers(getObservableList(), completeHandler);
}
- public void removeOpenOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
- removeOpenOffers(List.of(openOffer), completeHandler);
- }
-
- public void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) {
- int size = openOffers.size();
- // Copy list as we remove in the loop
- List openOffersList = new ArrayList<>(openOffers);
- openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> {
- }, errorMessage -> {
- log.warn("Error removing open offer: " + errorMessage);
- }));
- if (completeHandler != null)
- UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS);
+ private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) {
+ synchronized (openOffers) {
+ int size = openOffers.size();
+ // Copy list as we remove in the loop
+ List openOffersList = new ArrayList<>(openOffers);
+ openOffersList.forEach(openOffer -> cancelOpenOffer(openOffer, () -> {
+ }, errorMessage -> {
+ log.warn("Error removing open offer: " + errorMessage);
+ }));
+ if (completeHandler != null)
+ UserThread.runAfter(completeHandler, size * 200 + 500, TimeUnit.MILLISECONDS);
+ }
}
@@ -440,6 +393,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
maybeUpdatePersistedOffers();
+ // listen for spent key images to close open and signed offers
+ xmrConnectionService.getKeyImagePoller().addListener(new XmrKeyImageListener() {
+ @Override
+ public void onSpentStatusChanged(Map spentStatuses) {
+ for (Entry entry : spentStatuses.entrySet()) {
+ if (XmrKeyImagePoller.isSpent(entry.getValue())) {
+ cancelOpenOffersOnSpent(entry.getKey());
+ removeSignedOffers(entry.getKey());
+ }
+ }
+ }
+ });
+
// run off user thread so app is not blocked from starting
ThreadUtils.submitToPool(() -> {
@@ -473,29 +439,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
// .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
- // process pending offers
- processPendingOffers(false, (transaction) -> {}, (errorMessage) -> {
- log.warn("Error processing pending offers on bootstrap: " + errorMessage);
+ // processs offers
+ processOffers(false, (transaction) -> {}, (errorMessage) -> {
+ log.warn("Error processing offers on bootstrap: " + errorMessage);
});
- // register to process pending offers on new block
+ // register to process offers on new block
xmrWalletService.addWalletListener(new MoneroWalletListener() {
@Override
public void onNewBlock(long height) {
- // process each pending offer on new block a few times, then rely on period republish
- processPendingOffers(true, (transaction) -> {}, (errorMessage) -> {
- log.warn("Error processing pending offers on new block {}: {}", height, errorMessage);
+ // process each offer on new block a few times, then rely on period republish
+ processOffers(true, (transaction) -> {}, (errorMessage) -> {
+ log.warn("Error processing offers on new block {}: {}", height, errorMessage);
});
}
});
- // initialize key image poller for signed offers
- maybeInitializeKeyImagePoller();
+ // poll spent status of open offer key images
+ synchronized (openOffers.getList()) {
+ for (OpenOffer openOffer : openOffers.getList()) {
+ xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID);
+ }
+ }
- // poll spent status of key images
- for (SignedOffer signedOffer : signedOffers.getList()) {
- signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages());
+ // poll spent status of signed offer key images
+ synchronized (signedOffers.getList()) {
+ for (SignedOffer signedOffer : signedOffers.getList()) {
+ xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID);
+ }
}
}, THREAD_ID);
});
@@ -542,24 +514,66 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
long triggerPrice,
boolean reserveExactAmount,
boolean resetAddressEntriesOnError,
+ String sourceOfferId,
TransactionResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
-
- // create open offer
- OpenOffer openOffer = new OpenOffer(offer, triggerPrice, reserveExactAmount);
-
- // schedule or post offer
ThreadUtils.execute(() -> {
+
+ // check source offer and clone limit
+ OpenOffer sourceOffer = null;
+ if (sourceOfferId != null) {
+
+ // get source offer
+ Optional sourceOfferOptional = getOpenOffer(sourceOfferId);
+ if (!sourceOfferOptional.isPresent()) {
+ errorMessageHandler.handleErrorMessage("Source offer not found to clone, offerId=" + sourceOfferId);
+ return;
+ }
+ sourceOffer = sourceOfferOptional.get();
+
+ // check clone limit
+ int numClones = getOpenOfferGroup(sourceOffer.getGroupId()).size();
+ if (numClones >= Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS) {
+ errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of " + Restrictions.MAX_OFFERS_WITH_SHARED_FUNDS + " cloned offers with shared funds reached.");
+ return;
+ }
+ }
+
+ // create open offer
+ OpenOffer openOffer = new OpenOffer(offer, triggerPrice, sourceOffer == null ? reserveExactAmount : sourceOffer.isReserveExactAmount());
+
+ // set state from source offer
+ if (sourceOffer != null) {
+ openOffer.setReserveTxHash(sourceOffer.getReserveTxHash());
+ openOffer.setReserveTxHex(sourceOffer.getReserveTxHex());
+ openOffer.setReserveTxKey(sourceOffer.getReserveTxKey());
+ openOffer.setGroupId(sourceOffer.getGroupId());
+ openOffer.getOffer().getOfferPayload().setReserveTxKeyImages(sourceOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
+ xmrWalletService.cloneAddressEntries(sourceOffer.getOffer().getId(), openOffer.getOffer().getId());
+ if (hasConflictingClone(openOffer)) openOffer.setState(OpenOffer.State.DEACTIVATED);
+ }
+
+ // add the open offer
+ synchronized (processOffersLock) {
+ addOpenOffer(openOffer);
+ }
+
+ // done if source offer is pending
+ if (sourceOffer != null && sourceOffer.isPending()) {
+ resultHandler.handleResult(null);
+ return;
+ }
+
+ // schedule or post offer
synchronized (processOffersLock) {
CountDownLatch latch = new CountDownLatch(1);
- addOpenOffer(openOffer);
- processPendingOffer(getOpenOffers(), openOffer, (transaction) -> {
+ processOffer(getOpenOffers(), openOffer, (transaction) -> {
requestPersistence();
latch.countDown();
resultHandler.handleResult(transaction);
}, (errorMessage) -> {
if (!openOffer.isCanceled()) {
- log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage);
+ log.warn("Error processing offer {}: {}", openOffer.getId(), errorMessage);
doCancelOffer(openOffer, resetAddressEntriesOnError);
}
latch.countDown();
@@ -572,12 +586,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// Remove from offerbook
public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
- Optional openOfferOptional = getOpenOfferById(offer.getId());
+ Optional openOfferOptional = getOpenOffer(offer.getId());
if (openOfferOptional.isPresent()) {
cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler);
} else {
- log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook.");
- errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook.");
+ String errorMsg = "Offer was not found in our list of open offers. We still try to remove it from the offerbook.";
+ log.warn(errorMsg);
+ errorMessageHandler.handleErrorMessage(errorMsg);
offerBookService.removeOffer(offer.getOfferPayload(), () -> offer.setState(Offer.State.REMOVED), null);
}
}
@@ -588,28 +603,49 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.isPending()) {
resultHandler.handleResult(); // ignore if pending
} else if (offersToBeEdited.containsKey(openOffer.getId())) {
- errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited.");
+ errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivateEditedOffer.warning"));
+ } else if (hasConflictingClone(openOffer)) {
+ errorMessageHandler.handleErrorMessage(Res.get("offerbook.hasConflictingClone.warning"));
} else {
- Offer offer = openOffer.getOffer();
- offerBookService.activateOffer(offer,
- () -> {
- openOffer.setState(OpenOffer.State.AVAILABLE);
- requestPersistence();
- log.debug("activateOpenOffer, offerId={}", offer.getId());
- resultHandler.handleResult();
- },
- errorMessageHandler);
+ try {
+
+ // validate arbitrator signature
+ validateSignedState(openOffer);
+
+ // activate offer on offer book
+ Offer offer = openOffer.getOffer();
+ offerBookService.activateOffer(offer,
+ () -> {
+ openOffer.setState(OpenOffer.State.AVAILABLE);
+ applyTriggerState(openOffer);
+ requestPersistence();
+ log.debug("activateOpenOffer, offerId={}", offer.getId());
+ resultHandler.handleResult();
+ },
+ errorMessageHandler);
+ } catch (Exception e) {
+ errorMessageHandler.handleErrorMessage(e.getMessage());
+ return;
+ }
+ }
+ }
+
+ private void applyTriggerState(OpenOffer openOffer) {
+ if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
+ if (TriggerPriceService.isTriggered(priceFeedService.getMarketPrice(openOffer.getOffer().getCurrencyCode()), openOffer)) {
+ openOffer.deactivate(true);
}
}
public void deactivateOpenOffer(OpenOffer openOffer,
+ boolean deactivatedByTrigger,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
Offer offer = openOffer.getOffer();
if (openOffer.isAvailable()) {
offerBookService.deactivateOffer(offer.getOfferPayload(),
() -> {
- openOffer.setState(OpenOffer.State.DEACTIVATED);
+ openOffer.deactivate(deactivatedByTrigger);
requestPersistence();
log.debug("deactivateOpenOffer, offerId={}", offer.getId());
resultHandler.handleResult();
@@ -643,7 +679,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
});
}
} else {
- if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't remove an offer that is currently edited.");
+ if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("You can't cancel an offer that is currently edited.");
}
}
@@ -660,6 +696,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.isAvailable()) {
deactivateOpenOffer(openOffer,
+ false,
resultHandler,
errorMessage -> {
offersToBeEdited.remove(openOffer.getId());
@@ -675,7 +712,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
- Optional openOfferOptional = getOpenOfferById(editedOffer.getId());
+ Optional openOfferOptional = getOpenOffer(editedOffer.getId());
if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get();
@@ -685,16 +722,45 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
removeOpenOffer(openOffer);
OpenOffer editedOpenOffer = new OpenOffer(editedOffer, triggerPrice, openOffer);
- editedOpenOffer.setState(originalState);
+ if (originalState == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger()) {
+ if (hasConflictingClone(editedOpenOffer)) {
+ editedOpenOffer.setState(OpenOffer.State.DEACTIVATED);
+ } else {
+ editedOpenOffer.setState(OpenOffer.State.AVAILABLE);
+ }
+ applyTriggerState(editedOpenOffer);
+ } else {
+ if (originalState == OpenOffer.State.AVAILABLE && hasConflictingClone(editedOpenOffer)) {
+ editedOpenOffer.setState(OpenOffer.State.DEACTIVATED);
+ } else {
+ editedOpenOffer.setState(originalState);
+ }
+ }
addOpenOffer(editedOpenOffer);
- if (editedOpenOffer.isAvailable())
- maybeRepublishOffer(editedOpenOffer);
+ // check for valid arbitrator signature after editing
+ Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner());
+ if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) {
- offersToBeEdited.remove(openOffer.getId());
- requestPersistence();
- resultHandler.handleResult();
+ // reset arbitrator signature
+ editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
+ editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
+
+ // process offer to sign and publish
+ processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> {
+ offersToBeEdited.remove(openOffer.getId());
+ requestPersistence();
+ resultHandler.handleResult();
+ }, (errorMsg) -> {
+ errorMessageHandler.handleErrorMessage(errorMsg);
+ });
+ } else {
+ maybeRepublishOffer(editedOpenOffer, null);
+ offersToBeEdited.remove(openOffer.getId());
+ requestPersistence();
+ resultHandler.handleResult();
+ }
} else {
errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published.");
}
@@ -711,6 +777,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else {
resultHandler.handleResult();
}
+ requestPersistence();
} else {
errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited.");
}
@@ -720,31 +787,38 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
doCancelOffer(openOffer, true);
}
- // remove open offer which thaws its key images
+ // cancel open offer which thaws its key images
private void doCancelOffer(@NotNull OpenOffer openOffer, boolean resetAddressEntries) {
Offer offer = openOffer.getOffer();
offer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
+ boolean hasClonedOffer = hasClonedOffer(offer.getId()); // record before removing open offer
removeOpenOffer(openOffer);
- closedTradableManager.add(openOffer); // TODO: don't add these to closed tradables?
+ if (!hasClonedOffer) closedTradableManager.add(openOffer); // do not add clones to closed trades TODO: don't add canceled offers to closed tradables?
if (resetAddressEntries) xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId());
requestPersistence();
- xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
+ if (!hasClonedOffer) xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
}
- // close open offer after key images spent
- public void closeOpenOffer(Offer offer) {
- getOpenOfferById(offer.getId()).ifPresent(openOffer -> {
- removeOpenOffer(openOffer);
- openOffer.setState(OpenOffer.State.CLOSED);
- xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId());
- offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
- () -> log.info("Successfully removed offer {}", offer.getId()),
- log::error);
- requestPersistence();
+ // close open offer group after key images spent
+ public void closeSpentOffer(Offer offer) {
+ getOpenOffer(offer.getId()).ifPresent(openOffer -> {
+ for (OpenOffer groupOffer: getOpenOfferGroup(openOffer.getGroupId())) {
+ doCloseOpenOffer(groupOffer);
+ }
});
}
+ private void doCloseOpenOffer(OpenOffer openOffer) {
+ removeOpenOffer(openOffer);
+ openOffer.setState(OpenOffer.State.CLOSED);
+ xmrWalletService.resetAddressEntriesForOpenOffer(openOffer.getId());
+ offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
+ () -> log.info("Successfully removed offer {}", openOffer.getId()),
+ log::error);
+ requestPersistence();
+ }
+
public void reserveOpenOffer(OpenOffer openOffer) {
openOffer.setState(OpenOffer.State.RESERVED);
requestPersistence();
@@ -755,6 +829,37 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
requestPersistence();
}
+ public boolean hasConflictingClone(OpenOffer openOffer) {
+ for (OpenOffer clonedOffer : getOpenOfferGroup(openOffer.getGroupId())) {
+ if (clonedOffer.getId().equals(openOffer.getId())) continue;
+ if (clonedOffer.isDeactivated()) continue; // deactivated offers do not conflict
+
+ // pending offers later in the order do not conflict
+ List openOffers = getOpenOffers();
+ if (clonedOffer.isPending() && openOffers.indexOf(clonedOffer) > openOffers.indexOf(openOffer)) {
+ continue;
+ }
+
+ // conflicts if same payment method and currency
+ if (samePaymentMethodAndCurrency(clonedOffer.getOffer(), openOffer.getOffer())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean hasConflictingClone(Offer offer, OpenOffer sourceOffer) {
+ return getOpenOfferGroup(sourceOffer.getGroupId()).stream()
+ .filter(openOffer -> !openOffer.isDeactivated()) // we only check with activated offers
+ .anyMatch(openOffer -> samePaymentMethodAndCurrency(openOffer.getOffer(), offer));
+ }
+
+ private boolean samePaymentMethodAndCurrency(Offer offer1, Offer offer2) {
+ return offer1.getPaymentMethodId().equalsIgnoreCase(offer2.getPaymentMethodId()) &&
+ offer1.getCounterCurrencyCode().equalsIgnoreCase(offer2.getCounterCurrencyCode()) &&
+ offer1.getBaseCurrencyCode().equalsIgnoreCase(offer2.getBaseCurrencyCode());
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
@@ -763,32 +868,51 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return offer.isMyOffer(keyRing);
}
- public boolean hasOpenOffers() {
- synchronized (openOffers) {
- for (OpenOffer openOffer : getOpenOffers()) {
- if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
- return true;
- }
+ public boolean hasAvailableOpenOffers() {
+ for (OpenOffer openOffer : getOpenOffers()) {
+ if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
+ return true;
}
- return false;
}
+ return false;
}
public List getOpenOffers() {
- synchronized (openOffers) {
- return new ArrayList<>(getObservableList());
+ synchronized (openOffers.getList()) {
+ return ImmutableList.copyOf(getObservableList());
}
}
+ public List getOpenOfferGroup(String groupId) {
+ if (groupId == null) throw new IllegalArgumentException("groupId cannot be null");
+ return getOpenOffers().stream()
+ .filter(openOffer -> groupId.equals(openOffer.getGroupId()))
+ .collect(Collectors.toList());
+ }
+
+ public boolean hasClonedOffer(String offerId) {
+ OpenOffer openOffer = getOpenOffer(offerId).orElse(null);
+ if (openOffer == null) return false;
+ return getOpenOfferGroup(openOffer.getGroupId()).size() > 1;
+ }
+
+ public boolean hasClonedOffers() {
+ for (OpenOffer openOffer : getOpenOffers()) {
+ if (getOpenOfferGroup(openOffer.getGroupId()).size() > 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public List getSignedOffers() {
- synchronized (signedOffers) {
- return new ArrayList<>(signedOffers.getObservableList());
+ synchronized (signedOffers.getList()) {
+ return ImmutableList.copyOf(signedOffers.getObservableList());
}
}
-
public ObservableList getObservableSignedOffersList() {
- synchronized (signedOffers) {
+ synchronized (signedOffers.getList()) {
return signedOffers.getObservableList();
}
}
@@ -797,43 +921,62 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return openOffers.getObservableList();
}
- public Optional getOpenOfferById(String offerId) {
- synchronized (openOffers) {
- return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst();
- }
+ public Optional getOpenOffer(String offerId) {
+ return getOpenOffers().stream().filter(e -> e.getId().equals(offerId)).findFirst();
}
public boolean hasOpenOffer(String offerId) {
- return getOpenOfferById(offerId).isPresent();
+ return getOpenOffer(offerId).isPresent();
}
public Optional getSignedOfferById(String offerId) {
- synchronized (signedOffers) {
- return signedOffers.stream().filter(e -> e.getOfferId().equals(offerId)).findFirst();
- }
+ return getSignedOffers().stream().filter(e -> e.getOfferId().equals(offerId)).findFirst();
}
private void addOpenOffer(OpenOffer openOffer) {
log.info("Adding open offer {}", openOffer.getId());
- synchronized (openOffers) {
+ synchronized (openOffers.getList()) {
openOffers.add(openOffer);
+ if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) {
+ xmrConnectionService.getKeyImagePoller().addKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID);
+ }
}
}
private void removeOpenOffer(OpenOffer openOffer) {
log.info("Removing open offer {}", openOffer.getId());
- synchronized (openOffers) {
+ synchronized (openOffers.getList()) {
openOffers.remove(openOffer);
+ if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null) {
+ xmrConnectionService.getKeyImagePoller().removeKeyImages(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages(), OPEN_OFFER_GROUP_KEY_IMAGE_ID);
+ }
}
- synchronized (placeOfferProtocols) {
- PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId());
- if (protocol != null) protocol.cancelOffer();
+
+ // cancel place offer protocol
+ ThreadUtils.execute(() -> {
+ synchronized (processOffersLock) {
+ synchronized (placeOfferProtocols) {
+ PlaceOfferProtocol protocol = placeOfferProtocols.remove(openOffer.getId());
+ if (protocol != null) protocol.cancelOffer();
+ }
+ }
+ }, THREAD_ID);
+ }
+
+ private void cancelOpenOffersOnSpent(String keyImage) {
+ synchronized (openOffers.getList()) {
+ for (OpenOffer openOffer : openOffers.getList()) {
+ if (openOffer.getState() != OpenOffer.State.RESERVED && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
+ log.warn("Canceling open offer because reserved funds have been spent unexpectedly, offerId={}, state={}", openOffer.getId(), openOffer.getState());
+ cancelOpenOffer(openOffer, null, null);
+ }
+ }
}
}
private void addSignedOffer(SignedOffer signedOffer) {
log.info("Adding SignedOffer for offer {}", signedOffer.getOfferId());
- synchronized (signedOffers) {
+ synchronized (signedOffers.getList()) {
// remove signed offers with common key images
for (String keyImage : signedOffer.getReserveTxKeyImages()) {
@@ -842,22 +985,24 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// add new signed offer
signedOffers.add(signedOffer);
- signedOfferKeyImagePoller.addKeyImages(signedOffer.getReserveTxKeyImages());
+ xmrConnectionService.getKeyImagePoller().addKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID);
}
}
private void removeSignedOffer(SignedOffer signedOffer) {
log.info("Removing SignedOffer for offer {}", signedOffer.getOfferId());
- synchronized (signedOffers) {
+ synchronized (signedOffers.getList()) {
signedOffers.remove(signedOffer);
- signedOfferKeyImagePoller.removeKeyImages(signedOffer.getReserveTxKeyImages());
}
+ xmrConnectionService.getKeyImagePoller().removeKeyImages(signedOffer.getReserveTxKeyImages(), SIGNED_OFFER_KEY_IMAGE_GROUP_ID);
}
private void removeSignedOffers(String keyImage) {
- for (SignedOffer signedOffer : new ArrayList(signedOffers.getList())) {
- if (signedOffer.getReserveTxKeyImages().contains(keyImage)) {
- removeSignedOffer(signedOffer);
+ synchronized (signedOffers.getList()) {
+ for (SignedOffer signedOffer : getSignedOffers()) {
+ if (signedOffer.getReserveTxKeyImages().contains(keyImage)) {
+ removeSignedOffer(signedOffer);
+ }
}
}
}
@@ -865,30 +1010,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
// Place offer helpers
///////////////////////////////////////////////////////////////////////////////////////////
- private void processPendingOffers(boolean skipOffersWithTooManyAttempts,
+ private void processOffers(boolean skipOffersWithTooManyAttempts,
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
ErrorMessageHandler errorMessageHandler) {
ThreadUtils.execute(() -> {
List errorMessages = new ArrayList();
synchronized (processOffersLock) {
List openOffers = getOpenOffers();
- for (OpenOffer pendingOffer : openOffers) {
- if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
- if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts
+ for (OpenOffer offer : openOffers) {
+ if (skipOffersWithTooManyAttempts && offer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts
CountDownLatch latch = new CountDownLatch(1);
- processPendingOffer(openOffers, pendingOffer, (transaction) -> {
+ processOffer(openOffers, offer, (transaction) -> {
latch.countDown();
}, errorMessage -> {
- if (!pendingOffer.isCanceled()) {
- String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage;
- errorMessages.add(warnMessage);
-
- // cancel offer if invalid
- if (pendingOffer.getOffer().getState() == Offer.State.INVALID) {
- log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId());
- doCancelOffer(pendingOffer);
- }
- }
+ errorMessages.add(errorMessage);
latch.countDown();
});
HavenoUtils.awaitLatch(latch);
@@ -903,7 +1038,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, THREAD_ID);
}
- private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
+ private void processOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// skip if already processing
if (openOffer.isProcessing()) {
@@ -913,32 +1048,87 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// process offer
openOffer.setProcessing(true);
- doProcessPendingOffer(openOffers, openOffer, (transaction) -> {
+ doProcessOffer(openOffers, openOffer, (transaction) -> {
openOffer.setProcessing(false);
resultHandler.handleResult(transaction);
}, (errorMsg) -> {
openOffer.setProcessing(false);
openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1);
openOffer.getOffer().setErrorMessage(errorMsg);
+ if (!openOffer.isCanceled()) {
+ errorMsg = "Error processing offer, offerId=" + openOffer.getId() + ", attempt=" + openOffer.getNumProcessingAttempts() + ": " + errorMsg;
+ openOffer.getOffer().setErrorMessage(errorMsg);
+
+ // cancel offer if invalid
+ if (openOffer.getOffer().getState() == Offer.State.INVALID) {
+ log.warn("Canceling offer because it's invalid: {}", openOffer.getId());
+ doCancelOffer(openOffer);
+ }
+ }
errorMessageHandler.handleErrorMessage(errorMsg);
});
}
- private void doProcessPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
+ private void doProcessOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
new Thread(() -> {
try {
- // done processing if wallet not initialized
- if (xmrWalletService.getWallet() == null) {
+ // done processing if canceled or wallet not initialized
+ if (openOffer.isCanceled() || xmrWalletService.getWallet() == null) {
resultHandler.handleResult(null);
return;
}
+ // validate offer
+ try {
+ ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user);
+ } catch (Exception e) {
+ errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage());
+ return;
+ }
+
+ // handle pending offer
+ if (openOffer.isPending()) {
+
+ // only process the first offer of a pending clone group
+ if (openOffer.getGroupId() != null) {
+ List openOfferClones = getOpenOfferGroup(openOffer.getGroupId());
+ if (openOfferClones.size() > 1 && !openOfferClones.get(0).getId().equals(openOffer.getId()) && openOfferClones.get(0).isPending()) {
+ resultHandler.handleResult(null);
+ return;
+ }
+ }
+ } else {
+
+ // validate non-pending state
+ boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
+ if (!skipValidation) {
+ try {
+ validateSignedState(openOffer);
+ resultHandler.handleResult(null); // done processing if non-pending state is valid
+ return;
+ } catch (Exception e) {
+ log.warn(e.getMessage());
+
+ // reset arbitrator signature
+ openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
+ openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
+ if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
+ }
+ }
+ }
+
+ // sign and post offer if already funded
+ if (openOffer.getReserveTxHash() != null) {
+ signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler);
+ return;
+ }
+
// cancel offer if scheduled txs unavailable
if (openOffer.getScheduledTxHashes() != null) {
boolean scheduledTxsAvailable = true;
for (MoneroTxWallet tx : xmrWalletService.getTxs(openOffer.getScheduledTxHashes())) {
- if (!tx.isLocked() && !isOutputsAvailable(tx)) {
+ if (!tx.isLocked() && !hasSpendableAmount(tx)) {
scheduledTxsAvailable = false;
break;
}
@@ -963,48 +1153,50 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
setSplitOutputTx(openOffer, splitOutputTx);
}
- // if not found, create tx to split exact output
- if (splitOutputTx == null) {
- if (openOffer.getSplitOutputTxHash() != null) {
- log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash());
- setSplitOutputTx(openOffer, null);
- }
- try {
- splitOrSchedule(openOffers, openOffer, amountNeeded);
- } catch (Exception e) {
- log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage());
- openOffer.getOffer().setState(Offer.State.INVALID);
- errorMessageHandler.handleErrorMessage(e.getMessage());
- return;
- }
- } else if (!splitOutputTx.isLocked()) {
-
- // otherwise sign and post offer if split output available
- signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
+ // if wallet has exact available balance, try to sign and post directly
+ if (xmrWalletService.getAvailableBalance().equals(amountNeeded)) {
+ signAndPostOffer(openOffer, true, resultHandler, (errorMessage) -> {
+ splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler);
+ });
return;
+ } else {
+ splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler);
}
} else {
// sign and post offer if enough funds
- boolean hasFundsReserved = openOffer.getReserveTxHash() != null;
boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0;
- if (hasFundsReserved || hasSufficientBalance) {
+ if (hasSufficientBalance) {
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
return;
} else if (openOffer.getScheduledTxHashes() == null) {
scheduleWithEarliestTxs(openOffers, openOffer);
+ resultHandler.handleResult(null);
+ return;
}
}
-
- // handle result
- resultHandler.handleResult(null);
} catch (Exception e) {
- if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e);
+ if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e);
errorMessageHandler.handleErrorMessage(e.getMessage());
}
}).start();
}
+ private void validateSignedState(OpenOffer openOffer) {
+ Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner());
+ if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) {
+ throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signer");
+ } else if (openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null) {
+ throw new IllegalArgumentException("Offer " + openOffer.getId() + " has no arbitrator signature");
+ } else if (arbitrator == null) {
+ throw new IllegalArgumentException("Offer " + openOffer.getId() + " signed by unavailable arbitrator");
+ } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) {
+ throw new IllegalArgumentException("Offer " + openOffer.getId() + " has invalid arbitrator signature");
+ } else if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty() || openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty()) {
+ throw new IllegalArgumentException("Offer " + openOffer.getId() + " is missing reserve tx hash or key images");
+ }
+ }
+
private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer) {
XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex());
@@ -1063,13 +1255,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (output.isSpent() || output.isFrozen()) removeTxs.add(tx);
}
}
- if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx);
+ if (!hasExactOutput(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx);
}
splitOutputTxs.removeAll(removeTxs);
return splitOutputTxs;
}
- private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) {
+ private boolean hasExactOutput(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) {
boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery()
.setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex)
@@ -1091,7 +1283,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return earliestUnscheduledTx;
}
- private void splitOrSchedule(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) {
+ // if split tx not found and cannot reserve exact amount directly, create tx to split or reserve exact output
+ private void splitOrSchedule(MoneroTxWallet splitOutputTx, List openOffers, OpenOffer openOffer, BigInteger amountNeeded, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
+ if (splitOutputTx == null) {
+ if (openOffer.getSplitOutputTxHash() != null) {
+ log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash());
+ setSplitOutputTx(openOffer, null);
+ }
+ try {
+ splitOrScheduleAux(openOffers, openOffer, amountNeeded);
+ resultHandler.handleResult(null);
+ return;
+ } catch (Exception e) {
+ log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage());
+ openOffer.getOffer().setState(Offer.State.INVALID);
+ errorMessageHandler.handleErrorMessage(e.getMessage());
+ return;
+ }
+ } else if (!splitOutputTx.isLocked()) {
+
+ // otherwise sign and post offer if split output available
+ signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
+ return;
+ } else {
+ resultHandler.handleResult(null);
+ return;
+ }
+ }
+
+ private void splitOrScheduleAux(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) {
// handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getAvailableBalance().compareTo(offerReserveAmount) >= 0;
@@ -1149,34 +1369,26 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) {
- // check for sufficient balance - scheduled offers amount
+ // get earliest available or pending txs with sufficient spendable amount
BigInteger offerReserveAmount = openOffer.getOffer().getAmountNeeded();
- if (xmrWalletService.getBalance().subtract(getScheduledAmount(openOffers)).compareTo(offerReserveAmount) < 0) {
- throw new RuntimeException("Not enough money in Haveno wallet");
- }
-
- // get earliest available or pending txs with sufficient incoming amount
BigInteger scheduledAmount = BigInteger.ZERO;
Set scheduledTxs = new HashSet();
for (MoneroTxWallet tx : xmrWalletService.getTxs()) {
- // skip if outputs unavailable
- if (tx.getIncomingTransfers() == null || tx.getIncomingTransfers().isEmpty()) continue;
- if (!isOutputsAvailable(tx)) continue;
- if (isTxScheduledByOtherOffer(openOffers, openOffer, tx.getHash())) continue;
+ // get unscheduled spendable amount
+ BigInteger spendableAmount = getUnscheduledSpendableAmount(tx, openOffers);
- // add scheduled tx
- for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) {
- if (transfer.getAccountIndex() == 0) {
- scheduledAmount = scheduledAmount.add(transfer.getAmount());
- scheduledTxs.add(tx);
- }
- }
+ // skip if no spendable amount
+ if (spendableAmount.equals(BigInteger.ZERO)) continue;
+
+ // schedule tx
+ scheduledAmount = scheduledAmount.add(spendableAmount);
+ scheduledTxs.add(tx);
// break if sufficient funds
if (scheduledAmount.compareTo(offerReserveAmount) >= 0) break;
}
- if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to schedule offer");
+ if (scheduledAmount.compareTo(offerReserveAmount) < 0) throw new RuntimeException("Not enough funds to create offer");
// schedule txs
openOffer.setScheduledTxHashes(scheduledTxs.stream().map(tx -> tx.getHash()).collect(Collectors.toList()));
@@ -1184,21 +1396,56 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setState(OpenOffer.State.PENDING);
}
- private BigInteger getScheduledAmount(List openOffers) {
- BigInteger scheduledAmount = BigInteger.ZERO;
+ private BigInteger getUnscheduledSpendableAmount(MoneroTxWallet tx, List openOffers) {
+ if (isScheduledWithUnknownAmount(tx, openOffers)) return BigInteger.ZERO;
+ return getSpendableAmount(tx).subtract(getSplitAmount(tx, openOffers)).max(BigInteger.ZERO);
+ }
+
+ private boolean isScheduledWithUnknownAmount(MoneroTxWallet tx, List openOffers) {
for (OpenOffer openOffer : openOffers) {
- if (openOffer.getState() != OpenOffer.State.PENDING) continue;
if (openOffer.getScheduledTxHashes() == null) continue;
- List fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes());
- for (MoneroTxWallet fundingTx : fundingTxs) {
- if (fundingTx.getIncomingTransfers() != null) {
- for (MoneroIncomingTransfer transfer : fundingTx.getIncomingTransfers()) {
- if (transfer.getAccountIndex() == 0) scheduledAmount = scheduledAmount.add(transfer.getAmount());
+ if (openOffer.getScheduledTxHashes().contains(tx.getHash()) && !tx.getHash().equals(openOffer.getSplitOutputTxHash())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private BigInteger getSplitAmount(MoneroTxWallet tx, List openOffers) {
+ for (OpenOffer openOffer : openOffers) {
+ if (openOffer.getSplitOutputTxHash() == null) continue;
+ if (!openOffer.getSplitOutputTxHash().equals(tx.getHash())) continue;
+ return openOffer.getOffer().getAmountNeeded();
+ }
+ return BigInteger.ZERO;
+ }
+
+ private BigInteger getSpendableAmount(MoneroTxWallet tx) {
+
+ // compute spendable amount from outputs if confirmed
+ if (tx.isConfirmed()) {
+ BigInteger spendableAmount = BigInteger.ZERO;
+ if (tx.getOutputsWallet() != null) {
+ for (MoneroOutputWallet output : tx.getOutputsWallet()) {
+ if (!output.isSpent() && !output.isFrozen() && output.getAccountIndex() == 0) {
+ spendableAmount = spendableAmount.add(output.getAmount());
}
}
}
+ return spendableAmount;
}
- return scheduledAmount;
+
+ // funds sent to self always show 0 incoming amount, so compute from destinations manually
+ // TODO: this excludes change output, so change is missing from spendable amount until confirmed
+ BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx);
+ if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) return sentToSelfAmount;
+
+ // if not confirmed and not sent to self, return incoming amount
+ return tx.getIncomingAmount() == null ? BigInteger.ZERO : tx.getIncomingAmount();
+ }
+
+ private boolean hasSpendableAmount(MoneroTxWallet tx) {
+ return getSpendableAmount(tx).compareTo(BigInteger.ZERO) > 0;
}
private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) {
@@ -1215,17 +1462,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return false;
}
- private boolean isOutputsAvailable(MoneroTxWallet tx) {
- if (tx.getOutputsWallet() == null) return false;
- for (MoneroOutputWallet output : tx.getOutputsWallet()) {
- if (output.isSpent() || output.isFrozen()) return false;
- }
- return true;
- }
-
private void signAndPostOffer(OpenOffer openOffer,
boolean useSavingsWallet, // TODO: remove this?
- TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
+ TransactionResultHandler resultHandler,
+ ErrorMessageHandler errorMessageHandler) {
log.info("Signing and posting offer " + openOffer.getId());
// create model
@@ -1251,18 +1491,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
transaction -> {
// set offer state
- openOffer.setState(OpenOffer.State.AVAILABLE);
openOffer.setScheduledTxHashes(null);
openOffer.setScheduledAmount(null);
requestPersistence();
- resultHandler.handleResult(transaction);
if (!stopped) {
startPeriodicRepublishOffersTimer();
startPeriodicRefreshOffersTimer();
} else {
log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call.");
}
+ resultHandler.handleResult(transaction);
},
errorMessageHandler);
@@ -1290,7 +1529,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress();
if (thisArbitrator == null || !thisArbitrator.getNodeAddress().equals(thisAddress)) {
errorMessage = "Cannot sign offer because we are not a registered arbitrator";
- log.info(errorMessage);
+ log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
@@ -1298,47 +1537,135 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// verify arbitrator is signer of offer payload
if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) {
errorMessage = "Cannot sign offer because offer payload is for a different arbitrator";
- log.info(errorMessage);
+ log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
- // verify maker's trade fee
+ // private offers must have challenge hash
Offer offer = new Offer(request.getOfferPayload());
- if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) {
- errorMessage = "Wrong maker fee for offer " + request.offerId;
- log.info(errorMessage);
+ if (offer.isPrivateOffer() && (offer.getChallengeHash() == null || offer.getChallengeHash().length() == 0)) {
+ errorMessage = "Private offer must have challenge hash for offer " + request.offerId;
+ log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
- // verify taker's trade fee
- if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) {
- errorMessage = "Wrong taker fee for offer " + request.offerId;
- log.info(errorMessage);
+ // verify max length of extra info
+ if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
+ errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length();
+ log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
+ // verify the trade protocol version
+ if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) {
+ errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify the min version number
+ if (filterManager.getDisableTradeBelowVersion() != null) {
+ if (Version.compare(request.getOfferPayload().getVersionNr(), filterManager.getDisableTradeBelowVersion()) < 0) {
+ errorMessage = "Offer version number is too low: " + request.getOfferPayload().getVersionNr() + " < " + filterManager.getDisableTradeBelowVersion();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+ }
+
+ // verify maker and taker fees
+ boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
+ if (hasBuyerAsTakerWithoutDeposit) {
+
+ // verify maker's trade fee
+ if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT) {
+ errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT + " but got " + offer.getMakerFeePct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify taker's trade fee
+ if (offer.getTakerFeePct() != 0) {
+ errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected 0 but got " + offer.getTakerFeePct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify maker security deposit
+ if (offer.getSellerSecurityDepositPct() != Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
+ errorMessage = "Wrong seller security deposit for offer " + request.offerId + ". Expected " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify taker's security deposit
+ if (offer.getBuyerSecurityDepositPct() != 0) {
+ errorMessage = "Wrong buyer security deposit for offer " + request.offerId + ". Expected 0 but got " + offer.getBuyerSecurityDepositPct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+ } else {
+
+ // verify maker's trade fee
+ if (offer.getMakerFeePct() != HavenoUtils.MAKER_FEE_PCT) {
+ errorMessage = "Wrong maker fee for offer " + request.offerId + ". Expected " + HavenoUtils.MAKER_FEE_PCT + " but got " + offer.getMakerFeePct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify taker's trade fee
+ if (offer.getTakerFeePct() != HavenoUtils.TAKER_FEE_PCT) {
+ errorMessage = "Wrong taker fee for offer " + request.offerId + ". Expected " + HavenoUtils.TAKER_FEE_PCT + " but got " + offer.getTakerFeePct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify seller's security deposit
+ if (offer.getSellerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
+ errorMessage = "Insufficient seller security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getSellerSecurityDepositPct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // verify buyer's security deposit
+ if (offer.getBuyerSecurityDepositPct() < Restrictions.MIN_SECURITY_DEPOSIT_PCT) {
+ errorMessage = "Insufficient buyer security deposit for offer " + request.offerId + ". Expected at least " + Restrictions.MIN_SECURITY_DEPOSIT_PCT + " but got " + offer.getBuyerSecurityDepositPct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
+ // security deposits must be equal
+ if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) {
+ errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId + ": " + offer.getSellerSecurityDepositPct() + " vs " + offer.getBuyerSecurityDepositPct();
+ log.warn(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+ }
+
// verify penalty fee
if (offer.getPenaltyFeePct() != HavenoUtils.PENALTY_FEE_PCT) {
errorMessage = "Wrong penalty fee for offer " + request.offerId;
- log.info(errorMessage);
- sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
- return;
- }
-
- // verify security deposits are equal
- if (offer.getBuyerSecurityDepositPct() != offer.getSellerSecurityDepositPct()) {
- errorMessage = "Buyer and seller security deposits are not equal for offer " + request.offerId;
- log.info(errorMessage);
+ log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.PENALTY_FEE_PCT);
- BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), HavenoUtils.MAKER_FEE_PCT);
+ BigInteger maxTradeFee = HavenoUtils.multiply(offer.getAmount(), hasBuyerAsTakerWithoutDeposit ? HavenoUtils.MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT : HavenoUtils.MAKER_FEE_PCT);
BigInteger sendTradeAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
MoneroTx verifiedTx = xmrWalletService.verifyReserveTx(
@@ -1452,6 +1779,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
+ // Don't allow trade start if not connected to Monero node
+ if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) {
+ errorMessage = "We got a handleOfferAvailabilityRequest but we are not connected to a Monero node.";
+ log.info(errorMessage);
+ sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
+ return;
+ }
+
if (stopped) {
errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call.";
log.debug(errorMessage);
@@ -1470,7 +1805,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
try {
- Optional openOfferOptional = getOpenOfferById(request.offerId);
+ Optional openOfferOptional = getOpenOffer(request.offerId);
AvailabilityResult availabilityResult;
byte[] makerSignature = null;
if (openOfferOptional.isPresent()) {
@@ -1612,7 +1947,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
private void maybeUpdatePersistedOffers() {
- // We need to clone to avoid ConcurrentModificationException
List openOffersClone = getOpenOffers();
openOffersClone.forEach(originalOpenOffer -> {
Offer originalOffer = originalOpenOffer.getOffer();
@@ -1684,7 +2018,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.getAcceptedCountryCodes(),
originalOfferPayload.getBankId(),
originalOfferPayload.getAcceptedBankIds(),
- originalOfferPayload.getVersionNr(),
+ Version.VERSION,
originalOfferPayload.getBlockHeightAtOfferCreation(),
originalOfferPayload.getMaxTradeLimit(),
originalOfferPayload.getMaxTradePeriod(),
@@ -1693,29 +2027,23 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.getLowerClosePrice(),
originalOfferPayload.getUpperClosePrice(),
originalOfferPayload.isPrivateOffer(),
- originalOfferPayload.getHashOfChallenge(),
+ originalOfferPayload.getChallengeHash(),
updatedExtraDataMap,
protocolVersion,
- originalOfferPayload.getArbitratorSigner(),
- originalOfferPayload.getArbitratorSignature(),
- originalOfferPayload.getReserveTxKeyImages());
+ null,
+ null,
+ null,
+ originalOfferPayload.getExtraInfo());
- // Save states from original data to use for the updated
- Offer.State originalOfferState = originalOffer.getState();
- OpenOffer.State originalOpenOfferState = originalOpenOffer.getState();
+ // cancel old offer
+ log.info("Canceling outdated offer id={}", originalOffer.getId());
+ doCancelOffer(originalOpenOffer, false);
- // remove old offer
- originalOffer.setState(Offer.State.REMOVED);
- originalOpenOffer.setState(OpenOffer.State.CANCELED);
- removeOpenOffer(originalOpenOffer);
-
- // Create new Offer
+ // create new offer
Offer updatedOffer = new Offer(updatedPayload);
updatedOffer.setPriceFeedService(priceFeedService);
- updatedOffer.setState(originalOfferState);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice());
- updatedOpenOffer.setState(originalOpenOfferState);
addOpenOffer(updatedOpenOffer);
requestPersistence();
@@ -1737,10 +2065,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
stopPeriodicRefreshOffersTimer();
ThreadUtils.execute(() -> {
- processListForRepublishOffers(getOpenOffers());
+ processListForRepublishOffers(new ArrayList<>(getOpenOffers())); // list will be modified
}, THREAD_ID);
}
+ // modifies the given list
private void processListForRepublishOffers(List list) {
if (list.isEmpty()) {
return;
@@ -1748,7 +2077,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
OpenOffer openOffer = list.remove(0);
boolean contained = false;
- synchronized (openOffers) {
+ synchronized (openOffers.getList()) {
contained = openOffers.contains(openOffer);
}
if (contained) {
@@ -1767,10 +2096,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
}
- private void maybeRepublishOffer(OpenOffer openOffer) {
- maybeRepublishOffer(openOffer, null);
- }
-
private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
ThreadUtils.execute(() -> {
@@ -1780,82 +2105,58 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
- // determine if offer is valid
- boolean isValid = true;
- Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner());
- if (arbitrator == null) {
- log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId());
- isValid = false;
- } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) {
- log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId());
- isValid = false;
- }
- if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) {
- log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId());
- isValid = false;
- }
+ // reprocess offer then publish
+ synchronized (processOffersLock) {
+ CountDownLatch latch = new CountDownLatch(1);
+ processOffer(getOpenOffers(), openOffer, (transaction) -> {
+ requestPersistence();
+ latch.countDown();
- // if valid, re-add offer to book
- if (isValid) {
- offerBookService.addOffer(openOffer.getOffer(),
- () -> {
- if (!stopped) {
-
- // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.)
- if (periodicRefreshOffersTimer == null) {
- startPeriodicRefreshOffersTimer();
- }
- if (completeHandler != null) {
- completeHandler.run();
- }
- }
- },
- errorMessage -> {
- if (!stopped) {
- log.error("Adding offer to P2P network failed. " + errorMessage);
- stopRetryRepublishOffersTimer();
- retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers,
- RETRY_REPUBLISH_DELAY_SEC);
- if (completeHandler != null) completeHandler.run();
- }
- });
- } else {
-
- // reset offer state to pending
- openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
- openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
- openOffer.getOffer().setState(Offer.State.UNKNOWN);
- openOffer.setState(OpenOffer.State.PENDING);
-
- // republish offer
- synchronized (processOffersLock) {
- CountDownLatch latch = new CountDownLatch(1);
- processPendingOffer(getOpenOffers(), openOffer, (transaction) -> {
- requestPersistence();
- latch.countDown();
+ // skip if prevented from publishing
+ if (preventedFromPublishing(openOffer)) {
if (completeHandler != null) completeHandler.run();
- }, (errorMessage) -> {
- if (!openOffer.isCanceled()) {
- log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage);
- openOffer.getOffer().setErrorMessage(errorMessage);
+ return;
+ }
+
+ // publish offer to books
+ offerBookService.addOffer(openOffer.getOffer(),
+ () -> {
+ if (!stopped) {
- // cancel offer if invalid
- if (openOffer.getOffer().getState() == Offer.State.INVALID) {
- log.warn("Canceling offer because it's invalid: {}", openOffer.getId());
- doCancelOffer(openOffer);
- }
- }
- latch.countDown();
- if (completeHandler != null) completeHandler.run();
- });
- HavenoUtils.awaitLatch(latch);
- }
+ // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.)
+ if (periodicRefreshOffersTimer == null) {
+ startPeriodicRefreshOffersTimer();
+ }
+ if (completeHandler != null) {
+ completeHandler.run();
+ }
+ }
+ },
+ errorMessage -> {
+ if (!stopped) {
+ log.error("Adding offer to P2P network failed. " + errorMessage);
+ stopRetryRepublishOffersTimer();
+ retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers,
+ RETRY_REPUBLISH_DELAY_SEC);
+ if (completeHandler != null) completeHandler.run();
+ }
+ });
+ }, (errorMessage) -> {
+ log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage);
+ latch.countDown();
+ if (completeHandler != null) completeHandler.run();
+ });
+ HavenoUtils.awaitLatch(latch);
}
}, THREAD_ID);
}
private boolean preventedFromPublishing(OpenOffer openOffer) {
- return openOffer.isDeactivated() || openOffer.isCanceled();
+ if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true;
+ return openOffer.isDeactivated() ||
+ openOffer.isCanceled() ||
+ openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null ||
+ hasConflictingClone(openOffer);
}
private void startPeriodicRepublishOffersTimer() {
@@ -1877,25 +2178,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (periodicRefreshOffersTimer == null)
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
if (!stopped) {
- int size = openOffers.size();
- //we clone our list as openOffers might change during our delayed call
- final ArrayList openOffersList = new ArrayList<>(openOffers.getList());
- for (int i = 0; i < size; i++) {
- // we delay to avoid reaching throttle limits
- // roughly 4 offers per second
-
- long delay = 300;
- final long minDelay = (i + 1) * delay;
- final long maxDelay = (i + 2) * delay;
- final OpenOffer openOffer = openOffersList.get(i);
- UserThread.runAfterRandomDelay(() -> {
- // we need to check if in the meantime the offer has been removed
- boolean contained = false;
- synchronized (openOffers) {
- contained = openOffers.contains(openOffer);
- }
- if (contained) maybeRefreshOffer(openOffer, 0, 1);
- }, minDelay, maxDelay, TimeUnit.MILLISECONDS);
+ synchronized (openOffers.getList()) {
+ int size = openOffers.size();
+ //we clone our list as openOffers might change during our delayed call
+ final ArrayList openOffersList = new ArrayList<>(openOffers.getList());
+ for (int i = 0; i < size; i++) {
+ // we delay to avoid reaching throttle limits
+ // roughly 4 offers per second
+
+ long delay = 300;
+ final long minDelay = (i + 1) * delay;
+ final long maxDelay = (i + 2) * delay;
+ final OpenOffer openOffer = openOffersList.get(i);
+ UserThread.runAfterRandomDelay(() -> {
+ // we need to check if in the meantime the offer has been removed
+ boolean contained = false;
+ synchronized (openOffers.getList()) {
+ contained = openOffers.contains(openOffer);
+ }
+ if (contained) maybeRefreshOffer(openOffer, 0, 1);
+ }, minDelay, maxDelay, TimeUnit.MILLISECONDS);
+ }
}
} else {
log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call.");
diff --git a/core/src/main/java/haveno/core/offer/SignedOfferList.java b/core/src/main/java/haveno/core/offer/SignedOfferList.java
index a77d0402d9..4d75a14dd4 100644
--- a/core/src/main/java/haveno/core/offer/SignedOfferList.java
+++ b/core/src/main/java/haveno/core/offer/SignedOfferList.java
@@ -47,10 +47,12 @@ public final class SignedOfferList extends PersistableListAsObservable openOffersByCurrency.containsKey(marketPrice.getCurrencyCode()))
.forEach(marketPrice -> {
openOffersByCurrency.get(marketPrice.getCurrencyCode()).stream()
- .filter(openOffer -> !openOffer.isDeactivated())
.forEach(openOffer -> checkPriceThreshold(marketPrice, openOffer));
});
}
- public static boolean wasTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
+ public static boolean isTriggered(MarketPrice marketPrice, OpenOffer openOffer) {
Price price = openOffer.getOffer().getPrice();
if (price == null || marketPrice == null) {
return false;
@@ -125,13 +124,12 @@ public class TriggerPriceService {
}
private void checkPriceThreshold(MarketPrice marketPrice, OpenOffer openOffer) {
- if (wasTriggered(marketPrice, openOffer)) {
- String currencyCode = openOffer.getOffer().getCurrencyCode();
- int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
- TraditionalMoney.SMALLEST_UNIT_EXPONENT :
- CryptoMoney.SMALLEST_UNIT_EXPONENT;
- long triggerPrice = openOffer.getTriggerPrice();
+ String currencyCode = openOffer.getOffer().getCurrencyCode();
+ int smallestUnitExponent = CurrencyUtil.isTraditionalCurrency(currencyCode) ?
+ TraditionalMoney.SMALLEST_UNIT_EXPONENT :
+ CryptoMoney.SMALLEST_UNIT_EXPONENT;
+ if (openOffer.getState() == OpenOffer.State.AVAILABLE && isTriggered(marketPrice, openOffer)) {
log.info("Market price exceeded the trigger price of the open offer.\n" +
"We deactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
"Market price: {};\nTrigger price: {}",
@@ -139,14 +137,26 @@ public class TriggerPriceService {
currencyCode,
openOffer.getOffer().getDirection(),
marketPrice.getPrice(),
- MathUtils.scaleDownByPowerOf10(triggerPrice, smallestUnitExponent)
+ MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
);
- openOfferManager.deactivateOpenOffer(openOffer, () -> {
+ openOfferManager.deactivateOpenOffer(openOffer, true, () -> {
+ }, errorMessage -> {
+ });
+ } else if (openOffer.getState() == OpenOffer.State.DEACTIVATED && openOffer.isDeactivatedByTrigger() && !isTriggered(marketPrice, openOffer)) {
+ log.info("Market price is back within the trigger price of the open offer.\n" +
+ "We reactivate the open offer with ID {}.\nCurrency: {};\nOffer direction: {};\n" +
+ "Market price: {};\nTrigger price: {}",
+ openOffer.getOffer().getShortId(),
+ currencyCode,
+ openOffer.getOffer().getDirection(),
+ marketPrice.getPrice(),
+ MathUtils.scaleDownByPowerOf10(openOffer.getTriggerPrice(), smallestUnitExponent)
+ );
+
+ openOfferManager.activateOpenOffer(openOffer, () -> {
}, errorMessage -> {
});
- } else if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
- // TODO: check if open offer's reserve tx is failed or double spend seen
}
}
diff --git a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java
index 493ded6759..d6080e61e3 100644
--- a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java
+++ b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java
@@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task {
null, // reserve tx not sent from taker to maker
null,
null,
- payoutAddress);
+ payoutAddress,
+ null); // challenge is required when offer taken
// save trade request to later send to arbitrator
model.setTradeRequest(tradeRequest);
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java
index 0b22d9e40e..f5def0a433 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java
@@ -23,7 +23,7 @@ import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.locale.Res;
import haveno.core.offer.messages.SignOfferResponse;
-import haveno.core.offer.placeoffer.tasks.AddToOfferBook;
+import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook;
import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse;
import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest;
@@ -31,6 +31,8 @@ import haveno.core.offer.placeoffer.tasks.ValidateOffer;
import haveno.core.trade.handlers.TransactionResultHandler;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.network.p2p.NodeAddress;
+
+import org.bitcoinj.core.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,8 +41,8 @@ public class PlaceOfferProtocol {
private final PlaceOfferModel model;
private Timer timeoutTimer;
- private final TransactionResultHandler resultHandler;
- private final ErrorMessageHandler errorMessageHandler;
+ private TransactionResultHandler resultHandler;
+ private ErrorMessageHandler errorMessageHandler;
private TaskRunner taskRunner;
@@ -89,7 +91,6 @@ public class PlaceOfferProtocol {
handleError("Offer was canceled: " + model.getOpenOffer().getOffer().getId()); // cancel is treated as error for callers to handle
}
- // TODO (woodser): switch to fluent
public void handleSignOfferResponse(SignOfferResponse response, NodeAddress sender) {
log.debug("handleSignOfferResponse() " + model.getOpenOffer().getOffer().getId());
model.setSignOfferResponse(response);
@@ -119,7 +120,7 @@ public class PlaceOfferProtocol {
() -> {
log.debug("sequence at handleSignOfferResponse completed");
stopTimeoutTimer();
- resultHandler.handleResult(model.getTransaction()); // TODO (woodser): XMR transaction instead
+ handleResult(model.getTransaction()); // TODO: use XMR transaction instead
},
(errorMessage) -> {
if (model.isOfferAddedToOfferBook()) {
@@ -135,27 +136,33 @@ public class PlaceOfferProtocol {
);
taskRunner.addTasks(
MakerProcessSignOfferResponse.class,
- AddToOfferBook.class
+ MaybeAddToOfferBook.class
);
taskRunner.run();
}
- public void startTimeoutTimer() {
+ public synchronized void startTimeoutTimer() {
+ if (resultHandler == null) return;
stopTimeoutTimer();
timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing"));
}, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
}
- private void stopTimeoutTimer() {
+ private synchronized void stopTimeoutTimer() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
- private void handleError(String errorMessage) {
+ private synchronized void handleResult(Transaction transaction) {
+ resultHandler.handleResult(transaction);
+ resetHandlers();
+ }
+
+ private synchronized void handleError(String errorMessage) {
if (timeoutTimer != null) {
taskRunner.cancel();
if (!model.getOpenOffer().isCanceled()) {
@@ -164,5 +171,11 @@ public class PlaceOfferProtocol {
stopTimeoutTimer();
errorMessageHandler.handleErrorMessage(errorMessage);
}
+ resetHandlers();
+ }
+
+ private synchronized void resetHandlers() {
+ resultHandler = null;
+ errorMessageHandler = null;
}
}
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java
index 4b44a8102d..60eaa64cdc 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java
@@ -19,7 +19,9 @@ package haveno.core.offer.placeoffer.tasks;
import java.math.BigInteger;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
@@ -33,6 +35,7 @@ import haveno.core.xmr.model.XmrAddressEntry;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput;
+import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
@@ -62,7 +65,6 @@ public class MakerReserveOfferFunds extends Task {
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
// create reserve tx
- MoneroTxWallet reserveTx = null;
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// reset protocol timeout
@@ -78,7 +80,14 @@ public class MakerReserveOfferFunds extends Task {
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
+ // copy address entries to clones
+ for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
+ if (offerClone.getId().equals(offer.getId())) continue; // skip self
+ model.getXmrWalletService().cloneAddressEntries(openOffer.getId(), offerClone.getId());
+ }
+
// attempt creating reserve tx
+ MoneroTxWallet reserveTx = null;
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
@@ -86,6 +95,9 @@ public class MakerReserveOfferFunds extends Task {
try {
//if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
+ } catch (IllegalStateException e) {
+ log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage());
+ throw e;
} catch (Exception e) {
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
model.getXmrWalletService().handleWalletError(e, sourceConnection);
@@ -116,11 +128,43 @@ public class MakerReserveOfferFunds extends Task {
List reservedKeyImages = new ArrayList();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
- // update offer state
- openOffer.setReserveTxHash(reserveTx.getHash());
- openOffer.setReserveTxHex(reserveTx.getFullHex());
- openOffer.setReserveTxKey(reserveTx.getKey());
- offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
+ // update offer state including clones
+ if (openOffer.getGroupId() == null) {
+ openOffer.setReserveTxHash(reserveTx.getHash());
+ openOffer.setReserveTxHex(reserveTx.getFullHex());
+ openOffer.setReserveTxKey(reserveTx.getKey());
+ offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
+ } else {
+ for (OpenOffer offerClone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
+ offerClone.setReserveTxHash(reserveTx.getHash());
+ offerClone.setReserveTxHex(reserveTx.getFullHex());
+ offerClone.setReserveTxKey(reserveTx.getKey());
+ offerClone.getOffer().getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
+ }
+ }
+
+ // reset offer funding address entries if unused
+ if (fundingEntry != null) {
+
+ // get reserve tx inputs
+ List inputs = model.getXmrWalletService().getOutputs(reservedKeyImages);
+
+ // collect subaddress indices of inputs
+ Set inputSubaddressIndices = new HashSet<>();
+ for (MoneroOutputWallet input : inputs) {
+ if (input.getAccountIndex() == 0) inputSubaddressIndices.add(input.getSubaddressIndex());
+ }
+
+ // swap funding address entries to available if unused
+ for (OpenOffer clone : model.getOpenOfferManager().getOpenOfferGroup(model.getOpenOffer().getGroupId())) {
+ XmrAddressEntry cloneFundingEntry = model.getXmrWalletService().getAddressEntry(clone.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
+ if (cloneFundingEntry != null && !inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) {
+ if (inputSubaddressIndices.contains(cloneFundingEntry.getSubaddressIndex())) {
+ model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
+ }
+ }
+ }
+ }
}
complete();
} catch (Throwable t) {
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java
index 2037b51d09..3644492735 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java
@@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task {
offer.getOfferPayload().getReserveTxKeyImages(),
returnAddress);
- // send request to least used arbitrators until success
+ // send request to random arbitrators until success
sendSignOfferRequests(request, () -> {
complete();
}, (errorMessage) -> {
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java
similarity index 50%
rename from core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java
rename to core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java
index c5c3cf4f46..2f8a10108b 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java
@@ -20,13 +20,14 @@ package haveno.core.offer.placeoffer.tasks;
import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.Offer;
+import haveno.core.offer.OpenOffer;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import static com.google.common.base.Preconditions.checkNotNull;
-public class AddToOfferBook extends Task {
+public class MaybeAddToOfferBook extends Task {
- public AddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) {
+ public MaybeAddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) {
super(taskHandler, model);
}
@@ -35,17 +36,32 @@ public class AddToOfferBook extends Task {
try {
runInterceptHook();
checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId());
- model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()),
- () -> {
- model.setOfferAddedToOfferBook(true);
- complete();
- },
- errorMessage -> {
- model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
- "Please check your network connection and try again.");
- failed(errorMessage);
- });
+ // deactivate if conflicting offer exists
+ if (model.getOpenOfferManager().hasConflictingClone(model.getOpenOffer())) {
+ model.getOpenOffer().setState(OpenOffer.State.DEACTIVATED);
+ model.setOfferAddedToOfferBook(false);
+ complete();
+ return;
+ }
+
+ // add to offer book and activate if pending or available
+ if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) {
+ model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()),
+ () -> {
+ model.getOpenOffer().setState(OpenOffer.State.AVAILABLE);
+ model.setOfferAddedToOfferBook(true);
+ complete();
+ },
+ errorMessage -> {
+ model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" +
+ "Please check your network connection and try again.");
+ failed(errorMessage);
+ });
+ } else {
+ complete();
+ return;
+ }
} catch (Throwable t) {
model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" +
"Error message:\n"
diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java
index 5f683bacf2..3b1a01beeb 100644
--- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java
+++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/ValidateOffer.java
@@ -19,10 +19,13 @@ package haveno.core.offer.placeoffer.tasks;
import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
+import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.offer.Offer;
+import haveno.core.offer.OfferDirection;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage;
+import haveno.core.user.User;
import org.bitcoinj.core.Coin;
import java.math.BigInteger;
@@ -41,55 +44,7 @@ public class ValidateOffer extends Task {
try {
runInterceptHook();
- // Coins
- checkBINotNullOrZero(offer.getAmount(), "Amount");
- checkBINotNullOrZero(offer.getMinAmount(), "MinAmount");
- //checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model
- checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
- if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
- if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
- if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
- if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
-
- // We remove those checks to be more flexible with future changes.
- /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
- "createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " +
- "MakerFee=" + offer.getMakerFee().toFriendlyString());*/
- /*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value,
- "buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " +
- "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
- checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value,
- "buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " +
- "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
- checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value,
- "sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " +
- "sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/
- /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
- "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
-
- long maxAmount = model.getAccountAgeWitnessService().getMyTradeLimit(model.getUser().getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection());
- checkArgument(offer.getAmount().longValueExact() <= maxAmount,
- "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR");
- checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
-
- checkNotNull(offer.getPrice(), "Price is null");
- if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(),
- "Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString());
-
- checkArgument(offer.getDate().getTime() > 0,
- "Date must not be 0. date=" + offer.getDate().toString());
-
- checkNotNull(offer.getCurrencyCode(), "Currency is null");
- checkNotNull(offer.getDirection(), "Direction is null");
- checkNotNull(offer.getId(), "Id is null");
- checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
- checkNotNull(offer.getMinAmount(), "MinAmount is null");
- checkNotNull(offer.getPrice(), "Price is null");
- checkNotNull(offer.getVersionNr(), "VersionNr is null");
- checkArgument(offer.getMaxTradePeriod() > 0,
- "maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod());
- // TODO check upper and lower bounds for fiat
- // TODO check rest of new parameters
+ validateOffer(offer, model.getAccountAgeWitnessService(), model.getUser());
complete();
} catch (Exception e) {
@@ -100,42 +55,108 @@ public class ValidateOffer extends Task {
}
}
- public static void checkBINotNullOrZero(BigInteger value, String name) {
+ public static void validateOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService, User user) {
+
+ // Coins
+ checkBINotNullOrZero(offer.getAmount(), "Amount");
+ checkBINotNullOrZero(offer.getMinAmount(), "MinAmount");
+ //checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model
+ checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
+ if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
+ if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
+ offer.isPrivateOffer();
+ if (offer.isPrivateOffer()) {
+ boolean isBuyerMaker = offer.getDirection() == OfferDirection.BUY;
+ if (isBuyerMaker) {
+ if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
+ if (offer.getSellerSecurityDepositPct() < 0) throw new IllegalArgumentException("Seller security deposit percent must be >= 0% but was " + offer.getSellerSecurityDepositPct());
+ } else {
+ if (offer.getBuyerSecurityDepositPct() < 0) throw new IllegalArgumentException("Buyer security deposit percent must be >= 0% but was " + offer.getBuyerSecurityDepositPct());
+ if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
+ }
+ } else {
+ if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
+ if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
+ }
+
+
+ // We remove those checks to be more flexible with future changes.
+ /*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
+ "createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " +
+ "MakerFee=" + offer.getMakerFee().toFriendlyString());*/
+ /*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value,
+ "buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " +
+ "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
+ checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value,
+ "buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " +
+ "buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
+ checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value,
+ "sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " +
+ "sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/
+ /*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
+ "MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
+
+ long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection(), offer.hasBuyerAsTakerWithoutDeposit());
+ checkArgument(offer.getAmount().longValueExact() <= maxAmount,
+ "Amount is larger than " + HavenoUtils.atomicUnitsToXmr(maxAmount) + " XMR");
+ checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
+
+ checkNotNull(offer.getPrice(), "Price is null");
+ if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(),
+ "Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString());
+
+ checkArgument(offer.getDate().getTime() > 0,
+ "Date must not be 0. date=" + offer.getDate().toString());
+
+ checkNotNull(offer.getCurrencyCode(), "Currency is null");
+ checkNotNull(offer.getDirection(), "Direction is null");
+ checkNotNull(offer.getId(), "Id is null");
+ checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
+ checkNotNull(offer.getMinAmount(), "MinAmount is null");
+ checkNotNull(offer.getPrice(), "Price is null");
+ checkNotNull(offer.getVersionNr(), "VersionNr is null");
+ checkArgument(offer.getMaxTradePeriod() > 0,
+ "maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod());
+ // TODO check upper and lower bounds for fiat
+ // TODO check rest of new parameters
+ }
+
+ private static void checkBINotNullOrZero(BigInteger value, String name) {
checkNotNull(value, name + " is null");
checkArgument(value.compareTo(BigInteger.ZERO) > 0,
name + " must be positive. " + name + "=" + value);
}
- public static void checkCoinNotNullOrZero(Coin value, String name) {
+ private static void checkCoinNotNullOrZero(Coin value, String name) {
checkNotNull(value, name + " is null");
checkArgument(value.isPositive(),
name + " must be positive. " + name + "=" + value.toFriendlyString());
}
- public static String nonEmptyStringOf(String value) {
+ private static String nonEmptyStringOf(String value) {
checkNotNull(value);
checkArgument(value.length() > 0);
return value;
}
- public static long nonNegativeLongOf(long value) {
+ private static long nonNegativeLongOf(long value) {
checkArgument(value >= 0);
return value;
}
- public static Coin nonZeroCoinOf(Coin value) {
+ private static Coin nonZeroCoinOf(Coin value) {
checkNotNull(value);
checkArgument(!value.isZero());
return value;
}
- public static Coin positiveCoinOf(Coin value) {
+ private static Coin positiveCoinOf(Coin value) {
checkNotNull(value);
checkArgument(value.isPositive());
return value;
}
- public static void checkTradeId(String tradeId, TradeMessage tradeMessage) {
+ private static void checkTradeId(String tradeId, TradeMessage tradeMessage) {
checkArgument(tradeId.equals(tradeMessage.getOfferId()));
}
}
diff --git a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java
index 85d0b99bcf..49c6da12e9 100644
--- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java
+++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java
@@ -148,7 +148,8 @@ public class TakeOfferModel implements Model {
private long getMaxTradeLimit() {
return accountAgeWitnessService.getMyTradeLimit(paymentAccount,
offer.getCurrencyCode(),
- offer.getMirroredDirection());
+ offer.getMirroredDirection(),
+ offer.hasBuyerAsTakerWithoutDeposit());
}
@NotNull
diff --git a/core/src/main/java/haveno/core/payment/AliPayAccount.java b/core/src/main/java/haveno/core/payment/AliPayAccount.java
index b6f93b364d..1bff92b5cd 100644
--- a/core/src/main/java/haveno/core/payment/AliPayAccount.java
+++ b/core/src/main/java/haveno/core/payment/AliPayAccount.java
@@ -31,7 +31,34 @@ import java.util.List;
@EqualsAndHashCode(callSuper = true)
public final class AliPayAccount extends PaymentAccount {
- public static final List SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
+ public static final List SUPPORTED_CURRENCIES = List.of(
+ new TraditionalCurrency("AED"),
+ new TraditionalCurrency("AUD"),
+ new TraditionalCurrency("CAD"),
+ new TraditionalCurrency("CHF"),
+ new TraditionalCurrency("CNY"),
+ new TraditionalCurrency("CZK"),
+ new TraditionalCurrency("DKK"),
+ new TraditionalCurrency("EUR"),
+ new TraditionalCurrency("GBP"),
+ new TraditionalCurrency("HKD"),
+ new TraditionalCurrency("IDR"),
+ new TraditionalCurrency("ILS"),
+ new TraditionalCurrency("JPY"),
+ new TraditionalCurrency("KRW"),
+ new TraditionalCurrency("LKR"),
+ new TraditionalCurrency("MUR"),
+ new TraditionalCurrency("MYR"),
+ new TraditionalCurrency("NOK"),
+ new TraditionalCurrency("NZD"),
+ new TraditionalCurrency("PHP"),
+ new TraditionalCurrency("RUB"),
+ new TraditionalCurrency("SEK"),
+ new TraditionalCurrency("SGD"),
+ new TraditionalCurrency("THB"),
+ new TraditionalCurrency("USD"),
+ new TraditionalCurrency("ZAR")
+ );
public AliPayAccount() {
super(PaymentMethod.ALI_PAY);
diff --git a/core/src/main/java/haveno/core/payment/F2FAccount.java b/core/src/main/java/haveno/core/payment/F2FAccount.java
index b75718ec68..a16b4daf6c 100644
--- a/core/src/main/java/haveno/core/payment/F2FAccount.java
+++ b/core/src/main/java/haveno/core/payment/F2FAccount.java
@@ -93,7 +93,7 @@ public final class F2FAccount extends CountryBasedPaymentAccount {
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setComponent(PaymentAccountFormField.Component.SELECT_ONE);
if (field.getId() == PaymentAccountFormField.FieldId.CITY) field.setLabel(Res.get("payment.f2f.city"));
if (field.getId() == PaymentAccountFormField.FieldId.CONTACT) field.setLabel(Res.get("payment.f2f.contact"));
- if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt"));
+ if (field.getId() == PaymentAccountFormField.FieldId.EXTRA_INFO) field.setLabel(Res.get("payment.shared.extraInfo.prompt.paymentAccount"));
return field;
}
}
diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java
index 6a293b3452..14dd88482b 100644
--- a/core/src/main/java/haveno/core/payment/PaymentAccount.java
+++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java
@@ -36,6 +36,7 @@ package haveno.core.payment;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
import haveno.common.proto.ProtoUtil;
import haveno.common.proto.persistable.PersistablePayload;
import haveno.common.util.Utilities;
@@ -139,6 +140,10 @@ public abstract class PaymentAccount implements PersistablePayload {
return getSingleTradeCurrency() == null || CurrencyUtil.isFiatCurrency(getSingleTradeCurrency().getCode()); // TODO: check if trade currencies contain fiat
}
+ public boolean isCryptoCurrency() {
+ return getSingleTradeCurrency() != null && CurrencyUtil.isCryptoCurrency(getSingleTradeCurrency().getCode());
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
@@ -337,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload {
// ---------------------------- SERIALIZATION -----------------------------
public String toJson() {
- Map jsonMap = new HashMap();
- if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class));
+ Gson gson = gsonBuilder.create();
+ Map jsonMap = new HashMap<>();
+
+ if (paymentAccountPayload != null) {
+ String payloadJson = paymentAccountPayload.toJson();
+ Map payloadMap = gson.fromJson(payloadJson, new TypeToken