mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-05-14 20:42:21 -04:00
add offer validation
This commit is contained in:
parent
7069fb6c4d
commit
2cd3a5bae4
7 changed files with 79 additions and 117 deletions
10
src/main/java/io/bitsquare/btc/Restritions.java
Normal file
10
src/main/java/io/bitsquare/btc/Restritions.java
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package io.bitsquare.btc;
|
||||||
|
|
||||||
|
import com.google.bitcoin.core.Coin;
|
||||||
|
|
||||||
|
public class Restritions
|
||||||
|
{
|
||||||
|
public static final Coin MIN_TRADE_AMOUNT = Coin.CENT; // 0.01 Bitcoins
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,86 +0,0 @@
|
||||||
package io.bitsquare.btc.tasks;
|
|
||||||
|
|
||||||
import com.google.bitcoin.core.*;
|
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
|
||||||
import io.bitsquare.btc.AddressBasedCoinSelector;
|
|
||||||
import io.bitsquare.btc.AddressEntry;
|
|
||||||
import io.bitsquare.btc.FeePolicy;
|
|
||||||
import io.bitsquare.btc.WalletFacade;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class PayFeeTask
|
|
||||||
{
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(PayFeeTask.class);
|
|
||||||
|
|
||||||
private enum State
|
|
||||||
{
|
|
||||||
INIT,
|
|
||||||
TX_COMPLETED,
|
|
||||||
TX_COMMITTED,
|
|
||||||
TX_BROAD_CASTED,
|
|
||||||
}
|
|
||||||
|
|
||||||
private State state;
|
|
||||||
|
|
||||||
public String start(Wallet wallet, FeePolicy feePolicy, AddressEntry addressEntry, FutureCallback<Transaction> callback)
|
|
||||||
{
|
|
||||||
state = State.INIT;
|
|
||||||
|
|
||||||
Transaction tx = new Transaction(wallet.getParams());
|
|
||||||
Coin fee = FeePolicy.CREATE_OFFER_FEE.subtract(FeePolicy.TX_FEE);
|
|
||||||
log.trace("fee: " + fee.toFriendlyString());
|
|
||||||
tx.addOutput(fee, feePolicy.getAddressForCreateOfferFee());
|
|
||||||
|
|
||||||
Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx);
|
|
||||||
sendRequest.shuffleOutputs = false;
|
|
||||||
// we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation)
|
|
||||||
sendRequest.coinSelector = new AddressBasedCoinSelector(wallet.getParams(), addressEntry, true);
|
|
||||||
sendRequest.changeAddress = addressEntry.getAddress();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
|
|
||||||
state = State.TX_COMPLETED;
|
|
||||||
|
|
||||||
|
|
||||||
} catch (IllegalStateException e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (InsufficientMoneyException e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalArgumentException e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Wallet.DustySendRequested e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Wallet.CouldNotAdjustDownwards e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Wallet.ExceededMaxTransactionSize e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (VerificationException e)
|
|
||||||
{
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* @throws IllegalStateException if no transaction broadcaster has been configured.
|
|
||||||
* @throws InsufficientMoneyException if the request could not be completed due to not enough balance.
|
|
||||||
* @throws IllegalArgumentException if you try and complete the same SendRequest twice
|
|
||||||
* @throws DustySendRequested if the resultant transaction would violate the dust rules (an output that's too small to be worthwhile)
|
|
||||||
* @throws CouldNotAdjustDownwards if emptying the wallet was requested and the output can't be shrunk for fees without violating a protocol rule.
|
|
||||||
* @throws ExceededMaxTransactionSize if the resultant transaction is too big for Bitcoin to process (try breaking up the amounts of value)
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
WalletFacade.printInputs("payCreateOfferFee", tx);
|
|
||||||
log.debug("tx=" + tx);
|
|
||||||
|
|
||||||
return tx.getHashAsString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -46,7 +46,7 @@ public class MessageFacade implements MessageBroker
|
||||||
|
|
||||||
public static interface AddOfferListener
|
public static interface AddOfferListener
|
||||||
{
|
{
|
||||||
void onComplete(String offerId);
|
void onComplete();
|
||||||
|
|
||||||
void onFailed(String reason, Throwable throwable);
|
void onFailed(String reason, Throwable throwable);
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,11 @@ public class MessageFacade implements MessageBroker
|
||||||
private static final Logger log = LoggerFactory.getLogger(MessageFacade.class);
|
private static final Logger log = LoggerFactory.getLogger(MessageFacade.class);
|
||||||
private static final String ARBITRATORS_ROOT = "ArbitratorsRoot";
|
private static final String ARBITRATORS_ROOT = "ArbitratorsRoot";
|
||||||
|
|
||||||
|
public P2PNode getP2pNode()
|
||||||
|
{
|
||||||
|
return p2pNode;
|
||||||
|
}
|
||||||
|
|
||||||
private P2PNode p2pNode;
|
private P2PNode p2pNode;
|
||||||
|
|
||||||
private final List<OrderBookListener> orderBookListeners = new ArrayList<>();
|
private final List<OrderBookListener> orderBookListeners = new ArrayList<>();
|
||||||
|
@ -165,16 +170,16 @@ public class MessageFacade implements MessageBroker
|
||||||
@Override
|
@Override
|
||||||
public void operationComplete(BaseFuture future) throws Exception
|
public void operationComplete(BaseFuture future) throws Exception
|
||||||
{
|
{
|
||||||
Platform.runLater(() -> {
|
|
||||||
addOfferListener.onComplete(offer.getId());
|
|
||||||
orderBookListeners.stream().forEach(listener -> listener.onOfferAdded(data, future.isSuccess()));
|
|
||||||
|
|
||||||
// TODO will be removed when we don't use polling anymore
|
|
||||||
setDirty(locationKey);
|
|
||||||
});
|
|
||||||
if (future.isSuccess())
|
if (future.isSuccess())
|
||||||
{
|
{
|
||||||
Platform.runLater(() -> log.trace("Add offer to DHT was successful. Stored data: [key: " + locationKey + ", value: " + data + "]"));
|
Platform.runLater(() -> {
|
||||||
|
addOfferListener.onComplete();
|
||||||
|
orderBookListeners.stream().forEach(listener -> listener.onOfferAdded(data, future.isSuccess()));
|
||||||
|
|
||||||
|
// TODO will be removed when we don't use polling anymore
|
||||||
|
setDirty(locationKey);
|
||||||
|
log.trace("Add offer to DHT was successful. Stored data: [key: " + locationKey + ", value: " + data + "]");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -487,7 +492,7 @@ public class MessageFacade implements MessageBroker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDirty(Number160 locationKey)
|
public void setDirty(Number160 locationKey)
|
||||||
{
|
{
|
||||||
// we don't want to get an update from dirty for own changes, so update the lastTimeStamp to omit a change trigger
|
// we don't want to get an update from dirty for own changes, so update the lastTimeStamp to omit a change trigger
|
||||||
lastTimeStamp = System.currentTimeMillis();
|
lastTimeStamp = System.currentTimeMillis();
|
||||||
|
|
|
@ -2,16 +2,17 @@ package io.bitsquare.trade.protocol.createoffer;
|
||||||
|
|
||||||
import com.google.bitcoin.core.Transaction;
|
import com.google.bitcoin.core.Transaction;
|
||||||
import io.bitsquare.btc.WalletFacade;
|
import io.bitsquare.btc.WalletFacade;
|
||||||
|
import io.bitsquare.trade.protocol.createoffer.tasks.BroadCastOfferFeeTx;
|
||||||
|
import io.bitsquare.trade.protocol.createoffer.tasks.CreateOfferFeeTx;
|
||||||
import io.bitsquare.msg.MessageFacade;
|
import io.bitsquare.msg.MessageFacade;
|
||||||
|
import io.bitsquare.trade.protocol.createoffer.tasks.PublishOfferToDHT;
|
||||||
import io.bitsquare.storage.Persistence;
|
import io.bitsquare.storage.Persistence;
|
||||||
import io.bitsquare.trade.Offer;
|
import io.bitsquare.trade.Offer;
|
||||||
import io.bitsquare.trade.handlers.FaultHandler;
|
import io.bitsquare.trade.handlers.FaultHandler;
|
||||||
import io.bitsquare.trade.handlers.TransactionResultHandler;
|
import io.bitsquare.trade.handlers.TransactionResultHandler;
|
||||||
import io.bitsquare.trade.protocol.createoffer.tasks.BroadCastOfferFeeTx;
|
|
||||||
import io.bitsquare.trade.protocol.createoffer.tasks.CreateOfferFeeTx;
|
|
||||||
import io.bitsquare.trade.protocol.createoffer.tasks.PublishOfferToDHT;
|
|
||||||
import io.bitsquare.trade.protocol.createoffer.tasks.ValidateOffer;
|
import io.bitsquare.trade.protocol.createoffer.tasks.ValidateOffer;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -20,6 +21,8 @@ import org.slf4j.LoggerFactory;
|
||||||
* It holds the model.state of the current process and support recovery if possible.
|
* It holds the model.state of the current process and support recovery if possible.
|
||||||
*/
|
*/
|
||||||
//TODO recover policy, timer
|
//TODO recover policy, timer
|
||||||
|
|
||||||
|
@Immutable
|
||||||
public class CreateOfferCoordinator
|
public class CreateOfferCoordinator
|
||||||
{
|
{
|
||||||
public enum State
|
public enum State
|
||||||
|
@ -32,11 +35,14 @@ public class CreateOfferCoordinator
|
||||||
OFFER_PUBLISHED_TO_DHT
|
OFFER_PUBLISHED_TO_DHT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model is not immutable but only exposed to the CreateOfferCoordinator
|
||||||
|
*/
|
||||||
static class Model implements Serializable
|
static class Model implements Serializable
|
||||||
{
|
{
|
||||||
private static final long serialVersionUID = 3027720554200858916L;
|
private static final long serialVersionUID = 3027720554200858916L;
|
||||||
|
|
||||||
private Persistence persistence;
|
private final Persistence persistence;
|
||||||
private State state;
|
private State state;
|
||||||
//TODO use tx id and make Transaction transient
|
//TODO use tx id and make Transaction transient
|
||||||
Transaction transaction;
|
Transaction transaction;
|
||||||
|
@ -54,6 +60,8 @@ public class CreateOfferCoordinator
|
||||||
public void setState(State state)
|
public void setState(State state)
|
||||||
{
|
{
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
|
//TODO will have performance issues, but could be handled inside the persistence solution (queue up save requests and exec. them on dedicated thread)
|
||||||
persistence.write(this);
|
persistence.write(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +102,7 @@ public class CreateOfferCoordinator
|
||||||
private void onOfferValidated()
|
private void onOfferValidated()
|
||||||
{
|
{
|
||||||
model.setState(State.VALIDATED);
|
model.setState(State.VALIDATED);
|
||||||
CreateOfferFeeTx.run(this::onOfferFeeTxCreated, this::onFailed, walletFacade, offer);
|
CreateOfferFeeTx.run(this::onOfferFeeTxCreated, this::onFailed, walletFacade, offer.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onOfferFeeTxCreated(Transaction transaction)
|
private void onOfferFeeTxCreated(Transaction transaction)
|
||||||
|
@ -114,6 +122,9 @@ public class CreateOfferCoordinator
|
||||||
private void onOfferPublishedToDHT()
|
private void onOfferPublishedToDHT()
|
||||||
{
|
{
|
||||||
model.setState(State.OFFER_PUBLISHED_TO_DHT);
|
model.setState(State.OFFER_PUBLISHED_TO_DHT);
|
||||||
|
// TODO
|
||||||
|
//orderBookListeners.stream().forEach(listener -> listener.onOfferAdded(data, future.isSuccess()));
|
||||||
|
|
||||||
|
|
||||||
resultHandler.onResult(model.transaction);
|
resultHandler.onResult(model.transaction);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@ package io.bitsquare.trade.protocol.createoffer.tasks;
|
||||||
|
|
||||||
import com.google.bitcoin.core.InsufficientMoneyException;
|
import com.google.bitcoin.core.InsufficientMoneyException;
|
||||||
import io.bitsquare.btc.WalletFacade;
|
import io.bitsquare.btc.WalletFacade;
|
||||||
import io.bitsquare.trade.Offer;
|
|
||||||
import io.bitsquare.trade.handlers.TransactionResultHandler;
|
|
||||||
import io.bitsquare.trade.handlers.FaultHandler;
|
import io.bitsquare.trade.handlers.FaultHandler;
|
||||||
|
import io.bitsquare.trade.handlers.TransactionResultHandler;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -12,11 +11,11 @@ public class CreateOfferFeeTx
|
||||||
{
|
{
|
||||||
private static final Logger log = LoggerFactory.getLogger(CreateOfferFeeTx.class);
|
private static final Logger log = LoggerFactory.getLogger(CreateOfferFeeTx.class);
|
||||||
|
|
||||||
public static void run(TransactionResultHandler resultHandler, FaultHandler faultHandler, WalletFacade walletFacade, Offer offer)
|
public static void run(TransactionResultHandler resultHandler, FaultHandler faultHandler, WalletFacade walletFacade, String offerId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
resultHandler.onResult(walletFacade.createOfferFeeTx(offer.getId()));
|
resultHandler.onResult(walletFacade.createOfferFeeTx(offerId));
|
||||||
} catch (InsufficientMoneyException e)
|
} catch (InsufficientMoneyException e)
|
||||||
{
|
{
|
||||||
faultHandler.onFault("Offer fee payment failed because there is insufficient money in the trade pocket. ", e);
|
faultHandler.onFault("Offer fee payment failed because there is insufficient money in the trade pocket. ", e);
|
||||||
|
|
|
@ -16,7 +16,7 @@ public class PublishOfferToDHT
|
||||||
messageFacade.addOffer(offer, new MessageFacade.AddOfferListener()
|
messageFacade.addOffer(offer, new MessageFacade.AddOfferListener()
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public void onComplete(String offerId)
|
public void onComplete()
|
||||||
{
|
{
|
||||||
resultHandler.onResult();
|
resultHandler.onResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,54 @@
|
||||||
package io.bitsquare.trade.protocol.createoffer.tasks;
|
package io.bitsquare.trade.protocol.createoffer.tasks;
|
||||||
|
|
||||||
|
import io.bitsquare.btc.Restritions;
|
||||||
import io.bitsquare.trade.Offer;
|
import io.bitsquare.trade.Offer;
|
||||||
import io.bitsquare.trade.handlers.FaultHandler;
|
import io.bitsquare.trade.handlers.FaultHandler;
|
||||||
import io.bitsquare.trade.handlers.ResultHandler;
|
import io.bitsquare.trade.handlers.ResultHandler;
|
||||||
|
import javax.annotation.concurrent.Immutable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
@Immutable
|
||||||
public class ValidateOffer
|
public class ValidateOffer
|
||||||
{
|
{
|
||||||
private static final Logger log = LoggerFactory.getLogger(ValidateOffer.class);
|
private static final Logger log = LoggerFactory.getLogger(ValidateOffer.class);
|
||||||
|
|
||||||
public static void run(ResultHandler resultHandler, FaultHandler faultHandler, Offer offer)
|
public static void run(ResultHandler resultHandler, FaultHandler faultHandler, Offer offer)
|
||||||
{
|
{
|
||||||
boolean isValid = offer.getAmount().isGreaterThan(offer.getAmount());
|
try
|
||||||
|
{
|
||||||
|
checkNotNull(offer.getAcceptedCountries());
|
||||||
|
checkNotNull(offer.getAcceptedLanguageLocales());
|
||||||
|
checkNotNull(offer.getAmount());
|
||||||
|
checkNotNull(offer.getArbitrator());
|
||||||
|
checkNotNull(offer.getBankAccountCountry());
|
||||||
|
checkNotNull(offer.getBankAccountId());
|
||||||
|
checkNotNull(offer.getCollateral());
|
||||||
|
checkNotNull(offer.getCreationDate());
|
||||||
|
checkNotNull(offer.getCurrency());
|
||||||
|
checkNotNull(offer.getDirection());
|
||||||
|
checkNotNull(offer.getId());
|
||||||
|
checkNotNull(offer.getMessagePublicKey());
|
||||||
|
checkNotNull(offer.getMinAmount());
|
||||||
|
checkNotNull(offer.getOfferFeePaymentTxID());
|
||||||
|
checkNotNull(offer.getPrice());
|
||||||
|
|
||||||
if (offer.getAmount().compareTo(offer.getMinAmount()) < 0)
|
checkArgument(offer.getAcceptedCountries().size() > 0);
|
||||||
{
|
checkArgument(offer.getAcceptedLanguageLocales().size() > 0);
|
||||||
faultHandler.onFault("Offer validation failed: Min. amount is larger than amount.", new Exception("Offer validation failed: Min. amount is larger than amount."));
|
checkArgument(offer.getAmount().isGreaterThan(Restritions.MIN_TRADE_AMOUNT));
|
||||||
|
checkArgument(offer.getAmount().compareTo(Restritions.MIN_TRADE_AMOUNT) >= 0);
|
||||||
|
checkArgument(offer.getAmount().compareTo(offer.getMinAmount()) >= 0);
|
||||||
|
checkArgument(offer.getCollateral() > 0);
|
||||||
|
checkArgument(offer.getPrice() > 0);
|
||||||
|
// TODO when offer is flattened continue here...
|
||||||
|
|
||||||
}
|
|
||||||
else if (offer.getAcceptedCountries() == null || offer.getAcceptedCountries().size() == 0)
|
|
||||||
{
|
|
||||||
faultHandler.onFault("Offer validation failed: No accepted countries are defined.", new Exception("Offer validation failed: No accepted countries are defined."));
|
|
||||||
} //TODO...
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resultHandler.onResult();
|
resultHandler.onResult();
|
||||||
|
} catch (Throwable t)
|
||||||
|
{
|
||||||
|
faultHandler.onFault("Offer validation failed with exception: " + t.getMessage(), t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue