Merge pull request #33 from haveno-dex/master

v1.0.13
This commit is contained in:
retoaccess1 2024-11-12 17:31:22 +00:00 committed by GitHub
commit 06052ad7b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
95 changed files with 1157 additions and 642 deletions

View file

@ -1,9 +1,8 @@
<div align="center">
<img src="https://raw.githubusercontent.com/haveno-dex/haveno-meta/721e52919b28b44d12b6e1e5dac57265f1c05cda/logo/haveno_logo_landscape.svg" alt="Haveno logo">
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/505405b43cb74d5a996f106a3371588e)](https://app.codacy.com/gh/haveno-dex/haveno/dashboard)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/haveno-dex/haveno/build.yml?branch=master)
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=project%3Ahaveno-dex%2F2)](https://github.com/orgs/haveno-dex/projects/2) |
[![GitHub issues with bounty](https://img.shields.io/github/issues-search/haveno-dex/haveno?color=%23fef2c0&label=Issues%20with%20bounties&query=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty)
[![Twitter Follow](https://img.shields.io/twitter/follow/HavenoDEX?style=social)](https://twitter.com/havenodex)
[![Matrix rooms](https://img.shields.io/badge/Matrix%20room-%23haveno-blue)](https://matrix.to/#/#haveno:monero.social) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](https://github.com/haveno-dex/.github/blob/master/CODE_OF_CONDUCT.md)
</div>
@ -14,7 +13,7 @@ Haveno (pronounced ha‧ve‧no) is an open source platform to exchange [Monero]
Main features:
- All communications are routed through **Tor**, to preserve your privacy
- Communications are routed through **Tor**, to preserve your privacy.
- Trades are **peer-to-peer**: trades on Haveno happen between people only, there is no central authority.
@ -24,24 +23,24 @@ Main features:
See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
## Status of the project
## Installing Haveno
Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially endorse any networks at this time.
Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. We do not endorse any networks at this time.
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the network.
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network.
Alternatively, you can [start your own network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md).
Note that Haveno is being actively developed. If you find issues or bugs, please let us know.
Main repositories:
## Main repositories
- **[haveno](https://github.com/haveno-dex/haveno)** - This repository. The core of Haveno.
- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - The user interface.
- **[haveno-ts](https://github.com/haveno-dex/haveno-ts)** - TypeScript library for using Haveno.
- **[haveno-ui](https://github.com/haveno-dex/haveno-ui)** - A new user interface (WIP).
- **[haveno-meta](https://github.com/haveno-dex/haveno-meta)** - For project-wide discussions and proposals.
If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties)
The PGP keys of the core team members are in `gpg_keys/`.
If you wish to help, take a look at the repositories above and look for open issues. We run a bounty program to incentivize development. See [Bounties](#bounties).
## Keep in touch and help out!
@ -63,7 +62,7 @@ If you are not able to contribute code and want to contribute development resour
## Bounties
To incentivize development and reward contributors we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the issues eligible for a bounty on the [dedicated Kanban board](https://github.com/orgs/haveno-dex/projects/2) or look for [issues labelled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aissue+is%3Aopen+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
To incentivize development and reward contributors, we adopt a simple bounty system. Contributors may be awarded bounties after completing a task (resolving an issue). Take a look at the [issues labeled '💰bounty'](https://github.com/haveno-dex/haveno/issues?q=is%3Aopen+is%3Aissue+label%3A%F0%9F%92%B0bounty) in the main `haveno` repository. [Details and conditions for receiving a bounty](docs/bounties.md).
## Support and sponsorships

View file

@ -0,0 +1,29 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset;
/**
* Abstract base class for Tron-based {@link Token}s that implement the
* TRC-20 Token Standard.
*/
public abstract class Trc20Token extends Token {
public Trc20Token(String name, String tickerSymbol) {
super(name, tickerSymbol, new RegexAddressValidator("T[A-Za-z1-9]{33}"));
}
}

View file

@ -0,0 +1,11 @@
package haveno.asset.tokens;
import haveno.asset.Erc20Token;
public class TetherUSDERC20 extends Erc20Token {
public TetherUSDERC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (ERC20)", "USDT-ERC20");
}
}

View file

@ -0,0 +1,11 @@
package haveno.asset.tokens;
import haveno.asset.Trc20Token;
public class TetherUSDTRC20 extends Trc20Token {
public TetherUSDTRC20() {
// If you add a new USDT variant or want to change this ticker symbol you should also look here:
// core/src/main/java/haveno/core/provider/price/PriceProvider.java:getAll()
super("Tether USD (TRC20)", "USDT-TRC20");
}
}

View file

@ -7,3 +7,5 @@ haveno.asset.coins.BitcoinCash
haveno.asset.coins.Ether
haveno.asset.coins.Litecoin
haveno.asset.coins.Monero
haveno.asset.tokens.TetherUSDERC20
haveno.asset.tokens.TetherUSDTRC20

View file

@ -32,6 +32,7 @@ public class BitcoinTest extends AbstractAssetTest {
assertValidAddress("3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX");
assertValidAddress("1111111111111111111114oLvT2");
assertValidAddress("1BitcoinEaterAddressDontSendf59kuE");
assertValidAddress("bc1qj89046x7zv6pm4n00qgqp505nvljnfp6xfznyw");
}
@Test

View file

@ -0,0 +1,43 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import haveno.asset.tokens.TetherUSDERC20;
import org.junit.jupiter.api.Test;
public class TetherUSDERC20Test extends AbstractAssetTest {
public TetherUSDERC20Test() {
super(new TetherUSDERC20());
}
@Test
public void testValidAddresses() {
assertValidAddress("0x2a65Aca4D5fC5B5C859090a6c34d164135398226");
assertValidAddress("2a65Aca4D5fC5B5C859090a6c34d164135398226");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
}
}

View file

@ -0,0 +1,42 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.asset.coins;
import haveno.asset.AbstractAssetTest;
import haveno.asset.tokens.TetherUSDTRC20;
import org.junit.jupiter.api.Test;
public class TetherUSDTRC20Test extends AbstractAssetTest {
public TetherUSDTRC20Test() {
super(new TetherUSDTRC20());
}
@Test
public void testValidAddresses() {
assertValidAddress("TVnmu3E6DYVL4bpAoZnPNEPVUrgC7eSWaX");
}
@Test
public void testInvalidAddresses() {
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d1641353982266");
assertInvalidAddress("0x2a65Aca4D5fC5B5C859090a6c34d16413539822g");
assertInvalidAddress("2a65Aca4D5fC5B5C859090a6c34d16413539822g");
}
}

View file

@ -610,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.0.12-SNAPSHOT'
version = '1.0.13-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,

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.12";
public static final String VERSION = "1.0.13";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.

View file

@ -74,7 +74,7 @@ public class FileUtil {
}
}
public static File getLatestBackupFile(File dir, String fileName) {
public static List<File> getBackupFiles(File dir, String fileName) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
if (!backupDir.exists()) return null;
String dirName = "backups_" + fileName;
@ -82,9 +82,14 @@ public class FileUtil {
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
if (!backupFileDir.exists()) return null;
File[] files = backupFileDir.listFiles();
if (files == null || files.length == 0) return null;
Arrays.sort(files, Comparator.comparing(File::getName));
return files[files.length - 1];
return Arrays.asList(files);
}
public static File getLatestBackupFile(File dir, String fileName) {
List<File> files = getBackupFiles(dir, fileName);
if (files.isEmpty()) return null;
files.sort(Comparator.comparing(File::getName));
return files.get(files.size() - 1);
}
public static void deleteRollingBackup(File dir, String fileName) {

View file

@ -11,8 +11,8 @@
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
* You should have received a copy of the GNU Affero General Public
* License along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.common.util;
@ -25,38 +25,67 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/**
* Utility class for creating single-threaded executors.
*/
public class SingleThreadExecutorUtils {
private SingleThreadExecutorUtils() {
// Prevent instantiation
}
public static ExecutorService getSingleThreadExecutor(Class<?> aClass) {
String name = aClass.getSimpleName();
return getSingleThreadExecutor(name);
validateClass(aClass);
return getSingleThreadExecutor(aClass.getSimpleName());
}
public static ExecutorService getNonDaemonSingleThreadExecutor(Class<?> aClass) {
String name = aClass.getSimpleName();
return getSingleThreadExecutor(name, false);
validateClass(aClass);
return getSingleThreadExecutor(aClass.getSimpleName(), false);
}
public static ExecutorService getSingleThreadExecutor(String name) {
validateName(name);
return getSingleThreadExecutor(name, true);
}
public static ListeningExecutorService getSingleThreadListeningExecutor(String name) {
validateName(name);
return MoreExecutors.listeningDecorator(getSingleThreadExecutor(name));
}
public static ExecutorService getSingleThreadExecutor(ThreadFactory threadFactory) {
validateThreadFactory(threadFactory);
return Executors.newSingleThreadExecutor(threadFactory);
}
private static ExecutorService getSingleThreadExecutor(String name, boolean isDaemonThread) {
final ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
ThreadFactory threadFactory = getThreadFactory(name, isDaemonThread);
return Executors.newSingleThreadExecutor(threadFactory);
}
private static ThreadFactory getThreadFactory(String name, boolean isDaemonThread) {
return new ThreadFactoryBuilder()
.setNameFormat(name)
.setNameFormat(name + "-%d")
.setDaemon(isDaemonThread)
.build();
}
private static void validateClass(Class<?> aClass) {
if (aClass == null) {
throw new IllegalArgumentException("Class must not be null.");
}
}
private static void validateName(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Name must not be null or empty.");
}
}
private static void validateThreadFactory(ThreadFactory threadFactory) {
if (threadFactory == null) {
throw new IllegalArgumentException("ThreadFactory must not be null.");
}
}
}

View file

@ -737,14 +737,13 @@ public class AccountAgeWitnessService {
}
public Optional<SignedWitness> traderSignAndPublishPeersAccountAgeWitness(Trade trade) {
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
BigInteger tradeAmount = trade.getAmount();
checkNotNull(trade.getTradePeer().getPubKeyRing(), "Peer must have a keyring");
PublicKey peersPubKey = trade.getTradePeer().getPubKeyRing().getSignaturePubKey();
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}",
trade.toString());
checkNotNull(tradeAmount, "Trade amount must not be null");
checkNotNull(peersPubKey, "Peers pub key must not be null");
AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null);
checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade " + trade.toString());
BigInteger tradeAmount = trade.getAmount();
checkNotNull(tradeAmount, "Trade amount must not be null");
try {
return signedWitnessService.signAndPublishAccountAgeWitness(tradeAmount, peersWitness, peersPubKey);

View file

@ -64,9 +64,14 @@ class CorePaymentAccountsService {
}
PaymentAccount createPaymentAccount(PaymentAccountForm form) {
validateFormFields(form);
PaymentAccount paymentAccount = form.toPaymentAccount();
setSelectedTradeCurrency(paymentAccount); // TODO: selected trade currency is function of offer, not payment account payload
verifyPaymentAccountHasRequiredFields(paymentAccount);
if (paymentAccount instanceof CryptoCurrencyAccount) {
CryptoCurrencyAccount cryptoAccount = (CryptoCurrencyAccount) paymentAccount;
verifyCryptoCurrencyAddress(cryptoAccount.getSingleTradeCurrency().getCode(), cryptoAccount.getAddress());
}
user.addPaymentAccountIfNotExists(paymentAccount);
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
log.info("Saved payment account with id {} and payment method {}.",
@ -166,6 +171,12 @@ class CorePaymentAccountsService {
.collect(Collectors.toList());
}
private void validateFormFields(PaymentAccountForm form) {
for (PaymentAccountFormField field : form.getFields()) {
validateFormField(form, field.getId(), field.getValue());
}
}
void validateFormField(PaymentAccountForm form, PaymentAccountFormField.FieldId fieldId, String value) {
// get payment method id

View file

@ -72,7 +72,7 @@ class CorePriceService {
* @return Price per 1 XMR in the given currency (traditional or crypto)
*/
public double getMarketPrice(String currencyCode) throws ExecutionException, InterruptedException, TimeoutException, IllegalArgumentException {
var marketPrice = priceFeedService.requestAllPrices().get(currencyCode);
var marketPrice = priceFeedService.requestAllPrices().get(CurrencyUtil.getCurrencyCodeBase(currencyCode));
if (marketPrice == null) {
throw new IllegalArgumentException("Currency not found: " + currencyCode); // message sent to client
}

View file

@ -19,10 +19,8 @@ package haveno.core.locale;
import com.google.protobuf.Message;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
public final class CryptoCurrency extends TradeCurrency {
// http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618
private final static String PREFIX = "";

View file

@ -73,14 +73,6 @@ public class CurrencyUtil {
private static String baseCurrencyCode = "XMR";
private static List<TraditionalCurrency> getTraditionalNonFiatCurrencies() {
return Arrays.asList(
new TraditionalCurrency("XAG", "Silver"),
new TraditionalCurrency("XAU", "Gold"),
new TraditionalCurrency("XGB", "Goldback")
);
}
// Calls to isTraditionalCurrency and isCryptoCurrency are very frequent so we use a cache of the results.
// The main improvement was already achieved with using memoize for the source maps, but
// the caching still reduces performance costs by about 20% for isCryptoCurrency (1752 ms vs 2121 ms) and about 50%
@ -124,6 +116,14 @@ public class CurrencyUtil {
return new ArrayList<>(traditionalCurrencyMapSupplier.get().values());
}
public static List<TraditionalCurrency> getTraditionalNonFiatCurrencies() {
return Arrays.asList(
new TraditionalCurrency("XAG", "Silver"),
new TraditionalCurrency("XAU", "Gold"),
new TraditionalCurrency("XGB", "Goldback")
);
}
public static Collection<TraditionalCurrency> getAllSortedTraditionalCurrencies(Comparator comparator) {
return (List<TraditionalCurrency>) getAllSortedTraditionalCurrencies().stream()
.sorted(comparator)
@ -200,6 +200,7 @@ 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("USDT-ERC20", "Tether USD (ERC20)"));
result.sort(TradeCurrency::compareTo);
return result;
}
@ -295,6 +296,9 @@ public class CurrencyUtil {
if (currencyCode != null && isCryptoCurrencyMap.containsKey(currencyCode.toUpperCase())) {
return isCryptoCurrencyMap.get(currencyCode.toUpperCase());
}
if (isCryptoCurrencyBase(currencyCode)) {
return true;
}
boolean isCryptoCurrency;
if (currencyCode == null) {
@ -321,6 +325,19 @@ public class CurrencyUtil {
return isCryptoCurrency;
}
private static boolean isCryptoCurrencyBase(String currencyCode) {
if (currencyCode == null) return false;
currencyCode = currencyCode.toUpperCase();
return currencyCode.equals("USDT");
}
public static String getCurrencyCodeBase(String currencyCode) {
if (currencyCode == null) return null;
currencyCode = currencyCode.toUpperCase();
if (currencyCode.contains("USDT")) return "USDT";
return currencyCode;
}
public static Optional<CryptoCurrency> getCryptoCurrency(String currencyCode) {
return Optional.ofNullable(cryptoCurrencyMapSupplier.get().get(currencyCode));
}

View file

@ -19,19 +19,16 @@ package haveno.core.locale;
import haveno.common.proto.ProtobufferRuntimeException;
import haveno.common.proto.persistable.PersistablePayload;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@EqualsAndHashCode
@ToString
@Getter
@Slf4j
public abstract class TradeCurrency implements PersistablePayload, Comparable<TradeCurrency> {
protected final String code;
@EqualsAndHashCode.Exclude
protected final String name;
public TradeCurrency(String code, String name) {
@ -82,4 +79,23 @@ public abstract class TradeCurrency implements PersistablePayload, Comparable<Tr
return this.name.compareTo(other.name);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof TradeCurrency) {
TradeCurrency other = (TradeCurrency) obj;
return code.equals(other.code);
}
return false;
}
@Override
public int hashCode() {
return code.hashCode();
}
}

View file

@ -36,14 +36,12 @@ package haveno.core.locale;
import com.google.protobuf.Message;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import java.util.Currency;
import java.util.Locale;
@EqualsAndHashCode(callSuper = true)
@ToString
@Getter
public final class TraditionalCurrency extends TradeCurrency {

View file

@ -62,6 +62,7 @@ import haveno.core.offer.messages.SignOfferRequest;
import haveno.core.offer.messages.SignOfferResponse;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.offer.placeoffer.PlaceOfferProtocol;
import haveno.core.offer.placeoffer.tasks.ValidateOffer;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
@ -934,6 +935,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
// validate offer
try {
ValidateOffer.validateOffer(openOffer.getOffer(), accountAgeWitnessService, user);
} catch (Exception e) {
errorMessageHandler.handleErrorMessage("Failed to validate offer: " + e.getMessage());
return;
}
// cancel offer if scheduled txs unavailable
if (openOffer.getScheduledTxHashes() != null) {
boolean scheduledTxsAvailable = true;
@ -1855,7 +1864,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
private boolean preventedFromPublishing(OpenOffer openOffer) {
return openOffer.isDeactivated() || openOffer.isCanceled();
return openOffer.isDeactivated() || openOffer.isCanceled() || openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null;
}
private void startPeriodicRepublishOffersTimer() {

View file

@ -19,10 +19,12 @@ package haveno.core.offer.placeoffer.tasks;
import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.offer.Offer;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.messages.TradeMessage;
import haveno.core.user.User;
import org.bitcoinj.core.Coin;
import java.math.BigInteger;
@ -41,55 +43,7 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
try {
runInterceptHook();
// Coins
checkBINotNullOrZero(offer.getAmount(), "Amount");
checkBINotNullOrZero(offer.getMinAmount(), "MinAmount");
//checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
// We remove those checks to be more flexible with future changes.
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
"createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " +
"MakerFee=" + offer.getMakerFee().toFriendlyString());*/
/*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value,
"sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " +
"sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
long maxAmount = model.getAccountAgeWitnessService().getMyTradeLimit(model.getUser().getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection());
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR");
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
checkNotNull(offer.getPrice(), "Price is null");
if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(),
"Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString());
checkArgument(offer.getDate().getTime() > 0,
"Date must not be 0. date=" + offer.getDate().toString());
checkNotNull(offer.getCurrencyCode(), "Currency is null");
checkNotNull(offer.getDirection(), "Direction is null");
checkNotNull(offer.getId(), "Id is null");
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
checkNotNull(offer.getMinAmount(), "MinAmount is null");
checkNotNull(offer.getPrice(), "Price is null");
checkNotNull(offer.getVersionNr(), "VersionNr is null");
checkArgument(offer.getMaxTradePeriod() > 0,
"maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod());
// TODO check upper and lower bounds for fiat
// TODO check rest of new parameters
validateOffer(offer, model.getAccountAgeWitnessService(), model.getUser());
complete();
} catch (Exception e) {
@ -100,42 +54,95 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
}
}
public static void checkBINotNullOrZero(BigInteger value, String name) {
public static void validateOffer(Offer offer, AccountAgeWitnessService accountAgeWitnessService, User user) {
// Coins
checkBINotNullOrZero(offer.getAmount(), "Amount");
checkBINotNullOrZero(offer.getMinAmount(), "MinAmount");
//checkCoinNotNullOrZero(offer.getTxFee(), "txFee"); // TODO: remove from data model
checkBINotNullOrZero(offer.getMaxTradeLimit(), "MaxTradeLimit");
if (offer.getMakerFeePct() < 0) throw new IllegalArgumentException("Maker fee must be >= 0% but was " + offer.getMakerFeePct());
if (offer.getTakerFeePct() < 0) throw new IllegalArgumentException("Taker fee must be >= 0% but was " + offer.getTakerFeePct());
if (offer.getBuyerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Buyer security deposit percent must be positive but was " + offer.getBuyerSecurityDepositPct());
if (offer.getSellerSecurityDepositPct() <= 0) throw new IllegalArgumentException("Seller security deposit percent must be positive but was " + offer.getSellerSecurityDepositPct());
// We remove those checks to be more flexible with future changes.
/*checkArgument(offer.getMakerFee().value >= FeeService.getMinMakerFee(offer.isCurrencyForMakerFeeBtc()).value,
"createOfferFee must not be less than FeeService.MIN_CREATE_OFFER_FEE_IN_BTC. " +
"MakerFee=" + offer.getMakerFee().toFriendlyString());*/
/*checkArgument(offer.getBuyerSecurityDeposit().value >= ProposalConsensus.getMinBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be less than ProposalConsensus.MIN_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getBuyerSecurityDeposit().value <= ProposalConsensus.getMaxBuyerSecurityDeposit().value,
"buyerSecurityDeposit must not be larger than ProposalConsensus.MAX_BUYER_SECURITY_DEPOSIT. " +
"buyerSecurityDeposit=" + offer.getBuyerSecurityDeposit().toFriendlyString());
checkArgument(offer.getSellerSecurityDeposit().value == ProposalConsensus.getSellerSecurityDeposit().value,
"sellerSecurityDeposit must be equal to ProposalConsensus.SELLER_SECURITY_DEPOSIT. " +
"sellerSecurityDeposit=" + offer.getSellerSecurityDeposit().toFriendlyString());*/
/*checkArgument(offer.getMinAmount().compareTo(ProposalConsensus.getMinTradeAmount()) >= 0,
"MinAmount is less than " + ProposalConsensus.getMinTradeAmount().toFriendlyString());*/
long maxAmount = accountAgeWitnessService.getMyTradeLimit(user.getPaymentAccount(offer.getMakerPaymentAccountId()), offer.getCurrencyCode(), offer.getDirection());
checkArgument(offer.getAmount().longValueExact() <= maxAmount,
"Amount is larger than " + HavenoUtils.atomicUnitsToXmr(offer.getPaymentMethod().getMaxTradeLimit(offer.getCurrencyCode())) + " XMR");
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0, "MinAmount is larger than Amount");
checkNotNull(offer.getPrice(), "Price is null");
if (!offer.isUseMarketBasedPrice()) checkArgument(offer.getPrice().isPositive(),
"Price must be positive unless using market based price. price=" + offer.getPrice().toFriendlyString());
checkArgument(offer.getDate().getTime() > 0,
"Date must not be 0. date=" + offer.getDate().toString());
checkNotNull(offer.getCurrencyCode(), "Currency is null");
checkNotNull(offer.getDirection(), "Direction is null");
checkNotNull(offer.getId(), "Id is null");
checkNotNull(offer.getPubKeyRing(), "pubKeyRing is null");
checkNotNull(offer.getMinAmount(), "MinAmount is null");
checkNotNull(offer.getPrice(), "Price is null");
checkNotNull(offer.getVersionNr(), "VersionNr is null");
checkArgument(offer.getMaxTradePeriod() > 0,
"maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod());
// TODO check upper and lower bounds for fiat
// TODO check rest of new parameters
}
private static void checkBINotNullOrZero(BigInteger value, String name) {
checkNotNull(value, name + " is null");
checkArgument(value.compareTo(BigInteger.ZERO) > 0,
name + " must be positive. " + name + "=" + value);
}
public static void checkCoinNotNullOrZero(Coin value, String name) {
private static void checkCoinNotNullOrZero(Coin value, String name) {
checkNotNull(value, name + " is null");
checkArgument(value.isPositive(),
name + " must be positive. " + name + "=" + value.toFriendlyString());
}
public static String nonEmptyStringOf(String value) {
private static String nonEmptyStringOf(String value) {
checkNotNull(value);
checkArgument(value.length() > 0);
return value;
}
public static long nonNegativeLongOf(long value) {
private static long nonNegativeLongOf(long value) {
checkArgument(value >= 0);
return value;
}
public static Coin nonZeroCoinOf(Coin value) {
private static Coin nonZeroCoinOf(Coin value) {
checkNotNull(value);
checkArgument(!value.isZero());
return value;
}
public static Coin positiveCoinOf(Coin value) {
private static Coin positiveCoinOf(Coin value) {
checkNotNull(value);
checkArgument(value.isPositive());
return value;
}
public static void checkTradeId(String tradeId, TradeMessage tradeMessage) {
private static void checkTradeId(String tradeId, TradeMessage tradeMessage) {
checkArgument(tradeId.equals(tradeMessage.getOfferId()));
}
}

View file

@ -139,6 +139,10 @@ public abstract class PaymentAccount implements PersistablePayload {
return getSingleTradeCurrency() == null || CurrencyUtil.isFiatCurrency(getSingleTradeCurrency().getCode()); // TODO: check if trade currencies contain fiat
}
public boolean isCryptoCurrency() {
return getSingleTradeCurrency() != null && CurrencyUtil.isCryptoCurrency(getSingleTradeCurrency().getCode());
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER

View file

@ -37,6 +37,7 @@ package haveno.core.provider;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.common.config.Config;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -47,9 +48,11 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ProvidersRepository {
private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/";
private static final List<String> DEFAULT_NODES = Arrays.asList(
"http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno
"http://a66ulzwhhudtqy6k2efnhodj2n6wnc5mnzjs3ocqtf47lwtcuo4wxyqd.onion/" // Cake
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/" // Cake
);
private final Config config;
@ -78,19 +81,22 @@ public class ProvidersRepository {
this.providersFromProgramArgs = providers;
this.useLocalhostForP2P = useLocalhostForP2P;
Collections.shuffle(DEFAULT_NODES);
Collections.shuffle(DEFAULT_NODES); // randomize order of default nodes
applyBannedNodes(config.bannedPriceRelayNodes);
}
public void applyBannedNodes(@Nullable List<String> bannedNodes) {
this.bannedNodes = bannedNodes;
// fill provider list
fillProviderList();
selectNextProviderBaseUrl();
// select next provider if current provider is null or banned
if (baseUrl.isEmpty() || isBanned(baseUrl)) selectNextProviderBaseUrl();
if (bannedNodes != null && !bannedNodes.isEmpty()) {
log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}",
bannedNodes, baseUrl, providerList);
log.info("Excluded provider nodes from filter: nodes={}, selected provider baseUrl={}, providerList={}", bannedNodes, baseUrl, providerList);
}
}
@ -129,22 +135,30 @@ public class ProvidersRepository {
// If we run in localhost mode we don't have the tor node running, so we need a clearnet host
// Use localhost for using a locally running provider
providers = List.of(
"http://localhost:8078/",
DEFAULT_LOCAL_NODE,
"https://price.haveno.network/",
"http://173.230.142.36:8078/");
} else {
providers = DEFAULT_NODES;
providers = new ArrayList<String>();
//providers.add(DEFAULT_LOCAL_NODE); // try local provider first
providers.addAll(DEFAULT_NODES);
}
} else {
providers = providersFromProgramArgs;
}
providerList = providers.stream()
.filter(e -> bannedNodes == null ||
!bannedNodes.contains(e.replace("http://", "")
.replace("/", "")
.replace(".onion", "")))
.filter(e -> !isBanned(e))
.map(e -> e.endsWith("/") ? e : e + "/")
.map(e -> e.startsWith("http") ? e : "http://" + e)
.collect(Collectors.toList());
}
private boolean isBanned(String provider) {
if (bannedNodes == null) return false;
return bannedNodes.stream()
.anyMatch(e -> provider.replace("http://", "")
.replace("/", "")
.replace(".onion", "")
.equals(e));
}
}

View file

@ -292,15 +292,16 @@ public class PriceFeedService {
@Nullable
public MarketPrice getMarketPrice(String currencyCode) {
synchronized (cache) {
return cache.getOrDefault(currencyCode, null);
return cache.getOrDefault(CurrencyUtil.getCurrencyCodeBase(currencyCode), null);
}
}
private void setHavenoMarketPrice(String currencyCode, Price price) {
UserThread.execute(() -> {
String currencyCodeBase = CurrencyUtil.getCurrencyCodeBase(currencyCode);
synchronized (cache) {
if (!cache.containsKey(currencyCode) || !cache.get(currencyCode).isExternallyProvidedPrice()) {
cache.put(currencyCode, new MarketPrice(currencyCode,
if (!cache.containsKey(currencyCodeBase) || !cache.get(currencyCodeBase).isExternallyProvidedPrice()) {
cache.put(currencyCodeBase, new MarketPrice(currencyCodeBase,
MathUtils.scaleDownByPowerOf10(price.getValue(), CurrencyUtil.isCryptoCurrency(currencyCode) ? CryptoMoney.SMALLEST_UNIT_EXPONENT : TraditionalMoney.SMALLEST_UNIT_EXPONENT),
0,
false));

View file

@ -21,6 +21,7 @@ import com.google.gson.Gson;
import com.google.gson.internal.LinkedTreeMap;
import haveno.common.app.Version;
import haveno.common.util.MathUtils;
import haveno.core.locale.CurrencyUtil;
import haveno.core.provider.HttpClientProvider;
import haveno.network.http.HttpClient;
import haveno.network.p2p.P2PService;
@ -63,6 +64,7 @@ public class PriceProvider extends HttpClientProvider {
String baseCurrencyCode = (String) treeMap.get("baseCurrencyCode");
String counterCurrencyCode = (String) treeMap.get("counterCurrencyCode");
String currencyCode = baseCurrencyCode.equals("XMR") ? counterCurrencyCode : baseCurrencyCode;
currencyCode = CurrencyUtil.getCurrencyCodeBase(currencyCode);
double price = (Double) treeMap.get("price");
// json uses double for our timestampSec long value...
long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec"));

View file

@ -466,6 +466,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return this.disputeState == State.NEW;
}
public boolean isOpen() {
return this.disputeState == State.OPEN || this.disputeState == State.REOPENED;
}
public boolean isClosed() {
return this.disputeState == State.CLOSED;
}

View file

@ -393,8 +393,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage);
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// create dispute opened message
trade.exportMultisigHex();
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(),

View file

@ -70,7 +70,7 @@ public abstract class DisputeSession extends SupportSession {
@Override
public boolean chatIsOpen() {
return dispute != null && !dispute.isClosed();
return dispute != null && dispute.isOpen();
}
@Override

View file

@ -575,14 +575,14 @@ public class HavenoUtils {
// get original format
AudioFormat baseFormat = audioInputStream.getFormat();
// set target format: PCM_SIGNED, 16-bit
// set target format: PCM_SIGNED, 16-bit, 44100 Hz
AudioFormat targetFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
baseFormat.getSampleRate(),
44100.0f,
16, // 16-bit instead of 32-bit float
baseFormat.getChannels(),
baseFormat.getChannels() * 2, // Frame size: 2 bytes per channel (16-bit)
baseFormat.getSampleRate(),
44100.0f,
false // Little-endian
);

View file

@ -937,6 +937,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId());
stopPolling();
xmrWalletService.closeWallet(wallet, true);
maybeBackupWallet();
wallet = null;
pollPeriodMs = null;
}
@ -1064,6 +1065,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
public void importMultisigHexIfNeeded() {
synchronized (walletLock) {
if (wallet.isMultisigImportNeeded()) {
importMultisigHex();
}
}
}
public void importMultisigHex() {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
@ -1076,8 +1085,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
@ -1183,6 +1194,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// create payout tx
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
// import multisig hex if needed
importMultisigHexIfNeeded();
// create payout tx
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
@ -1190,8 +1206,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
@ -1250,8 +1268,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
throw e;
} catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
@ -1279,8 +1299,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e);
handleWalletError(e, sourceConnection);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} finally {
@ -1545,9 +1567,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
forceCloseWallet();
}
// backup trade wallet if applicable
maybeBackupWallet();
// de-initialize
if (idlePayoutSyncer != null) {
xmrWalletService.removeWalletListener(idlePayoutSyncer);
@ -2438,7 +2457,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
if (pollWallet) pollWallet();
if (pollWallet) doPollWallet();
} catch (Exception e) {
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
throw e;
@ -2500,10 +2519,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
private void doPollWallet() {
// skip if shut down started
if (isShutDownStarted) return;
// set poll in progress
boolean pollInProgressSet = false;
synchronized (pollLock) {
if (!pollInProgress) pollInProgressSet = true;
pollInProgress = true;
}
// poll wallet
try {
// skip if payout unlocked
@ -2628,8 +2655,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
} finally {
synchronized (pollLock) {
pollInProgress = false;
if (pollInProgressSet) {
synchronized (pollLock) {
pollInProgress = false;
}
}
requestSaveWallet();
}

View file

@ -450,6 +450,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
// skip if marked as failed
if (failedTradesManager.getObservableList().contains(trade)) {
log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId());
tradesToSkip.add(trade);
return;
}
// initialize trade
initPersistedTrade(trade);
@ -958,6 +965,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
public void unregisterTrade(Trade trade) {
log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId());
removeTrade(trade, true);
removeFailedTrade(trade);
requestPersistence();
@ -1059,7 +1067,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void addTradeToPendingTrades(Trade trade) {
if (!trade.isInitialized()) {
initPersistedTrade(trade);
try {
initPersistedTrade(trade);
} catch (Exception e) {
log.warn("Error initializing {} {} on move to pending trades", trade.getClass().getSimpleName(), trade.getShortId(), e);
}
}
addTrade(trade);
}

View file

@ -36,6 +36,7 @@ package haveno.core.trade.protocol.tasks;
import com.google.common.base.Preconditions;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
@ -79,15 +80,21 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// create payout tx if we have seller's updated multisig hex
if (trade.getSeller().getUpdatedMultisigHex() != null) {
// import multisig hex
trade.importMultisigHex();
// synchronize on lock for wallet operations
synchronized (trade.getWalletLock()) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
// create payout tx
log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.updatePayout(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
trade.requestPersistence();
// import multisig hex
trade.importMultisigHex();
// create payout tx
log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.updatePayout(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
trade.requestPersistence();
}
}
}
complete();

View file

@ -41,6 +41,7 @@ public class ProcessDepositResponse extends TradeTask {
// throw if error
DepositResponse message = (DepositResponse) processModel.getTradeMessage();
if (message.getErrorMessage() != null) {
log.warn("Unregistering trade {} {} because deposit response has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage());
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
processModel.getTradeManager().unregisterTrade(trade);
throw new RuntimeException(message.getErrorMessage());

View file

@ -105,12 +105,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
// advance state, arbitrator auto completes when payout published
trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
// publish signed witness
// buyer republishes signed witness for resilience
SignedWitness signedWitness = message.getBuyerSignedWitness();
if (signedWitness != null && trade instanceof BuyerTrade) {
// We received the signedWitness from the seller and publish the data to the network.
// The signer has published it as well but we prefer to re-do it on our side as well to achieve higher
// resilience.
processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness);
}
@ -146,12 +143,10 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
// handle if payout tx not published
if (!trade.isPayoutPublished()) {
// wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
boolean isSigned = message.getSignedPayoutTxHex() != null;
boolean deferSignAndPublish = trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout();
if (deferSignAndPublish) {
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
trade.pollWalletNormallyForMs(60000);
// wait to publish payout tx if defer flag set from seller (payout is expected)
if (message.isDeferPublishPayout()) {
log.info("Deferring publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
if (trade instanceof ArbitratorTrade) trade.pollWalletNormallyForMs(60000); // stop idling arbitrator
for (int i = 0; i < 5; i++) {
if (trade.isPayoutPublished()) break;
HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
@ -162,6 +157,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
// verify and publish payout tx
if (!trade.isPayoutPublished()) {
try {
boolean isSigned = message.getSignedPayoutTxHex() != null;
if (isSigned) {
log.info("{} {} publishing signed payout tx from seller", trade.getClass().getSimpleName(), trade.getId());
trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true);

View file

@ -19,6 +19,7 @@ package haveno.core.trade.protocol.tasks;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.support.dispute.Dispute;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.model.MoneroTxWallet;
@ -49,33 +50,39 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
trade.setPayoutTxHex(null);
}
// import multisig hex unless already signed
if (trade.getPayoutTxHex() == null) {
trade.importMultisigHex();
}
// synchronize on lock for wallet operations
synchronized (trade.getWalletLock()) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
// verify, sign, and publish payout tx if given
if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null) {
try {
// import multisig hex unless already signed
if (trade.getPayoutTxHex() == null) {
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true);
} else {
log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
trade.importMultisigHex();
}
} catch (IllegalArgumentException | IllegalStateException e) {
log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}: {}. Creating new unsigned payout tx", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e);
createUnsignedPayoutTx();
} catch (Exception e) {
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e);
throw e;
}
}
// otherwise create unsigned payout tx
else if (trade.getSelf().getUnsignedPayoutTxHex() == null) {
createUnsignedPayoutTx();
// verify, sign, and publish payout tx if given
if (trade.getBuyer().getPaymentSentMessage().getPayoutTxHex() != null) {
try {
if (trade.getPayoutTxHex() == null) {
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex(), true, true);
} else {
log.warn("Seller publishing previously signed payout tx for trade {}", trade.getId());
trade.processPayoutTx(trade.getPayoutTxHex(), false, true);
}
} catch (IllegalArgumentException | IllegalStateException e) {
log.warn("Illegal state or argument verifying, signing, and publishing payout tx for {} {}. Creating new unsigned payout tx. error={}. ", trade.getClass().getSimpleName(), trade.getId(), e.getMessage(), e);
createUnsignedPayoutTx();
} catch (Exception e) {
log.warn("Error verifying, signing, and publishing payout tx for trade {}: {}", trade.getId(), e.getMessage(), e);
throw e;
}
}
// otherwise create unsigned payout tx
else if (trade.getSelf().getUnsignedPayoutTxHex() == null) {
createUnsignedPayoutTx();
}
}
}
} else if (trade.getArbitrator().getPaymentReceivedMessage().getSignedPayoutTxHex() != null && !trade.isPayoutPublished()) {

View file

@ -90,8 +90,12 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
// sign account witness
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
log.info("{} {} signed and published peers account age witness", trade.getClass().getSimpleName(), trade.getId());
try {
accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
log.info("{} {} signed and published peers account age witness", trade.getClass().getSimpleName(), trade.getId());
} catch (Exception e) {
log.warn("Failed to sign and publish peer's account age witness for {} {}, error={}\n", getClass().getSimpleName(), trade.getId(), e.getMessage(), e);
}
}
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
@ -99,6 +103,7 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
// messages where only the one which gets processed by the peer would be removed we use the same uid. All
// other data stays the same when we re-send the message at any time later.
String deterministicId = HavenoUtils.getDeterministicId(trade, PaymentReceivedMessage.class, getReceiverNodeAddress());
boolean deferPublishPayout = trade.isPayoutPublished() || trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(); // informs receiver to expect payout so delay processing
PaymentReceivedMessage message = new PaymentReceivedMessage(
tradeId,
processModel.getMyNodeAddress(),
@ -106,7 +111,7 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
trade.getPayoutTxHex() == null ? trade.getSelf().getUnsignedPayoutTxHex() : null, // unsigned // TODO: phase in after next update to clear old style trades
trade.getPayoutTxHex() == null ? null : trade.getPayoutTxHex(), // signed
trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal(), // informs to expect payout
deferPublishPayout,
trade.getTradePeer().getAccountAgeWitness(),
signedWitness,
getReceiver() == trade.getArbitrator() ? trade.getBuyer().getPaymentSentMessage() : null // buyer already has payment sent message

View file

@ -566,6 +566,16 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence();
}
public void setBuyScreenOtherCurrencyCode(String buyScreenCurrencyCode) {
prefPayload.setBuyScreenOtherCurrencyCode(buyScreenCurrencyCode);
requestPersistence();
}
public void setSellScreenOtherCurrencyCode(String sellScreenCurrencyCode) {
prefPayload.setSellScreenOtherCurrencyCode(sellScreenCurrencyCode);
requestPersistence();
}
public void setIgnoreTradersList(List<String> ignoreTradersList) {
prefPayload.setIgnoreTradersList(ignoreTradersList);
requestPersistence();

View file

@ -77,6 +77,10 @@ public final class PreferencesPayload implements PersistableEnvelope {
private String buyScreenCryptoCurrencyCode;
@Nullable
private String sellScreenCryptoCurrencyCode;
@Nullable
private String buyScreenOtherCurrencyCode;
@Nullable
private String sellScreenOtherCurrencyCode;
private int tradeStatisticsTickUnitIndex = 3;
private boolean resyncSpvRequested;
private boolean sortMarketCurrenciesNumerically = true;
@ -212,6 +216,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
Optional.ofNullable(sellScreenCurrencyCode).ifPresent(builder::setSellScreenCurrencyCode);
Optional.ofNullable(buyScreenCryptoCurrencyCode).ifPresent(builder::setBuyScreenCryptoCurrencyCode);
Optional.ofNullable(sellScreenCryptoCurrencyCode).ifPresent(builder::setSellScreenCryptoCurrencyCode);
Optional.ofNullable(buyScreenOtherCurrencyCode).ifPresent(builder::setBuyScreenOtherCurrencyCode);
Optional.ofNullable(sellScreenOtherCurrencyCode).ifPresent(builder::setSellScreenOtherCurrencyCode);
Optional.ofNullable(selectedPaymentAccountForCreateOffer).ifPresent(
account -> builder.setSelectedPaymentAccountForCreateOffer(selectedPaymentAccountForCreateOffer.toProtoMessage()));
Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses);
@ -260,6 +266,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
ProtoUtil.stringOrNullFromProto(proto.getSellScreenCurrencyCode()),
ProtoUtil.stringOrNullFromProto(proto.getBuyScreenCryptoCurrencyCode()),
ProtoUtil.stringOrNullFromProto(proto.getSellScreenCryptoCurrencyCode()),
ProtoUtil.stringOrNullFromProto(proto.getBuyScreenOtherCurrencyCode()),
ProtoUtil.stringOrNullFromProto(proto.getSellScreenOtherCurrencyCode()),
proto.getTradeStatisticsTickUnitIndex(),
proto.getResyncSpvRequested(),
proto.getSortMarketCurrenciesNumerically(),

View file

@ -121,7 +121,7 @@ public class XmrWalletService extends XmrWalletBase {
private static final String MONERO_WALLET_NAME = "haveno_XMR";
private static final String KEYS_FILE_POSTFIX = ".keys";
private static final String ADDRESS_FILE_POSTFIX = ".address.txt";
private static final int NUM_MAX_WALLET_BACKUPS = 1;
private static final int NUM_MAX_WALLET_BACKUPS = 2;
private static final int MAX_SYNC_ATTEMPTS = 3;
private static final boolean PRINT_RPC_STACK_TRACE = false;
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
@ -1477,26 +1477,33 @@ public class XmrWalletService extends XmrWalletBase {
try {
walletFull = MoneroWalletFull.openWallet(config);
} catch (Exception e) {
log.warn("Failed to open full wallet '{}', attempting to use backup cache, error={}", config.getPath(), e.getMessage());
log.warn("Failed to open full wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage());
boolean retrySuccessful = false;
try {
// rename wallet cache to backup
String cachePath = walletDir.toString() + File.separator + MONERO_WALLET_NAME;
String cachePath = walletDir.toString() + File.separator + getWalletName(config.getPath());
File originalCacheFile = new File(cachePath);
if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup"));
// copy latest wallet cache backup to main folder
File backupCacheFile = FileUtil.getLatestBackupFile(walletDir, MONERO_WALLET_NAME);
if (backupCacheFile != null) FileUtil.copyFile(backupCacheFile, new File(cachePath));
// try opening wallet with backup cache files in descending order
List<File> backupCacheFiles = FileUtil.getBackupFiles(walletDir, getWalletName(config.getPath()));
Collections.reverse(backupCacheFiles);
for (File backupCacheFile : backupCacheFiles) {
try {
FileUtil.copyFile(backupCacheFile, new File(cachePath));
walletFull = MoneroWalletFull.openWallet(config);
log.warn("Successfully opened full wallet using backup cache");
retrySuccessful = true;
break;
} catch (Exception e2) {
// retry opening wallet without original cache
try {
walletFull = MoneroWalletFull.openWallet(config);
log.info("Successfully opened full wallet using backup cache");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
// delete cache file if failed to open
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
File unportableCacheFile = new File(cachePath + ".unportable");
if (unportableCacheFile.exists()) unportableCacheFile.delete();
}
}
// handle success or failure
@ -1505,14 +1512,30 @@ public class XmrWalletService extends XmrWalletBase {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open full wallet using backup cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// retry opening wallet after cache deleted
try {
log.warn("Failed to open full wallet using backup cache files, retrying with cache deleted");
walletFull = MoneroWalletFull.openWallet(config);
log.warn("Successfully opened full wallet after cache deleted");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
}
// throw exception
throw e;
// handle success or failure
if (retrySuccessful) {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open full wallet after deleting cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// throw original exception
throw e;
}
}
} catch (Exception e2) {
throw e; // throw original exception
@ -1582,26 +1605,33 @@ public class XmrWalletService extends XmrWalletBase {
try {
walletRpc.openWallet(config);
} catch (Exception e) {
log.warn("Failed to open RPC wallet '{}', attempting to use backup cache, error={}", config.getPath(), e.getMessage());
log.warn("Failed to open RPC wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage());
boolean retrySuccessful = false;
try {
// rename wallet cache to backup
String cachePath = walletDir.toString() + File.separator + MONERO_WALLET_NAME;
String cachePath = walletDir.toString() + File.separator + config.getPath();
File originalCacheFile = new File(cachePath);
if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup"));
// copy latest wallet cache backup to main folder
File backupCacheFile = FileUtil.getLatestBackupFile(walletDir, MONERO_WALLET_NAME);
if (backupCacheFile != null) FileUtil.copyFile(backupCacheFile, new File(cachePath));
// try opening wallet with backup cache files in descending order
List<File> backupCacheFiles = FileUtil.getBackupFiles(walletDir, config.getPath());
Collections.reverse(backupCacheFiles);
for (File backupCacheFile : backupCacheFiles) {
try {
FileUtil.copyFile(backupCacheFile, new File(cachePath));
walletRpc.openWallet(config);
log.warn("Successfully opened RPC wallet using backup cache");
retrySuccessful = true;
break;
} catch (Exception e2) {
// retry opening wallet without original cache
try {
walletRpc.openWallet(config);
log.info("Successfully opened RPC wallet using backup cache");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
// delete cache file if failed to open
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
File unportableCacheFile = new File(cachePath + ".unportable");
if (unportableCacheFile.exists()) unportableCacheFile.delete();
}
}
// handle success or failure
@ -1610,14 +1640,30 @@ public class XmrWalletService extends XmrWalletBase {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open RPC wallet using backup cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// retry opening wallet after cache deleted
try {
log.warn("Failed to open RPC wallet using backup cache files, retrying with cache deleted");
walletRpc.openWallet(config);
log.warn("Successfully opened RPC wallet after cache deleted");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
}
// throw exception
throw e;
// handle success or failure
if (retrySuccessful) {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open RPC wallet after deleting cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// throw original exception
throw e;
}
}
} catch (Exception e2) {
throw e; // throw original exception
@ -1837,10 +1883,18 @@ public class XmrWalletService extends XmrWalletBase {
}
private void doPollWallet(boolean updateTxs) {
// skip if shut down started
if (isShutDownStarted) return;
// set poll in progress
boolean pollInProgressSet = false;
synchronized (pollLock) {
if (!pollInProgress) pollInProgressSet = true;
pollInProgress = true;
}
if (isShutDownStarted) return;
// poll wallet
try {
// skip if daemon not synced
@ -1903,8 +1957,10 @@ public class XmrWalletService extends XmrWalletBase {
//e.printStackTrace();
}
} finally {
synchronized (pollLock) {
pollInProgress = false;
if (pollInProgressSet) {
synchronized (pollLock) {
pollInProgress = false;
}
}
// cache wallet info last

View file

@ -150,6 +150,7 @@ shared.addNewAccount=Add new account
shared.ExportAccounts=Export Accounts
shared.importAccounts=Import Accounts
shared.createNewAccount=Create new account
shared.createNewAccountDescription=Your account details are stored locally on your device and shared only with your trading peer and the arbitrator if a dispute is opened.
shared.saveNewAccount=Save new account
shared.selectedAccount=Selected account
shared.deleteAccount=Delete account
@ -207,6 +208,7 @@ shared.crypto=Crypto
shared.traditional=Traditional
shared.otherAssets=other assets
shared.other=Other
shared.preciousMetals=Precious Metals
shared.all=All
shared.edit=Edit
shared.advancedOptions=Advanced options
@ -245,8 +247,8 @@ shared.taker=Taker
####################################################################
mainView.menu.market=Market
mainView.menu.buy=Buy
mainView.menu.sell=Sell
mainView.menu.buyXmr=Buy XMR
mainView.menu.sellXmr=Sell XMR
mainView.menu.portfolio=Portfolio
mainView.menu.funds=Funds
mainView.menu.support=Support
@ -375,6 +377,8 @@ offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=buy XMR from a signed accoun
offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days
offerbook.timeSinceSigning.tooltip.learnMore=Learn more
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=Buy XMR with:
offerbook.sellXmrFor=Sell XMR for:
offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\
{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts.
@ -389,7 +393,7 @@ offerbook.volume={0} (min - max)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=Create new offer to {0} {1}
offerbook.createNewOffer=Create offer to {0} {1}
offerbook.createOfferDisabled.tooltip=You can only create one offer at a time
offerbook.takeOfferButton.tooltip=Take offer for {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Přidat nový účet
shared.ExportAccounts=Exportovat účty
shared.importAccounts=Importovat účty
shared.createNewAccount=Vytvořit nový účet
shared.createNewAccountDescription=Vaše údaje o účtu jsou uloženy místně na vašem zařízení a sdíleny pouze s vaším obchodním partnerem a rozhodcem, pokud dojde k otevření sporu.
shared.saveNewAccount=Uložit nový účet
shared.selectedAccount=Vybraný účet
shared.deleteAccount=Smazat účet
@ -192,6 +193,7 @@ shared.iConfirm=Potvrzuji
shared.openURL=Otevřené {0}
shared.fiat=Fiat
shared.crypto=Krypto
shared.preciousMetals=Drahé kovy
shared.all=Vše
shared.edit=Upravit
shared.advancedOptions=Pokročilé možnosti
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=účet byl zablokován
offerbook.timeSinceSigning.daysSinceSigning={0} dní
offerbook.timeSinceSigning.daysSinceSigning.long={0} od podpisu
offerbook.xmrAutoConf=Je automatické potvrzení povoleno
offerbook.buyXmrWith=Kupte XMR za:
offerbook.sellXmrFor=Prodat XMR za:
offerbook.timeSinceSigning.help=Když úspěšně dokončíte obchod s uživatelem, který má podepsaný platební účet, je váš platební účet podepsán.\n{0} dní později se počáteční limit {1} zruší a váš účet může podepisovat platební účty ostatních uživatelů.
offerbook.timeSinceSigning.notSigned=Dosud nepodepsáno
@ -362,6 +366,7 @@ offerbook.nrOffers=Počet nabídek: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Kauce XMR (%)
offerbook.deposit.help=Kauce zaplacená každým obchodníkem k zajištění obchodu. Bude vrácena po dokončení obchodu.
offerbook.createNewOffer=Vytvořit nabídku pro {0} {1}
offerbook.createOfferToBuy=Vytvořit novou nabídku k nákupu {0}
offerbook.createOfferToSell=Vytvořit novou nabídku k prodeji {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Neues Konto hinzufügen
shared.ExportAccounts=Konten exportieren
shared.importAccounts=Konten importieren
shared.createNewAccount=Neues Konto erstellen
shared.createNewAccountDescription=Ihre Kontodaten werden lokal auf Ihrem Gerät gespeichert und nur mit Ihrem Handelspartner und dem Schiedsrichter geteilt, wenn ein Streitfall eröffnet wird.
shared.saveNewAccount=Neues Konto speichern
shared.selectedAccount=Konto auswählen
shared.deleteAccount=Konto löschen
@ -192,6 +193,7 @@ shared.iConfirm=Ich bestätige
shared.openURL=Öffne {0}
shared.fiat=Fiat
shared.crypto=Crypto
shared.preciousMetals=Edelmetalle
shared.all=Alle
shared.edit=Bearbeiten
shared.advancedOptions=Erweiterte Optionen
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=Konto wurde geblockt
offerbook.timeSinceSigning.daysSinceSigning={0} Tage
offerbook.timeSinceSigning.daysSinceSigning.long={0} seit der Unterzeichnung
offerbook.xmrAutoConf=Automatische Bestätigung aktiviert
offerbook.buyXmrWith=XMR kaufen mit:
offerbook.sellXmrFor=XMR verkaufen für:
offerbook.timeSinceSigning.help=Wenn Sie einen Trade mit einem Partner erfolgreich abschließen, der ein unterzeichnetes Zahlungskonto hat, wird Ihr Zahlungskonto unterzeichnet.\n{0} Tage später wird das anfängliche Limit von {1} aufgehoben und Ihr Konto kann die Zahlungskonten anderer Partner unterzeichnen.
offerbook.timeSinceSigning.notSigned=Noch nicht unterzeichnet
@ -362,6 +366,7 @@ offerbook.nrOffers=Anzahl der Angebote: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Kaution XMR (%)
offerbook.deposit.help=Kaution die von beiden Handelspartnern bezahlt werden muss, um den Handel abzusichern. Wird zurückgezahlt, wenn der Handel erfolgreich abgeschlossen wurde.
offerbook.createNewOffer=Erstelle Angebot an {0} {1}
offerbook.createOfferToBuy=Neues Angebot erstellen, um {0} zu kaufen
offerbook.createOfferToSell=Neues Angebot erstellen, um {0} zu verkaufen

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Añadir una nueva cuenta
shared.ExportAccounts=Exportar cuentas
shared.importAccounts=Importar cuentas
shared.createNewAccount=Crear nueva cuenta
shared.createNewAccountDescription=Los detalles de su cuenta se almacenan localmente en su dispositivo y se comparten solo con su contraparte comercial y el árbitro si se abre una disputa.
shared.saveNewAccount=Guardar nueva cuenta
shared.selectedAccount=Cuenta seleccionada
shared.deleteAccount=Borrar cuenta
@ -192,6 +193,7 @@ shared.iConfirm=Confirmo
shared.openURL=Abrir {0}
shared.fiat=Fiat
shared.crypto=Cripto
shared.preciousMetals=Metales Preciosos
shared.all=Todos
shared.edit=Editar
shared.advancedOptions=Opciones avanzadas
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=La cuenta fue bloqueada
offerbook.timeSinceSigning.daysSinceSigning={0} días
offerbook.timeSinceSigning.daysSinceSigning.long={0} desde el firmado
offerbook.xmrAutoConf=¿Está habilitada la confirmación automática?
offerbook.buyXmrWith=Compra XMR con:
offerbook.sellXmrFor=Vender XMR por:
offerbook.timeSinceSigning.help=Cuando complete con éxito un intercambio con un par que tenga una cuenta de pago firmada, su cuenta de pago es firmada.\n{0} días después, el límite inicial de {1} se eleva y su cuenta puede firmar tras cuentas de pago.
offerbook.timeSinceSigning.notSigned=No firmada aún
@ -362,6 +366,7 @@ offerbook.nrOffers=Número de ofertas: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Depósito en XMR (%)
offerbook.deposit.help=Depósito pagado por cada comerciante para garantizar el intercambio. Será devuelto al acabar el intercambio.
offerbook.createNewOffer=Crear oferta a {0} {1}
offerbook.createOfferToBuy=Crear nueva oferta para comprar {0}
offerbook.createOfferToSell=Crear nueva oferta para vender {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=افزودن حساب جدید
shared.ExportAccounts=صادر کردن حساب‌ها
shared.importAccounts=وارد کردن حساب‌ها
shared.createNewAccount=ایجاد حساب جدید
shared.createNewAccountDescription=جزئیات حساب شما به‌طور محلی بر روی دستگاه شما ذخیره شده و تنها با هم‌تجارت شما و داور در صورت باز شدن یک اختلاف به اشتراک گذاشته می‌شود.
shared.saveNewAccount=ذخیره‌ی حساب جدید
shared.selectedAccount=حساب انتخاب شده
shared.deleteAccount=حذف حساب
@ -192,6 +193,7 @@ shared.iConfirm=تایید می‌کنم
shared.openURL=باز {0}
shared.fiat=فیات
shared.crypto=کریپتو
shared.preciousMetals=فلزات گرانبها
shared.all=همه
shared.edit=ویرایش
shared.advancedOptions=گزینه‌های پیشرفته
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=account was banned
offerbook.timeSinceSigning.daysSinceSigning={0} روز
offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=با XMR خرید کنید:
offerbook.sellXmrFor=فروش XMR برای:
offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts.
offerbook.timeSinceSigning.notSigned=Not signed yet
@ -362,6 +366,7 @@ offerbook.nrOffers=تعداد پیشنهادها: {0}
offerbook.volume={0} (حداقل - حداکثر)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=پیشنهاد ایجاد کنید به {0} {1}
offerbook.createOfferToBuy=پیشنهاد جدید برای خرید {0} ایجاد کن
offerbook.createOfferToSell=پیشنهاد جدید برای فروش {0} ایجاد کن

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Ajouter un nouveau compte
shared.ExportAccounts=Exporter les comptes
shared.importAccounts=Importer les comptes
shared.createNewAccount=Créer un nouveau compte
shared.createNewAccountDescription=Les détails de votre compte sont stockés localement sur votre appareil et partagés uniquement avec votre pair de trading et l'arbitre si un litige est ouvert.
shared.saveNewAccount=Sauvegarder un nouveau compte
shared.selectedAccount=Sélectionner un compte
shared.deleteAccount=Supprimer le compte
@ -192,6 +193,7 @@ shared.iConfirm=Je confirme
shared.openURL=Ouvert {0}
shared.fiat=Fiat
shared.crypto=Crypto
shared.preciousMetals=Métaux précieux
shared.all=Tout
shared.edit=Modifier
shared.advancedOptions=Options avancées
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=Ce compte a été banni
offerbook.timeSinceSigning.daysSinceSigning={0} jours
offerbook.timeSinceSigning.daysSinceSigning.long={0} depuis la signature
offerbook.xmrAutoConf=Est-ce-que la confirmation automatique est activée
offerbook.buyXmrWith=Acheter XMR avec :
offerbook.sellXmrFor=Vendre XMR pour :
offerbook.timeSinceSigning.help=Lorsque vous effectuez avec succès une transaction avec un pair disposant d''un compte de paiement signé, votre compte de paiement est signé.\n{0} Jours plus tard, la limite initiale de {1} est levée et votre compte peut signer les comptes de paiement d''un autre pair.
offerbook.timeSinceSigning.notSigned=Pas encore signé
@ -362,6 +366,7 @@ offerbook.nrOffers=Nombre d''ordres: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Déposer XMR (%)
offerbook.deposit.help=Les deux parties à la transaction ont payé un dépôt pour assurer que la transaction se déroule normalement. Ce montant sera remboursé une fois la transaction terminée.
offerbook.createNewOffer=Créer une offre à {0} {1}
offerbook.createOfferToBuy=Créer un nouvel ordre d''achat pour {0}
offerbook.createOfferToSell=Créer un nouvel ordre de vente pour {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Aggiungi nuovo account
shared.ExportAccounts=Esporta Account
shared.importAccounts=Importa Account
shared.createNewAccount=Crea nuovo account
shared.createNewAccountDescription=I dettagli del tuo account sono memorizzati localmente sul tuo dispositivo e condivisi solo con il tuo partner commerciale e l'arbitro se viene aperta una disputa.
shared.saveNewAccount=Salva nuovo account
shared.selectedAccount=Account selezionato
shared.deleteAccount=Elimina account
@ -192,6 +193,7 @@ shared.iConfirm=Confermo
shared.openURL=Aperti {0}
shared.fiat=Fiat
shared.crypto=Crypto
shared.preciousMetals=Metalli Preziosi
shared.all=Tutti
shared.edit=Modifica
shared.advancedOptions=Opzioni avanzate
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned= \nl'account è stato bannato
offerbook.timeSinceSigning.daysSinceSigning={0} giorni
offerbook.timeSinceSigning.daysSinceSigning.long={0} dalla firma
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=Compra XMR con:
offerbook.sellXmrFor=Vendi XMR per:
offerbook.timeSinceSigning.help=Quando completi correttamente un'operazione con un peer che ha un account di pagamento firmato, il tuo account di pagamento viene firmato.\n{0} giorni dopo, il limite iniziale di {1} viene alzato e il tuo account può firmare account di pagamento di altri peer.
offerbook.timeSinceSigning.notSigned=Non ancora firmato
@ -362,6 +366,7 @@ offerbook.nrOffers=N. di offerte: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=Crea offerta per {0} {1}
offerbook.createOfferToBuy=Crea una nuova offerta per comprare {0}
offerbook.createOfferToSell=Crea una nuova offerta per vendere {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=アカウントを追加
shared.ExportAccounts=アカウントをエクスポート
shared.importAccounts=アカウントをインポート
shared.createNewAccount=新しいアカウントを作る
shared.createNewAccountDescription=あなたのアカウント詳細は、デバイスにローカルに保存され、取引相手および紛争が発生した場合には仲裁人とのみ共有されます。
shared.saveNewAccount=新しいアカウントを保存する
shared.selectedAccount=選択したアカウント
shared.deleteAccount=アカウントを削除
@ -192,6 +193,7 @@ shared.iConfirm=確認します
shared.openURL={0} をオープン
shared.fiat=法定通貨
shared.crypto=暗号通貨
shared.preciousMetals=貴金属
shared.all=全て
shared.edit=編集
shared.advancedOptions=高度なオプション
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=このアカウントは禁止されま
offerbook.timeSinceSigning.daysSinceSigning={0}日
offerbook.timeSinceSigning.daysSinceSigning.long=署名する後から {0}
offerbook.xmrAutoConf=自動確認は有効されますか?
offerbook.buyXmrWith=XMRを購入:
offerbook.sellXmrFor=XMRを売る:
offerbook.timeSinceSigning.help=署名された支払いアカウントを持っているピアと成功にトレードすると、自身の支払いアカウントも署名されることになります。\n{0} 日後に、{1} という初期の制限は解除され、他のピアの支払いアカウントを署名できるようになります。
offerbook.timeSinceSigning.notSigned=まだ署名されていません
@ -362,6 +366,7 @@ offerbook.nrOffers=オファー数: {0}
offerbook.volume={0} (下限 - 上限)
offerbook.deposit=XMRの敷金(%)
offerbook.deposit.help=トレードを保証するため、両方の取引者が支払う敷金。トレードが完了されたら、返還されます。
offerbook.createNewOffer={0} {1}にオファーを作成する
offerbook.createOfferToBuy={0} を購入するオファーを作成
offerbook.createOfferToSell={0} を売却するオファーを作成

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Adicionar conta nova
shared.ExportAccounts=Exportar Contas
shared.importAccounts=Importar Contas
shared.createNewAccount=Criar nova conta
shared.createNewAccountDescription=Os detalhes da sua conta são armazenados localmente no seu dispositivo e compartilhados apenas com seu parceiro de negociação e o árbitro, caso uma disputa seja aberta.
shared.saveNewAccount=Salvar nova conta
shared.selectedAccount=Conta selecionada
shared.deleteAccount=Apagar conta
@ -192,6 +193,7 @@ shared.iConfirm=Eu confirmo
shared.openURL=Aberto {0}
shared.fiat=Fiat
shared.crypto=Cripto
shared.preciousMetals=Metais Preciosos
shared.all=Todos
shared.edit=Editar
shared.advancedOptions=Opções avançadas
@ -351,6 +353,8 @@ offerbook.timeSinceSigning.info.banned=conta foi banida
offerbook.timeSinceSigning.daysSinceSigning={0} dias
offerbook.timeSinceSigning.daysSinceSigning.long={0} desde a assinatura
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=Compre XMR com:
offerbook.sellXmrFor=Venda XMR por:
offerbook.timeSinceSigning.help=Quando você completa uma negociação bem sucedida com um par que tem uma conta de pagamento assinada, a sua conta de pagamento é assinada.\n{0} dias depois, o limite inicial de {1} é levantado e sua conta pode assinar as contas de pagamento de outros pares.
offerbook.timeSinceSigning.notSigned=Ainda não assinada
@ -365,6 +369,7 @@ offerbook.nrOffers=N.º de ofertas: {0}
offerbook.volume={0} (mín. - máx.)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=Criar oferta para {0} {1}
offerbook.createOfferToBuy=Criar oferta para comprar {0}
offerbook.createOfferToSell=Criar oferta para vender {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Adicionar uma nova conta
shared.ExportAccounts=Exportar Contas
shared.importAccounts=Importar Contas
shared.createNewAccount=Criar nova conta
shared.createNewAccountDescription=Os detalhes da sua conta são armazenados localmente no seu dispositivo e compartilhados apenas com seu parceiro de negociação e o árbitro, caso uma disputa seja aberta.
shared.saveNewAccount=Guardar nova conta
shared.selectedAccount=Conta selecionada
shared.deleteAccount=Apagar conta
@ -192,6 +193,7 @@ shared.iConfirm=Eu confirmo
shared.openURL=Abrir {0}
shared.fiat=Moeda fiduciária
shared.crypto=Cripto
shared.preciousMetals=TODO
shared.all=Tudo
shared.edit=Editar
shared.advancedOptions=Opções avançadas
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=account was banned
offerbook.timeSinceSigning.daysSinceSigning={0} dias
offerbook.timeSinceSigning.daysSinceSigning.long={0} desde a assinatura
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=Compre XMR com:
offerbook.sellXmrFor=Venda XMR por:
offerbook.timeSinceSigning.help=Quando você completa com sucesso um negócio com um par que tenha uma conta de pagamento assinada, a sua conta de pagamento é assinada .\n{0} dias depois, o limite inicial de {1} é aumentado e a sua conta pode assinar contas de pagamento de outros pares.
offerbook.timeSinceSigning.notSigned=Ainda não assinada
@ -362,6 +366,7 @@ offerbook.nrOffers=Nº de ofertas: {0}
offerbook.volume={0} (mín - máx)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=Criar oferta para {0} {1}
offerbook.createOfferToBuy=Criar nova oferta para comprar {0}
offerbook.createOfferToSell=Criar nova oferta para vender {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Добавить новый счёт
shared.ExportAccounts=Экспортировать счета
shared.importAccounts=Импортировать счета
shared.createNewAccount=Создать новый счёт
shared.createNewAccountDescription=Данные вашей учетной записи хранятся локально на вашем устройстве и передаются только вашему торговому партнеру и арбитру, если открывается спор.
shared.saveNewAccount=Сохранить новый счёт
shared.selectedAccount=Выбранный счёт
shared.deleteAccount=Удалить счёт
@ -192,6 +193,7 @@ shared.iConfirm=Подтверждаю
shared.openURL=Открыть {0}
shared.fiat=Нац. валюта
shared.crypto=Криптовалюта
shared.preciousMetals=Драгоценные металлы
shared.all=Все
shared.edit=Редактировать
shared.advancedOptions=Дополнительные настройки
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=account was banned
offerbook.timeSinceSigning.daysSinceSigning={0} дн.
offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=Купить XMR с помощью:
offerbook.sellXmrFor=Продать XMR за:
offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts.
offerbook.timeSinceSigning.notSigned=Not signed yet
@ -362,6 +366,7 @@ offerbook.nrOffers=Кол-во предложений: {0}
offerbook.volume={0} (мин. ⁠— макс.)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=Создать предложение для {0} {1}
offerbook.createOfferToBuy=Создать новое предложение на покупку {0}
offerbook.createOfferToSell=Создать новое предложение на продажу {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=เพิ่มบัญชีใหม่
shared.ExportAccounts=บัญชีส่งออก
shared.importAccounts=บัญชีนำเข้า
shared.createNewAccount=สร้างบัญชีใหม่
shared.createNewAccountDescription=รายละเอียดบัญชีของคุณถูกจัดเก็บไว้ในอุปกรณ์ของคุณและจะแบ่งปันเฉพาะกับคู่ค้าของคุณและผู้ตัดสินหากมีการเปิดข้อพิพาท
shared.saveNewAccount=บันทึกบัญชีใหม่
shared.selectedAccount=บัญชีที่เลือก
shared.deleteAccount=ลบบัญชี
@ -192,6 +193,7 @@ shared.iConfirm=ฉันยืนยัน
shared.openURL=เปิด {0}
shared.fiat=คำสั่ง
shared.crypto=คริปโต
shared.preciousMetals=โลหะมีค่า
shared.all=ทั้งหมด
shared.edit=แก้ไข
shared.advancedOptions=ทางเลือกขั้นสูง
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=account was banned
offerbook.timeSinceSigning.daysSinceSigning={0} วัน
offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=ซื้อ XMR ด้วย:
offerbook.sellXmrFor=ขาย XMR สำหรับ:
offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts.
offerbook.timeSinceSigning.notSigned=Not signed yet
@ -362,6 +366,7 @@ offerbook.nrOffers=No. ของข้อเสนอ: {0}
offerbook.volume={0} (ต่ำสุด - สูงสุด)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=สร้างข้อเสนอให้กับ {0} {1}
offerbook.createOfferToBuy=Create new offer to buy {0}
offerbook.createOfferToSell=Create new offer to sell {0}

View file

@ -150,6 +150,7 @@ shared.addNewAccount=Yeni hesap ekle
shared.ExportAccounts=Hesapları Dışa Aktar
shared.importAccounts=Hesapları İçe Aktar
shared.createNewAccount=Yeni hesap oluştur
shared.createNewAccountDescription=Hesap bilgileriniz yerel olarak cihazınızda saklanır ve yalnızca ticaret ortağınızla ve bir anlaşmazlık açılırsa hakemle paylaşılır.
shared.saveNewAccount=Yeni hesabı kaydet
shared.selectedAccount=Seçilen hesap
shared.deleteAccount=Hesabı sil
@ -204,6 +205,7 @@ shared.iConfirm=Onaylıyorum
shared.openURL={0}'i aç
shared.fiat=Fiat
shared.crypto=Kripto
shared.preciousMetals=Değerli Madenler
shared.traditional=Nakit
shared.otherAssets=diğer varlıklar
shared.other=Diğer
@ -245,8 +247,8 @@ shared.taker=Alıcı
####################################################################
mainView.menu.market=Piyasa
mainView.menu.buy=Satın Al
mainView.menu.sell=Sat
mainView.menu.buyXmr=XMR Satın Al
mainView.menu.sellXmr=XMR Sat
mainView.menu.portfolio=Portföy
mainView.menu.funds=Fonlar
mainView.menu.support=Destek
@ -372,6 +374,8 @@ offerbook.timeSinceSigning.tooltip.checkmark.buyXmr=imzalı bir hesaptan XMR al
offerbook.timeSinceSigning.tooltip.checkmark.wait=minimal {0} gün bekleyin
offerbook.timeSinceSigning.tooltip.learnMore=Daha fazla bilgi edin
offerbook.xmrAutoConf=Otomatik onay etkin mi
offerbook.buyXmrWith=XMR satın al:
offerbook.sellXmrFor=XMR'i şunlar için satın:
offerbook.timeSinceSigning.help=Bir imzalı ödeme hesabı olan bir eş ile başarılı bir şekilde işlem yaptığınızda, ödeme hesabınız imzalanır.\n\
{0} gün sonra, başlangıç limiti {1} kaldırılır ve hesabınız diğer eşlerin ödeme hesaplarını imzalayabilir.
@ -386,7 +390,7 @@ offerbook.volume={0} (min - maks)
offerbook.deposit=Mevduat XMR (%)
offerbook.deposit.help=Her yatırımcı tarafından işlemi garanti altına almak için ödenen mevduat. İşlem tamamlandığında geri verilecektir.
offerbook.createNewOffer=teklif aç {0} {1}
offerbook.createNewOffer=Teklif oluştur {0} {1}
offerbook.createOfferDisabled.tooltip=Bir seferde sadece bir teklif oluşturabilirsiniz
offerbook.takeOfferButton.tooltip=Teklifi al {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=Thêm tài khoản mới
shared.ExportAccounts=Truy xuất tài khoản
shared.importAccounts=Truy nhập tài khoản
shared.createNewAccount=Tạo tài khoản mới
shared.createNewAccountDescription=Thông tin tài khoản của bạn được lưu trữ cục bộ trên thiết bị của bạn và chỉ được chia sẻ với đối tác giao dịch của bạn và trọng tài nếu xảy ra tranh chấp.
shared.saveNewAccount=Lưu tài khoản mới
shared.selectedAccount=Tài khoản được chọn
shared.deleteAccount=Xóa tài khoản
@ -192,6 +193,7 @@ shared.iConfirm=Tôi xác nhận
shared.openURL=Mở {0}
shared.fiat=Tiền pháp định
shared.crypto=Tiền mã hóa
shared.preciousMetals=Kim loại quý
shared.all=Tất cả
shared.edit=Chỉnh sửa
shared.advancedOptions=Tùy chọn nâng cao
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=account was banned
offerbook.timeSinceSigning.daysSinceSigning={0} ngày
offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing
offerbook.xmrAutoConf=Is auto-confirm enabled
offerbook.buyXmrWith=Mua XMR với:
offerbook.sellXmrFor=Bán XMR để:
offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n{0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts.
offerbook.timeSinceSigning.notSigned=Not signed yet
@ -362,6 +366,7 @@ offerbook.nrOffers=Số chào giá: {0}
offerbook.volume={0} (min - max)
offerbook.deposit=Deposit XMR (%)
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
offerbook.createNewOffer=Tạo ưu đãi cho {0} {1}
offerbook.createOfferToBuy=Tạo chào giá mua mới {0}
offerbook.createOfferToSell=Tạo chào giá bán mới {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=添加新的账户
shared.ExportAccounts=导出账户
shared.importAccounts=导入账户
shared.createNewAccount=创建新的账户
shared.createNewAccountDescription=您的账户详情存储在您的设备上,仅与您的交易对手和仲裁员在出现争议时共享。
shared.saveNewAccount=保存新的账户
shared.selectedAccount=选中的账户
shared.deleteAccount=删除账户
@ -192,6 +193,7 @@ shared.iConfirm=我确认
shared.openURL=打开 {0}
shared.fiat=法定货币
shared.crypto=加密
shared.preciousMetals=贵金属
shared.all=全部
shared.edit=编辑
shared.advancedOptions=高级选项
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=账户已被封禁
offerbook.timeSinceSigning.daysSinceSigning={0} 天
offerbook.timeSinceSigning.daysSinceSigning.long=自验证{0}
offerbook.xmrAutoConf=是否开启自动确认
offerbook.buyXmrWith=使用以下方式购买 XMR
offerbook.sellXmrFor=出售 XMR 以换取:
offerbook.timeSinceSigning.help=当您成功地完成与拥有已验证付款帐户的伙伴交易时,您的付款帐户已验证。\n{0} 天后,最初的 {1} 的限制解除以及你的账户可以验证其他人的付款账户。
offerbook.timeSinceSigning.notSigned=尚未验证
@ -362,6 +366,7 @@ offerbook.nrOffers=报价数量:{0}
offerbook.volume={0}(最小 - 最大)
offerbook.deposit=XMR 保证金(%
offerbook.deposit.help=交易双方均已支付保证金确保这个交易正常进行。这会在交易完成时退还。
offerbook.createNewOffer=創建報價給 {0} {1}
offerbook.createOfferToBuy=创建新的报价来买入 {0}
offerbook.createOfferToSell=创建新的报价来卖出 {0}

View file

@ -139,6 +139,7 @@ shared.addNewAccount=添加新的賬户
shared.ExportAccounts=導出賬户
shared.importAccounts=導入賬户
shared.createNewAccount=創建新的賬户
shared.createNewAccountDescription=您的帳戶詳細資料儲存在您的裝置上,僅在開啟爭議時與您的交易夥伴和仲裁者分享。
shared.saveNewAccount=保存新的賬户
shared.selectedAccount=選中的賬户
shared.deleteAccount=刪除賬户
@ -192,6 +193,7 @@ shared.iConfirm=我確認
shared.openURL=打開 {0}
shared.fiat=法定貨幣
shared.crypto=加密
shared.preciousMetals=貴金屬
shared.all=全部
shared.edit=編輯
shared.advancedOptions=高級選項
@ -348,6 +350,8 @@ offerbook.timeSinceSigning.info.banned=賬户已被封禁
offerbook.timeSinceSigning.daysSinceSigning={0} 天
offerbook.timeSinceSigning.daysSinceSigning.long=自驗證{0}
offerbook.xmrAutoConf=是否開啟自動確認
offerbook.buyXmrWith=購買 XMR 使用:
offerbook.sellXmrFor=出售 XMR 以換取:
offerbook.timeSinceSigning.help=當您成功地完成與擁有已驗證付款帳户的夥伴交易時,您的付款帳户已驗證。\n{0} 天后,最初的 {1} 的限制解除以及你的賬户可以驗證其他人的付款賬户。
offerbook.timeSinceSigning.notSigned=尚未驗證
@ -362,6 +366,7 @@ offerbook.nrOffers=報價數量:{0}
offerbook.volume={0}(最小 - 最大)
offerbook.deposit=XMR 保證金(%
offerbook.deposit.help=交易雙方均已支付保證金確保這個交易正常進行。這會在交易完成時退還。
offerbook.createNewOffer=創建報價給 {0} {1}
offerbook.createOfferToBuy=創建新的報價來買入 {0}
offerbook.createOfferToSell=創建新的報價來賣出 {0}

View file

@ -5,10 +5,10 @@
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
<key>CFBundleVersion</key>
<string>1.0.12</string>
<string>1.0.13</string>
<key>CFBundleShortVersionString</key>
<string>1.0.12</string>
<string>1.0.13</string>
<key>CFBundleExecutable</key>
<string>Haveno</string>

View file

@ -38,6 +38,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.Window;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional;
@ -228,6 +229,17 @@ public class HavenoAppMain extends HavenoExecutable {
return null;
}
});
// Focus the password field when dialog is shown
Window window = getDialogPane().getScene().getWindow();
if (window instanceof Stage) {
Stage dialogStage = (Stage) window;
dialogStage.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
passwordField.requestFocus();
}
});
}
}
}
}

View file

@ -1237,6 +1237,14 @@ textfield */
-jfx-rippler-fill: -fx-accent;
}
.tab:disabled .jfx-rippler {
-jfx-rippler-fill: none !important;
}
.tab:disabled .tab-label {
-fx-cursor: default !important;
}
.jfx-tab-pane .headers-region .tab .tab-container .tab-close-button > .jfx-svg-glyph {
-fx-shape: "M810 274l-238 238 238 238-60 60-238-238-238 238-60-60 238-238-238-238 60-60 238 238 238-238z";
-jfx-size: 9;
@ -1267,6 +1275,7 @@ textfield */
-fx-padding: 14;
-fx-font-size: 0.769em;
-fx-font-weight: normal;
-fx-cursor: hand;
}
.jfx-tab-pane .depth-container {

View file

@ -165,8 +165,8 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
MainView.rootContainer.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
ToggleButton marketButton = new NavButton(MarketView.class, Res.get("mainView.menu.market").toUpperCase());
ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buy").toUpperCase());
ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sell").toUpperCase());
ToggleButton buyButton = new NavButton(BuyOfferView.class, Res.get("mainView.menu.buyXmr").toUpperCase());
ToggleButton sellButton = new NavButton(SellOfferView.class, Res.get("mainView.menu.sellXmr").toUpperCase());
ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio").toUpperCase());
ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds").toUpperCase());

View file

@ -136,6 +136,7 @@ import haveno.desktop.util.FormBuilder;
import static haveno.desktop.util.FormBuilder.add2ButtonsAfterGroup;
import static haveno.desktop.util.FormBuilder.add3ButtonsAfterGroup;
import static haveno.desktop.util.FormBuilder.addTitledGroupBg;
import static haveno.desktop.util.FormBuilder.addLabel;
import static haveno.desktop.util.FormBuilder.addTopLabelListView;
import haveno.desktop.util.GUIUtil;
import haveno.desktop.util.Layout;
@ -463,8 +464,9 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
removeAccountRows();
addAccountButton.setDisable(true);
accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE);
addLabel(root, gridRow, Res.get("shared.createNewAccountDescription"), Layout.COMPACT_FIRST_ROW_DISTANCE);
paymentMethodComboBox = FormBuilder.addAutocompleteComboBox(
root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE
root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE + Layout.PADDING
);
paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10));
paymentMethodComboBox.setPrefWidth(250);

View file

@ -162,8 +162,7 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
transactionColumn.setComparator(Comparator.comparing(TransactionsListItem::getTxId));
amountColumn.setComparator(Comparator.comparing(TransactionsListItem::getAmount));
confidenceColumn.setComparator(Comparator.comparingLong(TransactionsListItem::getNumConfirmations));
memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo));
memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo, Comparator.nullsLast(Comparator.naturalOrder())));
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);

View file

@ -207,16 +207,21 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
@Override
public String toString(Number object) {
final double doubleValue = (double) object;
if (CurrencyUtil.isCryptoCurrency(model.getCurrencyCode())) {
final String withCryptoPrecision = FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, cryptoPrecision);
if (withCryptoPrecision.startsWith("0.0")) {
return FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 8).replaceFirst("0+$", "");
try {
final double doubleValue = (double) object;
if (CurrencyUtil.isCryptoCurrency(model.getCurrencyCode())) {
final String withCryptoPrecision = FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, cryptoPrecision);
if (withCryptoPrecision.startsWith("0.0")) {
return FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 8).replaceFirst("0+$", "");
} else {
return withCryptoPrecision.replaceFirst("0+$", "");
}
} else {
return withCryptoPrecision.replaceFirst("0+$", "");
return df.format(Double.parseDouble(FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 0)));
}
} else {
return df.format(Double.parseDouble(FormattingUtils.formatRoundedDoubleWithPrecision(doubleValue, 0)));
} catch (IllegalArgumentException e) {
log.error("Error converting number to string, tradeCurrency={}, number={}\n", code, object, e);
return "NaN"; // TODO: occasionally getting invalid number
}
}

View file

@ -425,14 +425,10 @@ class OfferBookChartViewModel extends ActivatableViewModel {
if (isSellOffer(direction)) {
if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) {
preferences.setBuyScreenCurrencyCode(getCurrencyCode());
} else if (!getCurrencyCode().equals(GUIUtil.TOP_CRYPTO.getCode())) {
preferences.setBuyScreenCryptoCurrencyCode(getCurrencyCode());
}
} else {
if (CurrencyUtil.isTraditionalCurrency(getCurrencyCode())) {
preferences.setSellScreenCurrencyCode(getCurrencyCode());
} else if (!getCurrencyCode().equals(GUIUtil.TOP_CRYPTO.getCode())) {
preferences.setSellScreenCryptoCurrencyCode(getCurrencyCode());
}
}
}

View file

@ -18,6 +18,8 @@
package haveno.desktop.main.offer;
import com.google.inject.Inject;
import haveno.core.locale.Res;
import haveno.core.offer.OfferDirection;
import haveno.core.user.Preferences;
import haveno.core.user.User;
@ -42,4 +44,9 @@ public class BuyOfferView extends OfferView {
p2PService,
OfferDirection.BUY);
}
@Override
protected String getOfferLabel() {
return Res.get("offerbook.buyXmrWith");
}
}

View file

@ -57,7 +57,6 @@ import java.util.Comparator;
import static java.util.Comparator.comparing;
import java.util.Date;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
@ -257,10 +256,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
private Optional<PaymentAccount> getAnyPaymentAccount() {
if (CurrencyUtil.isFiatCurrency(tradeCurrency.getCode())) {
return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.isFiat()).findAny();
} else if (CurrencyUtil.isCryptoCurrency(tradeCurrency.getCode())) {
return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.isCryptoCurrency()).findAny();
} else {
return paymentAccounts.stream().filter(paymentAccount1 -> !paymentAccount1.isFiat() &&
paymentAccount1.getTradeCurrency().isPresent() &&
!Objects.equals(paymentAccount1.getTradeCurrency().get().getCode(), GUIUtil.TOP_CRYPTO.getCode())).findAny();
return paymentAccounts.stream().filter(paymentAccount1 -> paymentAccount1.getTradeCurrency().isPresent()).findAny();
}
}

View file

@ -268,7 +268,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
dataModel.getTradeCurrencyCode()));
}
volumePromptLabel.bind(createStringBinding(
() -> Res.get("createOffer.volume.prompt", dataModel.getTradeCurrencyCode().get()),
() -> Res.get("createOffer.volume.prompt", CurrencyUtil.getCurrencyCodeBase(dataModel.getTradeCurrencyCode().get())),
dataModel.getTradeCurrencyCode()));
totalToPay.bind(createStringBinding(() -> HavenoUtils.formatXmr(dataModel.totalToPayAsProperty().get(), true),

View file

@ -33,14 +33,15 @@ import haveno.desktop.common.view.View;
import haveno.desktop.common.view.ViewLoader;
import haveno.desktop.main.MainView;
import haveno.desktop.main.offer.createoffer.CreateOfferView;
import haveno.desktop.main.offer.offerbook.XmrOfferBookView;
import haveno.desktop.main.offer.offerbook.FiatOfferBookView;
import haveno.desktop.main.offer.offerbook.OfferBookView;
import haveno.desktop.main.offer.offerbook.CryptoOfferBookView;
import haveno.desktop.main.offer.offerbook.OtherOfferBookView;
import haveno.desktop.main.offer.offerbook.TopCryptoOfferBookView;
import haveno.desktop.main.offer.takeoffer.TakeOfferView;
import haveno.desktop.util.GUIUtil;
import haveno.network.p2p.P2PService;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import org.jetbrains.annotations.NotNull;
@ -50,9 +51,9 @@ import java.util.Optional;
public abstract class OfferView extends ActivatableView<TabPane, Void> {
private OfferBookView<?, ?> xmrOfferBookView, topCryptoOfferBookView, otherOfferBookView;
private OfferBookView<?, ?> fiatOfferBookView, cryptoOfferBookView, otherOfferBookView;
private Tab xmrOfferBookTab, topCryptoOfferBookTab, otherOfferBookTab;
private Tab labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab;
private final ViewLoader viewLoader;
private final Navigation navigation;
@ -95,17 +96,17 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
tabChangeListener = (observableValue, oldValue, newValue) -> {
UserThread.execute(() -> {
if (newValue != null) {
if (newValue.equals(xmrOfferBookTab)) {
if (xmrOfferBookView != null) {
xmrOfferBookView.onTabSelected(true);
if (newValue.equals(fiatOfferBookTab)) {
if (fiatOfferBookView != null) {
fiatOfferBookView.onTabSelected(true);
} else {
loadView(XmrOfferBookView.class, null, null);
loadView(FiatOfferBookView.class, null, null);
}
} else if (newValue.equals(topCryptoOfferBookTab)) {
if (topCryptoOfferBookView != null) {
topCryptoOfferBookView.onTabSelected(true);
} else if (newValue.equals(cryptoOfferBookTab)) {
if (cryptoOfferBookView != null) {
cryptoOfferBookView.onTabSelected(true);
} else {
loadView(TopCryptoOfferBookView.class, null, null);
loadView(CryptoOfferBookView.class, null, null);
}
} else if (newValue.equals(otherOfferBookTab)) {
if (otherOfferBookView != null) {
@ -116,10 +117,10 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
}
}
if (oldValue != null) {
if (oldValue.equals(xmrOfferBookTab) && xmrOfferBookView != null) {
xmrOfferBookView.onTabSelected(false);
} else if (oldValue.equals(topCryptoOfferBookTab) && topCryptoOfferBookView != null) {
topCryptoOfferBookView.onTabSelected(false);
if (oldValue.equals(fiatOfferBookTab) && fiatOfferBookView != null) {
fiatOfferBookView.onTabSelected(false);
} else if (oldValue.equals(cryptoOfferBookTab) && cryptoOfferBookView != null) {
cryptoOfferBookView.onTabSelected(false);
} else if (oldValue.equals(otherOfferBookTab) && otherOfferBookView != null) {
otherOfferBookView.onTabSelected(false);
}
@ -154,14 +155,8 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener);
navigation.addListener(navigationListener);
if (xmrOfferBookView == null) {
navigation.navigateTo(MainView.class, this.getClass(), XmrOfferBookView.class);
}
GUIUtil.updateTopCrypto(preferences);
if (topCryptoOfferBookTab != null) {
topCryptoOfferBookTab.setText(GUIUtil.TOP_CRYPTO.getName().toUpperCase());
if (fiatOfferBookView == null) {
navigation.navigateTo(MainView.class, this.getClass(), FiatOfferBookView.class);
}
}
@ -171,6 +166,8 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener);
}
protected abstract String getOfferLabel();
private void loadView(Class<? extends View> viewClass,
Class<? extends View> childViewClass,
@Nullable Object data) {
@ -179,66 +176,75 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
if (OfferBookView.class.isAssignableFrom(viewClass)) {
if (viewClass == XmrOfferBookView.class && xmrOfferBookTab != null && xmrOfferBookView != null) {
if (viewClass == FiatOfferBookView.class && fiatOfferBookTab != null && fiatOfferBookView != null) {
if (childViewClass == null) {
xmrOfferBookTab.setContent(xmrOfferBookView.getRoot());
fiatOfferBookTab.setContent(fiatOfferBookView.getRoot());
} else if (childViewClass == TakeOfferView.class) {
loadTakeViewClass(viewClass, childViewClass, xmrOfferBookTab);
loadTakeViewClass(viewClass, childViewClass, fiatOfferBookTab);
} else {
loadCreateViewClass(xmrOfferBookView, viewClass, childViewClass, xmrOfferBookTab, (PaymentMethod) data);
loadCreateViewClass(fiatOfferBookView, viewClass, childViewClass, fiatOfferBookTab, (PaymentMethod) data);
}
tabPane.getSelectionModel().select(xmrOfferBookTab);
} else if (viewClass == TopCryptoOfferBookView.class && topCryptoOfferBookTab != null && topCryptoOfferBookView != null) {
tabPane.getSelectionModel().select(fiatOfferBookTab);
} else if (viewClass == CryptoOfferBookView.class && cryptoOfferBookTab != null && cryptoOfferBookView != null) {
if (childViewClass == null) {
topCryptoOfferBookTab.setContent(topCryptoOfferBookView.getRoot());
cryptoOfferBookTab.setContent(cryptoOfferBookView.getRoot());
} else if (childViewClass == TakeOfferView.class) {
loadTakeViewClass(viewClass, childViewClass, topCryptoOfferBookTab);
loadTakeViewClass(viewClass, childViewClass, cryptoOfferBookTab);
} else {
tradeCurrency = GUIUtil.TOP_CRYPTO;
loadCreateViewClass(topCryptoOfferBookView, viewClass, childViewClass, topCryptoOfferBookTab, (PaymentMethod) data);
}
tabPane.getSelectionModel().select(topCryptoOfferBookTab);
} else if (viewClass == OtherOfferBookView.class && otherOfferBookTab != null && otherOfferBookView != null) {
if (childViewClass == null) {
otherOfferBookTab.setContent(otherOfferBookView.getRoot());
} else if (childViewClass == TakeOfferView.class) {
loadTakeViewClass(viewClass, childViewClass, otherOfferBookTab);
} else {
//add sanity check in case of app restart
// add sanity check in case of app restart
if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) {
Optional<TradeCurrency> tradeCurrencyOptional = (this.direction == OfferDirection.SELL) ?
CurrencyUtil.getTradeCurrency(preferences.getSellScreenCryptoCurrencyCode()) :
CurrencyUtil.getTradeCurrency(preferences.getBuyScreenCryptoCurrencyCode());
tradeCurrency = tradeCurrencyOptional.isEmpty() ? OfferViewUtil.getAnyOfMainCryptoCurrencies() : tradeCurrencyOptional.get();
}
loadCreateViewClass(cryptoOfferBookView, viewClass, childViewClass, cryptoOfferBookTab, (PaymentMethod) data);
}
tabPane.getSelectionModel().select(cryptoOfferBookTab);
} else if (viewClass == OtherOfferBookView.class && otherOfferBookTab != null && otherOfferBookView != null) {
if (childViewClass == null) {
otherOfferBookTab.setContent(otherOfferBookView.getRoot());
} else if (childViewClass == TakeOfferView.class) {
loadTakeViewClass(viewClass, childViewClass, otherOfferBookTab);
} else {
loadCreateViewClass(otherOfferBookView, viewClass, childViewClass, otherOfferBookTab, (PaymentMethod) data);
}
tabPane.getSelectionModel().select(otherOfferBookTab);
} else {
if (xmrOfferBookTab == null) {
xmrOfferBookTab = new Tab(Res.getBaseCurrencyName().toUpperCase());
xmrOfferBookTab.setClosable(false);
topCryptoOfferBookTab = new Tab(GUIUtil.TOP_CRYPTO.getName().toUpperCase());
topCryptoOfferBookTab.setClosable(false);
if (fiatOfferBookTab == null) {
// add preceding label tab
labelTab = new Tab();
labelTab.setDisable(true);
labelTab.setContent(new Label());
labelTab.setClosable(false);
Label offerLabel = new Label(getOfferLabel()); // use overlay for label for custom formatting
offerLabel.getStyleClass().add("titled-group-bg-label");
offerLabel.setStyle("-fx-font-size: 1.4em;");
labelTab.setGraphic(offerLabel);
fiatOfferBookTab = new Tab(Res.get("shared.fiat").toUpperCase());
fiatOfferBookTab.setClosable(false);
cryptoOfferBookTab = new Tab(Res.get("shared.crypto").toUpperCase());
cryptoOfferBookTab.setClosable(false);
otherOfferBookTab = new Tab(Res.get("shared.other").toUpperCase());
otherOfferBookTab.setClosable(false);
tabPane.getTabs().addAll(xmrOfferBookTab, topCryptoOfferBookTab, otherOfferBookTab);
tabPane.getTabs().addAll(labelTab, fiatOfferBookTab, cryptoOfferBookTab, otherOfferBookTab);
}
if (viewClass == XmrOfferBookView.class) {
xmrOfferBookView = (XmrOfferBookView) viewLoader.load(XmrOfferBookView.class);
xmrOfferBookView.setOfferActionHandler(offerActionHandler);
xmrOfferBookView.setDirection(direction);
xmrOfferBookView.onTabSelected(true);
tabPane.getSelectionModel().select(xmrOfferBookTab);
xmrOfferBookTab.setContent(xmrOfferBookView.getRoot());
} else if (viewClass == TopCryptoOfferBookView.class) {
topCryptoOfferBookView = (TopCryptoOfferBookView) viewLoader.load(TopCryptoOfferBookView.class);
topCryptoOfferBookView.setOfferActionHandler(offerActionHandler);
topCryptoOfferBookView.setDirection(direction);
topCryptoOfferBookView.onTabSelected(true);
tabPane.getSelectionModel().select(topCryptoOfferBookTab);
topCryptoOfferBookTab.setContent(topCryptoOfferBookView.getRoot());
if (viewClass == FiatOfferBookView.class) {
fiatOfferBookView = (FiatOfferBookView) viewLoader.load(FiatOfferBookView.class);
fiatOfferBookView.setOfferActionHandler(offerActionHandler);
fiatOfferBookView.setDirection(direction);
fiatOfferBookView.onTabSelected(true);
tabPane.getSelectionModel().select(fiatOfferBookTab);
fiatOfferBookTab.setContent(fiatOfferBookView.getRoot());
} else if (viewClass == CryptoOfferBookView.class) {
cryptoOfferBookView = (CryptoOfferBookView) viewLoader.load(CryptoOfferBookView.class);
cryptoOfferBookView.setOfferActionHandler(offerActionHandler);
cryptoOfferBookView.setDirection(direction);
cryptoOfferBookView.onTabSelected(true);
tabPane.getSelectionModel().select(cryptoOfferBookTab);
cryptoOfferBookTab.setContent(cryptoOfferBookView.getRoot());
} else if (viewClass == OtherOfferBookView.class) {
otherOfferBookView = (OtherOfferBookView) viewLoader.load(OtherOfferBookView.class);
otherOfferBookView.setOfferActionHandler(offerActionHandler);
@ -265,11 +271,7 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
// in different graphs
view = viewLoader.load(childViewClass);
// Invert direction for non-Fiat trade currencies -> BUY BCH is to SELL Monero
OfferDirection offerDirection = CurrencyUtil.isFiatCurrency(tradeCurrency.getCode()) ? direction :
direction == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY;
((CreateOfferView) view).initWithData(offerDirection, tradeCurrency, offerActionHandler);
((CreateOfferView) view).initWithData(direction, tradeCurrency, offerActionHandler);
((SelectableView) view).onTabSelected(true);
@ -329,9 +331,9 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
private Class<? extends OfferBookView<?, ?>> getOfferBookViewClassFor(String currencyCode) {
Class<? extends OfferBookView<?, ?>> offerBookViewClass;
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
offerBookViewClass = XmrOfferBookView.class;
} else if (currencyCode.equals(GUIUtil.TOP_CRYPTO.getCode())) {
offerBookViewClass = TopCryptoOfferBookView.class;
offerBookViewClass = FiatOfferBookView.class;
} else if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
offerBookViewClass = CryptoOfferBookView.class;
} else {
offerBookViewClass = OtherOfferBookView.class;
}

View file

@ -22,16 +22,16 @@ import haveno.core.locale.CryptoCurrency;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.locale.TraditionalCurrency;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.main.offer.offerbook.XmrOfferBookView;
import haveno.desktop.main.offer.offerbook.FiatOfferBookView;
import haveno.desktop.main.offer.offerbook.OfferBookView;
import haveno.desktop.main.offer.offerbook.CryptoOfferBookView;
import haveno.desktop.main.offer.offerbook.OtherOfferBookView;
import haveno.desktop.main.offer.offerbook.TopCryptoOfferBookView;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.GUIUtil;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
@ -44,7 +44,6 @@ import monero.daemon.model.MoneroSubmitTxResult;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@ -90,10 +89,10 @@ public class OfferViewUtil {
public static Class<? extends OfferBookView<?, ?>> getOfferBookViewClass(String currencyCode) {
Class<? extends OfferBookView<?, ?>> offerBookViewClazz;
if (CurrencyUtil.isTraditionalCurrency(currencyCode)) {
offerBookViewClazz = XmrOfferBookView.class;
} else if (currencyCode.equals(GUIUtil.TOP_CRYPTO.getCode())) {
offerBookViewClazz = TopCryptoOfferBookView.class;
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
offerBookViewClazz = FiatOfferBookView.class;
} else if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
offerBookViewClazz = CryptoOfferBookView.class;
} else {
offerBookViewClazz = OtherOfferBookView.class;
}
@ -109,7 +108,7 @@ public class OfferViewUtil {
}
public static boolean isShownAsSellOffer(String currencyCode, OfferDirection direction) {
return CurrencyUtil.isFiatCurrency(currencyCode) == (direction == OfferDirection.SELL);
return direction == OfferDirection.SELL;
}
public static boolean isShownAsBuyOffer(Offer offer) {
@ -124,10 +123,18 @@ public class OfferViewUtil {
return getMainCryptoCurrencies().findAny().get();
}
public static TradeCurrency getAnyOfOtherCurrencies() {
return getOtherCurrencies().findAny().get();
}
@NotNull
public static Stream<CryptoCurrency> getMainCryptoCurrencies() {
return CurrencyUtil.getMainCryptoCurrencies().stream().filter(cryptoCurrency ->
!Objects.equals(cryptoCurrency.getCode(), GUIUtil.TOP_CRYPTO.getCode()));
return CurrencyUtil.getMainCryptoCurrencies().stream();
}
@NotNull
public static Stream<TraditionalCurrency> getOtherCurrencies() {
return CurrencyUtil.getTraditionalNonFiatCurrencies().stream();
}
public static void submitTransactionHex(XmrWalletService xmrWalletService,

View file

@ -18,6 +18,8 @@
package haveno.desktop.main.offer;
import com.google.inject.Inject;
import haveno.core.locale.Res;
import haveno.core.offer.OfferDirection;
import haveno.core.user.Preferences;
import haveno.core.user.User;
@ -42,4 +44,9 @@ public class SellOfferView extends OfferView {
p2PService,
OfferDirection.SELL);
}
@Override
protected String getOfferLabel() {
return Res.get("offerbook.sellXmrFor");
}
}

View file

@ -31,8 +31,6 @@ import haveno.desktop.common.view.FxmlView;
import haveno.desktop.main.offer.MutableOfferView;
import haveno.desktop.main.offer.OfferView;
import haveno.desktop.main.overlays.windows.OfferDetailsWindow;
import haveno.desktop.util.GUIUtil;
import java.util.Objects;
import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -60,12 +58,12 @@ public class CreateOfferView extends MutableOfferView<CreateOfferViewModel> {
protected ObservableList<PaymentAccount> filterPaymentAccounts(ObservableList<PaymentAccount> paymentAccounts) {
return FXCollections.observableArrayList(
paymentAccounts.stream().filter(paymentAccount -> {
if (model.getTradeCurrency().equals(GUIUtil.TOP_CRYPTO)) {
return Objects.equals(paymentAccount.getSingleTradeCurrency(), GUIUtil.TOP_CRYPTO);
} else if (CurrencyUtil.isFiatCurrency(model.getTradeCurrency().getCode())) {
if (CurrencyUtil.isFiatCurrency(model.getTradeCurrency().getCode())) {
return paymentAccount.isFiat();
} else if (CurrencyUtil.isCryptoCurrency(model.getTradeCurrency().getCode())) {
return paymentAccount.isCryptoCurrency();
} else {
return !paymentAccount.isFiat() && !Objects.equals(paymentAccount.getSingleTradeCurrency(), GUIUtil.TOP_CRYPTO);
return !paymentAccount.isFiat() && !paymentAccount.isCryptoCurrency();
}
}).collect(Collectors.toList()));
}

View file

@ -19,7 +19,7 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:id="root" fx:controller="haveno.desktop.main.offer.offerbook.TopCryptoOfferBookView"
<GridPane fx:id="root" fx:controller="haveno.desktop.main.offer.offerbook.CryptoOfferBookView"
hgap="5.0" vgap="5"
xmlns:fx="http://javafx.com/fxml">

View file

@ -33,39 +33,29 @@ import haveno.desktop.main.overlays.windows.OfferDetailsWindow;
import javafx.scene.layout.GridPane;
@FxmlView
public class TopCryptoOfferBookView extends OfferBookView<GridPane, TopCryptoOfferBookViewModel> {
public class CryptoOfferBookView extends OfferBookView<GridPane, CryptoOfferBookViewModel> {
@Inject
TopCryptoOfferBookView(TopCryptoOfferBookViewModel model,
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
CryptoOfferBookView(CryptoOfferBookViewModel model,
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService);
}
@Override
protected String getMarketTitle() {
return model.getDirection().equals(OfferDirection.BUY) ?
Res.get("offerbook.availableOffersToBuy", TopCryptoOfferBookViewModel.TOP_CRYPTO.getCode(), Res.getBaseCurrencyCode()) :
Res.get("offerbook.availableOffersToSell", TopCryptoOfferBookViewModel.TOP_CRYPTO.getCode(), Res.getBaseCurrencyCode());
}
@Override
protected void activate() {
model.onSetTradeCurrency(TopCryptoOfferBookViewModel.TOP_CRYPTO);
super.activate();
currencyComboBoxContainer.setVisible(false);
currencyComboBoxContainer.setManaged(false);
Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.crypto")) :
Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.crypto"));
}
@Override
String getTradeCurrencyCode() {
return TopCryptoOfferBookViewModel.TOP_CRYPTO.getCode();
return Res.getBaseCurrencyCode();
}
}

View file

@ -0,0 +1,148 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.main.offer.offerbook;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.api.CoreApi;
import haveno.core.locale.CryptoCurrency;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.GlobalSettings;
import haveno.core.locale.TradeCurrency;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OfferFilterService;
import haveno.core.offer.OpenOfferManager;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.user.Preferences;
import haveno.core.user.User;
import haveno.core.util.FormattingUtils;
import haveno.core.util.PriceUtil;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.desktop.Navigation;
import haveno.desktop.main.offer.OfferViewUtil;
import haveno.desktop.util.GUIUtil;
import haveno.network.p2p.P2PService;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class CryptoOfferBookViewModel extends OfferBookViewModel {
@Inject
public CryptoOfferBookViewModel(User user,
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CoreApi coreApi) {
super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi);
}
@Override
void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) {
if (direction == OfferDirection.BUY) {
preferences.setBuyScreenCryptoCurrencyCode(code);
} else {
preferences.setSellScreenCryptoCurrencyCode(code);
}
}
@Override
protected ObservableList<PaymentMethod> filterPaymentMethods(ObservableList<PaymentMethod> list,
TradeCurrency selectedTradeCurrency) {
return FXCollections.observableArrayList(list.stream().filter(paymentMethod -> {
return paymentMethod.isBlockchain();
}).collect(Collectors.toList()));
}
@Override
void fillCurrencies(ObservableList<TradeCurrency> tradeCurrencies,
ObservableList<TradeCurrency> allCurrencies) {
tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""));
tradeCurrencies.addAll(preferences.getCryptoCurrenciesAsObservable().stream()
.collect(Collectors.toList()));
tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, ""));
allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""));
allCurrencies.addAll(CurrencyUtil.getAllSortedCryptoCurrencies().stream()
.collect(Collectors.toList()));
allCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, ""));
}
@Override
Predicate<OfferBookListItem> getCurrencyAndMethodPredicate(OfferDirection direction,
TradeCurrency selectedTradeCurrency) {
return offerBookListItem -> {
Offer offer = offerBookListItem.getOffer();
boolean directionResult = offer.getDirection() != direction; // offer to buy xmr appears as offer to sell in peer's offer book and vice versa
boolean currencyResult = CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode()) &&
(showAllTradeCurrenciesProperty.get() ||
offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()));
boolean paymentMethodResult = showAllPaymentMethods ||
offer.getPaymentMethod().equals(selectedPaymentMethod);
boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook();
return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated;
};
}
@Override
TradeCurrency getDefaultTradeCurrency() {
TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency();
if (CurrencyUtil.isCryptoCurrency(defaultTradeCurrency.getCode()) &&
hasPaymentAccountForCurrency(defaultTradeCurrency)) {
return defaultTradeCurrency;
}
ObservableList<TradeCurrency> tradeCurrencies = FXCollections.observableArrayList(getTradeCurrencies());
if (!tradeCurrencies.isEmpty()) {
// drop show all entry and select first currency with payment account available
tradeCurrencies.remove(0);
List<TradeCurrency> sortedList = tradeCurrencies.stream().sorted((o1, o2) ->
Boolean.compare(!hasPaymentAccountForCurrency(o1),
!hasPaymentAccountForCurrency(o2))).collect(Collectors.toList());
return sortedList.get(0);
} else {
return OfferViewUtil.getMainCryptoCurrencies().sorted((o1, o2) ->
Boolean.compare(!hasPaymentAccountForCurrency(o1),
!hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0);
}
}
@Override
String getCurrencyCodeFromPreferences(OfferDirection direction) {
return direction == OfferDirection.BUY ? preferences.getBuyScreenCryptoCurrencyCode() :
preferences.getSellScreenCryptoCurrencyCode();
}
}

View file

@ -19,7 +19,7 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:id="root" fx:controller="haveno.desktop.main.offer.offerbook.XmrOfferBookView"
<GridPane fx:id="root" fx:controller="haveno.desktop.main.offer.offerbook.FiatOfferBookView"
hgap="5.0" vgap="5"
xmlns:fx="http://javafx.com/fxml">

View file

@ -33,17 +33,17 @@ import haveno.desktop.main.overlays.windows.OfferDetailsWindow;
import javafx.scene.layout.GridPane;
@FxmlView
public class XmrOfferBookView extends OfferBookView<GridPane, XmrOfferBookViewModel> {
public class FiatOfferBookView extends OfferBookView<GridPane, FiatOfferBookViewModel> {
@Inject
XmrOfferBookView(XmrOfferBookViewModel model,
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
FiatOfferBookView(FiatOfferBookViewModel model,
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService);
}

View file

@ -49,23 +49,23 @@ import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class XmrOfferBookViewModel extends OfferBookViewModel {
public class FiatOfferBookViewModel extends OfferBookViewModel {
@Inject
public XmrOfferBookViewModel(User user,
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CoreApi coreApi) {
public FiatOfferBookViewModel(User user,
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CoreApi coreApi) {
super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi);
}
@ -141,9 +141,10 @@ public class XmrOfferBookViewModel extends OfferBookViewModel {
!hasPaymentAccountForCurrency(o2))).collect(Collectors.toList());
return sortedList.get(0);
} else {
return CurrencyUtil.getMainTraditionalCurrencies().stream().sorted((o1, o2) ->
Boolean.compare(!hasPaymentAccountForCurrency(o1),
!hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0);
return CurrencyUtil.getMainTraditionalCurrencies().stream()
.filter(withFiatCurrency())
.sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2)))
.collect(Collectors.toList()).get(0);
}
}

View file

@ -52,7 +52,6 @@ import haveno.desktop.components.ColoredDecimalPlacesWithZerosText;
import haveno.desktop.components.HyperlinkWithIcon;
import haveno.desktop.components.InfoAutoTooltipLabel;
import haveno.desktop.components.PeerInfoIconTrading;
import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.main.MainView;
import haveno.desktop.main.account.AccountView;
import haveno.desktop.main.account.content.cryptoaccounts.CryptoAccountsView;
@ -67,7 +66,6 @@ import haveno.desktop.main.portfolio.PortfolioView;
import haveno.desktop.main.portfolio.editoffer.EditOfferView;
import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.GUIUtil;
import haveno.desktop.util.Layout;
import haveno.network.p2p.NodeAddress;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
@ -106,7 +104,6 @@ import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import static haveno.desktop.util.FormBuilder.addTitledGroupBg;
import static haveno.desktop.util.FormBuilder.addTopLabelAutoToolTipTextField;
abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewModel> extends ActivatableViewAndModel<R, M> {
@ -119,7 +116,6 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
private final AccountAgeWitnessService accountAgeWitnessService;
private final SignedWitnessService signedWitnessService;
private TitledGroupBg titledGroupBg;
protected AutocompleteComboBox<TradeCurrency> currencyComboBox;
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
private AutoTooltipButton createOfferButton;
@ -170,18 +166,10 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
public void initialize() {
root.setPadding(new Insets(15, 15, 5, 15));
titledGroupBg = addTitledGroupBg(
root,
gridRow,
2,
""
);
titledGroupBg.getStyleClass().add("last");
HBox offerToolsBox = new HBox();
offerToolsBox.setAlignment(Pos.BOTTOM_LEFT);
offerToolsBox.setSpacing(10);
offerToolsBox.setPadding(new Insets(10, 0, 0, 0));
offerToolsBox.setPadding(new Insets(0, 0, 0, 0));
Tuple3<VBox, Label, AutocompleteComboBox<TradeCurrency>> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox(
Res.get("offerbook.filterByCurrency"));
@ -202,7 +190,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
createOfferButton = new AutoTooltipButton("");
createOfferButton.setMinHeight(40);
createOfferButton.setGraphicTextGap(10);
createOfferButton.setStyle("-fx-padding: 0 15 0 15;");
disabledCreateOfferButtonTooltip = new Label("");
disabledCreateOfferButtonTooltip.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
disabledCreateOfferButtonTooltip.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
@ -225,7 +213,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
GridPane.setHgrow(offerToolsBox, Priority.ALWAYS);
GridPane.setRowIndex(offerToolsBox, gridRow);
GridPane.setColumnSpan(offerToolsBox, 2);
GridPane.setMargin(offerToolsBox, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0));
GridPane.setMargin(offerToolsBox, new Insets(0, 0, 0, 0));
root.getChildren().add(offerToolsBox);
tableView = new TableView<>();
@ -332,10 +320,6 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
@Override
protected void activate() {
titledGroupBg.setText(getMarketTitle());
titledGroupBg.setHelpUrl(model.getDirection() == OfferDirection.SELL
? "https://haveno.exchange/wiki/Introduction#In_a_nutshell"
: "https://haveno.exchange/wiki/Taking_an_offer");
Map<String, Integer> offerCounts = OfferViewUtil.isShownAsBuyOffer(model.getDirection(), model.getSelectedTradeCurrency()) ? model.getSellOfferCounts() : model.getBuyOfferCounts();
currencyComboBox.setCellFactory(GUIUtil.getTradeCurrencyCellFactory(Res.get("shared.oneOffer"),
@ -366,7 +350,6 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
matchingOffersToggle.disableProperty().bind(model.disableMatchToggle);
matchingOffersToggle.setOnAction(e -> model.onShowOffersMatchingMyAccounts(matchingOffersToggle.isSelected()));
volumeColumn.sortableProperty().bind(model.showAllTradeCurrenciesProperty.not());
model.getOfferList().comparatorProperty().bind(tableView.comparatorProperty());
amountColumn.sortTypeProperty().addListener((observable, oldValue, newValue) -> {
@ -775,6 +758,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getAmountColumn() {
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(Res.get("shared.XMRMinMax"), Res.get("shared.amountHelp"));
column.setMinWidth(100);
column.setSortable(true);
column.getStyleClass().add("number-column");
column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
column.setCellFactory(
@ -918,6 +902,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>("") {
{
setMinWidth(125);
setSortable(true);
}
};
column.getStyleClass().add("number-column");

View file

@ -37,25 +37,25 @@ public class OtherOfferBookView extends OfferBookView<GridPane, OtherOfferBookVi
@Inject
OtherOfferBookView(OtherOfferBookViewModel model,
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
PrivateNotificationManager privateNotificationManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys,
AccountAgeWitnessService accountAgeWitnessService,
SignedWitnessService signedWitnessService) {
super(model, navigation, offerDetailsWindow, formatter, privateNotificationManager, useDevPrivilegeKeys, accountAgeWitnessService, signedWitnessService);
}
@Override
protected String getMarketTitle() {
return model.getDirection().equals(OfferDirection.BUY) ?
Res.get("offerbook.availableOffersToBuy", Res.get("shared.otherAssets"), Res.getBaseCurrencyCode()) :
Res.get("offerbook.availableOffersToSell", Res.get("shared.otherAssets"), Res.getBaseCurrencyCode());
Res.get("offerbook.availableOffersToBuy", Res.getBaseCurrencyCode(), Res.get("shared.otherAssets")) :
Res.get("offerbook.availableOffersToSell", Res.getBaseCurrencyCode(), Res.get("shared.otherAssets"));
}
@Override
String getTradeCurrencyCode() {
return model.showAllTradeCurrenciesProperty.get() ? "" : model.getSelectedTradeCurrency().getCode();
return Res.getBaseCurrencyCode();
}
}

View file

@ -40,7 +40,6 @@ import haveno.core.util.PriceUtil;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.desktop.Navigation;
import haveno.desktop.main.offer.OfferViewUtil;
import haveno.desktop.util.GUIUtil;
import haveno.network.p2p.P2PService;
import java.util.List;
@ -54,28 +53,28 @@ public class OtherOfferBookViewModel extends OfferBookViewModel {
@Inject
public OtherOfferBookViewModel(User user,
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CoreApi coreApi) {
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CoreApi coreApi) {
super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi);
}
@Override
void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) {
if (direction == OfferDirection.BUY) {
preferences.setBuyScreenCryptoCurrencyCode(code);
preferences.setBuyScreenOtherCurrencyCode(code);
} else {
preferences.setSellScreenCryptoCurrencyCode(code);
preferences.setBuyScreenOtherCurrencyCode(code);
}
}
@ -83,7 +82,6 @@ public class OtherOfferBookViewModel extends OfferBookViewModel {
protected ObservableList<PaymentMethod> filterPaymentMethods(ObservableList<PaymentMethod> list,
TradeCurrency selectedTradeCurrency) {
return FXCollections.observableArrayList(list.stream().filter(paymentMethod -> {
if (paymentMethod.isBlockchain()) return true;
if (paymentMethod.getSupportedAssetCodes() == null) return true;
for (String assetCode : paymentMethod.getSupportedAssetCodes()) {
if (!CurrencyUtil.isFiatCurrency(assetCode)) return true;
@ -95,20 +93,13 @@ public class OtherOfferBookViewModel extends OfferBookViewModel {
@Override
void fillCurrencies(ObservableList<TradeCurrency> tradeCurrencies,
ObservableList<TradeCurrency> allCurrencies) {
tradeCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""));
tradeCurrencies.addAll(preferences.getCryptoCurrenciesAsObservable().stream()
.filter(withoutTopCrypto())
.collect(Collectors.toList()));
tradeCurrencies.addAll(CurrencyUtil.getMainTraditionalCurrencies().stream()
.filter(withoutFiatCurrency())
.collect(Collectors.toList()));
tradeCurrencies.add(new CryptoCurrency(GUIUtil.EDIT_FLAG, ""));
allCurrencies.add(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, ""));
allCurrencies.addAll(CurrencyUtil.getAllSortedCryptoCurrencies().stream()
.filter(withoutTopCrypto())
.collect(Collectors.toList()));
allCurrencies.addAll(CurrencyUtil.getMainTraditionalCurrencies().stream()
.filter(withoutFiatCurrency())
.collect(Collectors.toList()));
@ -120,12 +111,9 @@ public class OtherOfferBookViewModel extends OfferBookViewModel {
TradeCurrency selectedTradeCurrency) {
return offerBookListItem -> {
Offer offer = offerBookListItem.getOffer();
// BUY Crypto is actually SELL Monero
boolean directionResult = offer.getDirection() == direction;
boolean currencyResult = !CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) &&
((showAllTradeCurrenciesProperty.get() &&
!offer.getCurrencyCode().equals(GUIUtil.TOP_CRYPTO.getCode())) ||
offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()));
boolean directionResult = offer.getDirection() != direction;
boolean currencyResult = CurrencyUtil.isTraditionalCurrency(offer.getCurrencyCode()) && !CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) &&
(showAllTradeCurrenciesProperty.get() || offer.getCurrencyCode().equals(selectedTradeCurrency.getCode()));
boolean paymentMethodResult = showAllPaymentMethods ||
offer.getPaymentMethod().equals(selectedPaymentMethod);
boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook();
@ -137,8 +125,8 @@ public class OtherOfferBookViewModel extends OfferBookViewModel {
TradeCurrency getDefaultTradeCurrency() {
TradeCurrency defaultTradeCurrency = GlobalSettings.getDefaultTradeCurrency();
if (!CurrencyUtil.isTraditionalCurrency(defaultTradeCurrency.getCode()) &&
!defaultTradeCurrency.equals(GUIUtil.TOP_CRYPTO) &&
if (CurrencyUtil.isTraditionalCurrency(defaultTradeCurrency.getCode()) &&
!CurrencyUtil.isFiatCurrency(defaultTradeCurrency.getCode()) &&
hasPaymentAccountForCurrency(defaultTradeCurrency)) {
return defaultTradeCurrency;
}
@ -152,22 +140,19 @@ public class OtherOfferBookViewModel extends OfferBookViewModel {
!hasPaymentAccountForCurrency(o2))).collect(Collectors.toList());
return sortedList.get(0);
} else {
return OfferViewUtil.getMainCryptoCurrencies().sorted((o1, o2) ->
Boolean.compare(!hasPaymentAccountForCurrency(o1),
!hasPaymentAccountForCurrency(o2))).collect(Collectors.toList()).get(0);
return CurrencyUtil.getMainTraditionalCurrencies().stream()
.filter(withoutFiatCurrency())
.sorted((o1, o2) -> Boolean.compare(!hasPaymentAccountForCurrency(o1), !hasPaymentAccountForCurrency(o2)))
.collect(Collectors.toList()).get(0);
}
}
@Override
String getCurrencyCodeFromPreferences(OfferDirection direction) {
return direction == OfferDirection.BUY ? preferences.getBuyScreenCryptoCurrencyCode() :
preferences.getSellScreenCryptoCurrencyCode();
}
// validate if previous stored currencies are Traditional ones
String currencyCode = direction == OfferDirection.BUY ? preferences.getBuyScreenOtherCurrencyCode() : preferences.getSellScreenOtherCurrencyCode();
@NotNull
private Predicate<CryptoCurrency> withoutTopCrypto() {
return cryptoCurrency ->
!cryptoCurrency.equals(GUIUtil.TOP_CRYPTO);
return CurrencyUtil.isTraditionalCurrency(currencyCode) ? currencyCode : null;
}
@NotNull

View file

@ -1,116 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.main.offer.offerbook;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.api.CoreApi;
import haveno.core.locale.TradeCurrency;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OfferFilterService;
import haveno.core.offer.OpenOfferManager;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.user.Preferences;
import haveno.core.user.User;
import haveno.core.util.FormattingUtils;
import haveno.core.util.PriceUtil;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.desktop.Navigation;
import haveno.desktop.util.GUIUtil;
import haveno.network.p2p.P2PService;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class TopCryptoOfferBookViewModel extends OfferBookViewModel {
public static TradeCurrency TOP_CRYPTO = GUIUtil.TOP_CRYPTO;
@Inject
public TopCryptoOfferBookViewModel(User user,
OpenOfferManager openOfferManager,
OfferBook offerBook,
Preferences preferences,
WalletsSetup walletsSetup,
P2PService p2PService,
PriceFeedService priceFeedService,
ClosedTradableManager closedTradableManager,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CoreApi coreApi) {
super(user, openOfferManager, offerBook, preferences, walletsSetup, p2PService, priceFeedService, closedTradableManager, accountAgeWitnessService, navigation, priceUtil, offerFilterService, btcFormatter, coreApi);
}
@Override
protected void activate() {
super.activate();
TOP_CRYPTO = GUIUtil.TOP_CRYPTO;
}
@Override
void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code) {
// No need to store anything as it is just one Crypto offers anyway
}
@Override
protected ObservableList<PaymentMethod> filterPaymentMethods(ObservableList<PaymentMethod> list,
TradeCurrency selectedTradeCurrency) {
return FXCollections.observableArrayList(list.stream().filter(PaymentMethod::isBlockchain).collect(Collectors.toList()));
}
@Override
void fillCurrencies(ObservableList<TradeCurrency> tradeCurrencies,
ObservableList<TradeCurrency> allCurrencies) {
tradeCurrencies.add(TOP_CRYPTO);
allCurrencies.add(TOP_CRYPTO);
}
@Override
Predicate<OfferBookListItem> getCurrencyAndMethodPredicate(OfferDirection direction,
TradeCurrency selectedTradeCurrency) {
return offerBookListItem -> {
Offer offer = offerBookListItem.getOffer();
// BUY Crypto is actually SELL Bitcoin
boolean directionResult = offer.getDirection() == direction;
boolean currencyResult = offer.getCurrencyCode().equals(TOP_CRYPTO.getCode());
boolean paymentMethodResult = showAllPaymentMethods ||
offer.getPaymentMethod().equals(selectedPaymentMethod);
boolean notMyOfferOrShowMyOffersActivated = !isMyOffer(offerBookListItem.getOffer()) || preferences.isShowOwnOffersInOfferBook();
return directionResult && currencyResult && paymentMethodResult && notMyOfferOrShowMyOffersActivated;
};
}
@Override
TradeCurrency getDefaultTradeCurrency() {
return TOP_CRYPTO;
}
@Override
String getCurrencyCodeFromPreferences(OfferDirection direction) {
return TOP_CRYPTO.getCode();
}
}

View file

@ -140,8 +140,10 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
@Override
protected void activate() {
failedTradesManager.getObservableList().addListener((ListChangeListener<Trade>) c -> {
if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3)
root.getTabs().add(failedTradesTab);
UserThread.execute(() -> {
if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3)
root.getTabs().add(failedTradesTab);
});
});
if (failedTradesManager.getObservableList().size() > 0 && root.getTabs().size() == 3)
root.getTabs().add(failedTradesTab);

View file

@ -548,10 +548,17 @@ public class PendingTradesDataModel extends ActivatableDataModel {
sendDisputeOpenedMessage(dispute, disputeManager);
tradeManager.requestPersistence();
} else if (useArbitration) {
// Only if we have completed mediation we allow arbitration
disputeManager = arbitrationManager;
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
trade.exportMultisigHex();
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// send dispute opened message
sendDisputeOpenedMessage(dispute, disputeManager);
tradeManager.requestPersistence();
} else {

View file

@ -404,7 +404,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
}
if (trade.getTakerDepositTx() == null) {
return Res.get("portfolio.pending.failedTrade.missingDepositTx"); // TODO (woodser): use .missingTakerDepositTx, .missingMakerDepositTx
return Res.get("portfolio.pending.failedTrade.missingDepositTx"); // TODO (woodser): use .missingTakerDepositTx, .missingMakerDepositTx, update translation to 2-of-3 multisig
}
if (trade.hasErrorMessage()) {

View file

@ -203,10 +203,8 @@ public class SellerStep3View extends TradeStepView {
.orElse("");
if (myPaymentAccountPayload instanceof AssetAccountPayload) {
if (myPaymentDetails.isEmpty()) {
// Not expected
myPaymentDetails = ((AssetAccountPayload) myPaymentAccountPayload).getAddress();
}
// for crypto always display the receiving address
myPaymentDetails = ((AssetAccountPayload) myPaymentAccountPayload).getAddress();
peersPaymentDetails = peersPaymentAccountPayload != null ?
((AssetAccountPayload) peersPaymentAccountPayload).getAddress() : "NA";
myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", currencyName);

View file

@ -110,9 +110,19 @@ public class MarketPricePresentation {
}
private void fillPriceFeedComboBoxItems() {
List<PriceFeedComboBoxItem> currencyItems = preferences.getTradeCurrenciesAsObservable()
// collect unique currency code bases
List<String> uniqueCurrencyCodeBases = preferences.getTradeCurrenciesAsObservable()
.stream()
.map(tradeCurrency -> new PriceFeedComboBoxItem(tradeCurrency.getCode()))
.map(TradeCurrency::getCode)
.map(CurrencyUtil::getCurrencyCodeBase)
.distinct()
.collect(Collectors.toList());
// create price feed items
List<PriceFeedComboBoxItem> currencyItems = uniqueCurrencyCodeBases
.stream()
.map(currencyCodeBase -> new PriceFeedComboBoxItem(currencyCodeBase))
.collect(Collectors.toList());
priceFeedComboBoxItems.setAll(currencyItems);
}
@ -171,7 +181,7 @@ public class MarketPricePresentation {
private Optional<PriceFeedComboBoxItem> findPriceFeedComboBoxItem(String currencyCode) {
return priceFeedComboBoxItems.stream()
.filter(item -> item.currencyCode.equals(currencyCode))
.filter(item -> CurrencyUtil.getCurrencyCodeBase(item.currencyCode).equals(CurrencyUtil.getCurrencyCodeBase(currencyCode)))
.findAny();
}

View file

@ -187,7 +187,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
@Override
protected void activate() {
// We want to have it updated in case an asset got removed
allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies( filterManager));
allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager));
allCryptoCurrencies.removeAll(cryptoCurrencies);
activateGeneralOptions();

View file

@ -1415,11 +1415,13 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
private String getDisputeStateText(Dispute dispute) {
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute's trade is null for trade {}", dispute.getTradeId());
log.warn("Dispute's trade is null for trade {}, defaulting to dispute state text 'closed'", dispute.getTradeId());
return Res.get("support.closed");
}
if (dispute.isClosed()) return Res.get("support.closed");
switch (trade.getDisputeState()) {
case NO_DISPUTE:
return Res.get("shared.pending");
case DISPUTE_REQUESTED:
return Res.get("support.requested");
default:

View file

@ -241,6 +241,10 @@
-fx-border-width: 0 0 10 0;
}
#address-text-field.jfx-text-field:readonly {
-fx-background-color: derive(-bs-background-color, 15%);
}
.wallet-seed-words {
-fx-text-fill: -bs-color-gray-6;
}

View file

@ -41,7 +41,7 @@
-bs-rd-green: #0b65da;
-bs-rd-green-dark: #3EA34A;
-bs-rd-nav-selected: #0b65da;
-bs-rd-nav-deselected: rgba(255, 255, 255, 0.59);
-bs-rd-nav-deselected: rgba(255, 255, 255, 0.75);
-bs-rd-nav-background: #0c59bd;
-bs-rd-nav-primary-background: #0b65da;
-bs-rd-nav-primary-border: #0B65DA;

View file

@ -134,8 +134,6 @@ public class GUIUtil {
private static Preferences preferences;
public static TradeCurrency TOP_CRYPTO = CurrencyUtil.getTradeCurrency("BTC").get();
public static void setPreferences(Preferences preferences) {
GUIUtil.preferences = preferences;
}
@ -1033,12 +1031,4 @@ public class GUIUtil {
columnConstraints2.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
}
public static void updateTopCrypto(Preferences preferences) {
TradeCurrency tradeCurrency = preferences.getPreferredTradeCurrency();
if (CurrencyUtil.isTraditionalCurrency(tradeCurrency.getCode())) {
return;
}
TOP_CRYPTO = tradeCurrency;
}
}

View file

@ -241,7 +241,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
assertEquals(0, model.maxPlacesForAmount.intValue());
}
@ -255,7 +255,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
model.activate();
@ -273,7 +273,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
model.activate();
@ -292,7 +292,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
assertEquals(0, model.maxPlacesForVolume.intValue());
}
@ -306,7 +306,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
model.activate();
@ -324,7 +324,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
model.activate();
@ -342,7 +342,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
assertEquals(0, model.maxPlacesForPrice.intValue());
}
@ -356,7 +356,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
model.activate();
@ -374,7 +374,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new XmrOfferBookViewModel(null, null, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue());
}
@ -409,7 +409,7 @@ public class OfferBookViewModelTest {
item4.getOffer().setPriceFeedService(priceFeedService);
offerBookListItems.addAll(item1, item2);
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, priceFeedService,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, priceFeedService,
null, null, null, getPriceUtil(), null, coinFormatter, null);
model.activate();
@ -430,7 +430,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
final OfferBookViewModel model = new XmrOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
final OfferBookViewModel model = new FiatOfferBookViewModel(user, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, null);
final OfferBookListItem item = make(xmrBuyItem.but(

View file

@ -1742,6 +1742,8 @@ message PreferencesPayload {
bool split_offer_output = 62;
bool use_sound_for_notifications = 63;
bool use_sound_for_notifications_initialized = 64;
string buy_screen_other_currency_code = 65;
string sell_screen_other_currency_code = 66;
}
message AutoConfirmSettings {

View file

@ -1,21 +1,22 @@
# Install Haveno on Tails
Install Haveno on Tails by following these steps:
After you already have a [Tails USB](https://tails.net/install/linux/index.en.html#download):
1. Enable persistent storage dotfiles and admin password before starting Tails.
2. Execute a one-line installation command to download and install Haveno:
1. Enable [persistent storage](https://tails.net/doc/persistent_storage/index.en.html).
2. Set up [administration password](https://tails.net/doc/first_steps/welcome_screen/administration_password/).
3. Activate dotfiles in persistent storage settings.
4. Execute the following command in the terminal to download and execute the installation script.
```
curl -x socks5h://127.0.0.1:9050 -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh "<REPLACE_WITH_BINARY_ZIP_URL>" "<REPLACE_WITH_PGP_FINGERPRINT>"
curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh <REPLACE_WITH_BINARY_ZIP_URL> <REPLACE_WITH_PGP_FINGERPRINT>
```
Replace the binary zip URL and PGP fingerprint for the network you're using. For example:
```
curl -x socks5h://127.0.0.1:9050 -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.12/haveno-linux-deb.zip" "FAA2 4D87 8B8D 36C9 0120 A897 CA02 DAC1 2DAE 2D0F"
curl -fsSLO https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/haveno-install.sh && bash haveno-install.sh https://github.com/havenoexample/haveno-example/releases/latest/download/haveno-linux-deb.zip FAA24D878B8D36C90120A897CA02DAC12DAE2D0F
```
3. Upon successful execution of the script (no errors), the Haveno release will be installed to persistent storage and can be launched via the desktop shortcut in the 'Other' section of the start menu.
5. Start Haveno by finding the icon in the launcher under **Applications > Other**.
> [!note]
> If you have already installed Haveno on Tails, we recommend moving your data directory (/home/amnesia/Persistent/Haveno-example) to the new default location (/home/amnesia/Persistent/haveno/Data/Haveno-example), to retain your history and for future support.

34
scripts/install_tails/haveno-install.sh Normal file → Executable file
View file

@ -38,29 +38,21 @@ install_dir="${persistence_dir}/haveno/Install"
dotfiles_dir="/live/persistence/TailsData_unlocked/dotfiles"
persistent_desktop_dir="$dotfiles_dir/.local/share/applications"
local_desktop_dir="/home/amnesia/.local/share/applications"
# Install dependencies
echo_blue "Installing dependencies ..."
sudo apt update && sudo apt install -y curl unzip
# Remove stale resources
rm -rf "${assets_dir}"
wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress"
# Create temp location for downloads
echo_blue "Creating temporary directory for Haveno resources ..."
mkdir "${assets_dir}" || { echo_red "Failed to create directory ${assets_dir}"; exit 1; }
mkdir -p "${assets_dir}" || { echo_red "Failed to create directory ${assets_dir}"; exit 1; }
# Download resources
echo_blue "Downloading resources for Haveno on Tails ..."
curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/exec.sh https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/exec.sh || { echo_red "Failed to download resource (exec.sh)."; exit 1; }
curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/install.sh https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/install.sh || { echo_red "Failed to download resource (install.sh)."; exit 1; }
curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/haveno.desktop https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.desktop || { echo_red "Failed to resource (haveno.desktop)."; exit 1; }
curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/icon.png https://raw.githubusercontent.com/haveno-dex/haveno/master/scripts/install_tails/assets/icon.png || { echo_red "Failed to download resource (icon.png)."; exit 1; }
curl --retry 10 --retry-delay 5 -fsSLo /tmp/assets/haveno.yml https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.yml || { echo_red "Failed to download resource (haveno.yml)."; exit 1; }
wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/exec.sh || { echo_red "Failed to download resource (exec.sh)."; exit 1; }
wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/install.sh || { echo_red "Failed to download resource (install.sh)."; exit 1; }
wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.desktop || { echo_red "Failed to resource (haveno.desktop)."; exit 1; }
wget "${wget_flags}" -cqP "${assets_dir}" https://raw.githubusercontent.com/haveno-dex/haveno/master/scripts/install_tails/assets/icon.png || { echo_red "Failed to download resource (icon.png)."; exit 1; }
wget "${wget_flags}" -cqP "${assets_dir}" https://github.com/haveno-dex/haveno/raw/master/scripts/install_tails/assets/haveno.yml || { echo_red "Failed to download resource (haveno.yml)."; exit 1; }
# Create persistent directory
@ -92,17 +84,17 @@ fi
# Download Haveno binary
echo_blue "Downloading Haveno from URL provided ..."
curl --retry 10 --retry-delay 5 -L -o "${binary_filename}" "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; }
wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; }
# Download Haveno signature file
echo_blue "Downloading Haveno signature ..."
curl --retry 10 --retry-delay 5 -L -o "${signature_filename}" "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; }
wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; }
# Download the GPG key
echo_blue "Downloading signing GPG key ..."
curl --retry 10 --retry-delay 5 -L -o "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; }
wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; }
# Import the GPG key
@ -132,7 +124,7 @@ OUTPUT=$(gpg --digest-algo SHA256 --verify "${signature_filename}" "${binary_fil
if ! echo "$OUTPUT" | grep -q "Good signature from"; then
echo_red "Verification failed: $OUTPUT"
exit 1;
else unzip "${binary_filename}" && mv haveno*.deb "${package_filename}"
else 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}"
fi
echo_blue "Haveno binaries have been successfully verified."
@ -148,5 +140,9 @@ mv "${binary_filename}" "${package_filename}" "${key_filename}" "${signature_fil
echo_blue "Files moved to persistent directory ${install_dir}"
# Remove stale resources
rm -rf "${assets_dir}"
# Completed confirmation
echo_blue "Haveno installation setup completed successfully."

View file

@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final String VERSION = "1.0.12";
private static final String VERSION = "1.0.13";
private SeedNode seedNode;
private Timer checkConnectionLossTime;