mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-04-19 07:15:54 -04:00
Merge branch 'haveno-dex:master' into EVMaddresses
This commit is contained in:
commit
fac51914ee
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@ -26,22 +26,23 @@ jobs:
|
||||
cache: gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --stacktrace --scan
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: error-reports-${{ matrix.os }}
|
||||
path: ${{ github.workspace }}/desktop/build/reports
|
||||
- name: cache nodes dependencies
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
include-hidden-files: true
|
||||
name: cached-localnet
|
||||
path: .localnet
|
||||
overwrite: true
|
||||
- name: Install dependencies
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y rpm libfuse2 flatpak flatpak-builder appstream
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm libfuse2 flatpak flatpak-builder appstream
|
||||
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
- name: Install WiX Toolset
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
@ -99,41 +100,41 @@ jobs:
|
||||
shell: powershell
|
||||
|
||||
# win
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Windows artifacts"
|
||||
if: ${{ matrix.os == 'windows-latest'}}
|
||||
with:
|
||||
name: haveno-windows
|
||||
path: ${{ github.workspace }}/release-windows
|
||||
# macos
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "macOS artifacts"
|
||||
if: ${{ matrix.os == 'macos-13' }}
|
||||
with:
|
||||
name: haveno-macos
|
||||
path: ${{ github.workspace }}/release-macos
|
||||
# linux
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - deb artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-deb
|
||||
path: ${{ github.workspace }}/release-linux-deb
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - rpm artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-rpm
|
||||
path: ${{ github.workspace }}/release-linux-rpm
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - AppImage artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
name: haveno-linux-appimage
|
||||
path: ${{ github.workspace }}/release-linux-appimage
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: "Linux - flatpak artifact"
|
||||
if: ${{ matrix.os == 'ubuntu-22.04' }}
|
||||
with:
|
||||
|
2
.github/workflows/codacy-code-reporter.yml
vendored
2
.github/workflows/codacy-code-reporter.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
build:
|
||||
if: github.repository == 'haveno-dex/haveno'
|
||||
name: Publish coverage
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -18,7 +18,7 @@ on:
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -68,4 +68,4 @@ jobs:
|
||||
run: ./gradlew build --stacktrace -x test -x checkstyleMain -x checkstyleTest
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
2
.github/workflows/label.yml
vendored
2
.github/workflows/label.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
issueLabeled:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Bounty explanation
|
||||
uses: peter-evans/create-or-update-comment@v3
|
||||
|
@ -73,18 +73,9 @@ To incentivize development and reward contributors, we adopt a simple bounty sys
|
||||
|
||||
To bring Haveno to life, we need resources. If you have the possibility, please consider [becoming a sponsor](https://haveno.exchange/sponsors/) or donating to the project:
|
||||
|
||||
### Monero
|
||||
|
||||
<p>
|
||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_monero.png" alt="Donate Monero" width="115" height="115"><br>
|
||||
<code>42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F</code>
|
||||
</p>
|
||||
|
||||
If you are using a wallet that supports OpenAlias (like the 'official' CLI and GUI wallets), you can simply put `fund@haveno.exchange` as the "receiver" address.
|
||||
|
||||
### Bitcoin
|
||||
|
||||
<p>
|
||||
<img src="https://raw.githubusercontent.com/haveno-dex/haveno/master/media/donate_bitcoin.png" alt="Donate Bitcoin" width="115" height="115"><br>
|
||||
<code>1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ</code>
|
||||
</p>
|
||||
|
@ -43,7 +43,7 @@ import java.util.stream.Collectors;
|
||||
import static haveno.apitest.config.ApiTestConfig.BTC;
|
||||
import static haveno.apitest.config.ApiTestRateMeterInterceptorConfig.getTestRateMeterInterceptorConfig;
|
||||
import static haveno.cli.table.builder.TableType.BTC_BALANCE_TBL;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
|
||||
import static java.lang.String.format;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Arrays.stream;
|
||||
@ -157,8 +157,8 @@ public class MethodTest extends ApiTestCase {
|
||||
return haveno.core.payment.PaymentAccount.fromProto(paymentAccount, CORE_PROTO_RESOLVER);
|
||||
}
|
||||
|
||||
public static final Supplier<Double> defaultBuyerSecurityDepositPct = () -> {
|
||||
var defaultPct = BigDecimal.valueOf(getDefaultBuyerSecurityDepositAsPercent());
|
||||
public static final Supplier<Double> defaultSecurityDepositPct = () -> {
|
||||
var defaultPct = BigDecimal.valueOf(getDefaultSecurityDepositAsPercent());
|
||||
if (defaultPct.precision() != 2)
|
||||
throw new IllegalStateException(format(
|
||||
"Unexpected decimal precision, expected 2 but actual is %d%n."
|
||||
|
@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
|
||||
10000000L,
|
||||
10000000L,
|
||||
0.00,
|
||||
defaultBuyerSecurityDepositPct.get(),
|
||||
defaultSecurityDepositPct.get(),
|
||||
paymentAccountId,
|
||||
NO_TRIGGER_PRICE);
|
||||
};
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
|
@ -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));
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
|
@ -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 {}.",
|
||||
|
@ -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;
|
||||
|
@ -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}.
|
||||
* <p>
|
||||
* 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
|
||||
|
@ -19,9 +19,9 @@ package haveno.asset.tokens;
|
||||
|
||||
import haveno.asset.Erc20Token;
|
||||
|
||||
public class DaiStablecoin extends Erc20Token {
|
||||
public class DaiStablecoinERC20 extends Erc20Token {
|
||||
|
||||
public DaiStablecoin() {
|
||||
super("Dai Stablecoin", "DAI");
|
||||
public DaiStablecoinERC20() {
|
||||
super("Dai Stablecoin", "DAI-ERC20");
|
||||
}
|
||||
}
|
@ -9,4 +9,5 @@ haveno.asset.coins.Litecoin
|
||||
haveno.asset.coins.Monero
|
||||
haveno.asset.tokens.TetherUSDERC20
|
||||
haveno.asset.tokens.TetherUSDTRC20
|
||||
haveno.asset.tokens.USDCoinERC20
|
||||
haveno.asset.tokens.USDCoinERC20
|
||||
haveno.asset.tokens.DaiStablecoinERC20
|
40
build.gradle
40
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 = '700ec94f0f' // Tor browser version 14.0.3 and tor binary version: 0.4.8.13
|
||||
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/release5/monero-bins-haveno-linux-x86_64.tar.gz',
|
||||
'linux-x86_64-sha256' : '92003b6d9104e8fe3c4dff292b782ed9b82b157aaff95200fda35e5c3dcb733a',
|
||||
'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-aarch64.tar.gz',
|
||||
'linux-aarch64-sha256' : '18b069c6c474ce18efea261c875a4d54022520e888712b2a56524d9c92f133b1',
|
||||
'mac' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-mac.tar.gz',
|
||||
'mac-sha256' : 'd308352191cd5a9e5e3932ad15869e033e22e209de459f4fd6460b111377dae2',
|
||||
'windows' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-windows.zip',
|
||||
'windows-sha256' : '9c9e1994d4738e2a89ca28bef343bcad460ea6c06e0dd40de8278ab3033bd6c7'
|
||||
]
|
||||
|
||||
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.14-SNAPSHOT'
|
||||
version = '1.0.18-SNAPSHOT'
|
||||
|
||||
jar.manifest.attributes(
|
||||
"Implementation-Title": project.name,
|
||||
|
@ -81,7 +81,7 @@ public class OffersServiceRequest {
|
||||
.setUseMarketBasedPrice(useMarketBasedPrice)
|
||||
.setPrice(fixedPrice)
|
||||
.setMarketPriceMarginPct(marketPriceMarginPct)
|
||||
.setBuyerSecurityDepositPct(securityDepositPct)
|
||||
.setSecurityDepositPct(securityDepositPct)
|
||||
.setPaymentAccountId(paymentAcctId)
|
||||
.setTriggerPrice(triggerPrice)
|
||||
.build();
|
||||
|
@ -59,8 +59,10 @@ public class Capabilities {
|
||||
}
|
||||
|
||||
public Capabilities(Collection<Capability> 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<Capability> 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<Capability> 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<Integer> 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<Integer> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.14";
|
||||
public static final String VERSION = "1.0.18";
|
||||
|
||||
/**
|
||||
* Holds a list of the tagged resource files for optimizing the getData requests.
|
||||
|
@ -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<Boolean> updateXmrBinariesOpt =
|
||||
parser.accepts(UPDATE_XMR_BINARIES,
|
||||
"Update Monero binaries if applicable")
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class)
|
||||
.defaultsTo(true);
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> 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;
|
||||
|
@ -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();
|
||||
|
@ -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)) {
|
||||
|
@ -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) {
|
||||
@ -419,10 +419,13 @@ public class CoreApi {
|
||||
double marketPriceMargin,
|
||||
long amountAsLong,
|
||||
long minAmountAsLong,
|
||||
double buyerSecurityDeposit,
|
||||
double securityDepositPct,
|
||||
String triggerPriceAsString,
|
||||
boolean reserveExactAmount,
|
||||
String paymentAccountId,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
coreOffersService.postOffer(currencyCode,
|
||||
@ -432,10 +435,13 @@ public class CoreApi {
|
||||
marketPriceMargin,
|
||||
amountAsLong,
|
||||
minAmountAsLong,
|
||||
buyerSecurityDeposit,
|
||||
securityDepositPct,
|
||||
triggerPriceAsString,
|
||||
reserveExactAmount,
|
||||
paymentAccountId,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo,
|
||||
resultHandler,
|
||||
errorMessageHandler);
|
||||
}
|
||||
@ -448,8 +454,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 +467,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 +547,11 @@ public class CoreApi {
|
||||
public void takeOffer(String offerId,
|
||||
String paymentAccountId,
|
||||
long amountAsLong,
|
||||
String challenge,
|
||||
Consumer<Trade> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
Offer offer = coreOffersService.getOffer(offerId);
|
||||
offer.setChallenge(challenge);
|
||||
coreTradesService.takeOffer(offer, paymentAccountId, amountAsLong, resultHandler, errorMessageHandler);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -172,10 +172,13 @@ 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,
|
||||
Consumer<Offer> resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
coreWalletsService.verifyWalletsAreAvailable();
|
||||
@ -199,8 +202,11 @@ public class CoreOffersService {
|
||||
price,
|
||||
useMarketBasedPrice,
|
||||
exactMultiply(marketPriceMargin, 0.01),
|
||||
securityDeposit,
|
||||
paymentAccount);
|
||||
securityDepositPct,
|
||||
paymentAccount,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo);
|
||||
|
||||
verifyPaymentAccountIsValidForNewOffer(offer, paymentAccount);
|
||||
|
||||
@ -223,8 +229,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 +242,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) {
|
||||
|
@ -132,7 +132,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);
|
||||
|
@ -255,18 +255,17 @@ public final class XmrConnectionService {
|
||||
updatePolling();
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getBestAvailableConnection() {
|
||||
accountService.checkAccountOpen();
|
||||
List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>();
|
||||
addLocalNodeIfIgnored(ignoredConnections);
|
||||
return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
|
||||
public MoneroRpcConnection getBestConnection() {
|
||||
return getBestConnection(new ArrayList<MoneroRpcConnection>());
|
||||
}
|
||||
|
||||
private MoneroRpcConnection getBestAvailableConnection(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||
private MoneroRpcConnection getBestConnection(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||
accountService.checkAccountOpen();
|
||||
Set<MoneroRpcConnection> 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 void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||
@ -278,7 +277,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);
|
||||
}
|
||||
|
||||
@ -329,7 +328,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(() -> {
|
||||
@ -337,7 +336,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;
|
||||
}
|
||||
@ -545,7 +544,7 @@ public final class XmrConnectionService {
|
||||
if (isConnected) {
|
||||
setConnection(connection.getUri());
|
||||
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
MoneroRpcConnection bestConnection = getBestConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection); // switch to best connection
|
||||
}
|
||||
}
|
||||
@ -610,7 +609,7 @@ public final class XmrConnectionService {
|
||||
|
||||
// update connection
|
||||
if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) {
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
MoneroRpcConnection bestConnection = getBestConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection);
|
||||
}
|
||||
} else if (!isInitialized) {
|
||||
@ -654,8 +653,7 @@ public final class XmrConnectionService {
|
||||
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) {
|
||||
@ -726,8 +724,8 @@ public final class XmrConnectionService {
|
||||
|
||||
// poll daemon
|
||||
if (daemon == null) switchToBestConnection();
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
||||
try {
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
||||
lastInfo = daemon.getInfo();
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -754,6 +752,7 @@ 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
|
||||
}
|
||||
|
||||
|
@ -166,11 +166,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()) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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))
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -369,7 +369,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);
|
||||
@ -379,7 +379,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);
|
||||
|
@ -200,7 +200,9 @@ 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;
|
||||
@ -297,7 +299,7 @@ public class CurrencyUtil {
|
||||
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
|
||||
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
|
||||
}
|
||||
if (isCryptoCurrencyBase(currencyCode)) {
|
||||
if (isCryptoCurrencyCodeBase(currencyCode)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -326,10 +328,10 @@ public class CurrencyUtil {
|
||||
return isCryptoCurrency;
|
||||
}
|
||||
|
||||
private static boolean isCryptoCurrencyBase(String currencyCode) {
|
||||
private static boolean isCryptoCurrencyCodeBase(String currencyCode) {
|
||||
if (currencyCode == null) return false;
|
||||
currencyCode = currencyCode.toUpperCase();
|
||||
return currencyCode.equals("USDT") || currencyCode.equals("USDC");
|
||||
return currencyCode.equals("USDT") || currencyCode.equals("USDC") || currencyCode.equals("DAI");
|
||||
}
|
||||
|
||||
public static String getCurrencyCodeBase(String currencyCode) {
|
||||
@ -337,6 +339,7 @@ public class CurrencyUtil {
|
||||
currencyCode = currencyCode.toUpperCase();
|
||||
if (currencyCode.contains("USDT")) return "USDT";
|
||||
if (currencyCode.contains("USDC")) return "USDC";
|
||||
if (currencyCode.contains("DAI")) return "DAI";
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
|
@ -33,10 +33,8 @@ import haveno.core.provider.price.PriceFeedService;
|
||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.util.coin.CoinUtil;
|
||||
import haveno.core.xmr.wallet.Restrictions;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.P2PService;
|
||||
@ -102,9 +100,11 @@ public class CreateOfferService {
|
||||
Price fixedPrice,
|
||||
boolean useMarketBasedPrice,
|
||||
double marketPriceMargin,
|
||||
double securityDepositAsDouble,
|
||||
PaymentAccount paymentAccount) {
|
||||
|
||||
double securityDepositPct,
|
||||
PaymentAccount paymentAccount,
|
||||
boolean isPrivateOffer,
|
||||
boolean buyerAsTakerWithoutDeposit,
|
||||
String extraInfo) {
|
||||
log.info("create and get offer with offerId={}, " +
|
||||
"currencyCode={}, " +
|
||||
"direction={}, " +
|
||||
@ -113,7 +113,10 @@ public class CreateOfferService {
|
||||
"marketPriceMargin={}, " +
|
||||
"amount={}, " +
|
||||
"minAmount={}, " +
|
||||
"securityDeposit={}",
|
||||
"securityDepositPct={}, " +
|
||||
"isPrivateOffer={}, " +
|
||||
"buyerAsTakerWithoutDeposit={}, " +
|
||||
"extraInfo={}",
|
||||
offerId,
|
||||
currencyCode,
|
||||
direction,
|
||||
@ -122,7 +125,19 @@ public class CreateOfferService {
|
||||
marketPriceMargin,
|
||||
amount,
|
||||
minAmount,
|
||||
securityDepositAsDouble);
|
||||
securityDepositPct,
|
||||
isPrivateOffer,
|
||||
buyerAsTakerWithoutDeposit,
|
||||
extraInfo);
|
||||
|
||||
// must nullify empty string so contracts match
|
||||
if ("".equals(extraInfo)) extraInfo = null;
|
||||
|
||||
// verify buyer as taker security deposit
|
||||
boolean isBuyerMaker = offerUtil.isBuyOffer(direction);
|
||||
if (!isBuyerMaker && !isPrivateOffer && buyerAsTakerWithoutDeposit) {
|
||||
throw new IllegalArgumentException("Buyer as taker deposit is required for public offers");
|
||||
}
|
||||
|
||||
// verify fixed price xor market price with margin
|
||||
if (fixedPrice != null) {
|
||||
@ -130,25 +145,29 @@ public class CreateOfferService {
|
||||
if (marketPriceMargin != 0) throw new IllegalArgumentException("Cannot set market price margin with fixed price");
|
||||
}
|
||||
|
||||
long creationTime = new Date().getTime();
|
||||
NodeAddress makerAddress = p2PService.getAddress();
|
||||
// verify price
|
||||
boolean useMarketBasedPriceValue = fixedPrice == null &&
|
||||
useMarketBasedPrice &&
|
||||
isMarketPriceAvailable(currencyCode) &&
|
||||
!PaymentMethod.isFixedPriceOnly(paymentAccount.getPaymentMethod().getId());
|
||||
|
||||
// verify price
|
||||
if (fixedPrice == null && !useMarketBasedPriceValue) {
|
||||
throw new IllegalArgumentException("Must provide fixed price");
|
||||
}
|
||||
|
||||
// adjust amount and min amount for fixed-price offer
|
||||
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
|
||||
if (fixedPrice != null) {
|
||||
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, maxTradeLimit, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
// adjust amount and min amount
|
||||
amount = CoinUtil.getRoundedAmount(amount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
minAmount = CoinUtil.getRoundedAmount(minAmount, fixedPrice, null, currencyCode, paymentAccount.getPaymentMethod().getId());
|
||||
|
||||
// generate one-time challenge for private offer
|
||||
String challenge = null;
|
||||
String challengeHash = null;
|
||||
if (isPrivateOffer) {
|
||||
challenge = HavenoUtils.generateChallenge();
|
||||
challengeHash = HavenoUtils.getChallengeHash(challenge);
|
||||
}
|
||||
|
||||
long creationTime = new Date().getTime();
|
||||
NodeAddress makerAddress = p2PService.getAddress();
|
||||
long priceAsLong = fixedPrice != null ? fixedPrice.getValue() : 0L;
|
||||
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
|
||||
long amountAsLong = amount != null ? amount.longValueExact() : 0L;
|
||||
@ -161,21 +180,16 @@ public class CreateOfferService {
|
||||
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
|
||||
List<String> 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<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
|
||||
currencyCode,
|
||||
direction);
|
||||
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount, currencyCode, direction);
|
||||
|
||||
offerUtil.validateOfferData(
|
||||
securityDepositAsDouble,
|
||||
securityDepositPct,
|
||||
paymentAccount,
|
||||
currencyCode);
|
||||
|
||||
@ -189,11 +203,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,46 +225,19 @@ 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) {
|
||||
|
||||
BigInteger reservedFundsForOffer = getSecurityDeposit(direction,
|
||||
amount,
|
||||
buyerSecurityDeposit,
|
||||
sellerSecurityDeposit);
|
||||
if (!offerUtil.isBuyOffer(direction))
|
||||
reservedFundsForOffer = reservedFundsForOffer.add(amount);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -259,26 +246,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);
|
||||
}
|
||||
}
|
||||
|
@ -115,6 +115,12 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
@Setter
|
||||
transient private boolean isReservedFundsSpent;
|
||||
|
||||
@JsonExclude
|
||||
@Getter
|
||||
@Setter
|
||||
@Nullable
|
||||
transient private String challenge;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -337,6 +343,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 +421,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 +448,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 "";
|
||||
}
|
||||
|
@ -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={}, ",
|
||||
|
@ -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<String, String> extraDataMap,
|
||||
int protocolVersion,
|
||||
@Nullable NodeAddress arbitratorSigner,
|
||||
@Nullable byte[] arbitratorSignature,
|
||||
@Nullable List<String> reserveTxKeyImages) {
|
||||
@Nullable List<String> 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,17 @@ 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);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -376,11 +387,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 +404,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
||||
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
|
||||
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
|
||||
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
||||
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||
null : proto.getExtraDataMap();
|
||||
|
||||
@ -428,12 +439,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 +487,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<OfferPayload> {
|
||||
@Override
|
||||
@ -519,6 +532,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;
|
||||
}
|
||||
}
|
||||
|
@ -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()),
|
||||
|
@ -96,6 +96,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 +110,9 @@ public final class OpenOffer implements Tradable {
|
||||
@Getter
|
||||
@Setter
|
||||
transient int numProcessingAttempts = 0;
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean deactivatedByTrigger;
|
||||
|
||||
public OpenOffer(Offer offer) {
|
||||
this(offer, 0, false);
|
||||
@ -120,6 +126,7 @@ public final class OpenOffer implements Tradable {
|
||||
this.offer = offer;
|
||||
this.triggerPrice = triggerPrice;
|
||||
this.reserveExactAmount = reserveExactAmount;
|
||||
this.challenge = offer.getChallenge();
|
||||
state = State.PENDING;
|
||||
}
|
||||
|
||||
@ -137,6 +144,8 @@ 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;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -153,7 +162,9 @@ 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) {
|
||||
this.offer = offer;
|
||||
this.state = state;
|
||||
this.triggerPrice = triggerPrice;
|
||||
@ -164,6 +175,8 @@ public final class OpenOffer implements Tradable {
|
||||
this.reserveTxHash = reserveTxHash;
|
||||
this.reserveTxHex = reserveTxHex;
|
||||
this.reserveTxKey = reserveTxKey;
|
||||
this.challenge = challenge;
|
||||
this.deactivatedByTrigger = deactivatedByTrigger;
|
||||
|
||||
// reset reserved state to available
|
||||
if (this.state == State.RESERVED) setState(State.AVAILABLE);
|
||||
@ -176,7 +189,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 +198,7 @@ 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));
|
||||
|
||||
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
|
||||
}
|
||||
@ -199,7 +214,9 @@ 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());
|
||||
return openOffer;
|
||||
}
|
||||
|
||||
@ -226,6 +243,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<State> stateProperty() {
|
||||
|
@ -79,6 +79,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;
|
||||
@ -114,7 +115,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;
|
||||
@ -595,6 +595,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
offerBookService.activateOffer(offer,
|
||||
() -> {
|
||||
openOffer.setState(OpenOffer.State.AVAILABLE);
|
||||
applyTriggerState(openOffer);
|
||||
requestPersistence();
|
||||
log.debug("activateOpenOffer, offerId={}", offer.getId());
|
||||
resultHandler.handleResult();
|
||||
@ -603,14 +604,22 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@ -661,6 +670,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
if (openOffer.isAvailable()) {
|
||||
deactivateOpenOffer(openOffer,
|
||||
false,
|
||||
resultHandler,
|
||||
errorMessage -> {
|
||||
offersToBeEdited.remove(openOffer.getId());
|
||||
@ -686,7 +696,12 @@ 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()) {
|
||||
editedOpenOffer.setState(OpenOffer.State.AVAILABLE);
|
||||
applyTriggerState(editedOpenOffer);
|
||||
} else {
|
||||
editedOpenOffer.setState(originalState);
|
||||
}
|
||||
|
||||
addOpenOffer(editedOpenOffer);
|
||||
|
||||
@ -947,7 +962,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
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;
|
||||
}
|
||||
@ -1158,42 +1173,26 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
private void scheduleWithEarliestTxs(List<OpenOffer> 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<MoneroTxWallet> scheduledTxs = new HashSet<MoneroTxWallet>();
|
||||
for (MoneroTxWallet tx : xmrWalletService.getTxs()) {
|
||||
|
||||
// skip if no funds available
|
||||
BigInteger sentToSelfAmount = xmrWalletService.getAmountSentToSelf(tx); // amount sent to self always shows 0, so compute from destinations manually
|
||||
if (sentToSelfAmount.equals(BigInteger.ZERO) && (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);
|
||||
|
||||
// schedule transaction if funds sent to self, because they are not included in incoming transfers // TODO: fix in libraries?
|
||||
if (sentToSelfAmount.compareTo(BigInteger.ZERO) > 0) {
|
||||
scheduledAmount = scheduledAmount.add(sentToSelfAmount);
|
||||
scheduledTxs.add(tx);
|
||||
} else if (tx.getIncomingTransfers() != null) {
|
||||
// skip if no spendable amount
|
||||
if (spendableAmount.equals(BigInteger.ZERO)) continue;
|
||||
|
||||
// schedule transaction if incoming tranfers to account 0
|
||||
for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) {
|
||||
if (transfer.getAccountIndex() == 0) {
|
||||
scheduledAmount = scheduledAmount.add(transfer.getAmount());
|
||||
scheduledTxs.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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()));
|
||||
@ -1201,21 +1200,56 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
openOffer.setState(OpenOffer.State.PENDING);
|
||||
}
|
||||
|
||||
private BigInteger getScheduledAmount(List<OpenOffer> openOffers) {
|
||||
BigInteger scheduledAmount = BigInteger.ZERO;
|
||||
private BigInteger getUnscheduledSpendableAmount(MoneroTxWallet tx, List<OpenOffer> openOffers) {
|
||||
if (isScheduledWithUnknownAmount(tx, openOffers)) return BigInteger.ZERO;
|
||||
return getSpendableAmount(tx).subtract(getSplitAmount(tx, openOffers)).max(BigInteger.ZERO);
|
||||
}
|
||||
|
||||
private boolean isScheduledWithUnknownAmount(MoneroTxWallet tx, List<OpenOffer> openOffers) {
|
||||
for (OpenOffer openOffer : openOffers) {
|
||||
if (openOffer.getState() != OpenOffer.State.PENDING) continue;
|
||||
if (openOffer.getScheduledTxHashes() == null) continue;
|
||||
List<MoneroTxWallet> 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<OpenOffer> 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<OpenOffer> openOffers, OpenOffer openOffer, String txHash) {
|
||||
@ -1232,14 +1266,6 @@ 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) {
|
||||
@ -1307,7 +1333,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;
|
||||
}
|
||||
@ -1315,47 +1341,109 @@ 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);
|
||||
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(
|
||||
@ -1710,12 +1798,13 @@ 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());
|
||||
originalOfferPayload.getReserveTxKeyImages(),
|
||||
originalOfferPayload.getExtraInfo());
|
||||
|
||||
// Save states from original data to use for the updated
|
||||
Offer.State originalOfferState = originalOffer.getState();
|
||||
|
@ -92,12 +92,11 @@ public class TriggerPriceService {
|
||||
.filter(marketPrice -> 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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,8 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
||||
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);
|
||||
|
@ -33,6 +33,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 +63,6 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
|
||||
|
||||
// create reserve tx
|
||||
MoneroTxWallet reserveTx = null;
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
|
||||
// reset protocol timeout
|
||||
@ -79,6 +79,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
|
||||
|
||||
// attempt creating reserve tx
|
||||
MoneroTxWallet reserveTx = null;
|
||||
try {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
@ -121,6 +122,19 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
openOffer.setReserveTxHex(reserveTx.getFullHex());
|
||||
openOffer.setReserveTxKey(reserveTx.getKey());
|
||||
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
||||
|
||||
// reset offer funding address entry if unused
|
||||
if (fundingEntry != null) {
|
||||
List<MoneroOutputWallet> inputs = model.getXmrWalletService().getOutputs(reservedKeyImages);
|
||||
boolean usesFundingEntry = false;
|
||||
for (MoneroOutputWallet input : inputs) {
|
||||
if (input.getAccountIndex() == 0 && input.getSubaddressIndex() == fundingEntry.getSubaddressIndex()) {
|
||||
usesFundingEntry = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!usesFundingEntry) model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
}
|
||||
}
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
|
@ -21,6 +21,7 @@ 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;
|
||||
@ -63,8 +64,21 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
||||
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());
|
||||
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,
|
||||
@ -82,9 +96,9 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
||||
/*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());
|
||||
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(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR");
|
||||
"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");
|
||||
|
@ -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
|
||||
|
@ -31,7 +31,34 @@ import java.util.List;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class AliPayAccount extends PaymentAccount {
|
||||
|
||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
|
||||
public static final List<TradeCurrency> 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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -378,6 +378,7 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||
@NonNull
|
||||
public abstract List<PaymentAccountFormField.FieldId> getInputFieldIds();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public PaymentAccountForm toForm() {
|
||||
|
||||
// convert to json map
|
||||
@ -387,7 +388,12 @@ public abstract class PaymentAccount implements PersistablePayload {
|
||||
PaymentAccountForm form = new PaymentAccountForm(PaymentAccountForm.FormId.valueOf(paymentMethod.getId()));
|
||||
for (PaymentAccountFormField.FieldId fieldId : getInputFieldIds()) {
|
||||
PaymentAccountFormField field = getEmptyFormField(fieldId);
|
||||
field.setValue((String) jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString())));
|
||||
Object value = jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString()));
|
||||
if (value instanceof List) { // TODO: list should already be serialized to comma delimited string in PaymentAccount.toJson() (PaymentAccountTypeAdapter?)
|
||||
field.setValue(String.join(",", (List<String>) value));
|
||||
} else {
|
||||
field.setValue((String) value);
|
||||
}
|
||||
form.getFields().add(field);
|
||||
}
|
||||
return form;
|
||||
|
@ -136,6 +136,8 @@ public class PaymentAccountFactory {
|
||||
return new CashAppAccount();
|
||||
case PaymentMethod.VENMO_ID:
|
||||
return new VenmoAccount();
|
||||
case PaymentMethod.PAYSAFE_ID:
|
||||
return new PaysafeAccount();
|
||||
|
||||
// Cannot be deleted as it would break old trade history entries
|
||||
case PaymentMethod.OK_PAY_ID:
|
||||
|
@ -24,7 +24,6 @@ import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import haveno.core.locale.Country;
|
||||
import haveno.core.locale.CountryUtil;
|
||||
import haveno.core.locale.TraditionalCurrency;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.locale.TradeCurrency;
|
||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||
@ -42,7 +41,6 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static haveno.common.util.ReflectionUtils.getSetterMethodForFieldInClassHierarchy;
|
||||
import static haveno.common.util.ReflectionUtils.getVisibilityModifierAsString;
|
||||
import static haveno.common.util.ReflectionUtils.handleSetFieldValueError;
|
||||
@ -50,7 +48,6 @@ import static haveno.common.util.ReflectionUtils.isSetterOnClass;
|
||||
import static haveno.common.util.ReflectionUtils.loadFieldListForClassHierarchy;
|
||||
import static haveno.common.util.Utilities.decodeFromHex;
|
||||
import static haveno.core.locale.CountryUtil.findCountryByCode;
|
||||
import static haveno.core.locale.CurrencyUtil.getCurrencyByCountryCode;
|
||||
import static haveno.core.locale.CurrencyUtil.getTradeCurrenciesInList;
|
||||
import static haveno.core.locale.CurrencyUtil.getTradeCurrency;
|
||||
import static haveno.core.payment.payload.PaymentMethod.MONEY_GRAM_ID;
|
||||
@ -435,8 +432,10 @@ class PaymentAccountTypeAdapter extends TypeAdapter<PaymentAccount> {
|
||||
|
||||
if (account.isCountryBasedPaymentAccount()) {
|
||||
((CountryBasedPaymentAccount) account).setCountry(country.get());
|
||||
TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
|
||||
account.setSingleTradeCurrency(fiatCurrency);
|
||||
|
||||
// TODO: applying single trade currency default can overwrite provided currencies, apply elsewhere?
|
||||
// TraditionalCurrency fiatCurrency = getCurrencyByCountryCode(checkNotNull(countryCode));
|
||||
// account.setSingleTradeCurrency(fiatCurrency);
|
||||
} else if (account.hasPaymentMethodWithId(MONEY_GRAM_ID)) {
|
||||
((MoneyGramAccount) account).setCountry(country.get());
|
||||
} else {
|
||||
|
@ -124,7 +124,7 @@ public class PaymentAccountUtil {
|
||||
AccountAgeWitnessService accountAgeWitnessService) {
|
||||
boolean hasChargebackRisk = hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode());
|
||||
boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount,
|
||||
offer.getCurrencyCode(), offer.getMirroredDirection()) >= offer.getMinAmount().longValueExact();
|
||||
offer.getCurrencyCode(), offer.getMirroredDirection(), offer.hasBuyerAsTakerWithoutDeposit()) >= offer.getMinAmount().longValueExact();
|
||||
return !hasChargebackRisk || hasValidAccountAgeWitness;
|
||||
}
|
||||
|
||||
|
112
core/src/main/java/haveno/core/payment/PaysafeAccount.java
Normal file
112
core/src/main/java/haveno/core/payment/PaysafeAccount.java
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.core.payment;
|
||||
|
||||
import haveno.core.api.model.PaymentAccountFormField;
|
||||
import haveno.core.locale.TraditionalCurrency;
|
||||
import haveno.core.locale.TradeCurrency;
|
||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||
import haveno.core.payment.payload.PaymentMethod;
|
||||
import haveno.core.payment.payload.PaysafeAccountPayload;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class PaysafeAccount extends PaymentAccount {
|
||||
|
||||
private static final List<PaymentAccountFormField.FieldId> INPUT_FIELD_IDS = List.of(
|
||||
PaymentAccountFormField.FieldId.ACCOUNT_NAME,
|
||||
PaymentAccountFormField.FieldId.EMAIL,
|
||||
PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
|
||||
PaymentAccountFormField.FieldId.SALT
|
||||
);
|
||||
|
||||
// https://developer.paysafe.com/en/support/reference-information/codes/
|
||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
|
||||
new TraditionalCurrency("AED"),
|
||||
new TraditionalCurrency("ARS"),
|
||||
new TraditionalCurrency("AUD"),
|
||||
new TraditionalCurrency("BGN"),
|
||||
new TraditionalCurrency("BRL"),
|
||||
new TraditionalCurrency("CAD"),
|
||||
new TraditionalCurrency("CHF"),
|
||||
new TraditionalCurrency("CZK"),
|
||||
new TraditionalCurrency("DKK"),
|
||||
new TraditionalCurrency("EGP"),
|
||||
new TraditionalCurrency("EUR"),
|
||||
new TraditionalCurrency("GBP"),
|
||||
new TraditionalCurrency("GEL"),
|
||||
new TraditionalCurrency("HUF"),
|
||||
new TraditionalCurrency("ILS"),
|
||||
new TraditionalCurrency("INR"),
|
||||
new TraditionalCurrency("JPY"),
|
||||
new TraditionalCurrency("ISK"),
|
||||
new TraditionalCurrency("KWD"),
|
||||
new TraditionalCurrency("KRW"),
|
||||
new TraditionalCurrency("MXN"),
|
||||
new TraditionalCurrency("NOK"),
|
||||
new TraditionalCurrency("NZD"),
|
||||
new TraditionalCurrency("PEN"),
|
||||
new TraditionalCurrency("PHP"),
|
||||
new TraditionalCurrency("PLN"),
|
||||
new TraditionalCurrency("RON"),
|
||||
new TraditionalCurrency("RSD"),
|
||||
new TraditionalCurrency("RUB"),
|
||||
new TraditionalCurrency("SAR"),
|
||||
new TraditionalCurrency("SEK"),
|
||||
new TraditionalCurrency("TRY"),
|
||||
new TraditionalCurrency("USD"),
|
||||
new TraditionalCurrency("UYU")
|
||||
);
|
||||
|
||||
public PaysafeAccount() {
|
||||
super(PaymentMethod.PAYSAFE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PaymentAccountPayload createPayload() {
|
||||
return new PaysafeAccountPayload(paymentMethod.getId(), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<TradeCurrency> getSupportedCurrencies() {
|
||||
return SUPPORTED_CURRENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<PaymentAccountFormField.FieldId> getInputFieldIds() {
|
||||
return INPUT_FIELD_IDS;
|
||||
}
|
||||
|
||||
public void setEmail(String accountId) {
|
||||
((PaysafeAccountPayload) paymentAccountPayload).setEmail(accountId);
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return ((PaysafeAccountPayload) paymentAccountPayload).getEmail();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PaymentAccountFormField getEmptyFormField(PaymentAccountFormField.FieldId fieldId) {
|
||||
var field = super.getEmptyFormField(fieldId);
|
||||
if (field.getId() == PaymentAccountFormField.FieldId.TRADE_CURRENCIES) field.setValue("");
|
||||
return field;
|
||||
}
|
||||
}
|
@ -30,7 +30,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class TradeLimits {
|
||||
private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(96.0); // max trade limit for lowest risk payment method. Others will get derived from that.
|
||||
private static final BigInteger MAX_TRADE_LIMIT = HavenoUtils.xmrToAtomicUnits(528); // max trade limit for lowest risk payment method. Others will get derived from that.
|
||||
private static final BigInteger MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT = HavenoUtils.xmrToAtomicUnits(1.5); // max trade limit without deposit from buyer
|
||||
|
||||
@Nullable
|
||||
@Getter
|
||||
private static TradeLimits INSTANCE;
|
||||
@ -57,6 +59,15 @@ public class TradeLimits {
|
||||
return MAX_TRADE_LIMIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum trade limit without a buyer deposit.
|
||||
*
|
||||
* @return the maximum trade limit for a buyer without a deposit
|
||||
*/
|
||||
public BigInteger getMaxTradeLimitBuyerAsTakerWithoutDeposit() {
|
||||
return MAX_TRADE_LIMIT_WITHOUT_BUYER_AS_TAKER_DEPOSIT;
|
||||
}
|
||||
|
||||
// We possibly rounded value for the first month gets multiplied by 4 to get the trade limit after the account
|
||||
// age witness is not considered anymore (> 2 months).
|
||||
|
||||
|
@ -31,11 +31,15 @@ import java.util.List;
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class WeChatPayAccount extends PaymentAccount {
|
||||
|
||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(new TraditionalCurrency("CNY"));
|
||||
public static final List<TradeCurrency> SUPPORTED_CURRENCIES = List.of(
|
||||
new TraditionalCurrency("CNY"),
|
||||
new TraditionalCurrency("USD"),
|
||||
new TraditionalCurrency("EUR"),
|
||||
new TraditionalCurrency("GBP")
|
||||
);
|
||||
|
||||
public WeChatPayAccount() {
|
||||
super(PaymentMethod.WECHAT_PAY);
|
||||
setSingleTradeCurrency(SUPPORTED_CURRENCIES.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -51,6 +51,7 @@ import haveno.core.payment.CashAppAccount;
|
||||
import haveno.core.payment.CashAtAtmAccount;
|
||||
import haveno.core.payment.PayByMailAccount;
|
||||
import haveno.core.payment.PayPalAccount;
|
||||
import haveno.core.payment.PaysafeAccount;
|
||||
import haveno.core.payment.CashDepositAccount;
|
||||
import haveno.core.payment.CelPayAccount;
|
||||
import haveno.core.payment.ZelleAccount;
|
||||
@ -124,13 +125,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) :
|
||||
TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
// Default trade limits.
|
||||
// We initialize very early before reading persisted data. We will apply later the limit from
|
||||
// the DAO param (Param.MAX_TRADE_LIMIT) but that can be only done after the dao is initialized.
|
||||
// The default values will be used for deriving the
|
||||
// risk factor so the relation between the risk categories stays the same as with the default values.
|
||||
// We must not change those values as it could lead to invalid offers if amount becomes lower then new trade limit.
|
||||
// Increasing might be ok, but needs more thought as well...
|
||||
// These values are not used except to derive the associated risk factor.
|
||||
private static final BigInteger DEFAULT_TRADE_LIMIT_CRYPTO = HavenoUtils.xmrToAtomicUnits(200);
|
||||
private static final BigInteger DEFAULT_TRADE_LIMIT_VERY_LOW_RISK = HavenoUtils.xmrToAtomicUnits(100);
|
||||
private static final BigInteger DEFAULT_TRADE_LIMIT_LOW_RISK = HavenoUtils.xmrToAtomicUnits(50);
|
||||
private static final BigInteger DEFAULT_TRADE_LIMIT_MID_RISK = HavenoUtils.xmrToAtomicUnits(25);
|
||||
@ -198,6 +194,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
public static final String CASH_APP_ID = "CASH_APP";
|
||||
public static final String VENMO_ID = "VENMO";
|
||||
public static final String PAYPAL_ID = "PAYPAL";
|
||||
public static final String PAYSAFE_ID = "PAYSAFE";
|
||||
|
||||
public static PaymentMethod UPHOLD;
|
||||
public static PaymentMethod MONEY_BEAM;
|
||||
@ -257,6 +254,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
public static PaymentMethod PAYPAL;
|
||||
public static PaymentMethod CASH_APP;
|
||||
public static PaymentMethod VENMO;
|
||||
public static PaymentMethod PAYSAFE;
|
||||
|
||||
// Cannot be deleted as it would break old trade history entries
|
||||
@Deprecated
|
||||
@ -288,7 +286,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
|
||||
// Global
|
||||
CASH_DEPOSIT = new PaymentMethod(CASH_DEPOSIT_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashDepositAccount.SUPPORTED_CURRENCIES)),
|
||||
PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)),
|
||||
PAY_BY_MAIL = new PaymentMethod(PAY_BY_MAIL_ID, 8 * DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PayByMailAccount.SUPPORTED_CURRENCIES)),
|
||||
CASH_AT_ATM = new PaymentMethod(CASH_AT_ATM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAtAtmAccount.SUPPORTED_CURRENCIES)),
|
||||
MONEY_GRAM = new PaymentMethod(MONEY_GRAM_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(MoneyGramAccount.SUPPORTED_CURRENCIES)),
|
||||
WESTERN_UNION = new PaymentMethod(WESTERN_UNION_ID, 4 * DAY, DEFAULT_TRADE_LIMIT_MID_RISK, getAssetCodes(WesternUnionAccount.SUPPORTED_CURRENCIES)),
|
||||
@ -327,6 +325,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
DOMESTIC_WIRE_TRANSFER = new PaymentMethod(DOMESTIC_WIRE_TRANSFER_ID, 3 * DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(DomesticWireTransferAccount.SUPPORTED_CURRENCIES)),
|
||||
PAYPAL = new PaymentMethod(PAYPAL_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PayPalAccount.SUPPORTED_CURRENCIES)),
|
||||
CASH_APP = new PaymentMethod(CASH_APP_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(CashAppAccount.SUPPORTED_CURRENCIES)),
|
||||
PAYSAFE = new PaymentMethod(PaymentMethod.PAYSAFE_ID, DAY, DEFAULT_TRADE_LIMIT_HIGH_RISK, getAssetCodes(PaysafeAccount.SUPPORTED_CURRENCIES)),
|
||||
|
||||
// Japan
|
||||
JAPAN_BANK = new PaymentMethod(JAPAN_BANK_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(JapanBankAccount.SUPPORTED_CURRENCIES)),
|
||||
@ -342,10 +341,10 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
PROMPT_PAY = new PaymentMethod(PROMPT_PAY_ID, DAY, DEFAULT_TRADE_LIMIT_LOW_RISK, getAssetCodes(PromptPayAccount.SUPPORTED_CURRENCIES)),
|
||||
|
||||
// Cryptos
|
||||
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList()),
|
||||
BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, DAY, DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList()),
|
||||
|
||||
// Cryptos with 1 hour trade period
|
||||
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_VERY_LOW_RISK, Arrays.asList())
|
||||
BLOCK_CHAINS_INSTANT = new PaymentMethod(BLOCK_CHAINS_INSTANT_ID, TimeUnit.HOURS.toMillis(1), DEFAULT_TRADE_LIMIT_CRYPTO, Arrays.asList())
|
||||
);
|
||||
|
||||
// TODO: delete this override method, which overrides the paymentMethods variable, when all payment methods supported using structured form api, and make paymentMethods private
|
||||
@ -369,7 +368,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
AUSTRALIA_PAYID_ID,
|
||||
CASH_APP_ID,
|
||||
PAYPAL_ID,
|
||||
VENMO_ID);
|
||||
VENMO_ID,
|
||||
PAYSAFE_ID);
|
||||
return paymentMethods.stream().filter(paymentMethod -> paymentMethodIds.contains(paymentMethod.getId())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -497,17 +497,21 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
}
|
||||
|
||||
// We use the class field maxTradeLimit only for mapping the risk factor.
|
||||
// The actual trade limit is calculated by dividing TradeLimits.MAX_TRADE_LIMIT by the
|
||||
// risk factor, and then further decreasing by chargeback risk, account signing, and age.
|
||||
long riskFactor;
|
||||
if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact())
|
||||
if (maxTradeLimit == DEFAULT_TRADE_LIMIT_CRYPTO.longValueExact())
|
||||
riskFactor = 1;
|
||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact())
|
||||
riskFactor = 2;
|
||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact())
|
||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_VERY_LOW_RISK.longValueExact())
|
||||
riskFactor = 4;
|
||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_LOW_RISK.longValueExact())
|
||||
riskFactor = 11;
|
||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_MID_RISK.longValueExact())
|
||||
riskFactor = 22;
|
||||
else if (maxTradeLimit == DEFAULT_TRADE_LIMIT_HIGH_RISK.longValueExact())
|
||||
riskFactor = 8;
|
||||
riskFactor = 44;
|
||||
else {
|
||||
riskFactor = 8;
|
||||
riskFactor = 44;
|
||||
log.warn("maxTradeLimit is not matching one of our default values. We use highest risk factor. " +
|
||||
"maxTradeLimit={}. PaymentMethod={}", maxTradeLimit, this);
|
||||
}
|
||||
@ -589,7 +593,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
id.equals(PaymentMethod.UPHOLD_ID) ||
|
||||
id.equals(PaymentMethod.CASH_APP_ID) ||
|
||||
id.equals(PaymentMethod.PAYPAL_ID) ||
|
||||
id.equals(PaymentMethod.VENMO_ID);
|
||||
id.equals(PaymentMethod.VENMO_ID) ||
|
||||
id.equals(PaymentMethod.PAYSAFE_ID);
|
||||
}
|
||||
|
||||
public static boolean isRoundedForAtmCash(String id) {
|
||||
@ -598,7 +603,6 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||
}
|
||||
|
||||
public static boolean isFixedPriceOnly(String id) {
|
||||
return id.equals(PaymentMethod.CASH_AT_ATM_ID) ||
|
||||
id.equals(PaymentMethod.HAL_CASH_ID);
|
||||
return id.equals(PaymentMethod.HAL_CASH_ID);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.core.payment.payload;
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
import haveno.core.locale.Res;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString
|
||||
@Setter
|
||||
@Getter
|
||||
@Slf4j
|
||||
public final class PaysafeAccountPayload extends PaymentAccountPayload {
|
||||
private String email = "";
|
||||
|
||||
public PaysafeAccountPayload(String paymentMethod, String id) {
|
||||
super(paymentMethod, id);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private PaysafeAccountPayload(String paymentMethod,
|
||||
String id,
|
||||
String email,
|
||||
long maxTradePeriod,
|
||||
Map<String, String> excludeFromJsonDataMap) {
|
||||
super(paymentMethod,
|
||||
id,
|
||||
maxTradePeriod,
|
||||
excludeFromJsonDataMap);
|
||||
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message toProtoMessage() {
|
||||
return getPaymentAccountPayloadBuilder()
|
||||
.setPaysafeAccountPayload(protobuf.PaysafeAccountPayload.newBuilder().setEmail(email))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static PaysafeAccountPayload fromProto(protobuf.PaymentAccountPayload proto) {
|
||||
return new PaysafeAccountPayload(proto.getPaymentMethodId(),
|
||||
proto.getId(),
|
||||
proto.getPaysafeAccountPayload().getEmail(),
|
||||
proto.getMaxTradePeriod(),
|
||||
new HashMap<>(proto.getExcludeFromJsonDataMap()));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public String getPaymentDetails() {
|
||||
return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email") + " " + email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPaymentDetailsForTradePopup() {
|
||||
return getPaymentDetails();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getAgeWitnessInputData() {
|
||||
return super.getAgeWitnessInputData(email.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
@ -59,7 +59,7 @@ public class SecurityDepositValidator extends NumberValidator {
|
||||
private ValidationResult validateIfNotTooLowPercentageValue(String input) {
|
||||
try {
|
||||
double percentage = ParsingUtils.parsePercentStringToDouble(input);
|
||||
double minPercentage = Restrictions.getMinBuyerSecurityDepositAsPercent();
|
||||
double minPercentage = Restrictions.getMinSecurityDepositAsPercent();
|
||||
if (percentage < minPercentage)
|
||||
return new ValidationResult(false,
|
||||
Res.get("validation.inputTooSmall", FormattingUtils.formatToPercentWithSymbol(minPercentage)));
|
||||
@ -73,7 +73,7 @@ public class SecurityDepositValidator extends NumberValidator {
|
||||
private ValidationResult validateIfNotTooHighPercentageValue(String input) {
|
||||
try {
|
||||
double percentage = ParsingUtils.parsePercentStringToDouble(input);
|
||||
double maxPercentage = Restrictions.getMaxBuyerSecurityDepositAsPercent();
|
||||
double maxPercentage = Restrictions.getMaxSecurityDepositAsPercent();
|
||||
if (percentage > maxPercentage)
|
||||
return new ValidationResult(false,
|
||||
Res.get("validation.inputTooLarge", FormattingUtils.formatToPercentWithSymbol(maxPercentage)));
|
||||
|
@ -54,6 +54,7 @@ import haveno.core.payment.payload.NequiAccountPayload;
|
||||
import haveno.core.payment.payload.OKPayAccountPayload;
|
||||
import haveno.core.payment.payload.PaxumAccountPayload;
|
||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||
import haveno.core.payment.payload.PaysafeAccountPayload;
|
||||
import haveno.core.payment.payload.PayPalAccountPayload;
|
||||
import haveno.core.payment.payload.PayseraAccountPayload;
|
||||
import haveno.core.payment.payload.PaytmAccountPayload;
|
||||
@ -239,6 +240,8 @@ public class CoreProtoResolver implements ProtoResolver {
|
||||
return VenmoAccountPayload.fromProto(proto);
|
||||
case PAYPAL_ACCOUNT_PAYLOAD:
|
||||
return PayPalAccountPayload.fromProto(proto);
|
||||
case PAYSAFE_ACCOUNT_PAYLOAD:
|
||||
return PaysafeAccountPayload.fromProto(proto);
|
||||
|
||||
default:
|
||||
throw new ProtobufferRuntimeException("Unknown proto message case(PB.PaymentAccountPayload). messageCase=" + messageCase);
|
||||
|
@ -537,15 +537,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
throw e;
|
||||
}
|
||||
|
||||
// try to validate payment account
|
||||
// try to validate payment accounts
|
||||
try {
|
||||
DisputeValidation.validatePaymentAccountPayload(dispute); // TODO: add field to dispute details: valid, invalid, missing
|
||||
DisputeValidation.validatePaymentAccountPayloads(dispute); // TODO: add field to dispute details: valid, invalid, missing
|
||||
} catch (Exception e) {
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
trade.prependErrorMessage(e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
|
||||
// set arbitrator's payment account payloads
|
||||
if (trade.isArbitrator()) {
|
||||
if (trade.getBuyer().getPaymentAccountPayload() == null) trade.getBuyer().setPaymentAccountPayload(dispute.getBuyerPaymentAccountPayload());
|
||||
if (trade.getSeller().getPaymentAccountPayload() == null) trade.getSeller().setPaymentAccountPayload(dispute.getSellerPaymentAccountPayload());
|
||||
}
|
||||
|
||||
// get sender
|
||||
TradePeer sender;
|
||||
if (reOpen) { // re-open can come from either peer
|
||||
|
@ -41,9 +41,12 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
@Slf4j
|
||||
public class DisputeValidation {
|
||||
|
||||
public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException {
|
||||
public static void validatePaymentAccountPayloads(Dispute dispute) throws ValidationException {
|
||||
if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
|
||||
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract");
|
||||
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of seller's payment account payload does not match contract");
|
||||
if (dispute.getBuyerPaymentAccountPayload() != null) {
|
||||
if (!Arrays.equals(dispute.getBuyerPaymentAccountPayload().getHash(), dispute.getContract().getBuyerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of buyer's payment account payload does not match contract");
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateDisputeData(Dispute dispute) throws ValidationException {
|
||||
|
@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import java.math.BigInteger;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Trade in the context of an arbitrator.
|
||||
*/
|
||||
@ -42,8 +44,9 @@ public class ArbitratorTrade extends Trade {
|
||||
String uid,
|
||||
NodeAddress makerNodeAddress,
|
||||
NodeAddress takerNodeAddress,
|
||||
NodeAddress arbitratorNodeAddress) {
|
||||
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress);
|
||||
NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer, tradeAmount, tradePrice, xmrWalletService, processModel, uid, makerNodeAddress, takerNodeAddress, arbitratorNodeAddress, challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -81,7 +84,8 @@ public class ArbitratorTrade extends Trade {
|
||||
uid,
|
||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
|
||||
proto,
|
||||
coreProtoResolver);
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import java.math.BigInteger;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Slf4j
|
||||
public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
|
||||
@ -43,7 +45,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
String uid,
|
||||
NodeAddress makerNodeAddress,
|
||||
NodeAddress takerNodeAddress,
|
||||
NodeAddress arbitratorNodeAddress) {
|
||||
NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
tradePrice,
|
||||
@ -52,7 +55,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -85,7 +89,8 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
uid,
|
||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null);
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
|
||||
|
||||
trade.setPrice(proto.getPrice());
|
||||
|
||||
|
@ -44,7 +44,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
|
||||
String uid,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
tradePrice,
|
||||
@ -53,7 +54,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +89,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
|
||||
uid,
|
||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
|
||||
proto,
|
||||
coreProtoResolver);
|
||||
}
|
||||
|
@ -38,7 +38,8 @@ public abstract class BuyerTrade extends Trade {
|
||||
String uid,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
tradePrice,
|
||||
@ -47,7 +48,8 @@ public abstract class BuyerTrade extends Trade {
|
||||
uid,
|
||||
takerNodeAddress,
|
||||
makerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,7 @@ package haveno.core.trade;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.proto.ProtoUtil;
|
||||
import haveno.common.proto.network.NetworkPayload;
|
||||
import haveno.common.util.JsonExclude;
|
||||
import haveno.common.util.Utilities;
|
||||
@ -53,6 +54,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@ -79,6 +81,7 @@ public final class Contract implements NetworkPayload {
|
||||
private final String makerPayoutAddressString;
|
||||
private final String takerPayoutAddressString;
|
||||
private final String makerDepositTxHash;
|
||||
@Nullable
|
||||
private final String takerDepositTxHash;
|
||||
|
||||
public Contract(OfferPayload offerPayload,
|
||||
@ -99,7 +102,7 @@ public final class Contract implements NetworkPayload {
|
||||
String makerPayoutAddressString,
|
||||
String takerPayoutAddressString,
|
||||
String makerDepositTxHash,
|
||||
String takerDepositTxHash) {
|
||||
@Nullable String takerDepositTxHash) {
|
||||
this.offerPayload = offerPayload;
|
||||
this.tradeAmount = tradeAmount;
|
||||
this.tradePrice = tradePrice;
|
||||
@ -134,6 +137,31 @@ public final class Contract implements NetworkPayload {
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public protobuf.Contract toProtoMessage() {
|
||||
protobuf.Contract.Builder builder = protobuf.Contract.newBuilder()
|
||||
.setOfferPayload(offerPayload.toProtoMessage().getOfferPayload())
|
||||
.setTradeAmount(tradeAmount)
|
||||
.setTradePrice(tradePrice)
|
||||
.setBuyerNodeAddress(buyerNodeAddress.toProtoMessage())
|
||||
.setSellerNodeAddress(sellerNodeAddress.toProtoMessage())
|
||||
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())
|
||||
.setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker)
|
||||
.setMakerAccountId(makerAccountId)
|
||||
.setTakerAccountId(takerAccountId)
|
||||
.setMakerPaymentMethodId(makerPaymentMethodId)
|
||||
.setTakerPaymentMethodId(takerPaymentMethodId)
|
||||
.setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash))
|
||||
.setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash))
|
||||
.setMakerPubKeyRing(makerPubKeyRing.toProtoMessage())
|
||||
.setTakerPubKeyRing(takerPubKeyRing.toProtoMessage())
|
||||
.setMakerPayoutAddressString(makerPayoutAddressString)
|
||||
.setTakerPayoutAddressString(takerPayoutAddressString)
|
||||
.setMakerDepositTxHash(makerDepositTxHash);
|
||||
Optional.ofNullable(takerDepositTxHash).ifPresent(builder::setTakerDepositTxHash);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) {
|
||||
return new Contract(OfferPayload.fromProto(proto.getOfferPayload()),
|
||||
proto.getTradeAmount(),
|
||||
@ -153,32 +181,7 @@ public final class Contract implements NetworkPayload {
|
||||
proto.getMakerPayoutAddressString(),
|
||||
proto.getTakerPayoutAddressString(),
|
||||
proto.getMakerDepositTxHash(),
|
||||
proto.getTakerDepositTxHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
public protobuf.Contract toProtoMessage() {
|
||||
return protobuf.Contract.newBuilder()
|
||||
.setOfferPayload(offerPayload.toProtoMessage().getOfferPayload())
|
||||
.setTradeAmount(tradeAmount)
|
||||
.setTradePrice(tradePrice)
|
||||
.setBuyerNodeAddress(buyerNodeAddress.toProtoMessage())
|
||||
.setSellerNodeAddress(sellerNodeAddress.toProtoMessage())
|
||||
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())
|
||||
.setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker)
|
||||
.setMakerAccountId(makerAccountId)
|
||||
.setTakerAccountId(takerAccountId)
|
||||
.setMakerPaymentMethodId(makerPaymentMethodId)
|
||||
.setTakerPaymentMethodId(takerPaymentMethodId)
|
||||
.setMakerPaymentAccountPayloadHash(ByteString.copyFrom(makerPaymentAccountPayloadHash))
|
||||
.setTakerPaymentAccountPayloadHash(ByteString.copyFrom(takerPaymentAccountPayloadHash))
|
||||
.setMakerPubKeyRing(makerPubKeyRing.toProtoMessage())
|
||||
.setTakerPubKeyRing(takerPubKeyRing.toProtoMessage())
|
||||
.setMakerPayoutAddressString(makerPayoutAddressString)
|
||||
.setTakerPayoutAddressString(takerPayoutAddressString)
|
||||
.setMakerDepositTxHash(makerDepositTxHash)
|
||||
.setTakerDepositTxHash(takerDepositTxHash)
|
||||
.build();
|
||||
ProtoUtil.stringOrNullFromProto(proto.getTakerDepositTxHash()));
|
||||
}
|
||||
|
||||
|
||||
|
@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.crypto.Sig;
|
||||
import haveno.common.file.FileUtil;
|
||||
import haveno.common.util.Base64;
|
||||
import haveno.common.util.Utilities;
|
||||
import haveno.core.api.CoreNotificationService;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
@ -48,7 +49,10 @@ import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.SimpleDateFormat;
|
||||
@ -67,6 +71,7 @@ import javax.sound.sampled.SourceDataLine;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.common.MoneroUtils;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
@ -87,13 +92,15 @@ public class HavenoUtils {
|
||||
|
||||
// configure fees
|
||||
public static final boolean ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS = true;
|
||||
public static final double PENALTY_FEE_PCT = 0.02; // 2%
|
||||
public static final double MAKER_FEE_PCT = 0.0015; // 0.15%
|
||||
public static final double TAKER_FEE_PCT = 0.0075; // 0.75%
|
||||
public static final double PENALTY_FEE_PCT = 0.02; // 2%
|
||||
public static final double MAKER_FEE_FOR_TAKER_WITHOUT_DEPOSIT_PCT = MAKER_FEE_PCT + TAKER_FEE_PCT; // customize maker's fee when no deposit or fee from taker
|
||||
|
||||
// other configuration
|
||||
public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes
|
||||
public static final long LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS = 1000 * 30; // log warnings when daemon not synced once every 30s
|
||||
public static final int PRIVATE_OFFER_PASSPHRASE_NUM_WORDS = 8; // number of words in a private offer passphrase
|
||||
|
||||
// synchronize requests to the daemon
|
||||
private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors?
|
||||
@ -198,11 +205,11 @@ public class HavenoUtils {
|
||||
}
|
||||
|
||||
public static double atomicUnitsToXmr(BigInteger atomicUnits) {
|
||||
return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue();
|
||||
return MoneroUtils.atomicUnitsToXmr(atomicUnits);
|
||||
}
|
||||
|
||||
public static BigInteger xmrToAtomicUnits(double xmr) {
|
||||
return new BigDecimal(xmr).multiply(new BigDecimal(XMR_AU_MULTIPLIER)).toBigInteger();
|
||||
return MoneroUtils.xmrToAtomicUnits(xmr);
|
||||
}
|
||||
|
||||
public static long xmrToCentineros(double xmr) {
|
||||
@ -214,11 +221,11 @@ public class HavenoUtils {
|
||||
}
|
||||
|
||||
public static double divide(BigInteger auDividend, BigInteger auDivisor) {
|
||||
return atomicUnitsToXmr(auDividend) / atomicUnitsToXmr(auDivisor);
|
||||
return MoneroUtils.divide(auDividend, auDivisor);
|
||||
}
|
||||
|
||||
public static BigInteger multiply(BigInteger amount1, double amount2) {
|
||||
return amount1 == null ? null : new BigDecimal(amount1).multiply(BigDecimal.valueOf(amount2)).toBigInteger();
|
||||
return MoneroUtils.multiply(amount1, amount2);
|
||||
}
|
||||
|
||||
// ------------------------- FORMAT UTILS ---------------------------------
|
||||
@ -286,6 +293,41 @@ public class HavenoUtils {
|
||||
|
||||
// ------------------------ SIGNING AND VERIFYING -------------------------
|
||||
|
||||
public static String generateChallenge() {
|
||||
try {
|
||||
|
||||
// load bip39 words
|
||||
String fileName = "bip39_english.txt";
|
||||
File bip39File = new File(havenoSetup.getConfig().appDataDir, fileName);
|
||||
if (!bip39File.exists()) FileUtil.resourceToFile(fileName, bip39File);
|
||||
List<String> bip39Words = Files.readAllLines(bip39File.toPath(), StandardCharsets.UTF_8);
|
||||
|
||||
// select words randomly
|
||||
List<String> passphraseWords = new ArrayList<String>();
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
for (int i = 0; i < PRIVATE_OFFER_PASSPHRASE_NUM_WORDS; i++) {
|
||||
passphraseWords.add(bip39Words.get(secureRandom.nextInt(bip39Words.size())));
|
||||
}
|
||||
return String.join(" ", passphraseWords);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to generate challenge", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getChallengeHash(String challenge) {
|
||||
if (challenge == null) return null;
|
||||
|
||||
// tokenize passphrase
|
||||
String[] words = challenge.toLowerCase().split(" ");
|
||||
|
||||
// collect first 4 letters of each word, which are unique in bip39
|
||||
List<String> prefixes = new ArrayList<String>();
|
||||
for (String word : words) prefixes.add(word.substring(0, Math.min(word.length(), 4)));
|
||||
|
||||
// hash the result
|
||||
return Base64.encode(Hash.getSha256Hash(String.join(" ", prefixes).getBytes()));
|
||||
}
|
||||
|
||||
public static byte[] sign(KeyRing keyRing, String message) {
|
||||
return sign(keyRing.getSignatureKeyPair().getPrivate(), message);
|
||||
}
|
||||
|
@ -44,7 +44,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
|
||||
String uid,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
tradePrice,
|
||||
@ -53,7 +54,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +89,8 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
|
||||
uid,
|
||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null);
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
|
||||
|
||||
trade.setPrice(proto.getPrice());
|
||||
|
||||
|
@ -44,7 +44,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
|
||||
String uid,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
tradePrice,
|
||||
@ -53,7 +54,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
|
||||
@ -87,7 +89,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
|
||||
uid,
|
||||
proto.getProcessModel().getMaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getMaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getTaker().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getTaker().getNodeAddress()) : null,
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null),
|
||||
proto.getProcessModel().getArbitrator().hasNodeAddress() ? NodeAddress.fromProto(proto.getProcessModel().getArbitrator().getNodeAddress()) : null,
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge())),
|
||||
proto,
|
||||
coreProtoResolver);
|
||||
}
|
||||
|
@ -36,7 +36,8 @@ public abstract class SellerTrade extends Trade {
|
||||
String uid,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
tradePrice,
|
||||
@ -45,7 +46,8 @@ public abstract class SellerTrade extends Trade {
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -486,6 +486,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
private IdlePayoutSyncer idlePayoutSyncer;
|
||||
@Getter
|
||||
private boolean isCompleted;
|
||||
@Getter
|
||||
private final String challenge;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructors
|
||||
@ -500,7 +502,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
String uid,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
super();
|
||||
this.offer = offer;
|
||||
this.amount = tradeAmount.longValueExact();
|
||||
@ -511,6 +514,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
this.uid = uid;
|
||||
this.takeOfferDate = new Date().getTime();
|
||||
this.tradeListeners = new ArrayList<TradeListener>();
|
||||
this.challenge = challenge;
|
||||
|
||||
getMaker().setNodeAddress(makerNodeAddress);
|
||||
getTaker().setNodeAddress(takerNodeAddress);
|
||||
@ -534,7 +538,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
String uid,
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable String challenge) {
|
||||
|
||||
this(offer,
|
||||
tradeAmount,
|
||||
@ -544,7 +549,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
}
|
||||
|
||||
// TODO: remove these constructors
|
||||
@ -559,7 +565,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
NodeAddress arbitratorNodeAddress,
|
||||
XmrWalletService xmrWalletService,
|
||||
ProcessModel processModel,
|
||||
String uid) {
|
||||
String uid,
|
||||
@Nullable String challenge) {
|
||||
|
||||
this(offer,
|
||||
tradeAmount,
|
||||
@ -569,7 +576,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
uid,
|
||||
makerNodeAddress,
|
||||
takerNodeAddress,
|
||||
arbitratorNodeAddress);
|
||||
arbitratorNodeAddress,
|
||||
challenge);
|
||||
|
||||
setAmount(tradeAmount);
|
||||
}
|
||||
@ -649,6 +657,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
ThreadUtils.submitToPool(() -> {
|
||||
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
|
||||
if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished();
|
||||
if (newValue == Trade.Phase.DEPOSITS_CONFIRMED) onDepositsConfirmed();
|
||||
if (newValue == Trade.Phase.DEPOSITS_UNLOCKED) onDepositsUnlocked();
|
||||
if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent();
|
||||
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
|
||||
if (isPaymentReceived()) {
|
||||
@ -722,7 +732,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
xmrWalletService.addWalletListener(idlePayoutSyncer);
|
||||
}
|
||||
|
||||
// TODO: buyer's payment sent message state property can become unsynced (after improper shut down?)
|
||||
// TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed in v1.0.19 so this check can be removed?
|
||||
if (isBuyer()) {
|
||||
MessageState expectedState = getPaymentSentMessageState();
|
||||
if (expectedState != null && expectedState != processModel.getPaymentSentMessageStateProperty().get()) {
|
||||
@ -842,14 +852,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
|
||||
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isIdling() {
|
||||
return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden
|
||||
}
|
||||
@ -899,6 +901,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestSaveWallet() {
|
||||
|
||||
// save wallet off main thread
|
||||
@ -909,6 +912,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}, getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveWallet() {
|
||||
synchronized (walletLock) {
|
||||
if (!walletExists()) {
|
||||
@ -1233,7 +1237,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null");
|
||||
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null");
|
||||
BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount();
|
||||
BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount();
|
||||
BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount();
|
||||
BigInteger tradeAmount = getAmount();
|
||||
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
|
||||
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
|
||||
@ -1324,7 +1328,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
MoneroWallet wallet = getWallet();
|
||||
Contract contract = getContract();
|
||||
BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount();
|
||||
BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount();
|
||||
BigInteger buyerDepositAmount = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : getBuyer().getDepositTx().getIncomingAmount();
|
||||
BigInteger tradeAmount = getAmount();
|
||||
|
||||
// describe payout tx
|
||||
@ -2016,12 +2020,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED;
|
||||
switch (state) {
|
||||
case BUYER_SENT_PAYMENT_SENT_MSG:
|
||||
case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG:
|
||||
return MessageState.SENT;
|
||||
case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG:
|
||||
return MessageState.ARRIVED;
|
||||
case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG:
|
||||
return MessageState.STORED_IN_MAILBOX;
|
||||
case SELLER_RECEIVED_PAYMENT_SENT_MSG:
|
||||
return MessageState.ARRIVED;
|
||||
return MessageState.ACKNOWLEDGED;
|
||||
case BUYER_SEND_FAILED_PAYMENT_SENT_MSG:
|
||||
return MessageState.FAILED;
|
||||
default:
|
||||
@ -2091,9 +2096,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
final long tradeTime = getTakeOfferDate().getTime();
|
||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||
if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod");
|
||||
if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?");
|
||||
if (getMakerDepositTx() == null || (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?");
|
||||
|
||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), hasBuyerAsTakerWithoutDeposit() ? 0l : getTakerDepositTx().getHeight());
|
||||
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||
|
||||
// If block date is in future (Date in blocks can be off by +/- 2 hours) we use our current date.
|
||||
@ -2125,7 +2130,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
|
||||
public boolean isDepositsPublished() {
|
||||
if (isDepositFailed()) return false;
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null;
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && (getTaker().getDepositTxHash() != null || hasBuyerAsTakerWithoutDeposit());
|
||||
}
|
||||
|
||||
public boolean isFundsLockedIn() {
|
||||
@ -2277,7 +2282,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
|
||||
public BigInteger getTakerFee() {
|
||||
return offer.getTakerFee(getAmount());
|
||||
return hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : offer.getTakerFee(getAmount());
|
||||
}
|
||||
|
||||
public BigInteger getSecurityDepositBeforeMiningFee() {
|
||||
return isBuyer() ? getBuyerSecurityDepositBeforeMiningFee() : getSellerSecurityDepositBeforeMiningFee();
|
||||
}
|
||||
|
||||
public BigInteger getBuyerSecurityDepositBeforeMiningFee() {
|
||||
@ -2288,6 +2297,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
return offer.getOfferPayload().getSellerSecurityDepositForTradeAmount(getAmount());
|
||||
}
|
||||
|
||||
public boolean isBuyerAsTakerWithoutDeposit() {
|
||||
return isBuyer() && isTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee());
|
||||
}
|
||||
|
||||
public boolean hasBuyerAsTakerWithoutDeposit() {
|
||||
return getBuyer() == getTaker() && BigInteger.ZERO.equals(getBuyerSecurityDepositBeforeMiningFee());
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getTotalTxFee() {
|
||||
return getSelf().getDepositTxFee().add(getSelf().getPayoutTxFee()); // sum my tx fees
|
||||
@ -2303,7 +2320,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
|
||||
public boolean isTxChainInvalid() {
|
||||
return processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null;
|
||||
return processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2364,7 +2381,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
return tradeVolumeProperty;
|
||||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
@Override
|
||||
protected void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// use current connection
|
||||
@ -2537,7 +2555,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
if (isPayoutUnlocked()) return;
|
||||
|
||||
// skip if deposit txs unknown or not requested
|
||||
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return;
|
||||
if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return;
|
||||
|
||||
// skip if daemon not synced
|
||||
if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return;
|
||||
@ -2553,7 +2571,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
|
||||
// get txs from trade wallet
|
||||
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
|
||||
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
|
||||
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && hasBuyerAsTakerWithoutDeposit()));
|
||||
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
|
||||
List<MoneroTxWallet> txs;
|
||||
if (!updatePool) txs = wallet.getTxs(query);
|
||||
@ -2565,22 +2583,22 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
setDepositTxs(txs);
|
||||
if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen
|
||||
if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen
|
||||
setStateDepositsSeen();
|
||||
|
||||
// set actual security deposits
|
||||
if (getBuyer().getSecurityDeposit().longValueExact() == 0) {
|
||||
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
|
||||
BigInteger buyerSecurityDeposit = hasBuyerAsTakerWithoutDeposit() ? BigInteger.ZERO : ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
|
||||
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount());
|
||||
getBuyer().setSecurityDeposit(buyerSecurityDeposit);
|
||||
getSeller().setSecurityDeposit(sellerSecurityDeposit);
|
||||
}
|
||||
|
||||
// check for deposit txs confirmation
|
||||
if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed();
|
||||
if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) setStateDepositsConfirmed();
|
||||
|
||||
// check for deposit txs unlocked
|
||||
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) {
|
||||
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) {
|
||||
setStateDepositsUnlocked();
|
||||
}
|
||||
}
|
||||
@ -2660,7 +2678,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
pollInProgress = false;
|
||||
}
|
||||
}
|
||||
requestSaveWallet();
|
||||
saveWalletWithDelay();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2750,7 +2768,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId());
|
||||
return true;
|
||||
}
|
||||
if (getTakerDepositTx() == null) {
|
||||
if (getTakerDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()) {
|
||||
log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId());
|
||||
return true;
|
||||
}
|
||||
@ -2872,10 +2890,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages()));
|
||||
}
|
||||
|
||||
private void onDepositsConfirmed() {
|
||||
HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_CONFIRMED, "Trade Deposits Confirmed", "The deposit transactions have confirmed");
|
||||
}
|
||||
|
||||
private void onDepositsUnlocked() {
|
||||
HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_UNLOCKED, "Trade Deposits Unlocked", "The deposit transactions have unlocked");
|
||||
}
|
||||
|
||||
private void onPaymentSent() {
|
||||
if (this instanceof SellerTrade) {
|
||||
HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); // TODO (woodser): use language translation
|
||||
}
|
||||
HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment");
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -2913,6 +2937,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
||||
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey));
|
||||
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
||||
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@ -2982,6 +3007,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
",\n refundResultState=" + refundResultState +
|
||||
",\n refundResultStateProperty=" + refundResultStateProperty +
|
||||
",\n isCompleted=" + isCompleted +
|
||||
",\n challenge='" + challenge + '\'' +
|
||||
"\n}";
|
||||
}
|
||||
}
|
||||
|
@ -561,6 +561,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
OpenOffer openOffer = openOfferOptional.get();
|
||||
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
|
||||
Offer offer = openOffer.getOffer();
|
||||
|
||||
// validate challenge
|
||||
if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) {
|
||||
log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender);
|
||||
return;
|
||||
}
|
||||
|
||||
// ensure trade does not already exist
|
||||
Optional<Trade> tradeOptional = getOpenTrade(request.getOfferId());
|
||||
@ -583,7 +589,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
UUID.randomUUID().toString(),
|
||||
request.getMakerNodeAddress(),
|
||||
request.getTakerNodeAddress(),
|
||||
request.getArbitratorNodeAddress());
|
||||
request.getArbitratorNodeAddress(),
|
||||
openOffer.getChallenge());
|
||||
else
|
||||
trade = new SellerAsMakerTrade(offer,
|
||||
BigInteger.valueOf(request.getTradeAmount()),
|
||||
@ -593,7 +600,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
UUID.randomUUID().toString(),
|
||||
request.getMakerNodeAddress(),
|
||||
request.getTakerNodeAddress(),
|
||||
request.getArbitratorNodeAddress());
|
||||
request.getArbitratorNodeAddress(),
|
||||
openOffer.getChallenge());
|
||||
trade.getMaker().setPaymentAccountId(trade.getOffer().getOfferPayload().getMakerPaymentAccountId());
|
||||
trade.getTaker().setPaymentAccountId(request.getTakerPaymentAccountId());
|
||||
trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing());
|
||||
@ -646,6 +654,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
return;
|
||||
}
|
||||
|
||||
// validate challenge hash
|
||||
if (offer.getChallengeHash() != null && !offer.getChallengeHash().equals(HavenoUtils.getChallengeHash(request.getChallenge()))) {
|
||||
log.warn("Ignoring InitTradeRequest to arbitrator because challenge hash is incorrect, tradeId={}, sender={}", request.getOfferId(), sender);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle trade
|
||||
Trade trade;
|
||||
Optional<Trade> tradeOptional = getOpenTrade(offer.getId());
|
||||
@ -679,7 +693,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
UUID.randomUUID().toString(),
|
||||
request.getMakerNodeAddress(),
|
||||
request.getTakerNodeAddress(),
|
||||
request.getArbitratorNodeAddress());
|
||||
request.getArbitratorNodeAddress(),
|
||||
request.getChallenge());
|
||||
|
||||
// set reserve tx hash if available
|
||||
Optional<SignedOffer> signedOfferOptional = openOfferManager.getSignedOfferById(request.getOfferId());
|
||||
@ -873,7 +888,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
UUID.randomUUID().toString(),
|
||||
offer.getMakerNodeAddress(),
|
||||
P2PService.getMyNodeAddress(),
|
||||
null);
|
||||
null,
|
||||
offer.getChallenge());
|
||||
} else {
|
||||
trade = new BuyerAsTakerTrade(offer,
|
||||
amount,
|
||||
@ -883,7 +899,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
UUID.randomUUID().toString(),
|
||||
offer.getMakerNodeAddress(),
|
||||
P2PService.getMyNodeAddress(),
|
||||
null);
|
||||
null,
|
||||
offer.getChallenge());
|
||||
}
|
||||
trade.getProcessModel().setUseSavingsWallet(useSavingsWallet);
|
||||
trade.getProcessModel().setFundsNeededForTrade(fundsNeededForTrade.longValueExact());
|
||||
@ -994,7 +1011,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
if (tradeOptional.isPresent()) {
|
||||
Trade trade = tradeOptional.get();
|
||||
trade.setDisputeState(disputeState);
|
||||
onTradeCompleted(trade);
|
||||
xmrWalletService.resetAddressEntriesForTrade(trade.getId());
|
||||
requestPersistence();
|
||||
}
|
||||
@ -1128,7 +1144,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
log.warn("We found a closed trade with locked up funds. " +
|
||||
"That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||
}
|
||||
} else {
|
||||
} else if (!trade.hasBuyerAsTakerWithoutDeposit()) {
|
||||
log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
||||
}
|
||||
|
@ -33,7 +33,9 @@ import java.util.Optional;
|
||||
public final class DepositRequest extends TradeMessage implements DirectMessage {
|
||||
private final long currentDate;
|
||||
private final byte[] contractSignature;
|
||||
@Nullable
|
||||
private final String depositTxHex;
|
||||
@Nullable
|
||||
private final String depositTxKey;
|
||||
@Nullable
|
||||
private final byte[] paymentAccountKey;
|
||||
@ -43,8 +45,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage
|
||||
String messageVersion,
|
||||
long currentDate,
|
||||
byte[] contractSignature,
|
||||
String depositTxHex,
|
||||
String depositTxKey,
|
||||
@Nullable String depositTxHex,
|
||||
@Nullable String depositTxKey,
|
||||
@Nullable byte[] paymentAccountKey) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.currentDate = currentDate;
|
||||
@ -63,13 +65,12 @@ public final class DepositRequest extends TradeMessage implements DirectMessage
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.DepositRequest.Builder builder = protobuf.DepositRequest.newBuilder()
|
||||
.setTradeId(offerId)
|
||||
.setUid(uid)
|
||||
.setDepositTxHex(depositTxHex)
|
||||
.setDepositTxKey(depositTxKey);
|
||||
.setUid(uid);
|
||||
builder.setCurrentDate(currentDate);
|
||||
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
|
||||
Optional.ofNullable(depositTxHex).ifPresent(builder::setDepositTxHex);
|
||||
Optional.ofNullable(depositTxKey).ifPresent(builder::setDepositTxKey);
|
||||
Optional.ofNullable(contractSignature).ifPresent(e -> builder.setContractSignature(ByteString.copyFrom(e)));
|
||||
|
||||
return getNetworkEnvelopeBuilder().setDepositRequest(builder).build();
|
||||
}
|
||||
|
||||
@ -81,8 +82,8 @@ public final class DepositRequest extends TradeMessage implements DirectMessage
|
||||
messageVersion,
|
||||
proto.getCurrentDate(),
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getContractSignature()),
|
||||
proto.getDepositTxHex(),
|
||||
proto.getDepositTxKey(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()),
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()));
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
||||
private final String reserveTxKey;
|
||||
@Nullable
|
||||
private final String payoutAddress;
|
||||
@Nullable
|
||||
private final String challenge;
|
||||
|
||||
public InitTradeRequest(TradeProtocolVersion tradeProtocolVersion,
|
||||
String offerId,
|
||||
@ -79,7 +81,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
||||
@Nullable String reserveTxHash,
|
||||
@Nullable String reserveTxHex,
|
||||
@Nullable String reserveTxKey,
|
||||
@Nullable String payoutAddress) {
|
||||
@Nullable String payoutAddress,
|
||||
@Nullable String challenge) {
|
||||
super(messageVersion, offerId, uid);
|
||||
this.tradeProtocolVersion = tradeProtocolVersion;
|
||||
this.tradeAmount = tradeAmount;
|
||||
@ -99,6 +102,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
||||
this.reserveTxHex = reserveTxHex;
|
||||
this.reserveTxKey = reserveTxKey;
|
||||
this.payoutAddress = payoutAddress;
|
||||
this.challenge = challenge;
|
||||
}
|
||||
|
||||
|
||||
@ -129,6 +133,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
||||
Optional.ofNullable(reserveTxHex).ifPresent(e -> builder.setReserveTxHex(reserveTxHex));
|
||||
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
|
||||
Optional.ofNullable(payoutAddress).ifPresent(e -> builder.setPayoutAddress(payoutAddress));
|
||||
Optional.ofNullable(challenge).ifPresent(e -> builder.setChallenge(challenge));
|
||||
Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e)));
|
||||
builder.setCurrentDate(currentDate);
|
||||
|
||||
@ -158,7 +163,8 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHash()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxHex()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getReserveTxKey()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()));
|
||||
ProtoUtil.stringOrNullFromProto(proto.getPayoutAddress()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getChallenge()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -183,6 +189,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
||||
",\n reserveTxHex=" + reserveTxHex +
|
||||
",\n reserveTxKey=" + reserveTxKey +
|
||||
",\n payoutAddress=" + payoutAddress +
|
||||
",\n challenge=" + challenge +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
|
||||
private final String accountId;
|
||||
private final byte[] paymentAccountPayloadHash;
|
||||
private final String payoutAddress;
|
||||
@Nullable
|
||||
private final String depositTxHash;
|
||||
@Nullable
|
||||
private final byte[] accountAgeWitnessSignatureOfDepositHash;
|
||||
|
||||
public SignContractRequest(String tradeId,
|
||||
@ -45,7 +47,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
|
||||
String accountId,
|
||||
byte[] paymentAccountPayloadHash,
|
||||
String payoutAddress,
|
||||
String depositTxHash,
|
||||
@Nullable String depositTxHash,
|
||||
@Nullable byte[] accountAgeWitnessSignatureOfDepositHash) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.currentDate = currentDate;
|
||||
@ -68,10 +70,9 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
|
||||
.setUid(uid)
|
||||
.setAccountId(accountId)
|
||||
.setPaymentAccountPayloadHash(ByteString.copyFrom(paymentAccountPayloadHash))
|
||||
.setPayoutAddress(payoutAddress)
|
||||
.setDepositTxHash(depositTxHash);
|
||||
|
||||
.setPayoutAddress(payoutAddress);
|
||||
Optional.ofNullable(accountAgeWitnessSignatureOfDepositHash).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfDepositHash(ByteString.copyFrom(e)));
|
||||
Optional.ofNullable(depositTxHash).ifPresent(builder::setDepositTxHash);
|
||||
builder.setCurrentDate(currentDate);
|
||||
|
||||
return getNetworkEnvelopeBuilder().setSignContractRequest(builder).build();
|
||||
@ -87,7 +88,7 @@ public final class SignContractRequest extends TradeMessage implements DirectMes
|
||||
proto.getAccountId(),
|
||||
proto.getPaymentAccountPayloadHash().toByteArray(),
|
||||
proto.getPayoutAddress(),
|
||||
proto.getDepositTxHash(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()),
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfDepositHash()));
|
||||
}
|
||||
|
||||
|
@ -158,7 +158,6 @@ public final class TradePeer implements PersistablePayload {
|
||||
}
|
||||
|
||||
public BigInteger getSecurityDeposit() {
|
||||
if (depositTxHash == null) return null;
|
||||
return BigInteger.valueOf(securityDeposit);
|
||||
}
|
||||
|
||||
|
@ -36,8 +36,9 @@ import monero.daemon.model.MoneroSubmitTxResult;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@ -83,72 +84,86 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
byte[] signature = request.getContractSignature();
|
||||
|
||||
// get trader info
|
||||
TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
|
||||
if (trader == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator");
|
||||
PubKeyRing peerPubKeyRing = trader.getPubKeyRing();
|
||||
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
|
||||
if (sender == null) throw new RuntimeException(request.getClass().getSimpleName() + " is not from maker, taker, or arbitrator");
|
||||
PubKeyRing senderPubKeyRing = sender.getPubKeyRing();
|
||||
|
||||
// verify signature
|
||||
if (!HavenoUtils.isSignatureValid(peerPubKeyRing, contractAsJson, signature)) {
|
||||
if (!HavenoUtils.isSignatureValid(senderPubKeyRing, contractAsJson, signature)) {
|
||||
throw new RuntimeException("Peer's contract signature is invalid");
|
||||
}
|
||||
|
||||
// set peer's signature
|
||||
trader.setContractSignature(signature);
|
||||
sender.setContractSignature(signature);
|
||||
|
||||
// collect expected values
|
||||
Offer offer = trade.getOffer();
|
||||
boolean isFromTaker = trader == trade.getTaker();
|
||||
boolean isFromBuyer = trader == trade.getBuyer();
|
||||
boolean isFromTaker = sender == trade.getTaker();
|
||||
boolean isFromBuyer = sender == trade.getBuyer();
|
||||
BigInteger tradeFee = isFromTaker ? trade.getTakerFee() : trade.getMakerFee();
|
||||
BigInteger sendTradeAmount = isFromBuyer ? BigInteger.ZERO : trade.getAmount();
|
||||
BigInteger securityDeposit = isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
String depositAddress = processModel.getMultisigAddress();
|
||||
sender.setSecurityDeposit(securityDeposit);
|
||||
|
||||
// verify deposit tx
|
||||
MoneroTx verifiedTx;
|
||||
try {
|
||||
verifiedTx = trade.getXmrWalletService().verifyDepositTx(
|
||||
offer.getId(),
|
||||
tradeFee,
|
||||
trade.getProcessModel().getTradeFeeAddress(),
|
||||
sendTradeAmount,
|
||||
securityDeposit,
|
||||
depositAddress,
|
||||
trader.getDepositTxHash(),
|
||||
request.getDepositTxHex(),
|
||||
request.getDepositTxKey(),
|
||||
null);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + trader.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||
boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && isFromTaker && trade.hasBuyerAsTakerWithoutDeposit();
|
||||
if (!isFromBuyerAsTakerWithoutDeposit) {
|
||||
MoneroTx verifiedTx;
|
||||
try {
|
||||
verifiedTx = trade.getXmrWalletService().verifyDepositTx(
|
||||
offer.getId(),
|
||||
tradeFee,
|
||||
trade.getProcessModel().getTradeFeeAddress(),
|
||||
sendTradeAmount,
|
||||
securityDeposit,
|
||||
depositAddress,
|
||||
sender.getDepositTxHash(),
|
||||
request.getDepositTxHex(),
|
||||
request.getDepositTxKey(),
|
||||
null);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + sender.getNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
// update trade state
|
||||
sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||
sender.setDepositTxFee(verifiedTx.getFee());
|
||||
sender.setDepositTxHex(request.getDepositTxHex());
|
||||
sender.setDepositTxKey(request.getDepositTxKey());
|
||||
}
|
||||
|
||||
// update trade state
|
||||
trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||
trader.setDepositTxFee(verifiedTx.getFee());
|
||||
trader.setDepositTxHex(request.getDepositTxHex());
|
||||
trader.setDepositTxKey(request.getDepositTxKey());
|
||||
if (request.getPaymentAccountKey() != null) trader.setPaymentAccountKey(request.getPaymentAccountKey());
|
||||
if (request.getPaymentAccountKey() != null) sender.setPaymentAccountKey(request.getPaymentAccountKey());
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// relay deposit txs when both available
|
||||
// relay deposit txs when both requests received
|
||||
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
|
||||
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
||||
if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
|
||||
|
||||
// check timeout and extend just before relaying
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||
trade.addInitProgressStep();
|
||||
|
||||
// relay deposit txs
|
||||
boolean depositTxsRelayed = false;
|
||||
List<String> txHashes = new ArrayList<>();
|
||||
try {
|
||||
|
||||
// submit txs to pool but do not relay
|
||||
// submit maker tx to pool but do not relay
|
||||
MoneroSubmitTxResult makerResult = daemon.submitTxHex(processModel.getMaker().getDepositTxHex(), true);
|
||||
MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true);
|
||||
if (!makerResult.isGood()) throw new RuntimeException("Error submitting maker deposit tx: " + JsonUtils.serialize(makerResult));
|
||||
if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult));
|
||||
txHashes.add(processModel.getMaker().getDepositTxHash());
|
||||
|
||||
// submit taker tx to pool but do not relay
|
||||
if (!trade.hasBuyerAsTakerWithoutDeposit()) {
|
||||
MoneroSubmitTxResult takerResult = daemon.submitTxHex(processModel.getTaker().getDepositTxHex(), true);
|
||||
if (!takerResult.isGood()) throw new RuntimeException("Error submitting taker deposit tx: " + JsonUtils.serialize(takerResult));
|
||||
txHashes.add(processModel.getTaker().getDepositTxHash());
|
||||
}
|
||||
|
||||
// relay txs
|
||||
daemon.relayTxsByHash(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()));
|
||||
daemon.relayTxsByHash(txHashes);
|
||||
depositTxsRelayed = true;
|
||||
|
||||
// update trade state
|
||||
@ -160,7 +175,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
|
||||
// flush txs from pool
|
||||
try {
|
||||
daemon.flushTxPool(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash());
|
||||
daemon.flushTxPool(txHashes);
|
||||
} catch (Exception e2) {
|
||||
log.warn("Error flushing deposit txs from pool for trade {}: {}\n", trade.getId(), e2.getMessage(), e2);
|
||||
}
|
||||
@ -180,7 +195,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
});
|
||||
|
||||
if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId());
|
||||
if (processModel.getTaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
|
||||
if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,10 +211,9 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
|
||||
// log error
|
||||
if (errorMessage != null) {
|
||||
log.warn("Sending deposit responses with error={}", errorMessage);
|
||||
Thread.dumpStack();
|
||||
log.warn("Sending deposit responses with error={}", errorMessage, new Throwable("Stack trace"));
|
||||
}
|
||||
|
||||
|
||||
// create deposit response
|
||||
DepositResponse response = new DepositResponse(
|
||||
trade.getOffer().getId(),
|
||||
|
@ -53,38 +53,44 @@ public class ArbitratorProcessReserveTx extends TradeTask {
|
||||
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
|
||||
boolean isFromMaker = sender == trade.getMaker();
|
||||
boolean isFromBuyer = isFromMaker ? offer.getDirection() == OfferDirection.BUY : offer.getDirection() == OfferDirection.SELL;
|
||||
sender = isFromMaker ? processModel.getMaker() : processModel.getTaker();
|
||||
BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
sender.setSecurityDeposit(securityDeposit);
|
||||
|
||||
// TODO (woodser): if signer online, should never be called by maker?
|
||||
|
||||
// process reserve tx with expected values
|
||||
BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct());
|
||||
BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee();
|
||||
BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount
|
||||
BigInteger securityDeposit = isFromMaker ? isFromBuyer ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit() : isFromBuyer ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
MoneroTx verifiedTx;
|
||||
try {
|
||||
verifiedTx = trade.getXmrWalletService().verifyReserveTx(
|
||||
offer.getId(),
|
||||
penaltyFee,
|
||||
tradeFee,
|
||||
sendAmount,
|
||||
securityDeposit,
|
||||
request.getPayoutAddress(),
|
||||
request.getReserveTxHash(),
|
||||
request.getReserveTxHex(),
|
||||
request.getReserveTxKey(),
|
||||
null);
|
||||
} catch (Exception e) {
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||
}
|
||||
// process reserve tx unless from buyer as taker without deposit
|
||||
boolean isFromBuyerAsTakerWithoutDeposit = isFromBuyer && !isFromMaker && trade.hasBuyerAsTakerWithoutDeposit();
|
||||
if (!isFromBuyerAsTakerWithoutDeposit) {
|
||||
|
||||
// save reserve tx to model
|
||||
TradePeer trader = isFromMaker ? processModel.getMaker() : processModel.getTaker();
|
||||
trader.setSecurityDeposit(securityDeposit.subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||
trader.setReserveTxHash(request.getReserveTxHash());
|
||||
trader.setReserveTxHex(request.getReserveTxHex());
|
||||
trader.setReserveTxKey(request.getReserveTxKey());
|
||||
// process reserve tx with expected values
|
||||
BigInteger penaltyFee = HavenoUtils.multiply(isFromMaker ? offer.getAmount() : trade.getAmount(), offer.getPenaltyFeePct());
|
||||
BigInteger tradeFee = isFromMaker ? offer.getMaxMakerFee() : trade.getTakerFee();
|
||||
BigInteger sendAmount = isFromBuyer ? BigInteger.ZERO : isFromMaker ? offer.getAmount() : trade.getAmount(); // maker reserve tx is for offer amount
|
||||
MoneroTx verifiedTx;
|
||||
try {
|
||||
verifiedTx = trade.getXmrWalletService().verifyReserveTx(
|
||||
offer.getId(),
|
||||
penaltyFee,
|
||||
tradeFee,
|
||||
sendAmount,
|
||||
securityDeposit,
|
||||
request.getPayoutAddress(),
|
||||
request.getReserveTxHash(),
|
||||
request.getReserveTxHex(),
|
||||
request.getReserveTxKey(),
|
||||
null);
|
||||
} catch (Exception e) {
|
||||
log.error(ExceptionUtils.getStackTrace(e));
|
||||
throw new RuntimeException("Error processing reserve tx from " + (isFromMaker ? "maker " : "taker ") + processModel.getTempTradePeerNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
// save reserve tx to model
|
||||
sender.setSecurityDeposit(sender.getSecurityDeposit().subtract(verifiedTx.getFee())); // subtract mining fee from security deposit
|
||||
sender.setReserveTxHash(request.getReserveTxHash());
|
||||
sender.setReserveTxHex(request.getReserveTxHex());
|
||||
sender.setReserveTxKey(request.getReserveTxKey());
|
||||
}
|
||||
|
||||
// persist trade
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
@ -78,6 +78,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
// send request to taker
|
||||
@ -118,7 +119,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
|
||||
|
||||
// ensure arbitrator has reserve txs
|
||||
if (processModel.getMaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have maker's reserve tx after initializing trade");
|
||||
if (processModel.getTaker().getReserveTxHash() == null) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade");
|
||||
if (processModel.getTaker().getReserveTxHash() == null && !trade.hasBuyerAsTakerWithoutDeposit()) throw new RuntimeException("Arbitrator does not have taker's reserve tx after initializing trade");
|
||||
|
||||
// create wallet for multisig
|
||||
MoneroWallet multisigWallet = trade.createWallet();
|
||||
|
@ -74,7 +74,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
Preconditions.checkNotNull(trade.getSeller().getPaymentAccountPayload(), "Seller's payment account payload is null");
|
||||
Preconditions.checkNotNull(trade.getAmount(), "trade.getTradeAmount() must not be null");
|
||||
Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null");
|
||||
Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
|
||||
if (!trade.hasBuyerAsTakerWithoutDeposit()) Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
|
||||
checkNotNull(trade.getOffer(), "offer must not be null");
|
||||
|
||||
// create payout tx if we have seller's updated multisig hex
|
||||
|
@ -138,7 +138,8 @@ public class MakerSendInitTradeRequestToArbitrator extends TradeTask {
|
||||
trade.getSelf().getReserveTxHash(),
|
||||
trade.getSelf().getReserveTxHex(),
|
||||
trade.getSelf().getReserveTxKey(),
|
||||
model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString());
|
||||
model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(),
|
||||
trade.getChallenge());
|
||||
|
||||
// send request to arbitrator
|
||||
log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress());
|
||||
|
@ -83,7 +83,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId());
|
||||
trade.startProtocolTimeout();
|
||||
|
||||
// collect relevant info
|
||||
// collect info
|
||||
Integer subaddressIndex = null;
|
||||
boolean reserveExactAmount = false;
|
||||
if (trade instanceof MakerTrade) {
|
||||
@ -97,53 +97,60 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
||||
}
|
||||
|
||||
// attempt creating deposit tx
|
||||
try {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||
if (!trade.isBuyerAsTakerWithoutDeposit()) {
|
||||
try {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
|
||||
// check for timeout
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
if (depositTx != null) break;
|
||||
}
|
||||
|
||||
// check for timeout
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
|
||||
if (depositTx != null) break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
// thaw deposit inputs
|
||||
if (depositTx != null) {
|
||||
trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx));
|
||||
trade.getSelf().setReserveTxKeyImages(null);
|
||||
}
|
||||
|
||||
// re-freeze maker offer inputs
|
||||
if (trade instanceof MakerTrade) {
|
||||
trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages());
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
// thaw deposit inputs
|
||||
if (depositTx != null) {
|
||||
trade.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(depositTx));
|
||||
trade.getSelf().setReserveTxKeyImages(null);
|
||||
}
|
||||
|
||||
// re-freeze maker offer inputs
|
||||
if (trade instanceof MakerTrade) {
|
||||
trade.getXmrWalletService().freezeOutputs(trade.getOffer().getOfferPayload().getReserveTxKeyImages());
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
// reset protocol timeout
|
||||
trade.addInitProgressStep();
|
||||
|
||||
// update trade state
|
||||
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
|
||||
trade.getSelf().setDepositTx(depositTx);
|
||||
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
||||
trade.getSelf().setDepositTxFee(depositTx.getFee());
|
||||
trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx));
|
||||
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
|
||||
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
|
||||
trade.getSelf().setPaymentAccountPayloadHash(trade.getSelf().getPaymentAccountPayload().getHash());
|
||||
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
if (depositTx == null) {
|
||||
trade.getSelf().setSecurityDeposit(securityDeposit);
|
||||
} else {
|
||||
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
|
||||
trade.getSelf().setDepositTx(depositTx);
|
||||
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
||||
trade.getSelf().setDepositTxFee(depositTx.getFee());
|
||||
trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(depositTx));
|
||||
}
|
||||
}
|
||||
|
||||
// maker signs deposit hash nonce to avoid challenge protocol
|
||||
@ -161,7 +168,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
||||
trade.getProcessModel().getAccountId(),
|
||||
trade.getSelf().getPaymentAccountPayload().getHash(),
|
||||
trade.getSelf().getPayoutAddressString(),
|
||||
depositTx.getHash(),
|
||||
depositTx == null ? null : depositTx.getHash(),
|
||||
sig);
|
||||
|
||||
// send request to trading peer
|
||||
|
@ -63,20 +63,20 @@ public class ProcessSignContractRequest extends TradeTask {
|
||||
// extract fields from request
|
||||
// TODO (woodser): verify request and from maker or taker
|
||||
SignContractRequest request = (SignContractRequest) processModel.getTradeMessage();
|
||||
TradePeer trader = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
|
||||
trader.setDepositTxHash(request.getDepositTxHash());
|
||||
trader.setAccountId(request.getAccountId());
|
||||
trader.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash());
|
||||
trader.setPayoutAddressString(request.getPayoutAddress());
|
||||
TradePeer sender = trade.getTradePeer(processModel.getTempTradePeerNodeAddress());
|
||||
sender.setDepositTxHash(request.getDepositTxHash());
|
||||
sender.setAccountId(request.getAccountId());
|
||||
sender.setPaymentAccountPayloadHash(request.getPaymentAccountPayloadHash());
|
||||
sender.setPayoutAddressString(request.getPayoutAddress());
|
||||
|
||||
// maker sends witness signature of deposit tx hash
|
||||
if (trader == trade.getMaker()) {
|
||||
trader.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8));
|
||||
trader.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash());
|
||||
if (sender == trade.getMaker()) {
|
||||
sender.setAccountAgeWitnessNonce(request.getDepositTxHash().getBytes(Charsets.UTF_8));
|
||||
sender.setAccountAgeWitnessSignature(request.getAccountAgeWitnessSignatureOfDepositHash());
|
||||
}
|
||||
|
||||
// sign contract only when both deposit txs hashes known
|
||||
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) {
|
||||
// sign contract only when received from both peers
|
||||
if (processModel.getMaker().getPaymentAccountPayloadHash() == null || processModel.getTaker().getPaymentAccountPayloadHash() == null) {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
@ -82,8 +82,8 @@ public class SendDepositRequest extends TradeTask {
|
||||
Version.getP2PMessageVersion(),
|
||||
new Date().getTime(),
|
||||
trade.getSelf().getContractSignature(),
|
||||
trade.getSelf().getDepositTx().getFullHex(),
|
||||
trade.getSelf().getDepositTx().getKey(),
|
||||
trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getFullHex(),
|
||||
trade.getSelf().getDepositTx() == null ? null : trade.getSelf().getDepositTx().getKey(),
|
||||
trade.getSelf().getPaymentAccountKey());
|
||||
|
||||
// update trade state
|
||||
|
@ -47,62 +47,63 @@ public class TakerReserveTradeFunds extends TradeTask {
|
||||
throw new RuntimeException("Expected taker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen.");
|
||||
}
|
||||
|
||||
// create reserve tx
|
||||
// create reserve tx unless deposit not required from buyer as taker
|
||||
MoneroTxWallet reserveTx = null;
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
if (!trade.isBuyerAsTakerWithoutDeposit()) {
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
|
||||
// check for timeout
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
|
||||
trade.startProtocolTimeout();
|
||||
// check for timeout
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
|
||||
trade.startProtocolTimeout();
|
||||
|
||||
// collect relevant info
|
||||
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
|
||||
BigInteger takerFee = trade.getTakerFee();
|
||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
|
||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
|
||||
String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
// collect relevant info
|
||||
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
|
||||
BigInteger takerFee = trade.getTakerFee();
|
||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
|
||||
BigInteger securityDeposit = trade.getSecurityDepositBeforeMiningFee();
|
||||
String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
|
||||
// attempt creating reserve tx
|
||||
try {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||
// attempt creating reserve tx
|
||||
try {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
|
||||
// check for timeout
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
if (reserveTx != null) break;
|
||||
}
|
||||
|
||||
// check for timeout
|
||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
|
||||
if (reserveTx != null) break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
|
||||
// reset state with wallet lock
|
||||
model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId());
|
||||
if (reserveTx != null) {
|
||||
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
|
||||
trade.getSelf().setReserveTxKeyImages(null);
|
||||
// reset state with wallet lock
|
||||
model.getXmrWalletService().resetAddressEntriesForTrade(trade.getId());
|
||||
if (reserveTx != null) {
|
||||
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
|
||||
trade.getSelf().setReserveTxKeyImages(null);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw e;
|
||||
// reset protocol timeout
|
||||
trade.startProtocolTimeout();
|
||||
|
||||
// update trade state
|
||||
trade.getTaker().setReserveTxHash(reserveTx.getHash());
|
||||
trade.getTaker().setReserveTxHex(reserveTx.getFullHex());
|
||||
trade.getTaker().setReserveTxKey(reserveTx.getKey());
|
||||
trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
|
||||
}
|
||||
|
||||
|
||||
// reset protocol timeout
|
||||
trade.startProtocolTimeout();
|
||||
|
||||
// update trade state
|
||||
trade.getTaker().setReserveTxHash(reserveTx.getHash());
|
||||
trade.getTaker().setReserveTxHex(reserveTx.getFullHex());
|
||||
trade.getTaker().setReserveTxKey(reserveTx.getKey());
|
||||
trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
|
||||
}
|
||||
|
||||
// save process state
|
||||
|
@ -48,7 +48,9 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
|
||||
InitTradeRequest sourceRequest = (InitTradeRequest) processModel.getTradeMessage(); // arbitrator's InitTradeRequest to taker
|
||||
checkNotNull(sourceRequest);
|
||||
checkTradeId(processModel.getOfferId(), sourceRequest);
|
||||
if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
|
||||
if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) {
|
||||
throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
|
||||
}
|
||||
|
||||
// create request to arbitrator
|
||||
Offer offer = processModel.getOffer();
|
||||
@ -73,7 +75,8 @@ public class TakerSendInitTradeRequestToArbitrator extends TradeTask {
|
||||
trade.getSelf().getReserveTxHash(),
|
||||
trade.getSelf().getReserveTxHex(),
|
||||
trade.getSelf().getReserveTxKey(),
|
||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString());
|
||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
||||
trade.getChallenge());
|
||||
|
||||
// send request to arbitrator
|
||||
log.info("Sending {} with offerId {} and uid {} to arbitrator {}", arbitratorRequest.getClass().getSimpleName(), arbitratorRequest.getOfferId(), arbitratorRequest.getUid(), trade.getArbitrator().getNodeAddress());
|
||||
|
@ -47,7 +47,9 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask {
|
||||
runInterceptHook();
|
||||
|
||||
// verify trade state
|
||||
if (trade.getSelf().getReserveTxHash() == null || trade.getSelf().getReserveTxHash().isEmpty()) throw new IllegalStateException("Reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
|
||||
if (!trade.isBuyerAsTakerWithoutDeposit() && trade.getSelf().getReserveTxHash() == null) {
|
||||
throw new IllegalStateException("Taker reserve tx id is not initialized: " + trade.getSelf().getReserveTxHash());
|
||||
}
|
||||
|
||||
// collect fields
|
||||
Offer offer = model.getOffer();
|
||||
@ -55,6 +57,7 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask {
|
||||
P2PService p2PService = processModel.getP2PService();
|
||||
XmrWalletService walletService = model.getXmrWalletService();
|
||||
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
String challenge = model.getChallenge();
|
||||
|
||||
// taker signs offer using offer id as nonce to avoid challenge protocol
|
||||
byte[] sig = HavenoUtils.sign(p2PService.getKeyRing(), offer.getId());
|
||||
@ -81,7 +84,8 @@ public class TakerSendInitTradeRequestToMaker extends TradeTask {
|
||||
null, // reserve tx not sent from taker to maker
|
||||
null,
|
||||
null,
|
||||
payoutAddress);
|
||||
payoutAddress,
|
||||
challenge);
|
||||
|
||||
// send request to maker
|
||||
log.info("Sending {} with offerId {} and uid {} to maker {}", makerRequest.getClass().getSimpleName(), makerRequest.getOfferId(), makerRequest.getUid(), trade.getMaker().getNodeAddress());
|
||||
|
@ -616,14 +616,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public void setBuyerSecurityDepositAsPercent(double buyerSecurityDepositAsPercent, PaymentAccount paymentAccount) {
|
||||
double max = Restrictions.getMaxBuyerSecurityDepositAsPercent();
|
||||
double min = Restrictions.getMinBuyerSecurityDepositAsPercent();
|
||||
public void setSecurityDepositAsPercent(double securityDepositAsPercent, PaymentAccount paymentAccount) {
|
||||
double max = Restrictions.getMaxSecurityDepositAsPercent();
|
||||
double min = Restrictions.getMinSecurityDepositAsPercent();
|
||||
|
||||
if (PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount))
|
||||
prefPayload.setBuyerSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent)));
|
||||
prefPayload.setSecurityDepositAsPercentForCrypto(Math.min(max, Math.max(min, securityDepositAsPercent)));
|
||||
else
|
||||
prefPayload.setBuyerSecurityDepositAsPercent(Math.min(max, Math.max(min, buyerSecurityDepositAsPercent)));
|
||||
prefPayload.setSecurityDepositAsPercent(Math.min(max, Math.max(min, securityDepositAsPercent)));
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
@ -755,6 +755,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public void setShowPrivateOffers(boolean value) {
|
||||
prefPayload.setShowPrivateOffers(value);
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
public void setDenyApiTaker(boolean value) {
|
||||
prefPayload.setDenyApiTaker(value);
|
||||
requestPersistence();
|
||||
@ -838,16 +843,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
||||
return prefPayload.isSplitOfferOutput();
|
||||
}
|
||||
|
||||
public double getBuyerSecurityDepositAsPercent(PaymentAccount paymentAccount) {
|
||||
public double getSecurityDepositAsPercent(PaymentAccount paymentAccount) {
|
||||
double value = PaymentAccountUtil.isCryptoCurrencyAccount(paymentAccount) ?
|
||||
prefPayload.getBuyerSecurityDepositAsPercentForCrypto() : prefPayload.getBuyerSecurityDepositAsPercent();
|
||||
prefPayload.getSecurityDepositAsPercentForCrypto() : prefPayload.getSecurityDepositAsPercent();
|
||||
|
||||
if (value < Restrictions.getMinBuyerSecurityDepositAsPercent()) {
|
||||
value = Restrictions.getMinBuyerSecurityDepositAsPercent();
|
||||
setBuyerSecurityDepositAsPercent(value, paymentAccount);
|
||||
if (value < Restrictions.getMinSecurityDepositAsPercent()) {
|
||||
value = Restrictions.getMinSecurityDepositAsPercent();
|
||||
setSecurityDepositAsPercent(value, paymentAccount);
|
||||
}
|
||||
|
||||
return value == 0 ? Restrictions.getDefaultBuyerSecurityDepositAsPercent() : value;
|
||||
return value == 0 ? Restrictions.getDefaultSecurityDepositAsPercent() : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -41,7 +41,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
|
||||
import static haveno.core.xmr.wallet.Restrictions.getDefaultSecurityDepositAsPercent;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@ -120,10 +120,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
private String rpcPw;
|
||||
@Nullable
|
||||
private String takeOfferSelectedPaymentAccountId;
|
||||
private double buyerSecurityDepositAsPercent = getDefaultBuyerSecurityDepositAsPercent();
|
||||
private double securityDepositAsPercent = getDefaultSecurityDepositAsPercent();
|
||||
private int ignoreDustThreshold = 600;
|
||||
private int clearDataAfterDays = Preferences.CLEAR_DATA_AFTER_DAYS_INITIAL;
|
||||
private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent();
|
||||
private double securityDepositAsPercentForCrypto = getDefaultSecurityDepositAsPercent();
|
||||
private int blockNotifyPort;
|
||||
private boolean tacAcceptedV120;
|
||||
private double bsqAverageTrimThreshold = 0.05;
|
||||
@ -134,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
// Added in 1.5.5
|
||||
private boolean hideNonAccountPaymentMethods;
|
||||
private boolean showOffersMatchingMyAccounts;
|
||||
private boolean showPrivateOffers;
|
||||
private boolean denyApiTaker;
|
||||
private boolean notifyOnPreRelease;
|
||||
|
||||
@ -193,10 +194,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
.setUseStandbyMode(useStandbyMode)
|
||||
.setUseSoundForNotifications(useSoundForNotifications)
|
||||
.setUseSoundForNotificationsInitialized(useSoundForNotificationsInitialized)
|
||||
.setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent)
|
||||
.setSecurityDepositAsPercent(securityDepositAsPercent)
|
||||
.setIgnoreDustThreshold(ignoreDustThreshold)
|
||||
.setClearDataAfterDays(clearDataAfterDays)
|
||||
.setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto)
|
||||
.setSecurityDepositAsPercentForCrypto(securityDepositAsPercentForCrypto)
|
||||
.setBlockNotifyPort(blockNotifyPort)
|
||||
.setTacAcceptedV120(tacAcceptedV120)
|
||||
.setBsqAverageTrimThreshold(bsqAverageTrimThreshold)
|
||||
@ -205,6 +206,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
.collect(Collectors.toList()))
|
||||
.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods)
|
||||
.setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts)
|
||||
.setShowPrivateOffers(showPrivateOffers)
|
||||
.setDenyApiTaker(denyApiTaker)
|
||||
.setNotifyOnPreRelease(notifyOnPreRelease);
|
||||
|
||||
@ -297,10 +299,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(),
|
||||
proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(),
|
||||
proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(),
|
||||
proto.getBuyerSecurityDepositAsPercent(),
|
||||
proto.getSecurityDepositAsPercent(),
|
||||
proto.getIgnoreDustThreshold(),
|
||||
proto.getClearDataAfterDays(),
|
||||
proto.getBuyerSecurityDepositAsPercentForCrypto(),
|
||||
proto.getSecurityDepositAsPercentForCrypto(),
|
||||
proto.getBlockNotifyPort(),
|
||||
proto.getTacAcceptedV120(),
|
||||
proto.getBsqAverageTrimThreshold(),
|
||||
@ -310,6 +312,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
|
||||
.collect(Collectors.toList())),
|
||||
proto.getHideNonAccountPaymentMethods(),
|
||||
proto.getShowOffersMatchingMyAccounts(),
|
||||
proto.getShowPrivateOffers(),
|
||||
proto.getDenyApiTaker(),
|
||||
proto.getNotifyOnPreRelease(),
|
||||
XmrNodeSettings.fromProto(proto.getXmrNodeSettings())
|
||||
|
@ -22,7 +22,6 @@ import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import haveno.common.util.MathUtils;
|
||||
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;
|
||||
@ -69,27 +68,8 @@ public class PriceUtil {
|
||||
if (!result.isValid) {
|
||||
return result;
|
||||
}
|
||||
|
||||
long triggerPriceAsLong = PriceUtil.getMarketPriceAsLong(triggerPriceAsString, marketPrice.getCurrencyCode());
|
||||
long marketPriceAsLong = PriceUtil.getMarketPriceAsLong("" + marketPrice.getPrice(), marketPrice.getCurrencyCode());
|
||||
String marketPriceAsString = FormattingUtils.formatMarketPrice(marketPrice.getPrice(), marketPrice.getCurrencyCode());
|
||||
|
||||
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
|
||||
if ((isSellOffer && !isCryptoCurrency) || (!isSellOffer && isCryptoCurrency)) {
|
||||
if (triggerPriceAsLong >= marketPriceAsLong) {
|
||||
return new InputValidator.ValidationResult(false,
|
||||
Res.get("createOffer.triggerPrice.invalid.tooHigh", marketPriceAsString));
|
||||
} else {
|
||||
return new InputValidator.ValidationResult(true);
|
||||
}
|
||||
} else {
|
||||
if (triggerPriceAsLong <= marketPriceAsLong) {
|
||||
return new InputValidator.ValidationResult(false,
|
||||
Res.get("createOffer.triggerPrice.invalid.tooLow", marketPriceAsString));
|
||||
} else {
|
||||
return new InputValidator.ValidationResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
return new InputValidator.ValidationResult(true);
|
||||
}
|
||||
|
||||
public static Price marketPriceToPrice(MarketPrice marketPrice) {
|
||||
|
@ -47,46 +47,46 @@ public class CoinUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value Btc amount to be converted to percent value. E.g. 0.01 BTC is 1% (of 1 BTC)
|
||||
* @param value Xmr amount to be converted to percent value. E.g. 0.01 XMR is 1% (of 1 XMR)
|
||||
* @return The percentage value as double (e.g. 1% is 0.01)
|
||||
*/
|
||||
public static double getAsPercentPerBtc(BigInteger value) {
|
||||
return getAsPercentPerBtc(value, HavenoUtils.xmrToAtomicUnits(1.0));
|
||||
public static double getAsPercentPerXmr(BigInteger value) {
|
||||
return getAsPercentPerXmr(value, HavenoUtils.xmrToAtomicUnits(1.0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param part Btc amount to be converted to percent value, based on total value passed.
|
||||
* E.g. 0.1 BTC is 25% (of 0.4 BTC)
|
||||
* @param total Total Btc amount the percentage part is calculated from
|
||||
* @param part Xmr amount to be converted to percent value, based on total value passed.
|
||||
* E.g. 0.1 XMR is 25% (of 0.4 XMR)
|
||||
* @param total Total Xmr amount the percentage part is calculated from
|
||||
*
|
||||
* @return The percentage value as double (e.g. 1% is 0.01)
|
||||
*/
|
||||
public static double getAsPercentPerBtc(BigInteger part, BigInteger total) {
|
||||
public static double getAsPercentPerXmr(BigInteger part, BigInteger total) {
|
||||
return MathUtils.roundDouble(HavenoUtils.divide(part == null ? BigInteger.ZERO : part, total == null ? BigInteger.valueOf(1) : total), 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param percent The percentage value as double (e.g. 1% is 0.01)
|
||||
* @param amount The amount as atomic units for the percentage calculation
|
||||
* @return The percentage as atomic units (e.g. 1% of 1 BTC is 0.01 BTC)
|
||||
* @return The percentage as atomic units (e.g. 1% of 1 XMR is 0.01 XMR)
|
||||
*/
|
||||
public static BigInteger getPercentOfAmount(double percent, BigInteger amount) {
|
||||
if (amount == null) amount = BigInteger.ZERO;
|
||||
return BigDecimal.valueOf(percent).multiply(new BigDecimal(amount)).setScale(8, RoundingMode.DOWN).toBigInteger();
|
||||
}
|
||||
|
||||
public static BigInteger getRoundedAmount(BigInteger amount, Price price, long maxTradeLimit, String currencyCode, String paymentMethodId) {
|
||||
if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) {
|
||||
return getRoundedAtmCashAmount(amount, price, maxTradeLimit);
|
||||
} else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) {
|
||||
return getRoundedAmountUnit(amount, price, maxTradeLimit);
|
||||
} else if (CurrencyUtil.isFiatCurrency(currencyCode)) {
|
||||
return getRoundedAmount4Decimals(amount, price, maxTradeLimit);
|
||||
public static BigInteger getRoundedAmount(BigInteger amount, Price price, Long maxTradeLimit, String currencyCode, String paymentMethodId) {
|
||||
if (price != null) {
|
||||
if (PaymentMethod.isRoundedForAtmCash(paymentMethodId)) {
|
||||
return getRoundedAtmCashAmount(amount, price, maxTradeLimit);
|
||||
} else if (CurrencyUtil.isVolumeRoundedToNearestUnit(currencyCode)) {
|
||||
return getRoundedAmountUnit(amount, price, maxTradeLimit);
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
return getRoundedAmount4Decimals(amount, maxTradeLimit);
|
||||
}
|
||||
|
||||
public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, long maxTradeLimit) {
|
||||
public static BigInteger getRoundedAtmCashAmount(BigInteger amount, Price price, Long maxTradeLimit) {
|
||||
return getAdjustedAmount(amount, price, maxTradeLimit, 10);
|
||||
}
|
||||
|
||||
@ -99,12 +99,12 @@ public class CoinUtil {
|
||||
* @param maxTradeLimit The max. trade limit of the users account, in atomic units.
|
||||
* @return The adjusted amount
|
||||
*/
|
||||
public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, long maxTradeLimit) {
|
||||
public static BigInteger getRoundedAmountUnit(BigInteger amount, Price price, Long maxTradeLimit) {
|
||||
return getAdjustedAmount(amount, price, maxTradeLimit, 1);
|
||||
}
|
||||
|
||||
public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Price price, long maxTradeLimit) {
|
||||
DecimalFormat decimalFormat = new DecimalFormat("#.####");
|
||||
public static BigInteger getRoundedAmount4Decimals(BigInteger amount, Long maxTradeLimit) {
|
||||
DecimalFormat decimalFormat = new DecimalFormat("#.####", HavenoUtils.DECIMAL_FORMAT_SYMBOLS);
|
||||
double roundedXmrAmount = Double.parseDouble(decimalFormat.format(HavenoUtils.atomicUnitsToXmr(amount)));
|
||||
return HavenoUtils.xmrToAtomicUnits(roundedXmrAmount);
|
||||
}
|
||||
@ -121,7 +121,7 @@ public class CoinUtil {
|
||||
* @return The adjusted amount
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static BigInteger getAdjustedAmount(BigInteger amount, Price price, long maxTradeLimit, int factor) {
|
||||
static BigInteger getAdjustedAmount(BigInteger amount, Price price, Long maxTradeLimit, int factor) {
|
||||
checkArgument(
|
||||
amount.longValueExact() >= Restrictions.getMinTradeAmount().longValueExact(),
|
||||
"amount needs to be above minimum of " + HavenoUtils.atomicUnitsToXmr(Restrictions.getMinTradeAmount()) + " xmr"
|
||||
@ -163,11 +163,13 @@ public class CoinUtil {
|
||||
|
||||
// If we are above our trade limit we reduce the amount by the smallestUnitForAmount
|
||||
BigInteger smallestUnitForAmountUnadjusted = price.getAmountByVolume(smallestUnitForVolume);
|
||||
while (adjustedAmount > maxTradeLimit) {
|
||||
adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact();
|
||||
if (maxTradeLimit != null) {
|
||||
while (adjustedAmount > maxTradeLimit) {
|
||||
adjustedAmount -= smallestUnitForAmountUnadjusted.longValueExact();
|
||||
}
|
||||
}
|
||||
adjustedAmount = Math.max(minTradeAmount, adjustedAmount);
|
||||
adjustedAmount = Math.min(maxTradeLimit, adjustedAmount);
|
||||
if (maxTradeLimit != null) adjustedAmount = Math.min(maxTradeLimit, adjustedAmount);
|
||||
return BigInteger.valueOf(adjustedAmount);
|
||||
}
|
||||
}
|
||||
|
@ -24,11 +24,13 @@ import org.bitcoinj.core.Coin;
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class Restrictions {
|
||||
|
||||
// configure restrictions
|
||||
public static final double MIN_SECURITY_DEPOSIT_PCT = 0.15;
|
||||
public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5;
|
||||
public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1);
|
||||
public static BigInteger MIN_BUYER_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1);
|
||||
// For the seller we use a fixed one as there is no way the seller can cancel the trade
|
||||
// To make it editable would just increase complexity.
|
||||
public static BigInteger MIN_SELLER_SECURITY_DEPOSIT = MIN_BUYER_SECURITY_DEPOSIT;
|
||||
public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1);
|
||||
|
||||
// At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the
|
||||
// mediated payout. For Refund agent cases we do not have that restriction.
|
||||
private static BigInteger MIN_REFUND_AT_MEDIATED_DISPUTE;
|
||||
@ -53,31 +55,20 @@ public class Restrictions {
|
||||
return MIN_TRADE_AMOUNT;
|
||||
}
|
||||
|
||||
public static double getDefaultBuyerSecurityDepositAsPercent() {
|
||||
return 0.15; // 15% of trade amount.
|
||||
public static double getDefaultSecurityDepositAsPercent() {
|
||||
return MIN_SECURITY_DEPOSIT_PCT;
|
||||
}
|
||||
|
||||
public static double getMinBuyerSecurityDepositAsPercent() {
|
||||
return 0.15; // 15% of trade amount.
|
||||
public static double getMinSecurityDepositAsPercent() {
|
||||
return MIN_SECURITY_DEPOSIT_PCT;
|
||||
}
|
||||
|
||||
public static double getMaxBuyerSecurityDepositAsPercent() {
|
||||
return 0.5; // 50% of trade amount. For a 1 BTC trade it is about 3500 USD @ 7000 USD/BTC
|
||||
public static double getMaxSecurityDepositAsPercent() {
|
||||
return MAX_SECURITY_DEPOSIT_PCT;
|
||||
}
|
||||
|
||||
// We use MIN_BUYER_SECURITY_DEPOSIT as well as lower bound in case of small trade amounts.
|
||||
// So 0.0005 BTC is the min. buyer security deposit even with amount of 0.0001 BTC and 0.05% percentage value.
|
||||
public static BigInteger getMinBuyerSecurityDeposit() {
|
||||
return MIN_BUYER_SECURITY_DEPOSIT;
|
||||
}
|
||||
|
||||
|
||||
public static double getSellerSecurityDepositAsPercent() {
|
||||
return 0.15; // 15% of trade amount.
|
||||
}
|
||||
|
||||
public static BigInteger getMinSellerSecurityDeposit() {
|
||||
return MIN_SELLER_SECURITY_DEPOSIT;
|
||||
public static BigInteger getMinSecurityDeposit() {
|
||||
return MIN_SECURITY_DEPOSIT;
|
||||
}
|
||||
|
||||
// This value must be lower than MIN_BUYER_SECURITY_DEPOSIT and SELLER_SECURITY_DEPOSIT
|
||||
|
@ -17,6 +17,7 @@ import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.common.TaskLooper;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.MoneroWallet;
|
||||
@ -24,16 +25,18 @@ import monero.wallet.MoneroWalletFull;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
|
||||
@Slf4j
|
||||
public class XmrWalletBase {
|
||||
public abstract class XmrWalletBase {
|
||||
|
||||
// constants
|
||||
public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120;
|
||||
public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100;
|
||||
public static final int SAVE_WALLET_DELAY_SECONDS = 300;
|
||||
|
||||
// inherited
|
||||
protected MoneroWallet wallet;
|
||||
@Getter
|
||||
protected final Object walletLock = new Object();
|
||||
protected Timer saveWalletDelayTimer;
|
||||
@Getter
|
||||
protected XmrConnectionService xmrConnectionService;
|
||||
protected boolean wasWalletSynced;
|
||||
@ -137,6 +140,34 @@ public class XmrWalletBase {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
|
||||
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void saveWalletWithDelay() {
|
||||
// delay writing to disk to avoid frequent write operations
|
||||
if (saveWalletDelayTimer == null) {
|
||||
saveWalletDelayTimer = UserThread.runAfter(() -> {
|
||||
requestSaveWallet();
|
||||
UserThread.execute(() -> saveWalletDelayTimer = null);
|
||||
}, SAVE_WALLET_DELAY_SECONDS, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------- ABSTRACT -----------------------------
|
||||
|
||||
public abstract void saveWallet();
|
||||
|
||||
public abstract void requestSaveWallet();
|
||||
|
||||
protected abstract void onConnectionChanged(MoneroRpcConnection connection);
|
||||
|
||||
// ------------------------------ PRIVATE HELPERS -------------------------
|
||||
|
||||
private void updateSyncProgress(long height, long targetHeight) {
|
||||
resetSyncProgressTimeout();
|
||||
UserThread.execute(() -> {
|
||||
|
@ -112,7 +112,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
public static final String MONERO_WALLET_RPC_NAME = Utilities.isWindows() ? "monero-wallet-rpc.exe" : "monero-wallet-rpc";
|
||||
public static final String MONERO_WALLET_RPC_PATH = MONERO_BINS_DIR + File.separator + MONERO_WALLET_RPC_NAME;
|
||||
public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee
|
||||
public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.ELEVATED;
|
||||
public static final MoneroTxPriority PROTOCOL_FEE_PRIORITY = MoneroTxPriority.DEFAULT;
|
||||
public static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable
|
||||
private static final MoneroNetworkType MONERO_NETWORK_TYPE = getMoneroNetworkType();
|
||||
private static final MoneroWalletRpcManager MONERO_WALLET_RPC_MANAGER = new MoneroWalletRpcManager();
|
||||
@ -245,16 +245,20 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
return user.getWalletCreationDate();
|
||||
}
|
||||
|
||||
public void saveMainWallet() {
|
||||
saveMainWallet(!(Utilities.isWindows() && wallet != null));
|
||||
@Override
|
||||
public void saveWallet() {
|
||||
saveWallet(!(Utilities.isWindows() && wallet != null));
|
||||
}
|
||||
|
||||
public void saveMainWallet(boolean backup) {
|
||||
saveWallet(getWallet(), backup);
|
||||
public void saveWallet(boolean backup) {
|
||||
synchronized (walletLock) {
|
||||
saveWallet(getWallet(), backup);
|
||||
}
|
||||
}
|
||||
|
||||
public void requestSaveMainWallet() {
|
||||
ThreadUtils.submitToPool(() -> saveMainWallet()); // save wallet off main thread
|
||||
@Override
|
||||
public void requestSaveWallet() {
|
||||
ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread
|
||||
}
|
||||
|
||||
public boolean isWalletAvailable() {
|
||||
@ -376,8 +380,8 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
}
|
||||
|
||||
public void saveWallet(MoneroWallet wallet, boolean backup) {
|
||||
wallet.save();
|
||||
if (backup) backupWallet(getWalletName(wallet.getPath()));
|
||||
wallet.save();
|
||||
}
|
||||
|
||||
public void closeWallet(MoneroWallet wallet, boolean save) {
|
||||
@ -385,8 +389,8 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
MoneroError err = null;
|
||||
String path = wallet.getPath();
|
||||
try {
|
||||
wallet.close(save);
|
||||
if (save) backupWallet(getWalletName(path));
|
||||
if (save) saveWallet(wallet, true);
|
||||
wallet.close();
|
||||
} catch (MoneroError e) {
|
||||
err = e;
|
||||
}
|
||||
@ -411,7 +415,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
public void deleteWallet(String walletName) {
|
||||
assertNotPath(walletName);
|
||||
log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName);
|
||||
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
||||
if (!walletExists(walletName)) throw new RuntimeException("Wallet does not exist at path: " + walletName);
|
||||
String path = walletDir.toString() + File.separator + walletName;
|
||||
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet cache file: " + path);
|
||||
if (!new File(path + KEYS_FILE_POSTFIX).delete()) throw new RuntimeException("Failed to delete wallet keys file: " + path + KEYS_FILE_POSTFIX);
|
||||
@ -443,7 +447,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
if (Boolean.TRUE.equals(txConfig.getRelay())) {
|
||||
cachedTxs.addFirst(tx);
|
||||
cacheWalletInfo();
|
||||
requestSaveMainWallet();
|
||||
requestSaveWallet();
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
@ -453,7 +457,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
public String relayTx(String metadata) {
|
||||
synchronized (walletLock) {
|
||||
String txId = wallet.relayTx(metadata);
|
||||
requestSaveMainWallet();
|
||||
requestSaveWallet();
|
||||
return txId;
|
||||
}
|
||||
}
|
||||
@ -552,7 +556,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
// freeze outputs
|
||||
for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage);
|
||||
cacheWalletInfo();
|
||||
requestSaveMainWallet();
|
||||
requestSaveWallet();
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,19 +578,10 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
// thaw outputs
|
||||
for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage);
|
||||
cacheWalletInfo();
|
||||
requestSaveMainWallet();
|
||||
requestSaveWallet();
|
||||
}
|
||||
}
|
||||
|
||||
public BigInteger getOutputsAmount(Collection<String> keyImages) {
|
||||
BigInteger sum = BigInteger.ZERO;
|
||||
for (String keyImage : keyImages) {
|
||||
List<MoneroOutputWallet> outputs = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
|
||||
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
private List<Integer> getSubaddressesWithExactInput(BigInteger amount) {
|
||||
|
||||
// fetch unspent, unfrozen, unlocked outputs
|
||||
@ -760,16 +755,16 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
if (keyImages != null) {
|
||||
Set<String> txKeyImages = new HashSet<String>();
|
||||
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
|
||||
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Tx inputs do not match claimed key images");
|
||||
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new RuntimeException("Tx inputs do not match claimed key images");
|
||||
}
|
||||
|
||||
// verify unlock height
|
||||
if (!BigInteger.ZERO.equals(tx.getUnlockTime())) throw new RuntimeException("Unlock height must be 0");
|
||||
|
||||
// verify miner fee
|
||||
BigInteger minerFeeEstimate = getElevatedFeeEstimate(tx.getWeight());
|
||||
BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight());
|
||||
double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue();
|
||||
if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee());
|
||||
if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff);
|
||||
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff);
|
||||
|
||||
// verify proof to fee address
|
||||
@ -786,11 +781,23 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
BigInteger actualSendAmount = transferCheck.getReceivedAmount();
|
||||
|
||||
// verify trade fee amount
|
||||
if (!actualTradeFee.equals(tradeFeeAmount)) throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee);
|
||||
if (!actualTradeFee.equals(tradeFeeAmount)) {
|
||||
if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) {
|
||||
log.warn("Trade tx fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee);
|
||||
}
|
||||
}
|
||||
|
||||
// verify send amount
|
||||
BigInteger expectedSendAmount = sendAmount.subtract(tx.getFee());
|
||||
if (!actualSendAmount.equals(expectedSendAmount)) throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee());
|
||||
if (!actualSendAmount.equals(expectedSendAmount)) {
|
||||
if (equalsWithinFractionError(actualSendAmount, expectedSendAmount)) {
|
||||
log.warn("Trade tx send amount is within fraction error, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee());
|
||||
} else {
|
||||
throw new RuntimeException("Invalid send amount, expected " + expectedSendAmount + " but was " + actualSendAmount + " with tx fee " + tx.getFee());
|
||||
}
|
||||
}
|
||||
return tx;
|
||||
} catch (Exception e) {
|
||||
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=\n" + tx) + ": " + e.getMessage());
|
||||
@ -806,17 +813,30 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: old bug in atomic unit conversion could cause fractional difference error, remove this in future release, maybe re-sign all offers then
|
||||
private static boolean equalsWithinFractionError(BigInteger a, BigInteger b) {
|
||||
return a.subtract(b).abs().compareTo(new BigInteger("1")) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tx fee estimate based on its weight.
|
||||
*
|
||||
* @param txWeight - the tx weight
|
||||
* @return the tx fee estimate
|
||||
*/
|
||||
private BigInteger getElevatedFeeEstimate(long txWeight) {
|
||||
private BigInteger getFeeEstimate(long txWeight) {
|
||||
|
||||
// get fee priority
|
||||
MoneroTxPriority priority;
|
||||
if (PROTOCOL_FEE_PRIORITY == MoneroTxPriority.DEFAULT) {
|
||||
priority = wallet.getDefaultFeePriority();
|
||||
} else {
|
||||
priority = PROTOCOL_FEE_PRIORITY;
|
||||
}
|
||||
|
||||
// get fee estimates per kB from daemon
|
||||
MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate();
|
||||
BigInteger baseFeeEstimate = feeEstimates.getFees().get(2); // get elevated fee per kB
|
||||
BigInteger baseFeeEstimate = feeEstimates.getFees().get(priority.ordinal() - 1);
|
||||
BigInteger qmask = feeEstimates.getQuantizationMask();
|
||||
log.info("Monero base fee estimate={}, qmask={}", baseFeeEstimate, qmask);
|
||||
|
||||
@ -1125,6 +1145,15 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
return subaddress == null ? BigInteger.ZERO : subaddress.getBalance();
|
||||
}
|
||||
|
||||
public BigInteger getBalanceForSubaddress(int subaddressIndex, boolean includeFrozen) {
|
||||
return getBalanceForSubaddress(subaddressIndex).add(includeFrozen ? getFrozenBalanceForSubaddress(subaddressIndex) : BigInteger.ZERO);
|
||||
}
|
||||
|
||||
public BigInteger getFrozenBalanceForSubaddress(int subaddressIndex) {
|
||||
List<MoneroOutputWallet> outputs = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setAccountIndex(0).setSubaddressIndex(subaddressIndex));
|
||||
return outputs.stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add);
|
||||
}
|
||||
|
||||
public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) {
|
||||
MoneroSubaddress subaddress = getSubaddress(subaddressIndex);
|
||||
return subaddress == null ? BigInteger.ZERO : subaddress.getUnlockedBalance();
|
||||
@ -1250,6 +1279,19 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
return filteredOutputs;
|
||||
}
|
||||
|
||||
public List<MoneroOutputWallet> getOutputs(Collection<String> keyImages) {
|
||||
List<MoneroOutputWallet> outputs = new ArrayList<MoneroOutputWallet>();
|
||||
for (String keyImage : keyImages) {
|
||||
List<MoneroOutputWallet> outputList = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
|
||||
if (!outputList.isEmpty()) outputs.add(outputList.get(0));
|
||||
}
|
||||
return outputs;
|
||||
}
|
||||
|
||||
public BigInteger getOutputsAmount(Collection<String> keyImages) {
|
||||
return getOutputs(keyImages).stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Util
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1321,11 +1363,12 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
|
||||
}
|
||||
|
||||
private void maybeInitMainWallet(boolean sync, int numAttempts) {
|
||||
private void maybeInitMainWallet(boolean sync, int numSyncAttempts) {
|
||||
ThreadUtils.execute(() -> {
|
||||
try {
|
||||
doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
|
||||
} catch (Exception e) {
|
||||
if (isShutDownStarted) return;
|
||||
log.warn("Error initializing main wallet: {}\n", e.getMessage(), e);
|
||||
HavenoUtils.setTopError(e.getMessage());
|
||||
throw e;
|
||||
@ -1333,7 +1376,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
}, THREAD_ID);
|
||||
}
|
||||
|
||||
private void doMaybeInitMainWallet(boolean sync, int numAttempts) {
|
||||
private void doMaybeInitMainWallet(boolean sync, int numSyncAttempts) {
|
||||
synchronized (walletLock) {
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
@ -1361,7 +1404,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
|
||||
// sync main wallet if applicable
|
||||
// TODO: error handling and re-initialization is jenky, refactor
|
||||
if (sync && numAttempts > 0) {
|
||||
if (sync && numSyncAttempts > 0) {
|
||||
try {
|
||||
|
||||
// switch connection if disconnected
|
||||
@ -1380,7 +1423,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
log.warn("Error syncing wallet with progress on startup: " + e.getMessage());
|
||||
forceCloseMainWallet();
|
||||
requestSwitchToNextBestConnection(sourceConnection);
|
||||
maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again
|
||||
maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again
|
||||
return;
|
||||
}
|
||||
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
|
||||
@ -1411,14 +1454,14 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
|
||||
|
||||
// save but skip backup on initialization
|
||||
saveMainWallet(false);
|
||||
saveWallet(false);
|
||||
} catch (Exception e) {
|
||||
if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized
|
||||
log.warn("Error initially syncing main wallet: {}", e.getMessage());
|
||||
if (numAttempts <= 1) {
|
||||
log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts);
|
||||
if (numSyncAttempts <= 1) {
|
||||
log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts);
|
||||
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
|
||||
saveMainWallet(false);
|
||||
saveWallet(false);
|
||||
|
||||
// reschedule to init main wallet
|
||||
UserThread.runAfter(() -> {
|
||||
@ -1427,7 +1470,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
} else {
|
||||
log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000);
|
||||
UserThread.runAfter(() -> {
|
||||
maybeInitMainWallet(true, numAttempts - 1);
|
||||
maybeInitMainWallet(true, numSyncAttempts - 1);
|
||||
}, xmrConnectionService.getRefreshPeriodMs() / 1000);
|
||||
}
|
||||
}
|
||||
@ -1493,10 +1536,11 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
// try opening wallet
|
||||
config.setNetworkType(getMoneroNetworkType());
|
||||
config.setServer(connection);
|
||||
log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri());
|
||||
log.info("Opening full wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri());
|
||||
try {
|
||||
walletFull = MoneroWalletFull.openWallet(config);
|
||||
} catch (Exception e) {
|
||||
if (isShutDownStarted) throw e;
|
||||
log.warn("Failed to open full wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage());
|
||||
boolean retrySuccessful = false;
|
||||
try {
|
||||
@ -1534,7 +1578,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
|
||||
// retry opening wallet after cache deleted
|
||||
try {
|
||||
log.warn("Failed to open full wallet using backup cache files, retrying with cache deleted");
|
||||
log.warn("Failed to open full wallet '{}' using backup cache files, retrying with cache deleted", config.getPath());
|
||||
walletFull = MoneroWalletFull.openWallet(config);
|
||||
log.warn("Successfully opened full wallet after cache deleted");
|
||||
retrySuccessful = true;
|
||||
@ -1548,7 +1592,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
} else {
|
||||
|
||||
// restore original wallet cache
|
||||
log.warn("Failed to open full wallet after deleting cache, restoring original cache");
|
||||
log.warn("Failed to open full wallet '{}' after deleting cache, restoring original cache", config.getPath());
|
||||
File cacheFile = new File(cachePath);
|
||||
if (cacheFile.exists()) cacheFile.delete();
|
||||
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
|
||||
@ -1620,11 +1664,12 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
if (!applyProxyUri) connection.setProxyUri(null);
|
||||
|
||||
// try opening wallet
|
||||
log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri());
|
||||
log.info("Opening RPC wallet '{}' with monerod={}, proxyUri={}", config.getPath(), connection.getUri(), connection.getProxyUri());
|
||||
config.setServer(connection);
|
||||
try {
|
||||
walletRpc.openWallet(config);
|
||||
} catch (Exception e) {
|
||||
if (isShutDownStarted) throw e;
|
||||
log.warn("Failed to open RPC wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage());
|
||||
boolean retrySuccessful = false;
|
||||
try {
|
||||
@ -1662,7 +1707,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
|
||||
// retry opening wallet after cache deleted
|
||||
try {
|
||||
log.warn("Failed to open RPC wallet using backup cache files, retrying with cache deleted");
|
||||
log.warn("Failed to open RPC wallet '{}' using backup cache files, retrying with cache deleted", config.getPath());
|
||||
walletRpc.openWallet(config);
|
||||
log.warn("Successfully opened RPC wallet after cache deleted");
|
||||
retrySuccessful = true;
|
||||
@ -1676,7 +1721,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
} else {
|
||||
|
||||
// restore original wallet cache
|
||||
log.warn("Failed to open RPC wallet after deleting cache, restoring original cache");
|
||||
log.warn("Failed to open RPC wallet '{}' after deleting cache, restoring original cache", config.getPath());
|
||||
File cacheFile = new File(cachePath);
|
||||
if (cacheFile.exists()) cacheFile.delete();
|
||||
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
|
||||
@ -1702,7 +1747,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
private MoneroWalletRpc startWalletRpcInstance(Integer port, boolean applyProxyUri) {
|
||||
|
||||
// check if monero-wallet-rpc exists
|
||||
if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new Error("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH
|
||||
if (!new File(MONERO_WALLET_RPC_PATH).exists()) throw new RuntimeException("monero-wallet-rpc executable doesn't exist at path " + MONERO_WALLET_RPC_PATH
|
||||
+ "; copy monero-wallet-rpc to the project root or set WalletConfig.java MONERO_WALLET_RPC_PATH for your system");
|
||||
|
||||
// build command to start monero-wallet-rpc
|
||||
@ -1741,7 +1786,8 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
||||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
@Override
|
||||
protected void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// use current connection
|
||||
@ -1795,7 +1841,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
tasks.add(() -> {
|
||||
try {
|
||||
wallet.changePassword(oldPassword, newPassword);
|
||||
saveMainWallet();
|
||||
saveWallet();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error changing main wallet password: " + e.getMessage() + "\n", e);
|
||||
throw e;
|
||||
@ -1845,13 +1891,13 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
log.warn("Force restarting main wallet");
|
||||
if (isClosingWallet) return;
|
||||
forceCloseMainWallet();
|
||||
maybeInitMainWallet(true);
|
||||
doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS);
|
||||
}
|
||||
|
||||
public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
|
||||
if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while
|
||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
|
||||
getWallet(); // re-open wallet
|
||||
requestSwitchToNextBestConnection(sourceConnection);
|
||||
if (wallet == null) doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS);
|
||||
}
|
||||
|
||||
private void startPolling() {
|
||||
@ -1988,7 +2034,7 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
if (wallet != null && !isShutDownStarted) {
|
||||
try {
|
||||
cacheWalletInfo();
|
||||
requestSaveMainWallet();
|
||||
saveWalletWithDelay();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error caching wallet info: " + e.getMessage() + "\n", e);
|
||||
}
|
||||
@ -2020,10 +2066,6 @@ public class XmrWalletService extends XmrWalletBase {
|
||||
return requestSwitchToNextBestConnection(null);
|
||||
}
|
||||
|
||||
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
|
||||
}
|
||||
|
||||
private void onNewBlock(long height) {
|
||||
UserThread.execute(() -> {
|
||||
walletHeight.set(height);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user