recover from error closing dispute, show payout confirmation w/ tx fee

This commit is contained in:
woodser 2022-12-30 06:39:54 +00:00
parent 48baa1e602
commit 2c3dabfbf7
18 changed files with 273 additions and 358 deletions

View file

@ -35,6 +35,7 @@ import java.util.List;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.model.MoneroTxWallet;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
@ -170,7 +171,7 @@ public class CoreDisputesService {
applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount);
// close dispute ticket
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> {
closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, null, () -> {
arbitrationManager.requestPersistence();
// close peer's dispute ticket
@ -182,12 +183,16 @@ public class CoreDisputesService {
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, () -> {
closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, null, () -> {
arbitrationManager.requestPersistence();
}, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err);
});
} else {
throw new IllegalStateException("could not find peer dispute");
}
}, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err);
});
}
} catch (Exception e) {
@ -239,10 +244,7 @@ public class CoreDisputesService {
}
}
// From DisputeSummaryWindow.java
public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler) {
dispute.setDisputeResult(disputeResult);
dispute.setIsClosed();
public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, MoneroTxWallet payoutTx, ResultHandler resultHandler, FaultHandler faultHandler) {
DisputeResult.Reason reason = disputeResult.getReason();
String role = Res.get("shared.arbitrator");
@ -272,7 +274,12 @@ public class CoreDisputesService {
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, resultHandler);
disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, payoutTx, () -> {
dispute.setDisputeResult(disputeResult);
dispute.setIsClosed();
resultHandler.handleResult();
}, faultHandler);
}
public void sendDisputeChatMessage(String disputeId, String message, ArrayList<Attachment> attachments) {

View file

@ -689,30 +689,118 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
// arbitrator sends result to trader when their dispute is closed
public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler) {
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return;
}
public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, MoneroTxWallet payoutTx, ResultHandler resultHandler, FaultHandler faultHandler) {
try {
ChatMessage chatMessage = new ChatMessage(
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) throw new RuntimeException("Dispute trade " + dispute.getTradeId() + " does not exist");
// create dispute payout tx if not given
if (payoutTx == null) payoutTx = createDisputePayoutTx(trade, dispute, disputeResult); // can be null if already published or we don't have receiver's multisig hex
// persist result in dispute's chat message
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
dispute.getTraderPubKeyRing().hashCode(),
false,
summaryText,
p2PService.getAddress());
disputeResult.setChatMessage(chatMessage);
dispute.addAndPersistChatMessage(chatMessage);
disputeResult.setChatMessage(chatMessage);
dispute.addAndPersistChatMessage(chatMessage);
// create dispute closed message
TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing());
String unsignedPayoutTxHex = payoutTx == null ? null : payoutTx.getTxSet().getMultisigTxHex();
TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ;
DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType(),
trade.getSelf().getUpdatedMultisigHex(),
trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
// get trade
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
// send dispute closed message
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(),
disputeClosedMessage.getUid(), chatMessage.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(),
dispute.getTraderPubKeyRing(),
disputeClosedMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " +
"chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.syncWalletNormallyForMs(30000);
requestPersistence();
resultHandler.handleResult();
}
@Override
public void onStoredInMailbox() {
log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence();
resultHandler.handleResult();
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}, errorMessage={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid(), errorMessage);
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence();
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
}
}
);
// save state
if (payoutTx != null) {
trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence();
} catch (Exception e) {
faultHandler.handleFault(e.getMessage(), e);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
public MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult) {
// sync and save wallet
trade.syncWallet();
@ -739,139 +827,55 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (!trade.isPayoutPublished()) {
log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId());
try {
MoneroTxWallet payoutTx = createDisputePayoutTx(trade, dispute, disputeResult);
trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
// trade wallet must be synced
if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId());
// collect winner and loser payout address and amounts
Contract contract = dispute.getContract();
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
BigInteger winnerPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger loserPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
// create transaction to get fee estimate
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
MoneroTxWallet feeEstimateTx = trade.getWallet().createTx(txConfig);
// create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
}
numAttempts++;
try {
payoutTx = trade.getWallet().createTx(txConfig);
} catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee?
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
log.info("Dispute payout transaction generated on attempt {}", numAttempts);
// save updated multisig hex
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
return payoutTx;
} catch (Exception e) {
if (!trade.isPayoutPublished()) throw e;
}
}
}
// create dispute closed message
String unsignedPayoutTxHex = receiver.getUpdatedMultisigHex() == null ? null : trade.getPayoutTxHex();
TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer();
boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ;
DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult,
p2PService.getAddress(),
UUID.randomUUID().toString(),
getSupportType(),
trade.getSelf().getUpdatedMultisigHex(),
trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
// send dispute closed message
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(),
disputeClosedMessage.getUid(), chatMessage.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(),
dispute.getTraderPubKeyRing(),
disputeClosedMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " +
"chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
trade.syncWalletNormallyForMs(30000);
requestPersistence();
resultHandler.handleResult();
}
@Override
public void onStoredInMailbox() {
log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid());
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setStoredInMailbox(true);
Trade trade = tradeManager.getTrade(dispute.getTradeId());
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
requestPersistence();
resultHandler.handleResult();
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " +
"chatMessage.uid={}, errorMessage={}",
disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(),
disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(),
chatMessage.getUid(), errorMessage);
// We use the chatMessage wrapped inside the DisputeClosedMessage for
// the state, as that is displayed to the user and we only persist that msg
chatMessage.setSendMessageError(errorMessage);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
requestPersistence();
resultHandler.handleResult();
}
}
);
trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult) {
// trade wallet must be synced
if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + dispute.getTradeId());
// collect winner and loser payout address and amounts
Contract contract = dispute.getContract();
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
BigInteger winnerPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
BigInteger loserPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
// create transaction to get fee estimate
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
MoneroTxWallet feeEstimateTx = trade.getWallet().createTx(txConfig);
// create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful
txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee");
if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee
}
numAttempts++;
try {
payoutTx = trade.getWallet().createTx(txConfig);
} catch (MoneroError e) {
// exception expected // TODO: better way of estimating fee?
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
log.info("Dispute payout transaction generated on attempt {}", numAttempts);
// save updated multisig hex
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
return payoutTx;
return null; // can be null if already published or we don't have receiver's multisig hex
}
private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {

View file

@ -1849,8 +1849,7 @@ disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n
disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n\
{1}{2}\
Transaction fee: {3} ({4} satoshis/vbyte)\n\
Transaction vsize: {5} vKb\n\n\
Transaction fee: {3}\n\n\
Are you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Zveřejněte transakci vrácení p
disputeSummaryWindow.close.txDetails.buyer=Kupující obdrží {0} na adresu: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Prodejce obdrží {0} na adresu: {1}\n
disputeSummaryWindow.close.txDetails=Výdaje: {0}\n{1} {2} Transakční poplatek: {3} ({4} satoshi/vbyte)\nTransakční vsize: {5} vKb\n\nOpravdu chcete tuto transakci zveřejnit?
disputeSummaryWindow.close.txDetails=Výdaje: {0}\n{1} {2} Transakční poplatek: {3}\n\nOpravdu chcete tuto transakci zveřejnit?
disputeSummaryWindow.close.noPayout.headline=Uzavřít bez jakékoli výplaty
disputeSummaryWindow.close.noPayout.text=Chcete zavřít bez výplaty?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Rückerstattungstransaktion veröf
disputeSummaryWindow.close.txDetails.buyer=Käufer erhält {0} an Adresse: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Verkäufer erhält {0} an Adresse: {1}\n
disputeSummaryWindow.close.txDetails=Ausgaben: {0}\n{1}{2}Transaktionsgebühr: {3} ({4} Satoshis/Byte)\nTransaktionsgröße: {5} Kb\n\nSind Sie sicher, dass Sie diese Transaktion veröffentlichen möchten?
disputeSummaryWindow.close.txDetails=Ausgaben: {0}\n{1}{2}Transaktionsgebühr: {3}\n\nSind Sie sicher, dass Sie diese Transaktion veröffentlichen möchten?
disputeSummaryWindow.close.noPayout.headline=Ohne Auszahlung schließen
disputeSummaryWindow.close.noPayout.text=Wollen Sie schließen ohne eine Auszahlung zu tätigen?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publicar transacción de devoluci
disputeSummaryWindow.close.txDetails.buyer=El comprador recibe {0} en la dirección: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=El vendedor recibe {0} en la dirección: {1}\n
disputeSummaryWindow.close.txDetails=Gastando: {0}\n{1}{2}Tasa de transacción: {3} ({4} satoshis/vbyte)\nTamaño virtual de transacción: {5} vKb\n\n¿Está seguro de que quiere publicar esta transacción?\n
disputeSummaryWindow.close.txDetails=Gastando: {0}\n{1}{2}Tasa de transacción: {3}\n\n¿Está seguro de que quiere publicar esta transacción?\n
disputeSummaryWindow.close.noPayout.headline=Cerrar sin realizar algún pago
disputeSummaryWindow.close.noPayout.text=¿Quiere cerrar sin realizar algún pago?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1396,7 +1396,7 @@ disputeSummaryWindow.close.txDetails.headline=Publier la transaction de rembours
disputeSummaryWindow.close.txDetails.buyer=L''acheteur reçoit {0} à l''adresse: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l''adresse: {1}\n
disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3} ({4} satoshis/vbyte)\nTaille virtuelle de la transaction: {5} vKb\n\nÊtes-vous sûr de vouloir publier cette transaction ?
disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3}\n\nÊtes-vous sûr de vouloir publier cette transaction ?
disputeSummaryWindow.close.noPayout.headline=Fermé sans paiement
disputeSummaryWindow.close.noPayout.text=Voulez-vous fermer sans paiement ?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Pubblica transazione di rimborso
disputeSummaryWindow.close.txDetails.buyer=L'acquirente riceve {0} all'indirizzo: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Il venditore riceve {0} all'indirizzo: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=払い戻しトランザクショ
disputeSummaryWindow.close.txDetails.buyer=買い手が {0} を受けます、入金先アドレス: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=売り手が {0} を受けます、入金先アドレス: {1}\n
disputeSummaryWindow.close.txDetails=支払う金額: {0}\n{1}{2}トランザクション手数料: {3}({4}サトシ/vバイト)\nトランザクションvサイズ: {5} vKb\n\nこのトランザクションを発行してもよろしいですか
disputeSummaryWindow.close.txDetails=支払う金額: {0}\n{1}{2}トランザクション手数料: {3}\n\nこのトランザクションを発行してもよろしいですか
disputeSummaryWindow.close.noPayout.headline=支払いなしで閉じる
disputeSummaryWindow.close.noPayout.text=支払いなしで閉じてもよろしいですか?

View file

@ -1399,7 +1399,7 @@ disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso
disputeSummaryWindow.close.txDetails.buyer=Comprador recebe {0} no endereço: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Vendedor recebe {0} no endereço: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso
disputeSummaryWindow.close.txDetails.buyer=O comprador recebe {0} no endereço: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=O vendedor recebe {0} no endereço: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1397,7 +1397,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=发布交易退款
disputeSummaryWindow.close.txDetails.buyer=买方收到{0}在地址:{1}
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=卖方收到{0}在地址:{1}
disputeSummaryWindow.close.txDetails=费用:{0}\n{1}{2}交易费:{3}({4}satoshis/byte)\n事务大小{5} Kb\n\n您确定要发布此交易吗
disputeSummaryWindow.close.txDetails=费用:{0}\n{1}{2}交易费:{3}\n\n您确定要发布此交易吗
disputeSummaryWindow.close.noPayout.headline=未支付关闭
disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情况下关闭吗?

View file

@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=發佈交易退款
disputeSummaryWindow.close.txDetails.buyer=買方收到{0}在地址:{1}
# suppress inspection "TrailingSpacesInProperty"
disputeSummaryWindow.close.txDetails.seller=賣方收到{0}在地址:{1}
disputeSummaryWindow.close.txDetails=費用:{0}\n{1}{2}交易費:{3}({4}satoshis/byte)\n事務大小{5} Kb\n\n您確定要發佈此交易嗎
disputeSummaryWindow.close.txDetails=費用:{0}\n{1}{2}交易費:{3}\n\n您確定要發佈此交易嗎
disputeSummaryWindow.close.noPayout.headline=未支付關閉
disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情況下關閉嗎?