mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04: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
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package bisq.core.api;
|
package bisq.core.api;
|
||||||
|
|
||||||
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.monetary.Altcoin;
|
import bisq.core.monetary.Altcoin;
|
||||||
import bisq.core.monetary.Price;
|
import bisq.core.monetary.Price;
|
||||||
import bisq.core.offer.CreateOfferService;
|
import bisq.core.offer.CreateOfferService;
|
||||||
@ -39,14 +40,17 @@ import javax.inject.Inject;
|
|||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||||
|
|
||||||
import static bisq.common.util.MathUtils.exactMultiply;
|
import static bisq.common.util.MathUtils.exactMultiply;
|
||||||
import static bisq.common.util.MathUtils.roundDoubleToLong;
|
import static bisq.common.util.MathUtils.roundDoubleToLong;
|
||||||
@ -77,6 +81,7 @@ class CoreOffersService {
|
|||||||
private final OpenOfferManager openOfferManager;
|
private final OpenOfferManager openOfferManager;
|
||||||
private final OfferUtil offerUtil;
|
private final OfferUtil offerUtil;
|
||||||
private final User user;
|
private final User user;
|
||||||
|
private final XmrWalletService xmrWalletService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CoreOffersService(CoreContext coreContext,
|
public CoreOffersService(CoreContext coreContext,
|
||||||
@ -87,7 +92,8 @@ class CoreOffersService {
|
|||||||
OfferFilter offerFilter,
|
OfferFilter offerFilter,
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
OfferUtil offerUtil,
|
OfferUtil offerUtil,
|
||||||
User user) {
|
User user,
|
||||||
|
XmrWalletService xmrWalletService) {
|
||||||
this.coreContext = coreContext;
|
this.coreContext = coreContext;
|
||||||
this.keyRing = keyRing;
|
this.keyRing = keyRing;
|
||||||
this.coreWalletsService = coreWalletsService;
|
this.coreWalletsService = coreWalletsService;
|
||||||
@ -97,6 +103,7 @@ class CoreOffersService {
|
|||||||
this.openOfferManager = openOfferManager;
|
this.openOfferManager = openOfferManager;
|
||||||
this.offerUtil = offerUtil;
|
this.offerUtil = offerUtil;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
this.xmrWalletService = xmrWalletService;
|
||||||
}
|
}
|
||||||
|
|
||||||
Offer getOffer(String id) {
|
Offer getOffer(String id) {
|
||||||
@ -117,20 +124,62 @@ class CoreOffersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Offer> getOffers(String direction, String currencyCode) {
|
List<Offer> getOffers(String direction, String currencyCode) {
|
||||||
return offerBookService.getOffers().stream()
|
List<Offer> offers = offerBookService.getOffers().stream()
|
||||||
.filter(o -> !o.isMyOffer(keyRing))
|
.filter(o -> !o.isMyOffer(keyRing))
|
||||||
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
|
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
|
||||||
.filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid())
|
.filter(o -> offerFilter.canTakeOffer(o, coreContext.isApiUser()).isValid())
|
||||||
.sorted(priceComparator(direction))
|
.sorted(priceComparator(direction))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
offers.removeAll(getUnreservedOffers(offers));
|
||||||
|
return offers;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Offer> getMyOffers(String direction, String currencyCode) {
|
List<Offer> getMyOffers(String direction, String currencyCode) {
|
||||||
return offerBookService.getOffers().stream()
|
List<Offer> offers = offerBookService.getOffers().stream()
|
||||||
.filter(o -> o.isMyOffer(keyRing))
|
.filter(o -> o.isMyOffer(keyRing))
|
||||||
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
|
.filter(o -> offerMatchesDirectionAndCurrency(o, direction, currencyCode))
|
||||||
.sorted(priceComparator(direction))
|
.sorted(priceComparator(direction))
|
||||||
.collect(Collectors.toList());
|
.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) {
|
OpenOffer getMyOpenOffer(String id) {
|
||||||
|
@ -140,7 +140,7 @@ public class WalletConfig extends AbstractIdleService {
|
|||||||
protected volatile BlockChain vChain;
|
protected volatile BlockChain vChain;
|
||||||
protected volatile SPVBlockStore vStore;
|
protected volatile SPVBlockStore vStore;
|
||||||
protected volatile MoneroDaemon vXmrDaemon;
|
protected volatile MoneroDaemon vXmrDaemon;
|
||||||
protected volatile MoneroWallet vXmrWallet;
|
protected volatile MoneroWalletRpc vXmrWallet;
|
||||||
protected volatile Wallet vBtcWallet;
|
protected volatile Wallet vBtcWallet;
|
||||||
protected volatile Wallet vBsqWallet;
|
protected volatile Wallet vBsqWallet;
|
||||||
protected volatile PeerGroup vPeerGroup;
|
protected volatile PeerGroup vPeerGroup;
|
||||||
@ -287,7 +287,7 @@ public class WalletConfig extends AbstractIdleService {
|
|||||||
// Meant to be overridden by subclasses
|
// Meant to be overridden by subclasses
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoneroWallet createWallet(MoneroWalletConfig config) {
|
public MoneroWalletRpc createWallet(MoneroWalletConfig config) {
|
||||||
|
|
||||||
// start monero-wallet-rpc instance
|
// start monero-wallet-rpc instance
|
||||||
MoneroWalletRpc walletRpc = startWalletRpcInstance();
|
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
|
// start monero-wallet-rpc instance
|
||||||
MoneroWalletRpc walletRpc = startWalletRpcInstance();
|
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 path: " + vXmrWallet.getPath());
|
||||||
System.out.println("Monero wallet address: " + vXmrWallet.getPrimaryAddress());
|
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.rescanSpent();
|
||||||
// vXmrWallet.rescanBlockchain();
|
// vXmrWallet.rescanBlockchain();
|
||||||
vXmrWallet.sync();
|
vXmrWallet.sync();
|
||||||
|
@ -232,6 +232,7 @@ public class CreateOfferService {
|
|||||||
extraDataMap,
|
extraDataMap,
|
||||||
Version.TRADE_PROTOCOL_VERSION,
|
Version.TRADE_PROTOCOL_VERSION,
|
||||||
arbitrator.getNodeAddress(),
|
arbitrator.getNodeAddress(),
|
||||||
|
null,
|
||||||
null);
|
null);
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
|
@ -171,9 +171,12 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
// address and signature of signing arbitrator
|
// address and signature of signing arbitrator
|
||||||
@Setter
|
@Setter
|
||||||
private NodeAddress arbitratorNodeAddress;
|
private NodeAddress arbitratorNodeAddress;
|
||||||
@Nullable
|
|
||||||
@Setter
|
@Setter
|
||||||
|
@Nullable
|
||||||
private String arbitratorSignature;
|
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,
|
@Nullable Map<String, String> extraDataMap,
|
||||||
int protocolVersion,
|
int protocolVersion,
|
||||||
NodeAddress arbitratorSigner,
|
NodeAddress arbitratorSigner,
|
||||||
@Nullable String arbitratorSignature) {
|
@Nullable String arbitratorSignature,
|
||||||
|
@Nullable List<String> reserveTxKeyImages) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.date = date;
|
this.date = date;
|
||||||
this.ownerNodeAddress = ownerNodeAddress;
|
this.ownerNodeAddress = ownerNodeAddress;
|
||||||
@ -256,6 +260,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
this.protocolVersion = protocolVersion;
|
this.protocolVersion = protocolVersion;
|
||||||
this.arbitratorNodeAddress = arbitratorSigner;
|
this.arbitratorNodeAddress = arbitratorSigner;
|
||||||
this.arbitratorSignature = arbitratorSignature;
|
this.arbitratorSignature = arbitratorSignature;
|
||||||
|
this.reserveTxKeyImages = reserveTxKeyImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -293,7 +298,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
.setLowerClosePrice(lowerClosePrice)
|
.setLowerClosePrice(lowerClosePrice)
|
||||||
.setUpperClosePrice(upperClosePrice)
|
.setUpperClosePrice(upperClosePrice)
|
||||||
.setIsPrivateOffer(isPrivateOffer)
|
.setIsPrivateOffer(isPrivateOffer)
|
||||||
.setProtocolVersion(protocolVersion);
|
.setProtocolVersion(protocolVersion)
|
||||||
|
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
|
||||||
|
|
||||||
builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId,
|
builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId,
|
||||||
"OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network."));
|
"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(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes);
|
||||||
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
|
Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge);
|
||||||
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
||||||
|
|
||||||
builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage());
|
|
||||||
Optional.ofNullable(arbitratorSignature).ifPresent(builder::setArbitratorSignature);
|
Optional.ofNullable(arbitratorSignature).ifPresent(builder::setArbitratorSignature);
|
||||||
|
Optional.ofNullable(reserveTxKeyImages).ifPresent(builder::addAllReserveTxKeyImages);
|
||||||
|
|
||||||
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
|
return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build();
|
||||||
}
|
}
|
||||||
@ -358,7 +363,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
extraDataMapMap,
|
extraDataMapMap,
|
||||||
proto.getProtocolVersion(),
|
proto.getProtocolVersion(),
|
||||||
NodeAddress.fromProto(proto.getArbitratorNodeAddress()),
|
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);
|
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();
|
int size = openOffers.size();
|
||||||
// Copy list as we remove in the loop
|
// Copy list as we remove in the loop
|
||||||
List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
|
List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
|
||||||
@ -670,8 +670,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify reserve tx not signed before
|
|
||||||
|
|
||||||
// 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 = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
|
||||||
@ -685,6 +683,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
request.getReserveTxHash(),
|
request.getReserveTxHash(),
|
||||||
request.getReserveTxHex(),
|
request.getReserveTxHex(),
|
||||||
request.getReserveTxKey(),
|
request.getReserveTxKey(),
|
||||||
|
request.getReserveTxKeyImages(),
|
||||||
true);
|
true);
|
||||||
|
|
||||||
// arbitrator signs offer to certify they have valid reserve tx
|
// arbitrator signs offer to certify they have valid reserve tx
|
||||||
@ -1034,7 +1033,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
updatedExtraDataMap,
|
updatedExtraDataMap,
|
||||||
protocolVersion,
|
protocolVersion,
|
||||||
originalOfferPayload.getArbitratorNodeAddress(),
|
originalOfferPayload.getArbitratorNodeAddress(),
|
||||||
originalOfferPayload.getArbitratorSignature());
|
originalOfferPayload.getArbitratorSignature(),
|
||||||
|
originalOfferPayload.getReserveTxKeyImages());
|
||||||
|
|
||||||
// Save states from original data to use for the updated
|
// Save states from original data to use for the updated
|
||||||
Offer.State originalOfferState = originalOffer.getState();
|
Offer.State originalOfferState = originalOffer.getState();
|
||||||
|
@ -21,6 +21,8 @@ import bisq.common.crypto.PubKeyRing;
|
|||||||
import bisq.core.offer.OfferPayload;
|
import bisq.core.offer.OfferPayload;
|
||||||
import bisq.network.p2p.DirectMessage;
|
import bisq.network.p2p.DirectMessage;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
|
||||||
@ -35,6 +37,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||||||
private final String reserveTxHash;
|
private final String reserveTxHash;
|
||||||
private final String reserveTxHex;
|
private final String reserveTxHex;
|
||||||
private final String reserveTxKey;
|
private final String reserveTxKey;
|
||||||
|
private final List<String> reserveTxKeyImages;
|
||||||
private final String payoutAddress;
|
private final String payoutAddress;
|
||||||
|
|
||||||
public SignOfferRequest(String offerId,
|
public SignOfferRequest(String offerId,
|
||||||
@ -48,6 +51,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||||||
String reserveTxHash,
|
String reserveTxHash,
|
||||||
String reserveTxHex,
|
String reserveTxHex,
|
||||||
String reserveTxKey,
|
String reserveTxKey,
|
||||||
|
List<String> reserveTxKeyImages,
|
||||||
String payoutAddress) {
|
String payoutAddress) {
|
||||||
super(messageVersion, offerId, uid);
|
super(messageVersion, offerId, uid);
|
||||||
this.senderNodeAddress = senderNodeAddress;
|
this.senderNodeAddress = senderNodeAddress;
|
||||||
@ -58,6 +62,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||||||
this.reserveTxHash = reserveTxHash;
|
this.reserveTxHash = reserveTxHash;
|
||||||
this.reserveTxHex = reserveTxHex;
|
this.reserveTxHex = reserveTxHex;
|
||||||
this.reserveTxKey = reserveTxKey;
|
this.reserveTxKey = reserveTxKey;
|
||||||
|
this.reserveTxKeyImages = reserveTxKeyImages;
|
||||||
this.payoutAddress = payoutAddress;
|
this.payoutAddress = payoutAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +84,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||||||
.setReserveTxHash(reserveTxHash)
|
.setReserveTxHash(reserveTxHash)
|
||||||
.setReserveTxHex(reserveTxHex)
|
.setReserveTxHex(reserveTxHex)
|
||||||
.setReserveTxKey(reserveTxKey)
|
.setReserveTxKey(reserveTxKey)
|
||||||
|
.addAllReserveTxKeyImages(reserveTxKeyImages)
|
||||||
.setPayoutAddress(payoutAddress);
|
.setPayoutAddress(payoutAddress);
|
||||||
|
|
||||||
return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build();
|
return getNetworkEnvelopeBuilder().setSignOfferRequest(builder).build();
|
||||||
@ -97,6 +103,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||||||
proto.getReserveTxHash(),
|
proto.getReserveTxHash(),
|
||||||
proto.getReserveTxHex(),
|
proto.getReserveTxHex(),
|
||||||
proto.getReserveTxKey(),
|
proto.getReserveTxKey(),
|
||||||
|
new ArrayList<String>(proto.getReserveTxKeyImagesList()),
|
||||||
proto.getPayoutAddress());
|
proto.getPayoutAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +116,7 @@ public final class SignOfferRequest extends OfferMessage implements DirectMessag
|
|||||||
",\n reserveTxHash='" + reserveTxHash +
|
",\n reserveTxHash='" + reserveTxHash +
|
||||||
",\n reserveTxHex='" + reserveTxHex +
|
",\n reserveTxHex='" + reserveTxHex +
|
||||||
",\n reserveTxKey='" + reserveTxKey +
|
",\n reserveTxKey='" + reserveTxKey +
|
||||||
|
",\n reserveTxKeyImages='" + reserveTxKeyImages +
|
||||||
",\n payoutAddress='" + payoutAddress +
|
",\n payoutAddress='" + payoutAddress +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
|
@ -53,16 +53,17 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
|
|||||||
|
|
||||||
// freeze reserved outputs
|
// freeze reserved outputs
|
||||||
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's 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();
|
MoneroWallet wallet = model.getXmrWalletService().getWallet();
|
||||||
for (MoneroOutput input : reserveTx.getInputs()) {
|
for (MoneroOutput input : reserveTx.getInputs()) {
|
||||||
frozenKeyImages.add(input.getKeyImage().getHex());
|
reservedKeyImages.add(input.getKeyImage().getHex());
|
||||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
wallet.freezeOutput(input.getKeyImage().getHex());
|
||||||
}
|
}
|
||||||
|
|
||||||
// save offer state
|
// save offer state
|
||||||
// TODO (woodser): persist
|
// TODO (woodser): persist
|
||||||
model.setReserveTx(reserveTx);
|
model.setReserveTx(reserveTx);
|
||||||
|
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
||||||
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id
|
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): rename this to reserve tx id
|
||||||
offer.setState(Offer.State.OFFER_FEE_RESERVED);
|
offer.setState(Offer.State.OFFER_FEE_RESERVED);
|
||||||
complete();
|
complete();
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
package bisq.core.offer.placeoffer.tasks;
|
package bisq.core.offer.placeoffer.tasks;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
import bisq.common.app.Version;
|
import bisq.common.app.Version;
|
||||||
import bisq.common.taskrunner.Task;
|
import bisq.common.taskrunner.Task;
|
||||||
import bisq.common.taskrunner.TaskRunner;
|
import bisq.common.taskrunner.TaskRunner;
|
||||||
@ -32,8 +34,6 @@ import java.util.UUID;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
|
|
||||||
public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
|
public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
private static final Logger log = LoggerFactory.getLogger(MakerSendsSignOfferRequest.class);
|
private static final Logger log = LoggerFactory.getLogger(MakerSendsSignOfferRequest.class);
|
||||||
|
|
||||||
@ -64,6 +64,7 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
|
|||||||
model.getReserveTx().getHash(),
|
model.getReserveTx().getHash(),
|
||||||
model.getReserveTx().getFullHex(),
|
model.getReserveTx().getFullHex(),
|
||||||
model.getReserveTx().getKey(),
|
model.getReserveTx().getKey(),
|
||||||
|
offer.getOfferPayload().getReserveTxKeyImages(),
|
||||||
returnAddress);
|
returnAddress);
|
||||||
|
|
||||||
// get signing arbitrator
|
// get signing arbitrator
|
||||||
|
@ -17,14 +17,6 @@
|
|||||||
|
|
||||||
package bisq.core.trade;
|
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 static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
import bisq.common.crypto.KeyRing;
|
import bisq.common.crypto.KeyRing;
|
||||||
@ -32,9 +24,20 @@ import bisq.common.crypto.PubKeyRing;
|
|||||||
import bisq.common.crypto.Sig;
|
import bisq.common.crypto.Sig;
|
||||||
import bisq.common.util.Tuple2;
|
import bisq.common.util.Tuple2;
|
||||||
import bisq.common.util.Utilities;
|
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.math.BigInteger;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
import monero.daemon.MoneroDaemon;
|
import monero.daemon.MoneroDaemon;
|
||||||
|
import monero.daemon.model.MoneroOutput;
|
||||||
import monero.daemon.model.MoneroSubmitTxResult;
|
import monero.daemon.model.MoneroSubmitTxResult;
|
||||||
import monero.daemon.model.MoneroTx;
|
import monero.daemon.model.MoneroTx;
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
@ -182,7 +185,7 @@ public class TradeUtils {
|
|||||||
/**
|
/**
|
||||||
* Process a reserve or deposit transaction used during trading.
|
* Process a reserve or deposit transaction used during trading.
|
||||||
* Checks double spends, deposit amount and destination, trade fee, and mining fee.
|
* 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 daemon is the Monero daemon to check for double spends
|
||||||
* @param wallet is the Monero wallet to verify the tx
|
* @param wallet is the Monero wallet to verify the tx
|
||||||
@ -192,41 +195,58 @@ public class TradeUtils {
|
|||||||
* @param txHash is the transaction hash
|
* @param txHash is the transaction hash
|
||||||
* @param txHex is the transaction hex
|
* @param txHex is the transaction hex
|
||||||
* @param txKey is the transaction key
|
* @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) {
|
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
|
// get tx from daemon
|
||||||
MoneroTx tx = daemon.getTx(txHash);
|
MoneroTx tx = daemon.getTx(txHash);
|
||||||
|
|
||||||
// if tx is not submitted, submit but do not relay
|
// if tx is not submitted, submit but do not relay
|
||||||
if (tx == null) {
|
if (tx == null) {
|
||||||
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
|
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));
|
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
|
||||||
} else if (tx.isRelayed()) {
|
submittedToPool = true;
|
||||||
throw new RuntimeException("Reserve tx must not be relayed");
|
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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,6 +64,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask {
|
|||||||
request.getReserveTxHash(),
|
request.getReserveTxHash(),
|
||||||
request.getReserveTxHex(),
|
request.getReserveTxHex(),
|
||||||
request.getReserveTxKey(),
|
request.getReserveTxKey(),
|
||||||
|
null,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
// save reserve tx to model
|
// save reserve tx to model
|
||||||
|
@ -90,7 +90,7 @@ public class ProcessDepositRequest extends TradeTask {
|
|||||||
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
|
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
|
||||||
daemon.flushTxPool(trader.getReserveTxHash());
|
daemon.flushTxPool(trader.getReserveTxHash());
|
||||||
|
|
||||||
// process and verify deposit tx which submits to the pool
|
// process and verify deposit tx
|
||||||
TradeUtils.processTradeTx(
|
TradeUtils.processTradeTx(
|
||||||
daemon,
|
daemon,
|
||||||
trade.getXmrWalletService().getWallet(),
|
trade.getXmrWalletService().getWallet(),
|
||||||
@ -100,6 +100,7 @@ public class ProcessDepositRequest extends TradeTask {
|
|||||||
trader.getDepositTxHash(),
|
trader.getDepositTxHash(),
|
||||||
request.getDepositTxHex(),
|
request.getDepositTxHex(),
|
||||||
request.getDepositTxKey(),
|
request.getDepositTxKey(),
|
||||||
|
null,
|
||||||
false);
|
false);
|
||||||
|
|
||||||
// sychronize to send only one response
|
// sychronize to send only one response
|
||||||
@ -114,8 +115,8 @@ public class ProcessDepositRequest extends TradeTask {
|
|||||||
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
||||||
|
|
||||||
// relay txs
|
// relay txs
|
||||||
daemon.relayTxByHash(processModel.getMaker().getDepositTxHash());
|
daemon.submitTxHex(processModel.getMaker().getDepositTxHex());
|
||||||
daemon.relayTxByHash(processModel.getTaker().getDepositTxHash());
|
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
|
||||||
|
|
||||||
// create deposit response
|
// create deposit response
|
||||||
DepositResponse response = new DepositResponse(
|
DepositResponse response = new DepositResponse(
|
||||||
|
@ -73,6 +73,7 @@ public class OfferMaker {
|
|||||||
null,
|
null,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null));
|
null));
|
||||||
|
|
||||||
public static final Maker<Offer> btcUsdOffer = a(Offer);
|
public static final Maker<Offer> btcUsdOffer = a(Offer);
|
||||||
|
@ -175,9 +175,9 @@ class GrpcTradesService extends TradesImplBase {
|
|||||||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||||
new HashMap<>() {{
|
new HashMap<>() {{
|
||||||
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||||
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||||
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||||
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||||
put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||||
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||||
}}
|
}}
|
||||||
|
@ -224,7 +224,8 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||||||
offerPayload.getExtraDataMap(),
|
offerPayload.getExtraDataMap(),
|
||||||
offerPayload.getProtocolVersion(),
|
offerPayload.getProtocolVersion(),
|
||||||
offerPayload.getArbitratorNodeAddress(),
|
offerPayload.getArbitratorNodeAddress(),
|
||||||
offerPayload.getArbitratorSignature());
|
offerPayload.getArbitratorSignature(),
|
||||||
|
offerPayload.getReserveTxKeyImages());
|
||||||
|
|
||||||
final Offer editedOffer = new Offer(editedPayload);
|
final Offer editedOffer = new Offer(editedPayload);
|
||||||
editedOffer.setPriceFeedService(priceFeedService);
|
editedOffer.setPriceFeedService(priceFeedService);
|
||||||
|
@ -97,6 +97,7 @@ public class TradesChartsViewModelTest {
|
|||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -621,6 +621,7 @@ public class OfferBookViewModelTest {
|
|||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null));
|
null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,7 @@ public class OfferMaker {
|
|||||||
null,
|
null,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
null));
|
null));
|
||||||
|
|
||||||
public static final Maker<Offer> btcUsdOffer = a(Offer);
|
public static final Maker<Offer> btcUsdOffer = a(Offer);
|
||||||
|
@ -177,7 +177,8 @@ message SignOfferRequest {
|
|||||||
string reserve_tx_hash = 8;
|
string reserve_tx_hash = 8;
|
||||||
string reserve_tx_hex = 9;
|
string reserve_tx_hex = 9;
|
||||||
string reserve_tx_key = 10;
|
string reserve_tx_key = 10;
|
||||||
string payout_address = 11;
|
repeated string reserve_tx_key_images = 11;
|
||||||
|
string payout_address = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SignOfferResponse {
|
message SignOfferResponse {
|
||||||
@ -940,6 +941,7 @@ message OfferPayload {
|
|||||||
|
|
||||||
NodeAddress arbitrator_node_address = 1001;
|
NodeAddress arbitrator_node_address = 1001;
|
||||||
string arbitrator_signature = 1002;
|
string arbitrator_signature = 1002;
|
||||||
|
repeated string reserve_tx_key_images = 1003;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AccountAgeWitness {
|
message AccountAgeWitness {
|
||||||
|
Loading…
Reference in New Issue
Block a user