Merge branch 'haveno-dex:master' into EVMaddresses

This commit is contained in:
XMRZombie 2025-02-18 12:05:54 +00:00 committed by GitHub
commit fac51914ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
194 changed files with 7143 additions and 2147 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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."

View File

@ -47,7 +47,7 @@ public class CancelOfferTest extends AbstractOfferTest {
10000000L,
10000000L,
0.00,
defaultBuyerSecurityDepositPct.get(),
defaultSecurityDepositPct.get(),
paymentAccountId,
NO_TRIGGER_PRICE);
};

View File

@ -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());

View File

@ -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());

View File

@ -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));

View File

@ -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());

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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());

View File

@ -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 {}.",

View File

@ -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;

View File

@ -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

View File

@ -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");
}
}

View File

@ -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

View File

@ -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,

View File

@ -81,7 +81,7 @@ public class OffersServiceRequest {
.setUseMarketBasedPrice(useMarketBasedPrice)
.setPrice(fixedPrice)
.setMarketPriceMarginPct(marketPriceMarginPct)
.setBuyerSecurityDepositPct(securityDepositPct)
.setSecurityDepositPct(securityDepositPct)
.setPaymentAccountId(paymentAcctId)
.setTriggerPrice(triggerPrice)
.build();

View File

@ -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();
}
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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();

View File

@ -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)) {

View File

@ -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);
}

View File

@ -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");

View File

@ -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) {

View File

@ -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);

View File

@ -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
}

View File

@ -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()) {

View File

@ -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();
}
}

View File

@ -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());

View File

@ -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))

View File

@ -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);
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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 "";
}

View File

@ -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={}, ",

View File

@ -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;
}
}

View File

@ -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()),

View File

@ -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() {

View File

@ -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();

View File

@ -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
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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");

View File

@ -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

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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:

View File

@ -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 {

View File

@ -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;
}

View 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;
}
}

View File

@ -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).

View File

@ -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

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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)));

View File

@ -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);

View File

@ -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

View File

@ -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 {

View File

@ -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);
}

View File

@ -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());

View File

@ -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);
}

View File

@ -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

View File

@ -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()));
}

View File

@ -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);
}

View File

@ -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());

View File

@ -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);
}

View File

@ -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

View File

@ -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}";
}
}

View File

@ -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())));
}

View File

@ -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()));
}

View File

@ -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();
}
}

View File

@ -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()));
}

View File

@ -158,7 +158,6 @@ public final class TradePeer implements PersistablePayload {
}
public BigInteger getSecurityDeposit() {
if (depositTxHash == null) return null;
return BigInteger.valueOf(securityDeposit);
}

View File

@ -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(),

View File

@ -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();

View File

@ -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();

View File

@ -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

View File

@ -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());

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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());

View File

@ -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());

View File

@ -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

View File

@ -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())

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -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

View File

@ -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(() -> {

View File

@ -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