mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-22 20:19:21 +00:00
filter offers with spent or duplicate funds using key images
reserve tx does not remain in arbitrator pool
This commit is contained in:
parent
b9228585c7
commit
6798630dfc
18 changed files with 166 additions and 71 deletions
|
@ -17,6 +17,7 @@
|
|||
|
||||
package bisq.core.api;
|
||||
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.monetary.Altcoin;
|
||||
import bisq.core.monetary.Price;
|
||||
import bisq.core.offer.CreateOfferService;
|
||||
|
@ -39,14 +40,17 @@ import javax.inject.Inject;
|
|||
import javax.inject.Singleton;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
|
||||
import static bisq.common.util.MathUtils.exactMultiply;
|
||||
import static bisq.common.util.MathUtils.roundDoubleToLong;
|
||||
|
@ -77,6 +81,7 @@ class CoreOffersService {
|
|||
private final OpenOfferManager openOfferManager;
|
||||
private final OfferUtil offerUtil;
|
||||
private final User user;
|
||||
private final XmrWalletService xmrWalletService;
|
||||
|
||||
@Inject
|
||||
public CoreOffersService(CoreContext coreContext,
|
||||
|
@ -87,7 +92,8 @@ class CoreOffersService {
|
|||
OfferFilter offerFilter,
|
||||
OpenOfferManager openOfferManager,
|
||||
OfferUtil offerUtil,
|
||||
User user) {
|
||||
User user,
|
||||
XmrWalletService xmrWalletService) {
|
||||
this.coreContext = coreContext;
|
||||
this.keyRing = keyRing;
|
||||
this.coreWalletsService = coreWalletsService;
|
||||
|
@ -97,6 +103,7 @@ class CoreOffersService {
|
|||
this.openOfferManager = openOfferManager;
|
||||
this.offerUtil = offerUtil;
|
||||
this.user = user;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
}
|
||||
|
||||
Offer getOffer(String id) {
|
||||
|
@ -117,20 +124,62 @@ class CoreOffersService {
|
|||
}
|
||||
|
||||
List<Offer> getOffers(String direction, String currencyCode) {
|
||||
return offerBookService.getOffers().stream()
|
||||
List<Offer> offers = offerBookService.getOffers().stream()
|
||||
.filter(o -> !o.isMyOffer(keyRing))
|
||||
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
|
||||
.filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid())
|
||||
.sorted(priceComparator(direction))
|
||||
.collect(Collectors.toList());
|
||||
offers.removeAll(getUnreservedOffers(offers));
|
||||
return offers;
|
||||
}
|
||||
|
||||
List<Offer> getMyOffers(String direction, String currencyCode) {
|
||||
return offerBookService.getOffers().stream()
|
||||
List<Offer> offers = offerBookService.getOffers().stream()
|
||||
.filter(o -> o.isMyOffer(keyRing))
|
||||
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
|
||||
.sorted(priceComparator(direction))
|
||||
.collect(Collectors.toList());
|
||||
Set<Offer> unreservedOffers = getUnreservedOffers(offers);
|
||||
offers.removeAll(unreservedOffers);
|
||||
|
||||
// remove my unreserved offers from offer manager
|
||||
List<OpenOffer> unreservedOpenOffers = new ArrayList<OpenOffer>();
|
||||
for (Offer unreservedOffer : unreservedOffers) {
|
||||
unreservedOpenOffers.add(openOfferManager.getOpenOfferById(unreservedOffer.getId()).get());
|
||||
}
|
||||
openOfferManager.removeOpenOffers(unreservedOpenOffers, null);
|
||||
return offers;
|
||||
}
|
||||
|
||||
private Set<Offer> getUnreservedOffers(List<Offer> offers) {
|
||||
Set<Offer> unreservedOffers = new HashSet<Offer>();
|
||||
|
||||
// collect reserved key images and check for duplicate funds
|
||||
List<String> allKeyImages = new ArrayList<String>();
|
||||
for (Offer offer : offers) {
|
||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||
if (!allKeyImages.add(keyImage)) unreservedOffers.add(offer);
|
||||
}
|
||||
}
|
||||
|
||||
// get spent key images
|
||||
// TODO (woodser): paginate offers and only check key images of current page
|
||||
List<String> spentKeyImages = new ArrayList<String>();
|
||||
List<MoneroKeyImageSpentStatus> spentStatuses = allKeyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : xmrWalletService.getDaemon().getKeyImageSpentStatuses(allKeyImages);
|
||||
for (int i = 0; i < spentStatuses.size(); i++) {
|
||||
if (spentStatuses.get(i) != MoneroKeyImageSpentStatus.NOT_SPENT) spentKeyImages.add(allKeyImages.get(i));
|
||||
}
|
||||
|
||||
// check for offers with spent key images
|
||||
for (Offer offer : offers) {
|
||||
if (unreservedOffers.contains(offer)) continue;
|
||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||
if (spentKeyImages.contains(keyImage)) unreservedOffers.add(offer);
|
||||
}
|
||||
}
|
||||
|
||||
return unreservedOffers;
|
||||
}
|
||||
|
||||
OpenOffer getMyOpenOffer(String id) {
|
||||
|
|
|
@ -140,7 +140,7 @@ public class WalletConfig extends AbstractIdleService {
|
|||
protected volatile BlockChain vChain;
|
||||
protected volatile SPVBlockStore vStore;
|
||||
protected volatile MoneroDaemon vXmrDaemon;
|
||||
protected volatile MoneroWallet vXmrWallet;
|
||||
protected volatile MoneroWalletRpc vXmrWallet;
|
||||
protected volatile Wallet vBtcWallet;
|
||||
protected volatile Wallet vBsqWallet;
|
||||
protected volatile PeerGroup vPeerGroup;
|
||||
|
@ -287,7 +287,7 @@ public class WalletConfig extends AbstractIdleService {
|
|||
// Meant to be overridden by subclasses
|
||||
}
|
||||
|
||||
public MoneroWallet createWallet(MoneroWalletConfig config) {
|
||||
public MoneroWalletRpc createWallet(MoneroWalletConfig config) {
|
||||
|
||||
// start monero-wallet-rpc instance
|
||||
MoneroWalletRpc walletRpc = startWalletRpcInstance();
|
||||
|
@ -304,7 +304,7 @@ public class WalletConfig extends AbstractIdleService {
|
|||
}
|
||||
}
|
||||
|
||||
public MoneroWallet openWallet(MoneroWalletConfig config) {
|
||||
public MoneroWalletRpc openWallet(MoneroWalletConfig config) {
|
||||
|
||||
// start monero-wallet-rpc instance
|
||||
MoneroWalletRpc walletRpc = startWalletRpcInstance();
|
||||
|
@ -362,7 +362,7 @@ public class WalletConfig extends AbstractIdleService {
|
|||
}
|
||||
System.out.println("Monero wallet path: " + vXmrWallet.getPath());
|
||||
System.out.println("Monero wallet address: " + vXmrWallet.getPrimaryAddress());
|
||||
System.out.println("Monero mnemonic: " + vXmrWallet.getMnemonic());
|
||||
System.out.println("Monero wallet uri: " + vXmrWallet.getRpcConnection().getUri());
|
||||
// vXmrWallet.rescanSpent();
|
||||
// vXmrWallet.rescanBlockchain();
|
||||
vXmrWallet.sync();
|
||||
|
|
|
@ -232,6 +232,7 @@ public class CreateOfferService {
|
|||
extraDataMap,
|
||||
Version.TRADE_PROTOCOL_VERSION,
|
||||
arbitrator.getNodeAddress(),
|
||||
null,
|
||||
null);
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
|
|
|
@ -171,9 +171,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||
// address and signature of signing arbitrator
|
||||
@Setter
|
||||
private NodeAddress arbitratorNodeAddress;
|
||||
@Nullable
|
||||
@Setter
|
||||
@Nullable
|
||||
private String arbitratorSignature;
|
||||
@Setter
|
||||
@Nullable
|
||||
private List<String> reserveTxKeyImages;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -217,7 +220,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||
@Nullable Map<String, String> extraDataMap,
|
||||
int protocolVersion,
|
||||
NodeAddress arbitratorSigner,
|
||||
@Nullable String arbitratorSignature) {
|
||||
@Nullable String arbitratorSignature,
|
||||
@Nullable List<String> reserveTxKeyImages) {
|
||||
this.id = id;
|
||||
this.date = date;
|
||||
this.ownerNodeAddress = ownerNodeAddress;
|
||||
|
@ -256,6 +260,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||
this.protocolVersion = protocolVersion;
|
||||
this.arbitratorNodeAddress = arbitratorSigner;
|
||||
this.arbitratorSignature = arbitratorSignature;
|
||||
this.reserveTxKeyImages = reserveTxKeyImages;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -293,7 +298,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||
.setLowerClosePrice(lowerClosePrice)
|
||||
.setUpperClosePrice(upperClosePrice)
|
||||
.setIsPrivateOffer(isPrivateOffer)
|
||||
.setProtocolVersion(protocolVersion);
|
||||
.setProtocolVersion(protocolVersion)
|
||||
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
|
||||
|
||||
builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId,
|
||||
"OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network."));
|
||||
|
@ -304,9 +310,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||
Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
|
||||
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
|
||||
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
||||
|
||||
builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
|
||||
Optional.ofNullable(arbitratorSignature).ifPresent(builder::setArbitratorSignature);
|
||||
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
|
||||
|
||||
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
|
||||
}
|
||||
|
@ -358,7 +363,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||
extraDataMapMap,
|
||||
proto.getProtocolVersion(),
|
||||
NodeAddress.fromProto(proto.getArbitratorNodeAddress()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature()));
|
||||
ProtoUtil.stringOrNullFromProto(proto.getArbitratorSignature()),
|
||||
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -282,7 +282,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
removeOpenOffers(getObservableList(), completeHandler);
|
||||
}
|
||||
|
||||
private void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) {
|
||||
public void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) {
|
||||
int size = openOffers.size();
|
||||
// Copy list as we remove in the loop
|
||||
List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
|
||||
|
@ -670,8 +670,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
return;
|
||||
}
|
||||
|
||||
// verify reserve tx not signed before
|
||||
|
||||
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
|
||||
Offer offer = new Offer(request.getOfferPayload());
|
||||
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
|
||||
|
@ -685,6 +683,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
request.getReserveTxHash(),
|
||||
request.getReserveTxHex(),
|
||||
request.getReserveTxKey(),
|
||||
request.getReserveTxKeyImages(),
|
||||
true);
|
||||
|
||||
// arbitrator signs offer to certify they have valid reserve tx
|
||||
|
@ -1034,7 +1033,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
updatedExtraDataMap,
|
||||
protocolVersion,
|
||||
originalOfferPayload.getArbitratorNodeAddress(),
|
||||
originalOfferPayload.getArbitratorSignature());
|
||||
originalOfferPayload.getArbitratorSignature(),
|
||||
originalOfferPayload.getReserveTxKeyImages());
|
||||
|
||||
// Save states from original data to use for the updated
|
||||
Offer.State originalOfferState = originalOffer.getState();
|
||||
|
|
|
@ -21,6 +21,8 @@ import bisq.common.crypto.PubKeyRing;
|
|||
import bisq.core.offer.OfferPayload;
|
||||
import bisq.network.p2p.DirectMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
|
@ -35,6 +37,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||
private final String reserveTxHash;
|
||||
private final String reserveTxHex;
|
||||
private final String reserveTxKey;
|
||||
private final List<String> reserveTxKeyImages;
|
||||
private final String payoutAddress;
|
||||
|
||||
public SignOfferRequest(String offerId,
|
||||
|
@ -48,6 +51,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||
String reserveTxHash,
|
||||
String reserveTxHex,
|
||||
String reserveTxKey,
|
||||
List<String> reserveTxKeyImages,
|
||||
String payoutAddress) {
|
||||
super(messageVersion, offerId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
|
@ -58,6 +62,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||
this.reserveTxHash = reserveTxHash;
|
||||
this.reserveTxHex = reserveTxHex;
|
||||
this.reserveTxKey = reserveTxKey;
|
||||
this.reserveTxKeyImages = reserveTxKeyImages;
|
||||
this.payoutAddress = payoutAddress;
|
||||
}
|
||||
|
||||
|
@ -79,6 +84,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||
.setReserveTxHash(reserveTxHash)
|
||||
.setReserveTxHex(reserveTxHex)
|
||||
.setReserveTxKey(reserveTxKey)
|
||||
.addAllReserveTxKeyImages(reserveTxKeyImages)
|
||||
.setPayoutAddress(payoutAddress);
|
||||
|
||||
return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build();
|
||||
|
@ -97,6 +103,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||
proto.getReserveTxHash(),
|
||||
proto.getReserveTxHex(),
|
||||
proto.getReserveTxKey(),
|
||||
new ArrayList<String>(proto.getReserveTxKeyImagesList()),
|
||||
proto.getPayoutAddress());
|
||||
}
|
||||
|
||||
|
@ -109,6 +116,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||
",\n reserveTxHash='" + reserveTxHash +
|
||||
",\n reserveTxHex='" + reserveTxHex +
|
||||
",\n reserveTxKey='" + reserveTxKey +
|
||||
",\n reserveTxKeyImages='" + reserveTxKeyImages +
|
||||
",\n payoutAddress='" + payoutAddress +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
|
|
|
@ -53,16 +53,17 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
|
|||
|
||||
// freeze reserved outputs
|
||||
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
|
||||
List<String> frozenKeyImages = new ArrayList<String>();
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
MoneroWallet wallet = model.getXmrWalletService().getWallet();
|
||||
for (MoneroOutput input : reserveTx.getInputs()) {
|
||||
frozenKeyImages.add(input.getKeyImage().getHex());
|
||||
reservedKeyImages.add(input.getKeyImage().getHex());
|
||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
||||
}
|
||||
|
||||
// save offer state
|
||||
// TODO (woodser): persist
|
||||
model.setReserveTx(reserveTx);
|
||||
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
||||
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id
|
||||
offer.setState(Offer.State.OFFER_FEE_RESERVED);
|
||||
complete();
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package bisq.core.offer.placeoffer.tasks;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.taskrunner.Task;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
@ -32,8 +34,6 @@ import java.util.UUID;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
|
||||
private static final Logger log = LoggerFactory.getLogger(MakerSendsSignOfferRequest.class);
|
||||
|
||||
|
@ -64,6 +64,7 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
|
|||
model.getReserveTx().getHash(),
|
||||
model.getReserveTx().getFullHex(),
|
||||
model.getReserveTx().getKey(),
|
||||
offer.getOfferPayload().getReserveTxKeyImages(),
|
||||
returnAddress);
|
||||
|
||||
// get signing arbitrator
|
||||
|
|
|
@ -17,14 +17,6 @@
|
|||
|
||||
package bisq.core.trade;
|
||||
|
||||
import bisq.core.btc.model.XmrAddressEntry;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
import bisq.core.offer.OfferPayload.Direction;
|
||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import common.utils.JsonUtils;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
@ -32,9 +24,20 @@ import bisq.common.crypto.PubKeyRing;
|
|||
import bisq.common.crypto.Sig;
|
||||
import bisq.common.util.Tuple2;
|
||||
import bisq.common.util.Utilities;
|
||||
import bisq.core.btc.model.XmrAddressEntry;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
import bisq.core.offer.OfferPayload.Direction;
|
||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import common.utils.JsonUtils;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.daemon.model.MoneroSubmitTxResult;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
@ -182,7 +185,7 @@ public class TradeUtils {
|
|||
/**
|
||||
* Process a reserve or deposit transaction used during trading.
|
||||
* Checks double spends, deposit amount and destination, trade fee, and mining fee.
|
||||
* The transaction is submitted but not relayed to the pool.
|
||||
* The transaction is submitted but not relayed to the pool then flushed.
|
||||
*
|
||||
* @param daemon is the Monero daemon to check for double spends
|
||||
* @param wallet is the Monero wallet to verify the tx
|
||||
|
@ -192,41 +195,58 @@ public class TradeUtils {
|
|||
* @param txHash is the transaction hash
|
||||
* @param txHex is the transaction hex
|
||||
* @param txKey is the transaction key
|
||||
* @param isReserveTx indicates if the tx is a reserve tx, which requires fee padding
|
||||
* @param keyImages are expected key images of inputs, ignored if null
|
||||
* @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase
|
||||
*/
|
||||
public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, boolean isReserveTx) {
|
||||
|
||||
// get tx from daemon
|
||||
MoneroTx tx = daemon.getTx(txHash);
|
||||
|
||||
// if tx is not submitted, submit but do not relay
|
||||
if (tx == null) {
|
||||
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
|
||||
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
|
||||
} else if (tx.isRelayed()) {
|
||||
throw new RuntimeException("Reserve tx must not be relayed");
|
||||
}
|
||||
|
||||
// verify trade fee
|
||||
String feeAddress = TradeUtils.FEE_ADDRESS;
|
||||
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
|
||||
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());
|
||||
public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean miningFeePadding) {
|
||||
boolean submittedToPool = false;
|
||||
try {
|
||||
|
||||
// get tx from daemon
|
||||
MoneroTx tx = daemon.getTx(txHash);
|
||||
|
||||
// if tx is not submitted, submit but do not relay
|
||||
if (tx == null) {
|
||||
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
|
||||
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
|
||||
submittedToPool = true;
|
||||
tx = daemon.getTx(txHash);
|
||||
} else if (tx.isRelayed()) {
|
||||
throw new RuntimeException("Trade tx must not be relayed");
|
||||
}
|
||||
|
||||
// verify reserved key images
|
||||
if (keyImages != null) {
|
||||
Set<String> txKeyImages = new HashSet<String>();
|
||||
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
|
||||
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images");
|
||||
}
|
||||
|
||||
// verify trade fee
|
||||
String feeAddress = TradeUtils.FEE_ADDRESS;
|
||||
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
|
||||
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());
|
||||
|
||||
// verify mining fee
|
||||
BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate
|
||||
BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee
|
||||
tx = daemon.getTx(txHash);
|
||||
if (tx.getFee().compareTo(feeThreshold) < 0) {
|
||||
throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee());
|
||||
}
|
||||
// verify mining fee
|
||||
BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate
|
||||
BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee
|
||||
tx = daemon.getTx(txHash);
|
||||
if (tx.getFee().compareTo(feeThreshold) < 0) {
|
||||
throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee());
|
||||
}
|
||||
|
||||
// verify deposit amount
|
||||
check = wallet.checkTxKey(txHash, txKey, depositAddress);
|
||||
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
|
||||
BigInteger depositThreshold = depositAmount;
|
||||
if (isReserveTx) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee)
|
||||
if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount());
|
||||
// verify deposit amount
|
||||
check = wallet.checkTxKey(txHash, txKey, depositAddress);
|
||||
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
|
||||
BigInteger depositThreshold = depositAmount;
|
||||
if (miningFeePadding) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee)
|
||||
if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount());
|
||||
} finally {
|
||||
|
||||
// flush tx from pool if we added it
|
||||
if (submittedToPool) daemon.flushTxPool(txHash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -64,6 +64,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask {
|
|||
request.getReserveTxHash(),
|
||||
request.getReserveTxHex(),
|
||||
request.getReserveTxKey(),
|
||||
null,
|
||||
true);
|
||||
|
||||
// save reserve tx to model
|
||||
|
|
|
@ -90,7 +90,7 @@ public class ProcessDepositRequest extends TradeTask {
|
|||
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
|
||||
daemon.flushTxPool(trader.getReserveTxHash());
|
||||
|
||||
// process and verify deposit tx which submits to the pool
|
||||
// process and verify deposit tx
|
||||
TradeUtils.processTradeTx(
|
||||
daemon,
|
||||
trade.getXmrWalletService().getWallet(),
|
||||
|
@ -100,6 +100,7 @@ public class ProcessDepositRequest extends TradeTask {
|
|||
trader.getDepositTxHash(),
|
||||
request.getDepositTxHex(),
|
||||
request.getDepositTxKey(),
|
||||
null,
|
||||
false);
|
||||
|
||||
// sychronize to send only one response
|
||||
|
@ -114,8 +115,8 @@ public class ProcessDepositRequest extends TradeTask {
|
|||
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
||||
|
||||
// relay txs
|
||||
daemon.relayTxByHash(processModel.getMaker().getDepositTxHash());
|
||||
daemon.relayTxByHash(processModel.getTaker().getDepositTxHash());
|
||||
daemon.submitTxHex(processModel.getMaker().getDepositTxHex());
|
||||
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
|
||||
|
||||
// create deposit response
|
||||
DepositResponse response = new DepositResponse(
|
||||
|
|
|
@ -73,6 +73,7 @@ public class OfferMaker {
|
|||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
|
||||
public static final Maker<Offer> btcUsdOffer = a(Offer);
|
||||
|
|
|
@ -175,9 +175,9 @@ class GrpcTradesService extends TradesImplBase {
|
|||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||
new HashMap<>() {{
|
||||
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||
put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
}}
|
||||
|
|
|
@ -224,7 +224,8 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
offerPayload.getExtraDataMap(),
|
||||
offerPayload.getProtocolVersion(),
|
||||
offerPayload.getArbitratorNodeAddress(),
|
||||
offerPayload.getArbitratorSignature());
|
||||
offerPayload.getArbitratorSignature(),
|
||||
offerPayload.getReserveTxKeyImages());
|
||||
|
||||
final Offer editedOffer = new Offer(editedPayload);
|
||||
editedOffer.setPriceFeedService(priceFeedService);
|
||||
|
|
|
@ -97,6 +97,7 @@ public class TradesChartsViewModelTest {
|
|||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
|
|
|
@ -621,6 +621,7 @@ public class OfferBookViewModelTest {
|
|||
null,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ public class OfferMaker {
|
|||
null,
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
|
||||
public static final Maker<Offer> btcUsdOffer = a(Offer);
|
||||
|
|
|
@ -177,7 +177,8 @@ message SignOfferRequest {
|
|||
string reserve_tx_hash = 8;
|
||||
string reserve_tx_hex = 9;
|
||||
string reserve_tx_key = 10;
|
||||
string payout_address = 11;
|
||||
repeated string reserve_tx_key_images = 11;
|
||||
string payout_address = 12;
|
||||
}
|
||||
|
||||
message SignOfferResponse {
|
||||
|
@ -940,6 +941,7 @@ message OfferPayload {
|
|||
|
||||
NodeAddress arbitrator_node_address = 1001;
|
||||
string arbitrator_signature = 1002;
|
||||
repeated string reserve_tx_key_images = 1003;
|
||||
}
|
||||
|
||||
message AccountAgeWitness {
|
||||
|
|
Loading…
Reference in a new issue