support funding make or take offer directly

QR code encodes payment URI
security deposit absorbs miner fee up to 5%
use binary search to maximize security deposit and minimize dust
show itemized funding popup on create offer
This commit is contained in:
woodser 2022-12-03 14:33:55 +00:00
parent 4dbbcd6217
commit dd0a307a84
44 changed files with 263 additions and 353 deletions

View file

@ -25,10 +25,10 @@ import bisq.core.offer.OpenOfferManager;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.failed.FailedTradesManager;
import bisq.core.util.ParsingUtils;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.List; import java.util.List;
@ -137,7 +137,7 @@ public class Balances {
} else { } else {
reservedAmt = trade.getContract().isMyRoleBuyer(tradeManager.getKeyRing().getPubKeyRing()) ? offerPayload.getBuyerSecurityDeposit() : offerPayload.getAmount() + offerPayload.getSellerSecurityDeposit(); reservedAmt = trade.getContract().isMyRoleBuyer(tradeManager.getKeyRing().getPubKeyRing()) ? offerPayload.getBuyerSecurityDeposit() : offerPayload.getAmount() + offerPayload.getSellerSecurityDeposit();
} }
sum = sum.add(Coin.valueOf(ParsingUtils.centinerosToAtomicUnits(reservedAmt).longValueExact())); sum = sum.add(Coin.valueOf(HavenoUtils.centinerosToAtomicUnits(reservedAmt).longValueExact()));
} }
reservedTradeBalance.set(sum); reservedTradeBalance.set(sum);
} }

View file

@ -17,16 +17,16 @@ import bisq.core.btc.setup.MoneroWalletRpcManager;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.trade.MakerTrade; import bisq.core.trade.MakerTrade;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.HavenoUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.util.ParsingUtils;
import com.google.common.util.concurrent.Service.State; import com.google.common.util.concurrent.Service.State;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import common.utils.JsonUtils; import common.utils.JsonUtils;
import java.io.File; import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -83,8 +83,8 @@ public class XmrWalletService {
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String MONERO_WALLET_NAME = "haveno_XMR";
private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_"; private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_";
private static final int MINER_FEE_PADDING_MULTIPLIER = 2; // extra padding for miner fees = estimated fee * multiplier public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee
private static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit absorbs miner fee up to percent
private final CoreAccountService accountService; private final CoreAccountService accountService;
private final CoreMoneroConnectionsService connectionsService; private final CoreMoneroConnectionsService connectionsService;
@ -265,106 +265,86 @@ public class XmrWalletService {
} }
/** /**
* Create the reserve tx and freeze its inputs. The deposit amount is returned * Create the reserve tx and freeze its inputs. The full amount is returned
* to the sender's payout address. Additional funds are reserved to allow * to the sender's payout address less the trade fee.
* fluctuations in the mining fee.
* *
* @param tradeFee - trade fee * @param returnAddress return address for reserved funds
* @param depositAmount - amount needed for the trade minus the trade fee * @param tradeFee trade fee
* @param returnAddress - return address for deposit amount * @param peerAmount amount to give peer
* @param addPadding - reserve additional padding to cover future mining fee * @param securityDeposit security deposit amount
* @return a transaction to reserve a trade * @return a transaction to reserve a trade
*/ */
public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount, boolean addPadding) { public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger peerAmount, BigInteger securityDeposit, String returnAddress) {
MoneroWallet wallet = getWallet(); log.info("Creating reserve tx with fee={}, peerAmount={}, securityDeposit={}", tradeFee, peerAmount, securityDeposit);
synchronized (wallet) { return createTradeTx(tradeFee, peerAmount, securityDeposit, returnAddress);
// add miner fee padding to deposit amount
if (addPadding) {
// get estimated mining fee with deposit amount
MoneroTxWallet feeEstimateTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount));
BigInteger feeEstimate = feeEstimateTx.getFee();
BigInteger daemonFeeEstimate = getFeeEstimate(feeEstimateTx.getWeight());
log.info("createReserveTx() 1st feeEstimateTx with weight {} has fee {} versus daemon fee estimate of {} (diff={})", feeEstimateTx.getWeight(), feeEstimateTx.getFee(), daemonFeeEstimate, (feeEstimateTx.getFee().subtract(daemonFeeEstimate)));
// get estimated mining fee with deposit amount + previous estimated mining fee for better accuracy
feeEstimateTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount.add(feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER)))));
feeEstimate = feeEstimateTx.getFee();
log.info("createReserveTx() 2nd feeEstimateTx with weight {} has fee {} versus daemon fee estimate of {} (diff={})", feeEstimateTx.getWeight(), feeEstimateTx.getFee(), daemonFeeEstimate, (feeEstimateTx.getFee().subtract(daemonFeeEstimate)));
// add padding to deposit amount
BigInteger minerFeePadding = feeEstimate.multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER));
depositAmount = depositAmount.add(minerFeePadding);
}
// create reserve tx
MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount));
log.info("Reserve tx weight={}, fee={}, depositAmount={}", reserveTx.getWeight(), reserveTx.getFee(), depositAmount);
// freeze inputs
for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
wallet.save();
return reserveTx;
}
} }
/** /**
* Create the multisig deposit tx and freeze its inputs. * Create the multisig deposit tx and freeze its inputs.
* *
* @param trade the trade to create a deposit tx from
* @return MoneroTxWallet the multisig deposit tx * @return MoneroTxWallet the multisig deposit tx
*/ */
public MoneroTxWallet createDepositTx(Trade trade) { public MoneroTxWallet createDepositTx(Trade trade) {
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee());
Offer offer = trade.getProcessModel().getOffer(); Offer offer = trade.getProcessModel().getOffer();
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit());
String multisigAddress = trade.getProcessModel().getMultisigAddress(); String multisigAddress = trade.getProcessModel().getMultisigAddress();
BigInteger tradeFee = HavenoUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee());
BigInteger peerAmount = HavenoUtils.coinToAtomicUnits(trade instanceof BuyerTrade ? Coin.ZERO : offer.getAmount());
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(trade instanceof BuyerTrade ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit());
log.info("Creating deposit tx with fee={}, peerAmount={}, securityDeposit={}", tradeFee, peerAmount, securityDeposit);
return createTradeTx(tradeFee, peerAmount, securityDeposit, multisigAddress);
}
private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger peerAmount, BigInteger securityDeposit, String address) {
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
synchronized (wallet) { synchronized (wallet) {
// create deposit tx // binary search to maximize security deposit, thereby minimizing potential dust
MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig() MoneroTxWallet tradeTx = null;
.setAccountIndex(0) double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee) double searchDiff = 1.0; // difference for next binary search
.addDestination(multisigAddress, depositAmount)); BigInteger maxAmount = peerAmount.add(securityDeposit);
for (int i = 0; i < 10; i++) {
try {
BigInteger amount = new BigDecimal(maxAmount).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger();
tradeTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(address, amount));
appliedTolerance -= searchDiff; // apply less tolerance to increase security deposit
if (appliedTolerance < 0.0) break; // can send full security deposit
} catch (MoneroError e) {
appliedTolerance += searchDiff; // apply more tolerance to decrease security deposit
if (appliedTolerance > 1.0) throw e; // not enough money
}
searchDiff /= 2;
}
// freeze deposit inputs // freeze inputs
for (MoneroOutput input : depositTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex()); for (MoneroOutput input : tradeTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
wallet.save(); wallet.save();
return depositTx; return tradeTx;
} }
} }
/** /**
* Verify a reserve or deposit transaction used during trading. * Verify a reserve or deposit transaction.
* Checks double spends, deposit amount and destination, trade fee, and mining fee. * Checks double spends, trade fee, deposit amount and destination, and miner fee.
* The transaction is submitted but not relayed to the pool then flushed. * The transaction is submitted to the pool then flushed without relaying.
* *
* @param depositAddress is the expected destination address for the deposit amount * @param tradeFee trade fee
* @param depositAmount is the expected amount deposited to multisig * @param peerAmount amount to give peer
* @param tradeFee is the expected fee for trading * @param securityDeposit security deposit amount
* @param txHash is the transaction hash * @param address expected destination address for the deposit amount
* @param txHex is the transaction hex * @param txHash transaction hash
* @param txKey is the transaction key * @param txHex transaction hex
* @param keyImages are expected key images of inputs, ignored if null * @param txKey transaction key
* @param addPadding verifies depositAmount has additional padding to cover future mining fee * @param keyImages expected key images of inputs, ignored if null
*/ */
public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean addPadding) { public void verifyTradeTx(BigInteger tradeFee, BigInteger peerAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List<String> keyImages) {
MoneroDaemonRpc daemon = getDaemon(); MoneroDaemonRpc daemon = getDaemon();
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
try { try {
log.info("Verifying trade tx with deposit amount={}", depositAmount);
// verify tx not submitted to pool // verify tx not submitted to pool
MoneroTx tx = daemon.getTx(txHash); MoneroTx tx = daemon.getTx(txHash);
@ -375,14 +355,14 @@ public class XmrWalletService {
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
tx = getTx(txHash); tx = getTx(txHash);
// verify reserved key images // verify key images
if (keyImages != null) { if (keyImages != null) {
Set<String> txKeyImages = new HashSet<String>(); Set<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images"); if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Tx inputs do not match claimed key images");
} }
// verify the unlock height // verify unlock height
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee // verify trade fee
@ -391,22 +371,17 @@ public class XmrWalletService {
if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee");
if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount());
// verify mining fee // verify miner fee
BigInteger feeEstimate = getFeeEstimate(tx.getWeight()); BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Mining fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee()); if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee());
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff); log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);
// verify deposit amount // verify deposit amount
check = wallet.checkTxKey(txHash, txKey, depositAddress); check = wallet.checkTxKey(txHash, txKey, address);
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
if (addPadding) { BigInteger minAmount = new BigDecimal(peerAmount.add(securityDeposit)).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger();
BigInteger minPadding = BigInteger.valueOf((long) (tx.getFee().multiply(BigInteger.valueOf(MINER_FEE_PADDING_MULTIPLIER)).doubleValue() * (1.0 - MINER_FEE_TOLERANCE))); if (check.getReceivedAmount().compareTo(minAmount) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + minAmount + " but was " + check.getReceivedAmount());
BigInteger actualPadding = check.getReceivedAmount().subtract(depositAmount);
if (actualPadding.compareTo(minPadding) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount.add(minPadding) + " (with padding) but was " + check.getReceivedAmount());
} else if (check.getReceivedAmount().compareTo(depositAmount) < 0) {
throw new RuntimeException("Deposit amount is not enough, needed " + depositAmount + " but was " + check.getReceivedAmount());
}
} finally { } finally {
try { try {
daemon.flushTxPool(txHash); // flush tx from pool daemon.flushTxPool(txHash); // flush tx from pool
@ -915,7 +890,6 @@ public class XmrWalletService {
return getBalanceForSubaddress(wallet.getAddressIndex(address).getIndex()); return getBalanceForSubaddress(wallet.getAddressIndex(address).getIndex());
} }
// TODO: Coin represents centineros everywhere, but here it's atomic units. reconcile
public Coin getBalanceForSubaddress(int subaddressIndex) { public Coin getBalanceForSubaddress(int subaddressIndex) {
// get subaddress balance // get subaddress balance
@ -931,12 +905,11 @@ public class XmrWalletService {
// } // }
System.out.println("Returning balance for subaddress " + subaddressIndex + ": " + balance.longValueExact()); System.out.println("Returning balance for subaddress " + subaddressIndex + ": " + balance.longValueExact());
return HavenoUtils.atomicUnitsToCoin(balance);
return Coin.valueOf(balance.longValueExact());
} }
public Coin getAvailableConfirmedBalance() { public Coin getAvailableConfirmedBalance() {
return wallet != null ? Coin.valueOf(wallet.getUnlockedBalance(0).longValueExact()) : Coin.ZERO; return wallet != null ? HavenoUtils.atomicUnitsToCoin(wallet.getUnlockedBalance(0)) : Coin.ZERO;
} }
public Coin getSavingWalletBalance() { public Coin getSavingWalletBalance() {

View file

@ -42,7 +42,6 @@ import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.user.User; import bisq.core.user.User;
import bisq.core.util.JsonUtil; import bisq.core.util.JsonUtil;
import bisq.core.util.ParsingUtils;
import bisq.core.util.Validator; import bisq.core.util.Validator;
import bisq.network.p2p.AckMessage; import bisq.network.p2p.AckMessage;
@ -626,14 +625,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
new Thread(() -> { new Thread(() -> {
List<String> errorMessages = new ArrayList<String>(); List<String> errorMessages = new ArrayList<String>();
for (OpenOffer scheduledOffer : openOffers.getObservableList()) { for (OpenOffer scheduledOffer : new ArrayList<OpenOffer>(openOffers.getObservableList())) {
if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue;
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
processUnpostedOffer(scheduledOffer, (transaction) -> { processUnpostedOffer(scheduledOffer, (transaction) -> {
latch.countDown(); latch.countDown();
}, errorMessage -> { }, errorMessage -> {
latch.countDown(); onRemoved(scheduledOffer);
errorMessages.add(errorMessage); errorMessages.add(errorMessage);
latch.countDown();
}); });
HavenoUtils.awaitLatch(latch); HavenoUtils.awaitLatch(latch);
} }
@ -655,7 +655,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// get offer reserve amount // get offer reserve amount
Coin offerReserveAmountCoin = openOffer.getOffer().getReserveAmount(); Coin offerReserveAmountCoin = openOffer.getOffer().getReserveAmount();
BigInteger offerReserveAmount = ParsingUtils.centinerosToAtomicUnits(offerReserveAmountCoin.value); BigInteger offerReserveAmount = HavenoUtils.centinerosToAtomicUnits(offerReserveAmountCoin.value);
// handle sufficient available balance // handle sufficient available balance
if (xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0) { if (xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0) {
@ -773,6 +773,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// set offer state // set offer state
openOffer.setState(OpenOffer.State.AVAILABLE); openOffer.setState(OpenOffer.State.AVAILABLE);
requestPersistence();
resultHandler.handleResult(transaction); resultHandler.handleResult(transaction);
if (!stopped) { if (!stopped) {
@ -832,17 +833,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
Offer offer = new Offer(request.getOfferPayload()); Offer offer = new Offer(request.getOfferPayload());
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); BigInteger tradeFee = HavenoUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); BigInteger peerAmount = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? Coin.ZERO : offer.getAmount());
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit());
xmrWalletService.verifyTradeTx( xmrWalletService.verifyTradeTx(
request.getPayoutAddress(),
depositAmount,
tradeFee, tradeFee,
peerAmount,
securityDeposit,
request.getPayoutAddress(),
request.getReserveTxHash(), request.getReserveTxHash(),
request.getReserveTxHex(), request.getReserveTxHex(),
request.getReserveTxKey(), request.getReserveTxKey(),
request.getReserveTxKeyImages(), request.getReserveTxKeyImages());
true);
// arbitrator signs offer to certify they have valid reserve tx // arbitrator signs offer to certify they have valid reserve tx
String offerPayloadAsJson = JsonUtil.objectToJson(request.getOfferPayload()); String offerPayloadAsJson = JsonUtil.objectToJson(request.getOfferPayload());

View file

@ -21,13 +21,17 @@ import bisq.common.taskrunner.Task;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection;
import bisq.core.offer.placeoffer.PlaceOfferModel; import bisq.core.offer.placeoffer.PlaceOfferModel;
import bisq.core.util.ParsingUtils; import bisq.core.trade.HavenoUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.bitcoinj.core.Coin;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -46,19 +50,18 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
try { try {
runInterceptHook(); runInterceptHook();
// create reserve tx with padding // create reserve tx
BigInteger makerFee = HavenoUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger peerAmount = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? Coin.ZERO : offer.getAmount());
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit());
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee()); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, peerAmount, securityDeposit, returnAddress);
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
log.info("Maker creating reserve tx with maker fee={} and depositAmount={}", makerFee, depositAmount);
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount, true);
// collect reserved key images // TODO (woodser): switch to proof of reserve? // collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save offer state // save offer state
// TODO (woodser): persist
model.setReserveTx(reserveTx); model.setReserveTx(reserveTx);
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field

View file

@ -68,7 +68,7 @@ import bisq.core.payment.UpiAccount;
import bisq.core.payment.VerseAccount; import bisq.core.payment.VerseAccount;
import bisq.core.payment.WeChatPayAccount; import bisq.core.payment.WeChatPayAccount;
import bisq.core.payment.WesternUnionAccount; import bisq.core.payment.WesternUnionAccount;
import bisq.core.util.ParsingUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency; import bisq.core.locale.TradeCurrency;
@ -490,8 +490,8 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
// TODO: remove this when trade credits supported // TODO: remove this when trade credits supported
boolean isFiat = CurrencyUtil.isFiatCurrency(currencyCode); boolean isFiat = CurrencyUtil.isFiatCurrency(currencyCode);
boolean isStagenet = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET; boolean isStagenet = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET;
if (isFiat && isStagenet && ParsingUtils.centinerosToXmr(riskBasedTradeLimit) > MAX_FIAT_STAGENET_XMR) { if (isFiat && isStagenet && HavenoUtils.centinerosToXmr(riskBasedTradeLimit) > MAX_FIAT_STAGENET_XMR) {
riskBasedTradeLimit = ParsingUtils.xmrToCentineros(MAX_FIAT_STAGENET_XMR); riskBasedTradeLimit = HavenoUtils.xmrToCentineros(MAX_FIAT_STAGENET_XMR);
} }
return Coin.valueOf(riskBasedTradeLimit); return Coin.valueOf(riskBasedTradeLimit);
} }

View file

@ -834,8 +834,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) : (contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString()); (contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); BigInteger winnerPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); BigInteger loserPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
// create transaction to get fee estimate // create transaction to get fee estimate
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);

View file

@ -35,9 +35,9 @@ import bisq.core.support.messages.ChatMessage;
import bisq.core.support.messages.SupportMessage; import bisq.core.support.messages.SupportMessage;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.ParsingUtils;
import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.AckMessageSourceType;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
@ -318,12 +318,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount()); BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount"); if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx) // TODO: verify miner fee is within expected range
// verify winner and loser payout amounts // verify winner and loser payout amounts
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); BigInteger expectedWinnerAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); BigInteger expectedLoserAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0 if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount(); BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();

View file

@ -29,6 +29,8 @@ import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.util.JsonUtil; import bisq.core.util.JsonUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI; import java.net.URI;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -36,6 +38,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.bitcoinj.core.Coin;
import com.google.common.base.CaseFormat; import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
@ -48,6 +52,50 @@ public class HavenoUtils {
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
public static final String LOCALHOST = "localhost"; public static final String LOCALHOST = "localhost";
// multipliers to convert units
private static BigInteger CENTINEROS_AU_MULTIPLIER = new BigInteger("10000");
private static BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000");
public static BigInteger coinToAtomicUnits(Coin coin) {
return centinerosToAtomicUnits(coin.value);
}
public static double coinToXmr(Coin coin) {
return atomicUnitsToXmr(coinToAtomicUnits(coin));
}
public static BigInteger centinerosToAtomicUnits(long centineros) {
return BigInteger.valueOf(centineros).multiply(CENTINEROS_AU_MULTIPLIER);
}
public static double centinerosToXmr(long centineros) {
return atomicUnitsToXmr(centinerosToAtomicUnits(centineros));
}
public static long atomicUnitsToCentineros(long atomicUnits) { // TODO: atomic units should be BigInteger; remove this?
return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue();
}
public static long atomicUnitsToCentineros(BigInteger atomicUnits) {
return atomicUnits.divide(CENTINEROS_AU_MULTIPLIER).longValueExact();
}
public static Coin atomicUnitsToCoin(BigInteger atomicUnits) {
return Coin.valueOf(atomicUnitsToCentineros(atomicUnits));
}
public static double atomicUnitsToXmr(BigInteger atomicUnits) {
return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue();
}
public static BigInteger xmrToAtomicUnits(double xmr) {
return BigDecimal.valueOf(xmr).multiply(new BigDecimal(XMR_AU_MULTIPLIER)).toBigInteger();
}
public static long xmrToCentineros(double xmr) {
return atomicUnitsToCentineros(xmrToAtomicUnits(xmr));
}
/** /**
* Get address to collect trade fees. * Get address to collect trade fees.
* *

View file

@ -710,7 +710,7 @@ public abstract class Trade implements Tradable, Model {
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null");
BigInteger sellerDepositAmount = multisigWallet.getTx(this.getSeller().getDepositTxHash()).getIncomingAmount(); BigInteger sellerDepositAmount = multisigWallet.getTx(this.getSeller().getDepositTxHash()).getIncomingAmount();
BigInteger buyerDepositAmount = multisigWallet.getTx(this.getBuyer().getDepositTxHash()).getIncomingAmount(); BigInteger buyerDepositAmount = multisigWallet.getTx(this.getBuyer().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(this.getAmount()); BigInteger tradeAmount = HavenoUtils.coinToAtomicUnits(this.getAmount());
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount); BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
@ -763,7 +763,7 @@ public abstract class Trade implements Tradable, Model {
Contract contract = getContract(); Contract contract = getContract();
BigInteger sellerDepositAmount = multisigWallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable? BigInteger sellerDepositAmount = multisigWallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
BigInteger buyerDepositAmount = multisigWallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount(); BigInteger buyerDepositAmount = multisigWallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(getAmount()); BigInteger tradeAmount = HavenoUtils.coinToAtomicUnits(getAmount());
// describe payout tx // describe payout tx
MoneroTxSet describedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); MoneroTxSet describedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));

View file

@ -24,11 +24,11 @@ import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferDirection;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.TradingPeer;
import bisq.core.util.ParsingUtils;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener; import bisq.network.p2p.SendDirectMessageListener;
import common.utils.JsonUtils; import common.utils.JsonUtils;
@ -37,6 +37,9 @@ import java.math.BigInteger;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
import org.bitcoinj.core.Coin;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.MoneroDaemon; import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroSubmitTxResult;
@ -70,28 +73,30 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// set peer's signature // set peer's signature
peer.setContractSignature(signature); peer.setContractSignature(signature);
// collect expected values of deposit tx // collect expected values
Offer offer = trade.getOffer(); Offer offer = trade.getOffer();
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress()); boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTaker().getNodeAddress());
boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY; boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferDirection.SELL : offer.getDirection() == OfferDirection.BUY;
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); BigInteger peerAmount = HavenoUtils.coinToAtomicUnits(isFromBuyer ? Coin.ZERO : offer.getAmount());
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit());
String depositAddress = processModel.getMultisigAddress(); String depositAddress = processModel.getMultisigAddress();
BigInteger tradeFee; BigInteger tradeFee;
TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress()); TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress());
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee()); if (trader == processModel.getMaker()) tradeFee = HavenoUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
else if (trader == processModel.getTaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); else if (trader == processModel.getTaker()) tradeFee = HavenoUtils.coinToAtomicUnits(trade.getTakerFee());
else throw new RuntimeException("DepositRequest is not from maker or taker"); else throw new RuntimeException("DepositRequest is not from maker or taker");
// verify deposit tx // verify deposit tx
try { try {
trade.getXmrWalletService().verifyTradeTx(depositAddress, trade.getXmrWalletService().verifyTradeTx(
depositAmount,
tradeFee, tradeFee,
peerAmount,
securityDeposit,
depositAddress,
trader.getDepositTxHash(), trader.getDepositTxHash(),
request.getDepositTxHex(), request.getDepositTxHex(),
request.getDepositTxKey(), request.getDepositTxKey(),
null, null);
false);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); throw new RuntimeException("Error processing deposit tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
} }

View file

@ -20,11 +20,14 @@ package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferDirection;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.TradingPeer;
import bisq.core.util.ParsingUtils;
import java.math.BigInteger; import java.math.BigInteger;
import org.bitcoinj.core.Coin;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**
@ -52,18 +55,19 @@ public class ArbitratorProcessReserveTx extends TradeTask {
// TODO (woodser): if signer online, should never be called by maker // TODO (woodser): if signer online, should never be called by maker
// process reserve tx with expected terms // process reserve tx with expected terms
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee()); BigInteger tradeFee = HavenoUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit())); BigInteger peerAmount = HavenoUtils.coinToAtomicUnits(isFromBuyer ? Coin.ZERO : offer.getAmount());
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit());
try { try {
trade.getXmrWalletService().verifyTradeTx( trade.getXmrWalletService().verifyTradeTx(
request.getPayoutAddress(),
depositAmount,
tradeFee, tradeFee,
peerAmount,
securityDeposit,
request.getPayoutAddress(),
request.getReserveTxHash(), request.getReserveTxHash(),
request.getReserveTxHex(), request.getReserveTxHex(),
request.getReserveTxKey(), request.getReserveTxKey(),
null, null);
true);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Error processing reserve tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage()); throw new RuntimeException("Error processing reserve tx from " + (isFromTaker ? "taker " : "maker ") + request.getSenderNodeAddress() + ", offerId=" + offer.getId() + ": " + e.getMessage());
} }

View file

@ -19,11 +19,15 @@ package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.OfferDirection;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.util.ParsingUtils;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.bitcoinj.core.Coin;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -38,11 +42,12 @@ public class TakerReserveTradeFunds extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
// create reserve tx without padding // create reserve tx
BigInteger takerFee = HavenoUtils.coinToAtomicUnits(trade.getTakerFee());
BigInteger peerAmount = HavenoUtils.coinToAtomicUnits(trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : Coin.ZERO);
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit());
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee()); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, peerAmount, securityDeposit, returnAddress);
BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong());
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, returnAddress, depositAmount, true);
// collect reserved key images // collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();
@ -51,7 +56,7 @@ public class TakerReserveTradeFunds extends TradeTask {
// save process state // save process state
processModel.setReserveTx(reserveTx); processModel.setReserveTx(reserveTx);
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages); processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
trade.setErrorMessage("An error occurred.\n" + trade.setErrorMessage("An error occurred.\n" +

View file

@ -9,58 +9,12 @@ import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.MonetaryFormat;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.math.BigDecimal;
import java.math.BigInteger;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class ParsingUtils { public class ParsingUtils {
// multipliers to convert units
private static BigInteger CENTINEROS_AU_MULTIPLIER = new BigInteger("10000");
private static BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000");
public static BigInteger coinToAtomicUnits(Coin coin) {
return centinerosToAtomicUnits(coin.value);
}
public static double coinToXmr(Coin coin) {
return atomicUnitsToXmr(coinToAtomicUnits(coin));
}
public static BigInteger centinerosToAtomicUnits(long centineros) {
return BigInteger.valueOf(centineros).multiply(ParsingUtils.CENTINEROS_AU_MULTIPLIER);
}
public static double centinerosToXmr(long centineros) {
return atomicUnitsToXmr(centinerosToAtomicUnits(centineros));
}
public static long atomicUnitsToCentineros(long atomicUnits) { // TODO: atomic units should be BigInteger; remove this?
return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue();
}
public static long atomicUnitsToCentineros(BigInteger atomicUnits) {
return atomicUnits.divide(CENTINEROS_AU_MULTIPLIER).longValueExact();
}
public static Coin atomicUnitsToCoin(BigInteger atomicUnits) {
return Coin.valueOf(atomicUnitsToCentineros(atomicUnits));
}
public static double atomicUnitsToXmr(BigInteger atomicUnits) {
return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue();
}
public static BigInteger xmrToAtomicUnits(double xmr) {
return BigDecimal.valueOf(xmr).multiply(new BigDecimal(XMR_AU_MULTIPLIER)).toBigInteger();
}
public static long xmrToCentineros(double xmr) {
return atomicUnitsToCentineros(xmrToAtomicUnits(xmr));
}
public static Coin parseToCoin(String input, CoinFormatter coinFormatter) { public static Coin parseToCoin(String input, CoinFormatter coinFormatter) {
return parseToCoin(input, coinFormatter.getMonetaryFormat()); return parseToCoin(input, coinFormatter.getMonetaryFormat());
} }

View file

@ -451,7 +451,7 @@ createOffer.fundsBox.offerFee=Trade fee
createOffer.fundsBox.networkFee=Mining fee createOffer.fundsBox.networkFee=Mining fee
createOffer.fundsBox.placeOfferSpinnerInfo=Offer publishing is in progress ... createOffer.fundsBox.placeOfferSpinnerInfo=Offer publishing is in progress ...
createOffer.fundsBox.paymentLabel=Haveno trade with ID {0} createOffer.fundsBox.paymentLabel=Haveno trade with ID {0}
createOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee, {2} mining fee) createOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee)
createOffer.success.headline=Your offer has been published createOffer.success.headline=Your offer has been published
createOffer.success.info=You can manage your open offers at \"Portfolio/My open offers\". createOffer.success.info=You can manage your open offers at \"Portfolio/My open offers\".
createOffer.info.sellAtMarketPrice=You will always sell at market price as the price of your offer will be continuously updated. createOffer.info.sellAtMarketPrice=You will always sell at market price as the price of your offer will be continuously updated.
@ -477,13 +477,11 @@ createOffer.createOfferFundWalletInfo.headline=Fund your offer
# suppress inspection "TrailingSpacesInProperty" # suppress inspection "TrailingSpacesInProperty"
createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n createOffer.createOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n
createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\n\ createOffer.createOfferFundWalletInfo.msg=You need to deposit {0} to this offer.\n\n\
Those funds are reserved in your local wallet and will get locked into the multisig deposit address once someone takes your offer.\n\n\ Those funds are reserved in your local wallet and will get locked into the multisig wallet once someone takes your offer.\n\n\
The amount is the sum of:\n\ The amount is the sum of:\n\
{1}\ {1}\
- Your security deposit: {2}\n\ - Your security deposit: {2}\n\
- Trading fee: {3}\n\ - Trading fee: {3}
- Mining fee: {4}\n\n\
You can choose between two options when funding your trade:\n- Use your Haveno wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup.
# only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!) # only first part "An error occurred when placing the offer:" has been used before. We added now the rest (need update in existing translations!)
createOffer.amountPriceBox.error.message=An error occurred when placing the offer:\n\n{0}\n\n\ createOffer.amountPriceBox.error.message=An error occurred when placing the offer:\n\n{0}\n\n\
@ -533,7 +531,7 @@ takeOffer.fundsBox.offerFee=Trade fee
takeOffer.fundsBox.networkFee=Total mining fees takeOffer.fundsBox.networkFee=Total mining fees
takeOffer.fundsBox.takeOfferSpinnerInfo=Take offer in progress ... takeOffer.fundsBox.takeOfferSpinnerInfo=Take offer in progress ...
takeOffer.fundsBox.paymentLabel=Haveno trade with ID {0} takeOffer.fundsBox.paymentLabel=Haveno trade with ID {0}
takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee, {2} mining fee) takeOffer.fundsBox.fundsStructure=({0} security deposit, {1} trade fee)
takeOffer.success.headline=You have successfully taken an offer. takeOffer.success.headline=You have successfully taken an offer.
takeOffer.success.info=You can see the status of your trade at \"Portfolio/Open trades\". takeOffer.success.info=You can see the status of your trade at \"Portfolio/Open trades\".
takeOffer.error.message=An error occurred when taking the offer.\n\n{0} takeOffer.error.message=An error occurred when taking the offer.\n\n{0}
@ -545,7 +543,7 @@ takeOffer.noPriceFeedAvailable=You cannot take that offer as it uses a percentag
takeOffer.takeOfferFundWalletInfo.headline=Fund your trade takeOffer.takeOfferFundWalletInfo.headline=Fund your trade
# suppress inspection "TrailingSpacesInProperty" # suppress inspection "TrailingSpacesInProperty"
takeOffer.takeOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n takeOffer.takeOfferFundWalletInfo.tradeAmount=- Trade amount: {0} \n
takeOffer.takeOfferFundWalletInfo.msg=You need to deposit {0} for taking this offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3}\n- Total mining fees: {4}\n\nYou can choose between two options when funding your trade:\n- Use your Haveno wallet (convenient, but transactions may be linkable) OR\n- Transfer from an external wallet (potentially more private)\n\nYou will see all funding options and details after closing this popup. takeOffer.takeOfferFundWalletInfo.msg=You need to deposit {0} for taking this offer.\n\nThe amount is the sum of:\n{1}- Your security deposit: {2}\n- Trading fee: {3}
takeOffer.alreadyPaidInFunds=If you have already paid in funds you can withdraw it in the \"Funds/Send funds\" screen. takeOffer.alreadyPaidInFunds=If you have already paid in funds you can withdraw it in the \"Funds/Send funds\" screen.
takeOffer.setAmountPrice=Set amount takeOffer.setAmountPrice=Set amount
takeOffer.alreadyFunded.askCancel=You have already funded that offer.\nIf you cancel now, your funds will be moved to your local Haveno wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel? takeOffer.alreadyFunded.askCancel=You have already funded that offer.\nIf you cancel now, your funds will be moved to your local Haveno wallet and are available for withdrawal in the \"Funds/Send funds\" screen.\nAre you sure you want to cancel?
@ -2242,9 +2240,6 @@ systemTray.tooltip=Haveno: A decentralized bitcoin exchange network
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is \
at least {0} satoshis/vbyte. Otherwise, the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=Trading accounts saved to path:\n{0} guiUtil.accountExport.savedToPath=Trading accounts saved to path:\n{0}
guiUtil.accountExport.noAccountSetup=You don't have trading accounts set up for exporting. guiUtil.accountExport.noAccountSetup=You don't have trading accounts set up for exporting.
guiUtil.accountExport.selectPath=Select path to {0} guiUtil.accountExport.selectPath=Select path to {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: Decentralizovaná směnárna bitcoinů
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Ujistěte se, že poplatek za těžbu používaný vaší externí peněženkou je alespoň {0} satoshi/vbyte. Jinak nemusí být obchodní transakce potvrzeny včas a obchod skončí sporem.
guiUtil.accountExport.savedToPath=Obchodní účty uložené na:\n{0} guiUtil.accountExport.savedToPath=Obchodní účty uložené na:\n{0}
guiUtil.accountExport.noAccountSetup=Nemáte nastaveny obchodní účty pro export. guiUtil.accountExport.noAccountSetup=Nemáte nastaveny obchodní účty pro export.
guiUtil.accountExport.selectPath=Vyberte cestu k {0} guiUtil.accountExport.selectPath=Vyberte cestu k {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: Ein dezentrales Bitcoin-Tauschbörsen-Netzwerk
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Bitte stellen Sie sicher, dass die Mining-Gebühr für Ihre externe Wallet mindestens {0} satoshis/byte ist. Ansonsten wird die Transaktion des Trades nicht rechtzeitig bestätigt und der Trade wird in einem Konflikt enden.
guiUtil.accountExport.savedToPath=Handelskonten in Verzeichnis gespeichert:\n{0} guiUtil.accountExport.savedToPath=Handelskonten in Verzeichnis gespeichert:\n{0}
guiUtil.accountExport.noAccountSetup=Sie haben kein Handelskonto zum Exportieren eingerichtet. guiUtil.accountExport.noAccountSetup=Sie haben kein Handelskonto zum Exportieren eingerichtet.
guiUtil.accountExport.selectPath=Verzeichnis auswählen zum {0} guiUtil.accountExport.selectPath=Verzeichnis auswählen zum {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: Una red de intercambio de bitcoin descentralizada
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Por favor asegúrese de que la comisión de minado usada en su monedero externo es de al menos {0} sat/vbyte. De lo contrario, las transacciones de intercambio podrían no confirmarse y el intercambio acabaría en disputa.
guiUtil.accountExport.savedToPath=Las cuentas de intercambio se han guardado en el directorio:\n{0} guiUtil.accountExport.savedToPath=Las cuentas de intercambio se han guardado en el directorio:\n{0}
guiUtil.accountExport.noAccountSetup=No tiene cuentas de intercambio configuradas para exportar. guiUtil.accountExport.noAccountSetup=No tiene cuentas de intercambio configuradas para exportar.
guiUtil.accountExport.selectPath=Seleccionar directorio a {0} guiUtil.accountExport.selectPath=Seleccionar directorio a {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: A decentralized bitcoin exchange network
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=حساب های معاملاتی در مسیر ذیل ذخیره شد:\n{0} guiUtil.accountExport.savedToPath=حساب های معاملاتی در مسیر ذیل ذخیره شد:\n{0}
guiUtil.accountExport.noAccountSetup=شما حساب های معاملاتی برای صادرات ندارید. guiUtil.accountExport.noAccountSetup=شما حساب های معاملاتی برای صادرات ندارید.
guiUtil.accountExport.selectPath=انتخاب مسیر به {0} guiUtil.accountExport.selectPath=انتخاب مسیر به {0}

View file

@ -1711,8 +1711,6 @@ systemTray.tooltip=Bisq: Une plateforme d''échange décentralisée sur le rése
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Veuillez vous assurer que les frais de minage utilisés par votre portefeuille externe sont d'au moins {0} satoshis/vbyte. Le cas échéant les transactions de trade pourraient ne peut être confirmée à temps et le trade aboutirait à une dispute.
guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l''arborescence:\n{0} guiUtil.accountExport.savedToPath=Les comptes de trading sont sauvegardés vers l''arborescence:\n{0}
guiUtil.accountExport.noAccountSetup=Vous n'avez pas de comptes de trading configurés pour exportation. guiUtil.accountExport.noAccountSetup=Vous n'avez pas de comptes de trading configurés pour exportation.
guiUtil.accountExport.selectPath=Sélectionner l''arborescence vers {0} guiUtil.accountExport.selectPath=Sélectionner l''arborescence vers {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: una rete di scambio decentralizzata di bitcoin
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=Account di trading salvati nel percorso:\n{0} guiUtil.accountExport.savedToPath=Account di trading salvati nel percorso:\n{0}
guiUtil.accountExport.noAccountSetup=Non hai account di trading impostati per l'esportazione. guiUtil.accountExport.noAccountSetup=Non hai account di trading impostati per l'esportazione.
guiUtil.accountExport.selectPath=Seleziona il percorso per {0} guiUtil.accountExport.selectPath=Seleziona il percorso per {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: 分散的ビットコイン取引ネットワーク
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=外部ウォレットで使用されるマイニング手数料が少なくとも{0} satoshis/vbyte あることを確認してください。 そうでなければ、このトレードトランザクションは承認されない可能性、トレードは係争に終わる可能性があります。
guiUtil.accountExport.savedToPath=取引アカウントを下記パスに保存しました:\n{0} guiUtil.accountExport.savedToPath=取引アカウントを下記パスに保存しました:\n{0}
guiUtil.accountExport.noAccountSetup=取引アカウントのエクスポート設定がされていません guiUtil.accountExport.noAccountSetup=取引アカウントのエクスポート設定がされていません
guiUtil.accountExport.selectPath={0}のパスを選択 guiUtil.accountExport.selectPath={0}のパスを選択

View file

@ -1718,8 +1718,6 @@ systemTray.tooltip=Bisq: a rede de exchange decentralizada de bitcoin
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=Contas de negociação salvas na pasta:\n{0} guiUtil.accountExport.savedToPath=Contas de negociação salvas na pasta:\n{0}
guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação para exportar. guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação para exportar.
guiUtil.accountExport.selectPath=Selecione pasta de {0} guiUtil.accountExport.selectPath=Selecione pasta de {0}

View file

@ -1708,8 +1708,6 @@ systemTray.tooltip=Bisq: Uma rede de echange de bitcoin descentralizada
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=Contas de negociação guardadas em:\n{0} guiUtil.accountExport.savedToPath=Contas de negociação guardadas em:\n{0}
guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação prontas para exportar. guiUtil.accountExport.noAccountSetup=Você não tem contas de negociação prontas para exportar.
guiUtil.accountExport.selectPath=Selecione diretório de {0} guiUtil.accountExport.selectPath=Selecione diretório de {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: A decentralized bitcoin exchange network
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=Торговые счета в:\n{0} guiUtil.accountExport.savedToPath=Торговые счета в:\n{0}
guiUtil.accountExport.noAccountSetup=У вас нет торговых счетов для экспортирования. guiUtil.accountExport.noAccountSetup=У вас нет торговых счетов для экспортирования.
guiUtil.accountExport.selectPath=Выбрать путь к {0} guiUtil.accountExport.selectPath=Выбрать путь к {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq: A decentralized bitcoin exchange network
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=บัญชีการค้าที่บันทึกไว้ในเส้นทาง: \n{0} guiUtil.accountExport.savedToPath=บัญชีการค้าที่บันทึกไว้ในเส้นทาง: \n{0}
guiUtil.accountExport.noAccountSetup=คุณไม่มีบัญชีการซื้อขายที่ตั้งค่าไว้สำหรับการส่งออก guiUtil.accountExport.noAccountSetup=คุณไม่มีบัญชีการซื้อขายที่ตั้งค่าไว้สำหรับการส่งออก
guiUtil.accountExport.selectPath=เลือกเส้นทางไปที่ {0} guiUtil.accountExport.selectPath=เลือกเส้นทางไปที่ {0}

View file

@ -1712,8 +1712,6 @@ systemTray.tooltip=Bisq: A decentralized bitcoin exchange network
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute.
guiUtil.accountExport.savedToPath=tài khoản giao dịch được lưu vào đường dẫn:\n{0} guiUtil.accountExport.savedToPath=tài khoản giao dịch được lưu vào đường dẫn:\n{0}
guiUtil.accountExport.noAccountSetup=Bạn không có tài khoản giao dịch được thiết lập để truy xuất. guiUtil.accountExport.noAccountSetup=Bạn không có tài khoản giao dịch được thiết lập để truy xuất.
guiUtil.accountExport.selectPath=Chọn đường dẫn đến {0} guiUtil.accountExport.selectPath=Chọn đường dẫn đến {0}

View file

@ -1714,8 +1714,6 @@ systemTray.tooltip=Bisq去中心化比特币交易网络
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=请确保您的外部钱包使用的矿工手续费费用足够高至少为 {0} 聪/字节,以便矿工接受资金交易。\n否则交易所交易无法确认交易最终将会出现纠纷。
guiUtil.accountExport.savedToPath=交易账户保存在路径:\n{0} guiUtil.accountExport.savedToPath=交易账户保存在路径:\n{0}
guiUtil.accountExport.noAccountSetup=您没有交易账户设置导出。 guiUtil.accountExport.noAccountSetup=您没有交易账户设置导出。
guiUtil.accountExport.selectPath=选择路径 {0} guiUtil.accountExport.selectPath=选择路径 {0}

View file

@ -1710,8 +1710,6 @@ systemTray.tooltip=Bisq去中心化比特幣交易網絡
# GUI Util # GUI Util
#################################################################### ####################################################################
guiUtil.miningFeeInfo=請確保您的外部錢包使用的礦工手續費費用足夠高至少為 {0} 聰/字節,以便礦工接受資金交易。\n否則交易所交易無法確認交易最終將會出現糾紛。
guiUtil.accountExport.savedToPath=交易賬户保存在路徑:\n{0} guiUtil.accountExport.savedToPath=交易賬户保存在路徑:\n{0}
guiUtil.accountExport.noAccountSetup=您沒有交易賬户設置導出。 guiUtil.accountExport.noAccountSetup=您沒有交易賬户設置導出。
guiUtil.accountExport.selectPath=選擇路徑 {0} guiUtil.accountExport.selectPath=選擇路徑 {0}

View file

@ -3,7 +3,7 @@ package bisq.daemon.grpc;
import bisq.core.api.CoreApi; import bisq.core.api.CoreApi;
import bisq.core.support.dispute.Attachment; import bisq.core.support.dispute.Attachment;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.util.ParsingUtils; import bisq.core.trade.HavenoUtils;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
@ -109,7 +109,7 @@ public class GrpcDisputesService extends DisputesImplBase {
var winner = ProtoUtil.enumFromProto(DisputeResult.Winner.class, req.getWinner().name()); var winner = ProtoUtil.enumFromProto(DisputeResult.Winner.class, req.getWinner().name());
var reason = ProtoUtil.enumFromProto(DisputeResult.Reason.class, req.getReason().name()); var reason = ProtoUtil.enumFromProto(DisputeResult.Reason.class, req.getReason().name());
// scale atomic unit to centineros for consistency TODO switch base to atomic units? // scale atomic unit to centineros for consistency TODO switch base to atomic units?
var customPayoutAmount = ParsingUtils.atomicUnitsToCentineros(req.getCustomPayoutAmount()); var customPayoutAmount = HavenoUtils.atomicUnitsToCentineros(req.getCustomPayoutAmount());
coreApi.resolveDispute(req.getTradeId(), winner, reason, req.getSummaryNotes(), customPayoutAmount); coreApi.resolveDispute(req.getTradeId(), winner, reason, req.getSummaryNotes(), customPayoutAmount);
var reply = ResolveDisputeReply.newBuilder().build(); var reply = ResolveDisputeReply.newBuilder().build();
responseObserver.onNext(reply); responseObserver.onNext(reply);

View file

@ -21,7 +21,7 @@ import bisq.core.api.CoreApi;
import bisq.core.api.model.OfferInfo; import bisq.core.api.model.OfferInfo;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.util.ParsingUtils; import bisq.core.trade.HavenoUtils;
import bisq.proto.grpc.CancelOfferReply; import bisq.proto.grpc.CancelOfferReply;
import bisq.proto.grpc.CancelOfferRequest; import bisq.proto.grpc.CancelOfferRequest;
import bisq.proto.grpc.PostOfferReply; import bisq.proto.grpc.PostOfferReply;
@ -154,8 +154,8 @@ class GrpcOffersService extends OffersImplBase {
req.getPrice(), req.getPrice(),
req.getUseMarketBasedPrice(), req.getUseMarketBasedPrice(),
req.getMarketPriceMarginPct(), req.getMarketPriceMarginPct(),
ParsingUtils.atomicUnitsToCentineros(req.getAmount()), // scale atomic unit to centineros for consistency TODO switch base to atomic units? HavenoUtils.atomicUnitsToCentineros(req.getAmount()), // scale atomic unit to centineros for consistency TODO switch base to atomic units?
ParsingUtils.atomicUnitsToCentineros(req.getMinAmount()), HavenoUtils.atomicUnitsToCentineros(req.getMinAmount()),
req.getBuyerSecurityDepositPct(), req.getBuyerSecurityDepositPct(),
req.getTriggerPrice(), req.getTriggerPrice(),
req.getPaymentAccountId(), req.getPaymentAccountId(),

View file

@ -71,8 +71,7 @@ public class AddressTextField extends AnchorPane {
textField.setOnMousePressed(event -> wasPrimaryButtonDown = event.isPrimaryButtonDown()); textField.setOnMousePressed(event -> wasPrimaryButtonDown = event.isPrimaryButtonDown());
textField.setOnMouseReleased(event -> { textField.setOnMouseReleased(event -> {
if (wasPrimaryButtonDown) if (wasPrimaryButtonDown) openWallet();
GUIUtil.showFeeInfoBeforeExecute(AddressTextField.this::openWallet);
wasPrimaryButtonDown = false; wasPrimaryButtonDown = false;
}); });
@ -83,17 +82,17 @@ public class AddressTextField extends AnchorPane {
extWalletIcon.getStyleClass().addAll("icon", "highlight"); extWalletIcon.getStyleClass().addAll("icon", "highlight");
extWalletIcon.setTooltip(new Tooltip(tooltipText)); extWalletIcon.setTooltip(new Tooltip(tooltipText));
AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN); AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN);
extWalletIcon.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet)); extWalletIcon.setOnMouseClicked(e -> openWallet());
Label copyIcon = new Label(); Label copyIcon = new Label();
copyIcon.setLayoutY(3); copyIcon.setLayoutY(3);
copyIcon.getStyleClass().addAll("icon", "highlight"); copyIcon.getStyleClass().addAll("icon", "highlight");
Tooltip.install(copyIcon, new Tooltip(Res.get("addressTextField.copyToClipboard"))); Tooltip.install(copyIcon, new Tooltip(Res.get("addressTextField.copyToClipboard")));
AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY);
copyIcon.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute(() -> { copyIcon.setOnMouseClicked(e -> {
if (address.get() != null && address.get().length() > 0) if (address.get() != null && address.get().length() > 0)
Utilities.copyToClipboard(address.get()); Utilities.copyToClipboard(address.get());
})); });
AnchorPane.setRightAnchor(copyIcon, 30.0); AnchorPane.setRightAnchor(copyIcon, 30.0);
AnchorPane.setRightAnchor(extWalletIcon, 5.0); AnchorPane.setRightAnchor(extWalletIcon, 5.0);

View file

@ -21,7 +21,7 @@ import bisq.core.btc.listeners.XmrBalanceListener;
import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.util.ParsingUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import bisq.desktop.components.indicator.TxConfidenceIndicator; import bisq.desktop.components.indicator.TxConfidenceIndicator;
import bisq.desktop.util.GUIUtil; import bisq.desktop.util.GUIUtil;
@ -66,15 +66,14 @@ class DepositListItem {
balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) { balanceListener = new XmrBalanceListener(addressEntry.getSubaddressIndex()) {
@Override @Override
public void onBalanceChanged(BigInteger balance) { public void onBalanceChanged(BigInteger balance) {
DepositListItem.this.balanceAsCoin = ParsingUtils.atomicUnitsToCoin(balance); DepositListItem.this.balanceAsCoin = HavenoUtils.atomicUnitsToCoin(balance);
DepositListItem.this.balance.set(formatter.formatCoin(balanceAsCoin)); DepositListItem.this.balance.set(formatter.formatCoin(balanceAsCoin));
updateUsage(addressEntry.getSubaddressIndex()); updateUsage(addressEntry.getSubaddressIndex());
} }
}; };
xmrWalletService.addBalanceListener(balanceListener); xmrWalletService.addBalanceListener(balanceListener);
balanceAsCoin = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); // TODO: Coin represents centineros everywhere, but here it's atomic units. reconcile balanceAsCoin = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex());
balanceAsCoin = Coin.valueOf(ParsingUtils.atomicUnitsToCentineros(balanceAsCoin.longValue())); // in centineros
balance.set(formatter.formatCoin(balanceAsCoin)); balance.set(formatter.formatCoin(balanceAsCoin));
updateUsage(addressEntry.getSubaddressIndex()); updateUsage(addressEntry.getSubaddressIndex());

View file

@ -34,6 +34,7 @@ import bisq.core.btc.listeners.XmrBalanceListener;
import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.trade.HavenoUtils;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
@ -166,10 +167,9 @@ public class DepositView extends ActivatableView<VBox, Void> {
qrCodeImageView = new ImageView(); qrCodeImageView = new ImageView();
qrCodeImageView.getStyleClass().add("qr-code"); qrCodeImageView.getStyleClass().add("qr-code");
Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow")));
qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter(
() -> UserThread.runAfter(
() -> new QRCodeWindow(getPaymentUri()).show(), () -> new QRCodeWindow(getPaymentUri()).show(),
200, TimeUnit.MILLISECONDS))); 200, TimeUnit.MILLISECONDS));
GridPane.setRowIndex(qrCodeImageView, gridRow); GridPane.setRowIndex(qrCodeImageView, gridRow);
GridPane.setRowSpan(qrCodeImageView, 4); GridPane.setRowSpan(qrCodeImageView, 4);
GridPane.setColumnIndex(qrCodeImageView, 1); GridPane.setColumnIndex(qrCodeImageView, 1);
@ -308,7 +308,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
private String getPaymentUri() { private String getPaymentUri() {
return xmrWalletService.getWallet().getPaymentUri(new MoneroTxConfig() return xmrWalletService.getWallet().getPaymentUri(new MoneroTxConfig()
.setAddress(addressTextField.getAddress()) .setAddress(addressTextField.getAddress())
.setAmount(ParsingUtils.coinToAtomicUnits(getAmountAsCoin())) .setAmount(HavenoUtils.coinToAtomicUnits(getAmountAsCoin()))
.setNote(paymentLabelString)); .setNote(paymentLabelString));
} }

View file

@ -21,9 +21,9 @@ import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Tradable; import bisq.core.trade.Tradable;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import bisq.desktop.components.indicator.TxConfidenceIndicator; import bisq.desktop.components.indicator.TxConfidenceIndicator;
import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.DisplayUtils;
@ -94,8 +94,8 @@ class TransactionsListItem {
Optional<Tradable> optionalTradable = Optional.ofNullable(transactionAwareTradable) Optional<Tradable> optionalTradable = Optional.ofNullable(transactionAwareTradable)
.map(TransactionAwareTradable::asTradable); .map(TransactionAwareTradable::asTradable);
Coin valueSentToMe = ParsingUtils.atomicUnitsToCoin(tx.getIncomingAmount() == null ? new BigInteger("0") : tx.getIncomingAmount()); Coin valueSentToMe = HavenoUtils.atomicUnitsToCoin(tx.getIncomingAmount() == null ? new BigInteger("0") : tx.getIncomingAmount());
Coin valueSentFromMe = ParsingUtils.atomicUnitsToCoin(tx.getOutgoingAmount() == null ? new BigInteger("0") : tx.getOutgoingAmount()); Coin valueSentFromMe = HavenoUtils.atomicUnitsToCoin(tx.getOutgoingAmount() == null ? new BigInteger("0") : tx.getOutgoingAmount());
if (tx.getTransfers().get(0).isIncoming()) { if (tx.getTransfers().get(0).isIncoming()) {
addressString = ((MoneroIncomingTransfer) tx.getTransfers().get(0)).getAddress(); addressString = ((MoneroIncomingTransfer) tx.getTransfers().get(0)).getAddress();

View file

@ -72,8 +72,7 @@ class WithdrawalListItem {
} }
private void updateBalance() { private void updateBalance() {
balance = walletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex()); // TODO: Coin represents centineros everywhere, but here it's atomic units. reconcile balance = walletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex());
balance = Coin.valueOf(ParsingUtils.atomicUnitsToCentineros(balance.longValue())); // in centineros
if (balance != null) if (balance != null)
balanceLabel.setText(formatter.formatCoin(this.balance)); balanceLabel.setText(formatter.formatCoin(this.balance));
} }

View file

@ -32,6 +32,7 @@ import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.provider.fee.FeeService; import bisq.core.provider.fee.FeeService;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.DontShowAgainLookup; import bisq.core.user.DontShowAgainLookup;
@ -191,12 +192,12 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// create tx // create tx
MoneroTxWallet tx = xmrWalletService.getWallet().createTx(new MoneroTxConfig() MoneroTxWallet tx = xmrWalletService.getWallet().createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.setAmount(ParsingUtils.coinToAtomicUnits(receiverAmount)) // TODO: rename to centinerosToAtomicUnits()? .setAmount(HavenoUtils.coinToAtomicUnits(receiverAmount)) // TODO: rename to centinerosToAtomicUnits()?
.setAddress(withdrawToAddress) .setAddress(withdrawToAddress)
.setNote(withdrawMemoTextField.getText())); .setNote(withdrawMemoTextField.getText()));
// create confirmation message // create confirmation message
Coin fee = ParsingUtils.atomicUnitsToCoin(tx.getFee()); Coin fee = HavenoUtils.atomicUnitsToCoin(tx.getFee());
Coin sendersAmount = receiverAmount.add(fee); Coin sendersAmount = receiverAmount.add(fee);
String messageText = Res.get("shared.sendFundsDetailsWithFee", String messageText = Res.get("shared.sendFundsDetailsWithFee",
formatter.formatCoinWithCode(sendersAmount), formatter.formatCoinWithCode(sendersAmount),

View file

@ -260,10 +260,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); priceFeedService.setCurrencyCode(tradeCurrencyCode.get());
// Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values)
// But offer creation happens usually after that so we should have already the value from the estimation service.
txFeeFromFeeService = feeService.getTxFee(feeTxVsize);
calculateVolume(); calculateVolume();
calculateTotalToPay(); calculateTotalToPay();
updateBalance(); updateBalance();
@ -557,8 +553,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
// The mining fee for the createOfferFee tx is deducted from the createOfferFee and not visible to the trader // The mining fee for the createOfferFee tx is deducted from the createOfferFee and not visible to the trader
final Coin makerFee = getMakerFee(); final Coin makerFee = getMakerFee();
if (direction != null && amount.get() != null && makerFee != null) { if (direction != null && amount.get() != null && makerFee != null) {
Coin feeAndSecDeposit = getTxFee().add(getSecurityDeposit()); Coin feeAndSecDeposit = getSecurityDeposit().add(makerFee);
feeAndSecDeposit = feeAndSecDeposit.add(makerFee);
Coin total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get()); Coin total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get());
totalToPayAsCoin.set(total); totalToPayAsCoin.set(total);
updateBalance(); updateBalance();
@ -569,10 +564,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
return isBuyOffer() ? getBuyerSecurityDepositAsCoin() : getSellerSecurityDepositAsCoin(); return isBuyOffer() ? getBuyerSecurityDepositAsCoin() : getSellerSecurityDepositAsCoin();
} }
public Coin getTxFee() {
return txFeeFromFeeService;
}
void swapTradeToSavings() { void swapTradeToSavings() {
xmrWalletService.resetAddressEntriesForOpenOffer(offerId); xmrWalletService.resetAddressEntriesForOpenOffer(offerId);
} }

View file

@ -22,7 +22,6 @@ import bisq.desktop.common.view.ActivatableViewAndModel;
import bisq.desktop.components.AddressTextField; import bisq.desktop.components.AddressTextField;
import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipSlideToggleButton;
import bisq.desktop.components.BalanceTextField; import bisq.desktop.components.BalanceTextField;
import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.BusyAnimation;
import bisq.desktop.components.FundsTextField; import bisq.desktop.components.FundsTextField;
@ -32,10 +31,6 @@ import bisq.desktop.components.TitledGroupBg;
import bisq.desktop.main.MainView; import bisq.desktop.main.MainView;
import bisq.desktop.main.account.AccountView; import bisq.desktop.main.account.AccountView;
import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView;
import bisq.desktop.main.offer.ClosableView;
import bisq.desktop.main.offer.OfferView;
import bisq.desktop.main.offer.OfferViewUtil;
import bisq.desktop.main.offer.SelectableView;
import bisq.desktop.main.overlays.notifications.Notification; import bisq.desktop.main.overlays.notifications.Notification;
import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
@ -391,8 +386,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
model.getTotalToPayInfo(), model.getTotalToPayInfo(),
tradeAmountText, tradeAmountText,
model.getSecurityDepositInfo(), model.getSecurityDepositInfo(),
model.getTradeFee(), model.getTradeFee()
model.getTxFee()
); );
new Popup().headLine(Res.get("createOffer.createOfferFundWalletInfo.headline")) new Popup().headLine(Res.get("createOffer.createOfferFundWalletInfo.headline"))
.instruction(message) .instruction(message)
@ -770,7 +764,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
missingCoinListener = (observable, oldValue, newValue) -> { missingCoinListener = (observable, oldValue, newValue) -> {
if (!newValue.toString().equals("")) { if (!newValue.toString().equals("")) {
final byte[] imageBytes = QRCode final byte[] imageBytes = QRCode
.from(getBitcoinURI()) .from(getMoneroURI())
.withSize(98, 98) // code has 41 elements 8 px is border with 98 we get double scale and min. border .withSize(98, 98) // code has 41 elements 8 px is border with 98 we get double scale and min. border
.to(ImageType.PNG) .to(ImageType.PNG)
.stream() .stream()
@ -1080,10 +1074,9 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
qrCodeImageView.setFitWidth(150); qrCodeImageView.setFitWidth(150);
qrCodeImageView.getStyleClass().add("qr-code"); qrCodeImageView.getStyleClass().add("qr-code");
Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow")));
qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter(
() -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(),
() -> new QRCodeWindow(getBitcoinURI()).show(), 200, TimeUnit.MILLISECONDS));
200, TimeUnit.MILLISECONDS)));
GridPane.setRowIndex(qrCodeImageView, gridRow); GridPane.setRowIndex(qrCodeImageView, gridRow);
GridPane.setColumnIndex(qrCodeImageView, 1); GridPane.setColumnIndex(qrCodeImageView, 1);
GridPane.setRowSpan(qrCodeImageView, 3); GridPane.setRowSpan(qrCodeImageView, 3);
@ -1111,7 +1104,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
label.setPadding(new Insets(5, 0, 0, 0)); label.setPadding(new Insets(5, 0, 0, 0));
Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton")); Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton"));
fundFromExternalWalletButton.setDefaultButton(false); fundFromExternalWalletButton.setDefaultButton(false);
fundFromExternalWalletButton.setOnAction(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet)); fundFromExternalWalletButton.setOnAction(e -> openWallet());
waitingForFundsSpinner = new BusyAnimation(false); waitingForFundsSpinner = new BusyAnimation(false);
waitingForFundsLabel = new AutoTooltipLabel(); waitingForFundsLabel = new AutoTooltipLabel();
waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0)); waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0));
@ -1178,7 +1171,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
private void openWallet() { private void openWallet() {
try { try {
Utilities.openURI(URI.create(getBitcoinURI())); Utilities.openURI(URI.create(getMoneroURI()));
} catch (Exception ex) { } catch (Exception ex) {
log.warn(ex.getMessage()); log.warn(ex.getMessage());
new Popup().warning(Res.get("shared.openDefaultWalletFailed")).show(); new Popup().warning(Res.get("shared.openDefaultWalletFailed")).show();
@ -1186,10 +1179,12 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
} }
@NotNull @NotNull
private String getBitcoinURI() { private String getMoneroURI() {
return "TODO"; // TODO (woodser): wallet.createPaymentUri(); return GUIUtil.getMoneroURI(
// return GUIUtil.getBitcoinURI(addressTextField.getAddress(), model.getDataModel().getMissingCoin().get(), addressTextField.getAddress(),
// model.getPaymentLabel()); model.getDataModel().getMissingCoin().get(),
model.getPaymentLabel(),
model.dataModel.getXmrWalletService().getWallet());
} }
private void addAmountPriceFields() { private void addAmountPriceFields() {
@ -1274,6 +1269,9 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
firstRowHBox.getChildren().add(2, fixedPriceBox); firstRowHBox.getChildren().add(2, fixedPriceBox);
if (!secondRowHBox.getChildren().contains(percentagePriceBox)) if (!secondRowHBox.getChildren().contains(percentagePriceBox))
secondRowHBox.getChildren().add(2, percentagePriceBox); secondRowHBox.getChildren().add(2, percentagePriceBox);
model.triggerPrice.set("");
model.onTriggerPriceTextFieldChanged();
} else { } else {
firstRowHBox.getChildren().remove(fixedPriceBox); firstRowHBox.getChildren().remove(fixedPriceBox);
secondRowHBox.getChildren().remove(percentagePriceBox); secondRowHBox.getChildren().remove(percentagePriceBox);
@ -1394,7 +1392,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo());
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.offerFee"), model.getTradeFee()); addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.offerFee"), model.getTradeFee());
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.networkFee"), model.getTxFee());
Separator separator = new Separator(); Separator separator = new Separator();
separator.setOrientation(Orientation.HORIZONTAL); separator.setOrientation(Orientation.HORIZONTAL);
separator.getStyleClass().add("offer-separator"); separator.getStyleClass().add("offer-separator");

View file

@ -648,6 +648,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
} }
void onShowPayFundsScreen(Runnable actionHandler) { void onShowPayFundsScreen(Runnable actionHandler) {
actionHandler.run();
showPayFundsScreenDisplayed.set(true); showPayFundsScreenDisplayed.set(true);
updateSpinnerInfo(); updateSpinnerInfo();
} }
@ -1015,24 +1016,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
public String getFundsStructure() { public String getFundsStructure() {
String fundsStructure; String fundsStructure;
fundsStructure = Res.get("createOffer.fundsBox.fundsStructure", fundsStructure = Res.get("createOffer.fundsBox.fundsStructure",
getSecurityDepositWithCode(), getMakerFeePercentage(), getTxFeePercentage()); getSecurityDepositWithCode(), getMakerFeePercentage());
return fundsStructure; return fundsStructure;
} }
public String getTxFee() {
return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getTxFee(),
dataModel.getAmount().get(),
btcFormatter,
Coin.ZERO
);
}
public String getTxFeePercentage() {
Coin txFeeAsCoin = dataModel.getTxFee();
return GUIUtil.getPercentage(txFeeAsCoin, dataModel.getAmount().get());
}
public PaymentAccount getPaymentAccount() { public PaymentAccount getPaymentAccount() {
return dataModel.getPaymentAccount(); return dataModel.getPaymentAccount();
} }

View file

@ -41,6 +41,7 @@ import static bisq.core.util.coin.CoinUtil.minCoin;
* needed in that UI element. * needed in that UI element.
*/ */
public abstract class OfferDataModel extends ActivatableDataModel { public abstract class OfferDataModel extends ActivatableDataModel {
@Getter
protected final XmrWalletService xmrWalletService; protected final XmrWalletService xmrWalletService;
protected final OfferUtil offerUtil; protected final OfferUtil offerUtil;

View file

@ -454,12 +454,11 @@ class TakeOfferDataModel extends OfferDataModel {
// The mining fee for the takeOfferFee tx is deducted from the createOfferFee and not visible to the trader // The mining fee for the takeOfferFee tx is deducted from the createOfferFee and not visible to the trader
final Coin takerFee = getTakerFee(); final Coin takerFee = getTakerFee();
if (offer != null && amount.get() != null && takerFee != null) { if (offer != null && amount.get() != null && takerFee != null) {
Coin feeAndSecDeposit = getTotalTxFee().add(securityDeposit).add(takerFee); Coin feeAndSecDeposit = securityDeposit.add(takerFee);
if (isBuyOffer()) if (isBuyOffer())
totalToPayAsCoin.set(feeAndSecDeposit.add(amount.get())); totalToPayAsCoin.set(feeAndSecDeposit.add(amount.get()));
else else
totalToPayAsCoin.set(feeAndSecDeposit); totalToPayAsCoin.set(feeAndSecDeposit);
updateBalance(); updateBalance();
log.debug("totalToPayAsCoin {}", totalToPayAsCoin.get().toFriendlyString()); log.debug("totalToPayAsCoin {}", totalToPayAsCoin.get().toFriendlyString());
} }
@ -556,10 +555,6 @@ class TakeOfferDataModel extends OfferDataModel {
return CurrencyUtil.getNameByCode(offer.getCurrencyCode()); return CurrencyUtil.getNameByCode(offer.getCurrencyCode());
} }
public Coin getTotalTxFee() {
return txFeeFromFeeService.add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx());
}
@NotNull @NotNull
private Coin getFundsNeededForTrade() { private Coin getFundsNeededForTrade() {
return getSecurityDeposit().add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx()); return getSecurityDeposit().add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx());

View file

@ -23,7 +23,6 @@ import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AddressTextField; import bisq.desktop.components.AddressTextField;
import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipSlideToggleButton;
import bisq.desktop.components.BalanceTextField; import bisq.desktop.components.BalanceTextField;
import bisq.desktop.components.BusyAnimation; import bisq.desktop.components.BusyAnimation;
import bisq.desktop.components.FundsTextField; import bisq.desktop.components.FundsTextField;
@ -88,7 +87,6 @@ import javafx.scene.control.Tooltip;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
@ -455,8 +453,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
model.getTotalToPayInfo(), model.getTotalToPayInfo(),
tradeAmountText, tradeAmountText,
model.getSecurityDepositInfo(), model.getSecurityDepositInfo(),
model.getTradeFee(), model.getTradeFee()
model.getTxFee()
); );
String key = "takeOfferFundWalletInfo"; String key = "takeOfferFundWalletInfo";
new Popup().headLine(Res.get("takeOffer.takeOfferFundWalletInfo.headline")) new Popup().headLine(Res.get("takeOffer.takeOfferFundWalletInfo.headline"))
@ -477,7 +474,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
balanceTextField.setVisible(true); balanceTextField.setVisible(true);
totalToPayTextField.setFundsStructure(Res.get("takeOffer.fundsBox.fundsStructure", totalToPayTextField.setFundsStructure(Res.get("takeOffer.fundsBox.fundsStructure",
model.getSecurityDepositWithCode(), model.getTakerFeePercentage(), model.getTxFeePercentage())); model.getSecurityDepositWithCode(), model.getTakerFeePercentage()));
totalToPayTextField.setContentForInfoPopOver(createInfoPopover()); totalToPayTextField.setContentForInfoPopOver(createInfoPopover());
if (model.dataModel.getIsBtcWalletFunded().get()) { if (model.dataModel.getIsBtcWalletFunded().get()) {
@ -491,7 +488,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
} }
final byte[] imageBytes = QRCode final byte[] imageBytes = QRCode
.from(getBitcoinURI()) .from(getMoneroURI())
.withSize(98, 98) // code has 41 elements 8 px is border with 98 we get double scale and min. border .withSize(98, 98) // code has 41 elements 8 px is border with 98 we get double scale and min. border
.to(ImageType.PNG) .to(ImageType.PNG)
.stream() .stream()
@ -861,10 +858,9 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
qrCodeImageView.setFitWidth(150); qrCodeImageView.setFitWidth(150);
qrCodeImageView.getStyleClass().add("qr-code"); qrCodeImageView.getStyleClass().add("qr-code");
Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow")));
qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter(
() -> UserThread.runAfter( () -> new QRCodeWindow(getMoneroURI()).show(),
() -> new QRCodeWindow(getBitcoinURI()).show(), 200, TimeUnit.MILLISECONDS));
200, TimeUnit.MILLISECONDS)));
GridPane.setRowIndex(qrCodeImageView, gridRow); GridPane.setRowIndex(qrCodeImageView, gridRow);
GridPane.setColumnIndex(qrCodeImageView, 1); GridPane.setColumnIndex(qrCodeImageView, 1);
GridPane.setRowSpan(qrCodeImageView, 3); GridPane.setRowSpan(qrCodeImageView, 3);
@ -890,7 +886,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
label.setPadding(new Insets(5, 0, 0, 0)); label.setPadding(new Insets(5, 0, 0, 0));
Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton")); Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton"));
fundFromExternalWalletButton.setDefaultButton(false); fundFromExternalWalletButton.setDefaultButton(false);
fundFromExternalWalletButton.setOnAction(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet)); fundFromExternalWalletButton.setOnAction(e -> openWallet());
waitingForFundsBusyAnimation = new BusyAnimation(false); waitingForFundsBusyAnimation = new BusyAnimation(false);
waitingForFundsLabel = new AutoTooltipLabel(); waitingForFundsLabel = new AutoTooltipLabel();
waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0)); waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0));
@ -955,7 +951,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private void openWallet() { private void openWallet() {
try { try {
Utilities.openURI(URI.create(getBitcoinURI())); Utilities.openURI(URI.create(getMoneroURI()));
} catch (Exception ex) { } catch (Exception ex) {
log.warn(ex.getMessage()); log.warn(ex.getMessage());
new Popup().warning(Res.get("shared.openDefaultWalletFailed")).show(); new Popup().warning(Res.get("shared.openDefaultWalletFailed")).show();
@ -963,11 +959,12 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
} }
@NotNull @NotNull
private String getBitcoinURI() { private String getMoneroURI() {
return "TODO"; return GUIUtil.getMoneroURI(
// return GUIUtil.getBitcoinURI(model.dataModel.getAddressEntry().getAddressString(), model.dataModel.getAddressEntry().getAddressString(),
// model.dataModel.getMissingCoin().get(), model.dataModel.getMissingCoin().get(),
// model.getPaymentLabel()); model.getPaymentLabel(),
model.dataModel.getXmrWalletService().getWallet());
} }
private void addAmountPriceFields() { private void addAmountPriceFields() {
@ -1165,7 +1162,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo());
addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.offerFee"), model.getTradeFee()); addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.offerFee"), model.getTradeFee());
addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.networkFee"), model.getTxFee());
Separator separator = new Separator(); Separator separator = new Separator();
separator.setOrientation(Orientation.HORIZONTAL); separator.setOrientation(Orientation.HORIZONTAL);
separator.getStyleClass().add("offer-separator"); separator.getStyleClass().add("offer-separator");

View file

@ -51,7 +51,6 @@ import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection; import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.ConnectionListener; import bisq.network.p2p.network.ConnectionListener;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
@ -706,20 +705,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
return totalToPay; return totalToPay;
} }
public String getTxFee() {
return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getTotalTxFee(),
dataModel.getAmount().get(),
btcFormatter,
Coin.ZERO
);
}
public String getTxFeePercentage() {
Coin txFeeAsCoin = dataModel.getTotalTxFee();
return GUIUtil.getPercentage(txFeeAsCoin, dataModel.getAmount().get());
}
ObservableList<PaymentAccount> getPossiblePaymentAccounts() { ObservableList<PaymentAccount> getPossiblePaymentAccounts() {
return dataModel.getPossiblePaymentAccounts(); return dataModel.getPossiblePaymentAccounts();
} }

View file

@ -42,11 +42,13 @@ import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountList; import bisq.core.payment.PaymentAccountList;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService; import bisq.core.provider.fee.FeeService;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.txproof.AssetTxProofResult; import bisq.core.trade.txproof.AssetTxProofResult;
import bisq.core.user.DontShowAgainLookup; import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.user.User; import bisq.core.user.User;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
@ -135,6 +137,9 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxConfig;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -190,19 +195,6 @@ public class GUIUtil {
}); });
} }
public static void showFeeInfoBeforeExecute(Runnable runnable) {
String key = "miningFeeInfo";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
new Popup().attention(Res.get("guiUtil.miningFeeInfo", String.valueOf(GUIUtil.feeService.getTxFeePerVbyte().value)))
.onClose(runnable)
.useIUnderstandButton()
.show();
DontShowAgainLookup.dontShowAgain(key, true);
} else {
runnable.run();
}
}
public static void exportAccounts(ArrayList<PaymentAccount> accounts, public static void exportAccounts(ArrayList<PaymentAccount> accounts,
String fileName, String fileName,
Preferences preferences, Preferences preferences,
@ -715,6 +707,13 @@ public class GUIUtil {
.show(); .show();
} }
public static String getMoneroURI(String address, Coin amount, String label, MoneroWallet wallet) {
return wallet.getPaymentUri(new MoneroTxConfig()
.setAddress(address)
.setAmount(HavenoUtils.coinToAtomicUnits(amount))
.setNote(label));
}
public static String getBitcoinURI(String address, Coin amount, String label) { public static String getBitcoinURI(String address, Coin amount, String label) {
return address != null ? return address != null ?
BitcoinURI.convertToBitcoinURI(Address.fromString(Config.baseCurrencyNetworkParameters(), BitcoinURI.convertToBitcoinURI(Address.fromString(Config.baseCurrencyNetworkParameters(),