diff --git a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java index 30dfaca4ac3..b038d9d264a 100644 --- a/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java +++ b/apitest/src/test/java/bisq/apitest/method/offer/CreateOfferUsingMarketPriceMarginTest.java @@ -41,7 +41,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static protobuf.OfferPayload.Direction.BUY; +import static protobuf.FeeTxOfferPayload.Direction.BUY; @Disabled @Slf4j diff --git a/core/src/main/java/bisq/core/offer/CreateOfferService.java b/core/src/main/java/bisq/core/offer/CreateOfferService.java index 733eee1a7f9..ffa24c3b8ac 100644 --- a/core/src/main/java/bisq/core/offer/CreateOfferService.java +++ b/core/src/main/java/bisq/core/offer/CreateOfferService.java @@ -182,7 +182,7 @@ public Offer createAndGetOffer(String offerId, currencyCode, makerFeeAsCoin); - OfferPayload offerPayload = new OfferPayload(offerId, + FeeTxOfferPayload offerPayload = new FeeTxOfferPayload(offerId, creationTime, makerAddress, pubKeyRing, diff --git a/core/src/main/java/bisq/core/offer/FeeTxOfferPayload.java b/core/src/main/java/bisq/core/offer/FeeTxOfferPayload.java new file mode 100644 index 00000000000..6d375665459 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/FeeTxOfferPayload.java @@ -0,0 +1,394 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.offer; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; +import bisq.common.util.CollectionUtils; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.JsonExclude; + +import java.security.PublicKey; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +// OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a +// backward compatible way. Maybe a candidate when segwit activation is done as hardfork? + +@EqualsAndHashCode(callSuper = true) +@Getter +@Slf4j +public final class FeeTxOfferPayload extends OfferPayload { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final String id; + private final long date; + private final NodeAddress ownerNodeAddress; + @JsonExclude + private final PubKeyRing pubKeyRing; + private final Direction direction; + // price if fixed price is used (usePercentageBasedPrice = false), otherwise 0 + private final long price; + // Distance form market price if percentage based price is used (usePercentageBasedPrice = true), otherwise 0. + // E.g. 0.1 -> 10%. Can be negative as well. Depending on direction the marketPriceMargin is above or below the market price. + // Positive values is always the usual case where you want a better price as the market. + // E.g. Buy offer with market price 400.- leads to a 360.- price. + // Sell offer with market price 400.- leads to a 440.- price. + private final double marketPriceMargin; + // We use 2 type of prices: fixed price or price based on distance from market price + private final boolean useMarketBasedPrice; + private final long amount; + private final long minAmount; + + // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency + // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. + private final String baseCurrencyCode; + private final String counterCurrencyCode; + + @Deprecated + // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) + private final List arbitratorNodeAddresses; + @Deprecated + // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) + private final List mediatorNodeAddresses; + private final String paymentMethodId; + private final String makerPaymentAccountId; + // Mutable property. Has to be set before offer is save in P2P network as it changes the objects hash! + @Nullable + @Setter + private String offerFeePaymentTxId; + @Nullable + private final String countryCode; + @Nullable + private final List acceptedCountryCodes; + @Nullable + private final String bankId; + @Nullable + private final List acceptedBankIds; + private final String versionNr; + private final long blockHeightAtOfferCreation; + private final long txFee; + private final long makerFee; + private final boolean isCurrencyForMakerFeeBtc; + private final long buyerSecurityDeposit; + private final long sellerSecurityDeposit; + private final long maxTradeLimit; + private final long maxTradePeriod; + + // reserved for future use cases + // Close offer when certain price is reached + private final boolean useAutoClose; + // If useReOpenAfterAutoClose=true we re-open a new offer with the remaining funds if the trade amount + // was less than the offer's max. trade amount. + private final boolean useReOpenAfterAutoClose; + // Used when useAutoClose is set for canceling the offer when lowerClosePrice is triggered + private final long lowerClosePrice; + // Used when useAutoClose is set for canceling the offer when upperClosePrice is triggered + private final long upperClosePrice; + // Reserved for possible future use to support private trades where the taker needs to have an accessKey + private final boolean isPrivateOffer; + @Nullable + private final String hashOfChallenge; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + + // extraDataMap used from v0.6 on for hashOfPaymentAccount + // key ACCOUNT_AGE_WITNESS, value: hex string of hashOfPaymentAccount byte array + @Nullable + private final Map extraDataMap; + private final int protocolVersion; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public FeeTxOfferPayload(String id, + long date, + NodeAddress ownerNodeAddress, + PubKeyRing pubKeyRing, + Direction direction, + long price, + double marketPriceMargin, + boolean useMarketBasedPrice, + long amount, + long minAmount, + String baseCurrencyCode, + String counterCurrencyCode, + List arbitratorNodeAddresses, + List mediatorNodeAddresses, + String paymentMethodId, + String makerPaymentAccountId, + @Nullable String offerFeePaymentTxId, + @Nullable String countryCode, + @Nullable List acceptedCountryCodes, + @Nullable String bankId, + @Nullable List acceptedBankIds, + String versionNr, + long blockHeightAtOfferCreation, + long txFee, + long makerFee, + boolean isCurrencyForMakerFeeBtc, + long buyerSecurityDeposit, + long sellerSecurityDeposit, + long maxTradeLimit, + long maxTradePeriod, + boolean useAutoClose, + boolean useReOpenAfterAutoClose, + long lowerClosePrice, + long upperClosePrice, + boolean isPrivateOffer, + @Nullable String hashOfChallenge, + @Nullable Map extraDataMap, + int protocolVersion) { + this.id = id; + this.date = date; + this.ownerNodeAddress = ownerNodeAddress; + this.pubKeyRing = pubKeyRing; + this.direction = direction; + this.price = price; + this.marketPriceMargin = marketPriceMargin; + this.useMarketBasedPrice = useMarketBasedPrice; + this.amount = amount; + this.minAmount = minAmount; + this.baseCurrencyCode = baseCurrencyCode; + this.counterCurrencyCode = counterCurrencyCode; + this.arbitratorNodeAddresses = arbitratorNodeAddresses; + this.mediatorNodeAddresses = mediatorNodeAddresses; + this.paymentMethodId = paymentMethodId; + this.makerPaymentAccountId = makerPaymentAccountId; + this.offerFeePaymentTxId = offerFeePaymentTxId; + this.countryCode = countryCode; + this.acceptedCountryCodes = acceptedCountryCodes; + this.bankId = bankId; + this.acceptedBankIds = acceptedBankIds; + this.versionNr = versionNr; + this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; + this.txFee = txFee; + this.makerFee = makerFee; + this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; + this.buyerSecurityDeposit = buyerSecurityDeposit; + this.sellerSecurityDeposit = sellerSecurityDeposit; + this.maxTradeLimit = maxTradeLimit; + this.maxTradePeriod = maxTradePeriod; + this.useAutoClose = useAutoClose; + this.useReOpenAfterAutoClose = useReOpenAfterAutoClose; + this.lowerClosePrice = lowerClosePrice; + this.upperClosePrice = upperClosePrice; + this.isPrivateOffer = isPrivateOffer; + this.hashOfChallenge = hashOfChallenge; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + this.protocolVersion = protocolVersion; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.StoragePayload toProtoMessage() { + protobuf.FeeTxOfferPayload.Builder builder = protobuf.FeeTxOfferPayload.newBuilder() + .setId(id) + .setDate(date) + .setOwnerNodeAddress(ownerNodeAddress.toProtoMessage()) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .setDirection(toProtoMessage(direction)) + .setPrice(price) + .setMarketPriceMargin(marketPriceMargin) + .setUseMarketBasedPrice(useMarketBasedPrice) + .setAmount(amount) + .setMinAmount(minAmount) + .setBaseCurrencyCode(baseCurrencyCode) + .setCounterCurrencyCode(counterCurrencyCode) + .addAllArbitratorNodeAddresses(arbitratorNodeAddresses.stream() + .map(NodeAddress::toProtoMessage) + .collect(Collectors.toList())) + .addAllMediatorNodeAddresses(mediatorNodeAddresses.stream() + .map(NodeAddress::toProtoMessage) + .collect(Collectors.toList())) + .setPaymentMethodId(paymentMethodId) + .setMakerPaymentAccountId(makerPaymentAccountId) + .setVersionNr(versionNr) + .setBlockHeightAtOfferCreation(blockHeightAtOfferCreation) + .setTxFee(txFee) + .setMakerFee(makerFee) + .setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc) + .setBuyerSecurityDeposit(buyerSecurityDeposit) + .setSellerSecurityDeposit(sellerSecurityDeposit) + .setMaxTradeLimit(maxTradeLimit) + .setMaxTradePeriod(maxTradePeriod) + .setUseAutoClose(useAutoClose) + .setUseReOpenAfterAutoClose(useReOpenAfterAutoClose) + .setLowerClosePrice(lowerClosePrice) + .setUpperClosePrice(upperClosePrice) + .setIsPrivateOffer(isPrivateOffer) + .setProtocolVersion(protocolVersion); + + builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId, + "OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network.")); + + Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode); + Optional.ofNullable(bankId).ifPresent(builder::setBankId); + Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); + Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); + Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + + return protobuf.StoragePayload.newBuilder().setFeeTxOfferPayload(builder).build(); + } + + public static FeeTxOfferPayload fromProto(protobuf.FeeTxOfferPayload proto) { + checkArgument(!proto.getOfferFeePaymentTxId().isEmpty(), "OfferFeePaymentTxId must be set in PB.OfferPayload"); + List acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ? + null : new ArrayList<>(proto.getAcceptedBankIdsList()); + List acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ? + null : new ArrayList<>(proto.getAcceptedCountryCodesList()); + String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); + Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? + null : proto.getExtraDataMap(); + + return new FeeTxOfferPayload(proto.getId(), + proto.getDate(), + NodeAddress.fromProto(proto.getOwnerNodeAddress()), + PubKeyRing.fromProto(proto.getPubKeyRing()), + fromProto(proto.getDirection()), + proto.getPrice(), + proto.getMarketPriceMargin(), + proto.getUseMarketBasedPrice(), + proto.getAmount(), + proto.getMinAmount(), + proto.getBaseCurrencyCode(), + proto.getCounterCurrencyCode(), + proto.getArbitratorNodeAddressesList().stream() + .map(NodeAddress::fromProto) + .collect(Collectors.toList()), + proto.getMediatorNodeAddressesList().stream() + .map(NodeAddress::fromProto) + .collect(Collectors.toList()), + proto.getPaymentMethodId(), + proto.getMakerPaymentAccountId(), + proto.getOfferFeePaymentTxId(), + ProtoUtil.stringOrNullFromProto(proto.getCountryCode()), + acceptedCountryCodes, + ProtoUtil.stringOrNullFromProto(proto.getBankId()), + acceptedBankIds, + proto.getVersionNr(), + proto.getBlockHeightAtOfferCreation(), + proto.getTxFee(), + proto.getMakerFee(), + proto.getIsCurrencyForMakerFeeBtc(), + proto.getBuyerSecurityDeposit(), + proto.getSellerSecurityDeposit(), + proto.getMaxTradeLimit(), + proto.getMaxTradePeriod(), + proto.getUseAutoClose(), + proto.getUseReOpenAfterAutoClose(), + proto.getLowerClosePrice(), + proto.getUpperClosePrice(), + proto.getIsPrivateOffer(), + hashOfChallenge, + extraDataMapMap, + proto.getProtocolVersion()); + } + + public static Direction fromProto(protobuf.FeeTxOfferPayload.Direction direction) { + return ProtoUtil.enumFromProto(Direction.class, direction.name()); + } + + public static protobuf.FeeTxOfferPayload.Direction toProtoMessage(Direction direction) { + return protobuf.FeeTxOfferPayload.Direction.valueOf(direction.name()); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getOwnerPubKey() { + return pubKeyRing.getSignaturePubKey(); + } + + @Override + public String toString() { + return "OfferPayload{" + + "\n id='" + id + '\'' + + ",\n date=" + new Date(date) + + ",\n ownerNodeAddress=" + ownerNodeAddress + + ",\n pubKeyRing=" + pubKeyRing + + ",\n direction=" + direction + + ",\n price=" + price + + ",\n marketPriceMargin=" + marketPriceMargin + + ",\n useMarketBasedPrice=" + useMarketBasedPrice + + ",\n amount=" + amount + + ",\n minAmount=" + minAmount + + ",\n baseCurrencyCode='" + baseCurrencyCode + '\'' + + ",\n counterCurrencyCode='" + counterCurrencyCode + '\'' + + ",\n paymentMethodId='" + paymentMethodId + '\'' + + ",\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + + ",\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' + + ",\n countryCode='" + countryCode + '\'' + + ",\n acceptedCountryCodes=" + acceptedCountryCodes + + ",\n bankId='" + bankId + '\'' + + ",\n acceptedBankIds=" + acceptedBankIds + + ",\n versionNr='" + versionNr + '\'' + + ",\n blockHeightAtOfferCreation=" + blockHeightAtOfferCreation + + ",\n txFee=" + txFee + + ",\n makerFee=" + makerFee + + ",\n isCurrencyForMakerFeeBtc=" + isCurrencyForMakerFeeBtc + + ",\n buyerSecurityDeposit=" + buyerSecurityDeposit + + ",\n sellerSecurityDeposit=" + sellerSecurityDeposit + + ",\n maxTradeLimit=" + maxTradeLimit + + ",\n maxTradePeriod=" + maxTradePeriod + + ",\n useAutoClose=" + useAutoClose + + ",\n useReOpenAfterAutoClose=" + useReOpenAfterAutoClose + + ",\n lowerClosePrice=" + lowerClosePrice + + ",\n upperClosePrice=" + upperClosePrice + + ",\n isPrivateOffer=" + isPrivateOffer + + ",\n hashOfChallenge='" + hashOfChallenge + '\'' + + ",\n extraDataMap=" + extraDataMap + + ",\n protocolVersion=" + protocolVersion + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java index 05305dadf0d..f92a14bd68a 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -95,6 +95,7 @@ public enum State { @Getter private final OfferPayload offerPayload; + @JsonExclude @Getter final transient private ObjectProperty stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN); @@ -130,11 +131,12 @@ public Offer(OfferPayload offerPayload) { @Override public protobuf.Offer toProtoMessage() { - return protobuf.Offer.newBuilder().setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()).build(); + return protobuf.Offer.newBuilder().setFeeTxOfferPayload(offerPayload.toProtoMessage().getFeeTxOfferPayload()) + .build(); } public static Offer fromProto(protobuf.Offer proto) { - return new Offer(OfferPayload.fromProto(proto.getOfferPayload())); + return new Offer(FeeTxOfferPayload.fromProto(proto.getFeeTxOfferPayload())); } @@ -166,40 +168,40 @@ public void cancelAvailabilityRequest() { @Nullable public Price getPrice() { String currencyCode = getCurrencyCode(); - if (offerPayload.isUseMarketBasedPrice()) { - checkNotNull(priceFeedService, "priceFeed must not be null"); - MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); - if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { - double factor; - double marketPriceMargin = offerPayload.getMarketPriceMargin(); - if (CurrencyUtil.isCryptoCurrency(currencyCode)) { - factor = getDirection() == OfferPayload.Direction.SELL ? - 1 - marketPriceMargin : 1 + marketPriceMargin; - } else { - factor = getDirection() == OfferPayload.Direction.BUY ? - 1 - marketPriceMargin : 1 + marketPriceMargin; - } - double marketPriceAsDouble = marketPrice.getPrice(); - double targetPriceAsDouble = marketPriceAsDouble * factor; - try { - int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? - Altcoin.SMALLEST_UNIT_EXPONENT : - Fiat.SMALLEST_UNIT_EXPONENT; - double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); - final long roundedToLong = MathUtils.roundDoubleToLong(scaled); - return Price.valueOf(currencyCode, roundedToLong); - } catch (Exception e) { - log.error("Exception at getPrice / parseToFiat: " + e.toString() + "\n" + - "That case should never happen."); - return null; - } + if (!offerPayload.isUseMarketBasedPrice()) { + return Price.valueOf(currencyCode, offerPayload.getPrice()); + } + + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double factor; + double marketPriceMargin = offerPayload.getMarketPriceMargin(); + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + factor = getDirection() == OfferPayload.Direction.SELL ? + 1 - marketPriceMargin : 1 + marketPriceMargin; } else { - log.trace("We don't have a market price. " + - "That case could only happen if you don't have a price feed."); + factor = getDirection() == OfferPayload.Direction.BUY ? + 1 - marketPriceMargin : 1 + marketPriceMargin; + } + double marketPriceAsDouble = marketPrice.getPrice(); + double targetPriceAsDouble = marketPriceAsDouble * factor; + try { + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(targetPriceAsDouble, precision); + final long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } catch (Exception e) { + log.error("Exception at getPrice / parseToFiat: " + e.toString() + "\n" + + "That case should never happen."); return null; } } else { - return Price.valueOf(currencyCode, offerPayload.getPrice()); + log.trace("We don't have a market price. " + + "That case could only happen if you don't have a price feed."); + return null; } } @@ -234,17 +236,16 @@ public void checkTradePriceTolerance(long takersTradePrice) throws TradePriceOut @Nullable public Volume getVolumeByAmount(Coin amount) { Price price = getPrice(); - if (price != null && amount != null) { - Volume volumeByAmount = price.getVolumeByAmount(amount); - if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) - volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); - else if (CurrencyUtil.isFiatCurrency(offerPayload.getCurrencyCode())) - volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); - - return volumeByAmount; - } else { + if (price == null || amount == null) { return null; } + Volume volumeByAmount = price.getVolumeByAmount(amount); + if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID)) + volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount); + else if (CurrencyUtil.isFiatCurrency(offerPayload.getCurrencyCode())) + volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount); + + return volumeByAmount; } public void resetState() { @@ -265,7 +266,7 @@ public ObjectProperty stateProperty() { } public void setOfferFeePaymentTxId(String offerFeePaymentTxID) { - offerPayload.setOfferFeePaymentTxId(offerFeePaymentTxID); + getFeeTxPayload(offerPayload).ifPresent(payload -> payload.setOfferFeePaymentTxId(offerFeePaymentTxID)); } public void setErrorMessage(String errorMessage) { @@ -278,6 +279,7 @@ public void setErrorMessage(String errorMessage) { /////////////////////////////////////////////////////////////////////////////////////////// // converted payload properties + public Coin getTxFee() { return Coin.valueOf(offerPayload.getTxFee()); } @@ -464,7 +466,7 @@ public String getMakerPaymentAccountId() { } public String getOfferFeePaymentTxId() { - return offerPayload.getOfferFeePaymentTxId(); + return getFeeTxPayload(offerPayload).map(FeeTxOfferPayload::getOfferFeePaymentTxId).orElse(null); } public String getVersionNr() { @@ -533,6 +535,13 @@ public boolean isXmr() { return getCurrencyCode().equals("XMR"); } + private Optional getFeeTxPayload(OfferPayload offerPayload) { + if (offerPayload instanceof FeeTxOfferPayload) { + return Optional.of((FeeTxOfferPayload) offerPayload); + } + return Optional.empty(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index 92f6ece603b..fdb5131346b 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -17,44 +17,21 @@ package bisq.core.offer; -import bisq.network.p2p.NodeAddress; import bisq.network.p2p.storage.payload.ExpirablePayload; import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; import bisq.common.crypto.PubKeyRing; -import bisq.common.proto.ProtoUtil; -import bisq.common.util.CollectionUtils; -import bisq.common.util.ExtraDataMapValidator; -import bisq.common.util.JsonExclude; -import java.security.PublicKey; - -import java.util.ArrayList; -import java.util.Date; import java.util.List; -import java.util.Map; -import java.util.Optional; import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -// OfferPayload has about 1.4 kb. We should look into options to make it smaller but will be hard to do it in a -// backward compatible way. Maybe a candidate when segwit activation is done as hardfork? - @EqualsAndHashCode -@Getter -@Slf4j -public final class OfferPayload implements ProtectedStoragePayload, ExpirablePayload, RequiresOwnerIsOnlinePayload { +public abstract class OfferPayload implements ProtectedStoragePayload, ExpirablePayload, RequiresOwnerIsOnlinePayload { public static final long TTL = TimeUnit.MINUTES.toMillis(9); /////////////////////////////////////////////////////////////////////////////////////////// @@ -63,15 +40,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public enum Direction { BUY, - SELL; - - public static OfferPayload.Direction fromProto(protobuf.OfferPayload.Direction direction) { - return ProtoUtil.enumFromProto(OfferPayload.Direction.class, direction.name()); - } - - public static protobuf.OfferPayload.Direction toProtoMessage(Direction direction) { - return protobuf.OfferPayload.Direction.valueOf(direction.name()); - } + SELL } // Keys for extra map @@ -93,295 +62,86 @@ public static protobuf.OfferPayload.Direction toProtoMessage(Direction direction /////////////////////////////////////////////////////////////////////////////////////////// - // Instance fields + // API /////////////////////////////////////////////////////////////////////////////////////////// - private final String id; - private final long date; - private final NodeAddress ownerNodeAddress; - @JsonExclude - private final PubKeyRing pubKeyRing; - private final Direction direction; - // price if fixed price is used (usePercentageBasedPrice = false), otherwise 0 - private final long price; - // Distance form market price if percentage based price is used (usePercentageBasedPrice = true), otherwise 0. - // E.g. 0.1 -> 10%. Can be negative as well. Depending on direction the marketPriceMargin is above or below the market price. - // Positive values is always the usual case where you want a better price as the market. - // E.g. Buy offer with market price 400.- leads to a 360.- price. - // Sell offer with market price 400.- leads to a 440.- price. - private final double marketPriceMargin; - // We use 2 type of prices: fixed price or price based on distance from market price - private final boolean useMarketBasedPrice; - private final long amount; - private final long minAmount; + @Override + abstract public protobuf.StoragePayload toProtoMessage(); + + abstract public long getAmount(); + + abstract public long getMinAmount(); // For fiat offer the baseCurrencyCode is BTC and the counterCurrencyCode is the fiat currency // For altcoin offers it is the opposite. baseCurrencyCode is the altcoin and the counterCurrencyCode is BTC. - private final String baseCurrencyCode; - private final String counterCurrencyCode; - - @Deprecated - // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) - private final List arbitratorNodeAddresses; - @Deprecated - // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) - private final List mediatorNodeAddresses; - private final String paymentMethodId; - private final String makerPaymentAccountId; - // Mutable property. Has to be set before offer is save in P2P network as it changes the objects hash! - @Nullable - @Setter - private String offerFeePaymentTxId; - @Nullable - private final String countryCode; - @Nullable - private final List acceptedCountryCodes; - @Nullable - private final String bankId; + abstract public String getBaseCurrencyCode(); + + abstract public String getCounterCurrencyCode(); + + abstract public long getDate(); + + abstract public Direction getDirection(); + @Nullable - private final List acceptedBankIds; - private final String versionNr; - private final long blockHeightAtOfferCreation; - private final long txFee; - private final long makerFee; - private final boolean isCurrencyForMakerFeeBtc; - private final long buyerSecurityDeposit; - private final long sellerSecurityDeposit; - private final long maxTradeLimit; - private final long maxTradePeriod; - - // reserved for future use cases - // Close offer when certain price is reached - private final boolean useAutoClose; - // If useReOpenAfterAutoClose=true we re-open a new offer with the remaining funds if the trade amount - // was less than the offer's max. trade amount. - private final boolean useReOpenAfterAutoClose; - // Used when useAutoClose is set for canceling the offer when lowerClosePrice is triggered - private final long lowerClosePrice; - // Used when useAutoClose is set for canceling the offer when upperClosePrice is triggered - private final long upperClosePrice; + abstract public String getHashOfChallenge(); + + abstract public String getId(); + + abstract public boolean isCurrencyForMakerFeeBtc(); + // Reserved for possible future use to support private trades where the taker needs to have an accessKey - private final boolean isPrivateOffer; - @Nullable - private final String hashOfChallenge; + abstract public boolean isPrivateOffer(); - // Should be only used in emergency case if we need to add data but do not want to break backward compatibility - // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new - // field in a class would break that hash and therefore break the storage mechanism. + abstract public long getMakerFee(); - // extraDataMap used from v0.6 on for hashOfPaymentAccount - // key ACCOUNT_AGE_WITNESS, value: hex string of hashOfPaymentAccount byte array - @Nullable - private final Map extraDataMap; - private final int protocolVersion; + abstract public long getTxFee(); + abstract public String getMakerPaymentAccountId(); - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor - /////////////////////////////////////////////////////////////////////////////////////////// + abstract public long getBuyerSecurityDeposit(); - public OfferPayload(String id, - long date, - NodeAddress ownerNodeAddress, - PubKeyRing pubKeyRing, - Direction direction, - long price, - double marketPriceMargin, - boolean useMarketBasedPrice, - long amount, - long minAmount, - String baseCurrencyCode, - String counterCurrencyCode, - List arbitratorNodeAddresses, - List mediatorNodeAddresses, - String paymentMethodId, - String makerPaymentAccountId, - @Nullable String offerFeePaymentTxId, - @Nullable String countryCode, - @Nullable List acceptedCountryCodes, - @Nullable String bankId, - @Nullable List acceptedBankIds, - String versionNr, - long blockHeightAtOfferCreation, - long txFee, - long makerFee, - boolean isCurrencyForMakerFeeBtc, - long buyerSecurityDeposit, - long sellerSecurityDeposit, - long maxTradeLimit, - long maxTradePeriod, - boolean useAutoClose, - boolean useReOpenAfterAutoClose, - long lowerClosePrice, - long upperClosePrice, - boolean isPrivateOffer, - @Nullable String hashOfChallenge, - @Nullable Map extraDataMap, - int protocolVersion) { - this.id = id; - this.date = date; - this.ownerNodeAddress = ownerNodeAddress; - this.pubKeyRing = pubKeyRing; - this.direction = direction; - this.price = price; - this.marketPriceMargin = marketPriceMargin; - this.useMarketBasedPrice = useMarketBasedPrice; - this.amount = amount; - this.minAmount = minAmount; - this.baseCurrencyCode = baseCurrencyCode; - this.counterCurrencyCode = counterCurrencyCode; - this.arbitratorNodeAddresses = arbitratorNodeAddresses; - this.mediatorNodeAddresses = mediatorNodeAddresses; - this.paymentMethodId = paymentMethodId; - this.makerPaymentAccountId = makerPaymentAccountId; - this.offerFeePaymentTxId = offerFeePaymentTxId; - this.countryCode = countryCode; - this.acceptedCountryCodes = acceptedCountryCodes; - this.bankId = bankId; - this.acceptedBankIds = acceptedBankIds; - this.versionNr = versionNr; - this.blockHeightAtOfferCreation = blockHeightAtOfferCreation; - this.txFee = txFee; - this.makerFee = makerFee; - this.isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc; - this.buyerSecurityDeposit = buyerSecurityDeposit; - this.sellerSecurityDeposit = sellerSecurityDeposit; - this.maxTradeLimit = maxTradeLimit; - this.maxTradePeriod = maxTradePeriod; - this.useAutoClose = useAutoClose; - this.useReOpenAfterAutoClose = useReOpenAfterAutoClose; - this.lowerClosePrice = lowerClosePrice; - this.upperClosePrice = upperClosePrice; - this.isPrivateOffer = isPrivateOffer; - this.hashOfChallenge = hashOfChallenge; - this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); - this.protocolVersion = protocolVersion; - } + abstract public long getSellerSecurityDeposit(); - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// + abstract public long getMaxTradeLimit(); - @Override - public protobuf.StoragePayload toProtoMessage() { - protobuf.OfferPayload.Builder builder = protobuf.OfferPayload.newBuilder() - .setId(id) - .setDate(date) - .setOwnerNodeAddress(ownerNodeAddress.toProtoMessage()) - .setPubKeyRing(pubKeyRing.toProtoMessage()) - .setDirection(Direction.toProtoMessage(direction)) - .setPrice(price) - .setMarketPriceMargin(marketPriceMargin) - .setUseMarketBasedPrice(useMarketBasedPrice) - .setAmount(amount) - .setMinAmount(minAmount) - .setBaseCurrencyCode(baseCurrencyCode) - .setCounterCurrencyCode(counterCurrencyCode) - .addAllArbitratorNodeAddresses(arbitratorNodeAddresses.stream() - .map(NodeAddress::toProtoMessage) - .collect(Collectors.toList())) - .addAllMediatorNodeAddresses(mediatorNodeAddresses.stream() - .map(NodeAddress::toProtoMessage) - .collect(Collectors.toList())) - .setPaymentMethodId(paymentMethodId) - .setMakerPaymentAccountId(makerPaymentAccountId) - .setVersionNr(versionNr) - .setBlockHeightAtOfferCreation(blockHeightAtOfferCreation) - .setTxFee(txFee) - .setMakerFee(makerFee) - .setIsCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc) - .setBuyerSecurityDeposit(buyerSecurityDeposit) - .setSellerSecurityDeposit(sellerSecurityDeposit) - .setMaxTradeLimit(maxTradeLimit) - .setMaxTradePeriod(maxTradePeriod) - .setUseAutoClose(useAutoClose) - .setUseReOpenAfterAutoClose(useReOpenAfterAutoClose) - .setLowerClosePrice(lowerClosePrice) - .setUpperClosePrice(upperClosePrice) - .setIsPrivateOffer(isPrivateOffer) - .setProtocolVersion(protocolVersion); - - builder.setOfferFeePaymentTxId(checkNotNull(offerFeePaymentTxId, - "OfferPayload is in invalid state: offerFeePaymentTxID is not set when adding to P2P network.")); - - Optional.ofNullable(countryCode).ifPresent(builder::setCountryCode); - Optional.ofNullable(bankId).ifPresent(builder::setBankId); - Optional.ofNullable(acceptedBankIds).ifPresent(builder::addAllAcceptedBankIds); - Optional.ofNullable(acceptedCountryCodes).ifPresent(builder::addAllAcceptedCountryCodes); - Optional.ofNullable(hashOfChallenge).ifPresent(builder::setHashOfChallenge); - Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); - - return protobuf.StoragePayload.newBuilder().setOfferPayload(builder).build(); - } + abstract public long getMaxTradePeriod(); - public static OfferPayload fromProto(protobuf.OfferPayload proto) { - checkArgument(!proto.getOfferFeePaymentTxId().isEmpty(), "OfferFeePaymentTxId must be set in PB.OfferPayload"); - List acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ? - null : new ArrayList<>(proto.getAcceptedBankIdsList()); - List acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ? - null : new ArrayList<>(proto.getAcceptedCountryCodesList()); - String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); - Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? - null : proto.getExtraDataMap(); - - return new OfferPayload(proto.getId(), - proto.getDate(), - NodeAddress.fromProto(proto.getOwnerNodeAddress()), - PubKeyRing.fromProto(proto.getPubKeyRing()), - OfferPayload.Direction.fromProto(proto.getDirection()), - proto.getPrice(), - proto.getMarketPriceMargin(), - proto.getUseMarketBasedPrice(), - proto.getAmount(), - proto.getMinAmount(), - proto.getBaseCurrencyCode(), - proto.getCounterCurrencyCode(), - proto.getArbitratorNodeAddressesList().stream() - .map(NodeAddress::fromProto) - .collect(Collectors.toList()), - proto.getMediatorNodeAddressesList().stream() - .map(NodeAddress::fromProto) - .collect(Collectors.toList()), - proto.getPaymentMethodId(), - proto.getMakerPaymentAccountId(), - proto.getOfferFeePaymentTxId(), - ProtoUtil.stringOrNullFromProto(proto.getCountryCode()), - acceptedCountryCodes, - ProtoUtil.stringOrNullFromProto(proto.getBankId()), - acceptedBankIds, - proto.getVersionNr(), - proto.getBlockHeightAtOfferCreation(), - proto.getTxFee(), - proto.getMakerFee(), - proto.getIsCurrencyForMakerFeeBtc(), - proto.getBuyerSecurityDeposit(), - proto.getSellerSecurityDeposit(), - proto.getMaxTradeLimit(), - proto.getMaxTradePeriod(), - proto.getUseAutoClose(), - proto.getUseReOpenAfterAutoClose(), - proto.getLowerClosePrice(), - proto.getUpperClosePrice(), - proto.getIsPrivateOffer(), - hashOfChallenge, - extraDataMapMap, - proto.getProtocolVersion()); - } + abstract public String getPaymentMethodId(); + abstract public long getPrice(); - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// + abstract public double getMarketPriceMargin(); - @Override - public long getTTL() { - return TTL; - } + abstract public boolean isUseMarketBasedPrice(); + + abstract public int getProtocolVersion(); + + abstract public PubKeyRing getPubKeyRing(); + + abstract public String getVersionNr(); + + @Nullable + abstract public List getAcceptedBankIds(); + + @Nullable + abstract public String getBankId(); + + @Nullable + abstract public List getAcceptedCountryCodes(); + + @Nullable + abstract public String getCountryCode(); + + abstract public boolean isUseAutoClose(); + + abstract public long getBlockHeightAtOfferCreation(); + + abstract public long getLowerClosePrice(); + + abstract public long getUpperClosePrice(); + + abstract public boolean isUseReOpenAfterAutoClose(); - @Override - public PublicKey getOwnerPubKey() { - return pubKeyRing.getSignaturePubKey(); - } // In the offer we support base and counter currency // Fiat offers have base currency BTC and counterCurrency Fiat @@ -391,46 +151,4 @@ public PublicKey getOwnerPubKey() { public String getCurrencyCode() { return getBaseCurrencyCode().equals("BTC") ? getCounterCurrencyCode() : getBaseCurrencyCode(); } - - @Override - public String toString() { - return "OfferPayload{" + - "\n id='" + id + '\'' + - ",\n date=" + new Date(date) + - ",\n ownerNodeAddress=" + ownerNodeAddress + - ",\n pubKeyRing=" + pubKeyRing + - ",\n direction=" + direction + - ",\n price=" + price + - ",\n marketPriceMargin=" + marketPriceMargin + - ",\n useMarketBasedPrice=" + useMarketBasedPrice + - ",\n amount=" + amount + - ",\n minAmount=" + minAmount + - ",\n baseCurrencyCode='" + baseCurrencyCode + '\'' + - ",\n counterCurrencyCode='" + counterCurrencyCode + '\'' + - ",\n paymentMethodId='" + paymentMethodId + '\'' + - ",\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + - ",\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' + - ",\n countryCode='" + countryCode + '\'' + - ",\n acceptedCountryCodes=" + acceptedCountryCodes + - ",\n bankId='" + bankId + '\'' + - ",\n acceptedBankIds=" + acceptedBankIds + - ",\n versionNr='" + versionNr + '\'' + - ",\n blockHeightAtOfferCreation=" + blockHeightAtOfferCreation + - ",\n txFee=" + txFee + - ",\n makerFee=" + makerFee + - ",\n isCurrencyForMakerFeeBtc=" + isCurrencyForMakerFeeBtc + - ",\n buyerSecurityDeposit=" + buyerSecurityDeposit + - ",\n sellerSecurityDeposit=" + sellerSecurityDeposit + - ",\n maxTradeLimit=" + maxTradeLimit + - ",\n maxTradePeriod=" + maxTradePeriod + - ",\n useAutoClose=" + useAutoClose + - ",\n useReOpenAfterAutoClose=" + useReOpenAfterAutoClose + - ",\n lowerClosePrice=" + lowerClosePrice + - ",\n upperClosePrice=" + upperClosePrice + - ",\n isPrivateOffer=" + isPrivateOffer + - ",\n hashOfChallenge='" + hashOfChallenge + '\'' + - ",\n extraDataMap=" + extraDataMap + - ",\n protocolVersion=" + protocolVersion + - "\n}"; - } } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 3f07eaa1f45..e55d5c1587c 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -766,13 +766,20 @@ public void onFault(String errorMessage) { // Update persisted offer if a new capability is required after a software update /////////////////////////////////////////////////////////////////////////////////////////// + // This kind of update make sense for offers with an attached maker fee tx + // For offers without a maker fee it makes more sense to delete the old offer and create new ones with current + // capabilities private void maybeUpdatePersistedOffers() { // We need to clone to avoid ConcurrentModificationException ArrayList openOffersClone = new ArrayList<>(openOffers.getList()); openOffersClone.forEach(originalOpenOffer -> { Offer originalOffer = originalOpenOffer.getOffer(); - OfferPayload originalOfferPayload = originalOffer.getOfferPayload(); + // Only FeeTxOfferPayload type offers have a maker fee tx attached, don't update any other kind + if (!(originalOffer.getOfferPayload() instanceof FeeTxOfferPayload)) { + return; + } + FeeTxOfferPayload originalOfferPayload = (FeeTxOfferPayload) originalOffer.getOfferPayload(); // We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and // Capability.REFUND_AGENT in v1.2.0 and want to rewrite a // persisted offer after the user has updated to 1.2.0 so their offer will be accepted by the network. @@ -816,7 +823,7 @@ private void maybeUpdatePersistedOffers() { log.info("Updated the owner nodeaddress of offer id={}", originalOffer.getId()); } - OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), + FeeTxOfferPayload updatedPayload = new FeeTxOfferPayload(originalOfferPayload.getId(), originalOfferPayload.getDate(), ownerNodeAddress, originalOfferPayload.getPubKeyRing(), diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index d30a671bc8f..539a23f677c 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -36,7 +36,7 @@ import bisq.core.filter.Filter; import bisq.core.network.p2p.inventory.messages.GetInventoryRequest; import bisq.core.network.p2p.inventory.messages.GetInventoryResponse; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.messages.OfferAvailabilityRequest; import bisq.core.offer.messages.OfferAvailabilityResponse; import bisq.core.proto.CoreProtoResolver; @@ -273,8 +273,8 @@ public NetworkPayload fromProto(protobuf.StoragePayload proto) { return Filter.fromProto(proto.getFilter()); case MAILBOX_STORAGE_PAYLOAD: return MailboxStoragePayload.fromProto(proto.getMailboxStoragePayload()); - case OFFER_PAYLOAD: - return OfferPayload.fromProto(proto.getOfferPayload()); + case FEE_TX_OFFER_PAYLOAD: + return FeeTxOfferPayload.fromProto(proto.getFeeTxOfferPayload()); case TEMP_PROPOSAL_PAYLOAD: return TempProposalPayload.fromProto(proto.getTempProposalPayload()); default: diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index baa11de0965..2f6bac8e6b1 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -26,7 +26,7 @@ import bisq.core.locale.Res; import bisq.core.monetary.Altcoin; import bisq.core.monetary.Price; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.OpenOfferManager; import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; @@ -859,7 +859,7 @@ protected void addPriceInfoMessage(Dispute dispute, int counter) { } Contract contract = dispute.getContract(); - OfferPayload offerPayload = contract.getOfferPayload(); + FeeTxOfferPayload offerPayload = contract.getOfferPayload(); Price priceAtDisputeOpening = getPrice(offerPayload.getCurrencyCode()); if (priceAtDisputeOpening == null) { log.info("Price provider did not provide a price for {}. " + diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java index 81602d226d0..c51aa568150 100644 --- a/core/src/main/java/bisq/core/trade/Contract.java +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -20,7 +20,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; @@ -49,7 +49,7 @@ @Slf4j @Value public final class Contract implements NetworkPayload { - private final OfferPayload offerPayload; + private final FeeTxOfferPayload offerPayload; private final long tradeAmount; private final long tradePrice; private final String takerFeeTxID; @@ -76,7 +76,7 @@ public final class Contract implements NetworkPayload { private long lockTime; private final NodeAddress refundAgentNodeAddress; - public Contract(OfferPayload offerPayload, + public Contract(FeeTxOfferPayload offerPayload, long tradeAmount, long tradePrice, String takerFeeTxID, @@ -134,7 +134,7 @@ public Contract(OfferPayload offerPayload, /////////////////////////////////////////////////////////////////////////////////////////// public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { - return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), + return new Contract(FeeTxOfferPayload.fromProto(proto.getFeeTxOfferPayload()), proto.getTradeAmount(), proto.getTradePrice(), proto.getTakerFeeTxId(), @@ -159,7 +159,7 @@ public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver core @Override public protobuf.Contract toProtoMessage() { return protobuf.Contract.newBuilder() - .setOfferPayload(offerPayload.toProtoMessage().getOfferPayload()) + .setFeeTxOfferPayload(offerPayload.toProtoMessage().getFeeTxOfferPayload()) .setTradeAmount(tradeAmount) .setTradePrice(tradePrice) .setTakerFeeTxId(takerFeeTxID) diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java index 4c9d05c4c3f..42b09c935dc 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java @@ -19,6 +19,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.trade.BuyerAsMakerTrade; import bisq.core.trade.Contract; import bisq.core.trade.Trade; @@ -34,6 +35,7 @@ import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -61,8 +63,12 @@ protected void run() { byte[] makerMultiSigPubKey = makerAddressEntry.getPubKey(); AddressEntry takerAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT); + + checkArgument(processModel.getOffer().getOfferPayload() instanceof FeeTxOfferPayload); + FeeTxOfferPayload offerPayload = (FeeTxOfferPayload) processModel.getOffer().getOfferPayload(); + Contract contract = new Contract( - processModel.getOffer().getOfferPayload(), + offerPayload, checkNotNull(trade.getTradeAmount()).value, trade.getTradePrice().getValue(), takerFeeTxId, diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java index afe14cba205..84841131cf5 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java @@ -19,6 +19,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.trade.Contract; import bisq.core.trade.SellerAsTakerTrade; @@ -77,8 +78,12 @@ protected void run() { "takerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); Coin tradeAmount = checkNotNull(trade.getTradeAmount()); + + checkArgument(processModel.getOffer().getOfferPayload() instanceof FeeTxOfferPayload); + FeeTxOfferPayload offerPayload = (FeeTxOfferPayload) processModel.getOffer().getOfferPayload(); + Contract contract = new Contract( - processModel.getOffer().getOfferPayload(), + offerPayload, tradeAmount.value, trade.getTradePrice().getValue(), takerFeeTxId, diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java index 864b08f9677..cb6138dbbe5 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -21,6 +21,7 @@ import bisq.core.monetary.AltcoinExchangeRate; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.trade.Trade; @@ -60,6 +61,7 @@ import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** @@ -93,7 +95,11 @@ public static TradeStatistics2 from(Trade trade, Offer offer = trade.getOffer(); checkNotNull(offer, "offer must not ne null"); checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not ne null"); - return new TradeStatistics2(offer.getOfferPayload(), + + checkArgument(offer.getOfferPayload() instanceof FeeTxOfferPayload); + FeeTxOfferPayload offerPayload = (FeeTxOfferPayload) offer.getOfferPayload(); + + return new TradeStatistics2(offerPayload, trade.getTradePrice(), trade.getTradeAmount(), trade.getDate(), @@ -137,7 +143,7 @@ public static TradeStatistics2 from(Trade trade, @JsonExclude private Map extraDataMap; - public TradeStatistics2(OfferPayload offerPayload, + public TradeStatistics2(FeeTxOfferPayload offerPayload, Price tradePrice, Coin tradeAmount, Date tradeDate, @@ -209,7 +215,7 @@ public byte[] createHash() { private protobuf.TradeStatistics2.Builder getBuilder() { final protobuf.TradeStatistics2.Builder builder = protobuf.TradeStatistics2.newBuilder() - .setDirection(OfferPayload.Direction.toProtoMessage(direction)) + .setDirection(FeeTxOfferPayload.toProtoMessage(direction)) .setBaseCurrency(baseCurrency) .setCounterCurrency(counterCurrency) .setPaymentMethodId(offerPaymentMethod) @@ -239,7 +245,7 @@ public protobuf.PersistableNetworkPayload toProtoMessage() { public static TradeStatistics2 fromProto(protobuf.TradeStatistics2 proto) { return new TradeStatistics2( - OfferPayload.Direction.fromProto(proto.getDirection()), + FeeTxOfferPayload.fromProto(proto.getDirection()), proto.getBaseCurrency(), proto.getCounterCurrency(), proto.getPaymentMethodId(), diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index 9782e37ce13..9d9090eac13 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -21,7 +21,7 @@ import bisq.core.account.sign.SignedWitnessService; import bisq.core.filter.FilterManager; import bisq.core.locale.CountryUtil; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.payment.ChargeBackRisk; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; @@ -229,7 +229,7 @@ public void testArbitratorSignWitness() { when(contract.getSellerPubKeyRing()).thenReturn(sellerPubKeyRing); when(contract.getBuyerPaymentAccountPayload()).thenReturn(buyerPaymentAccountPayload); when(contract.getSellerPaymentAccountPayload()).thenReturn(sellerPaymentAccountPayload); - when(contract.getOfferPayload()).thenReturn(mock(OfferPayload.class)); + when(contract.getOfferPayload()).thenReturn(mock(FeeTxOfferPayload.class)); List items = service.getTraderPaymentAccounts(now, getPaymentMethodById(PaymentMethod.SEPA_ID), disputes); assertEquals(2, items.size()); diff --git a/core/src/test/java/bisq/core/offer/OfferMaker.java b/core/src/test/java/bisq/core/offer/OfferMaker.java index aa9d294c2e2..d2a6208d04b 100644 --- a/core/src/test/java/bisq/core/offer/OfferMaker.java +++ b/core/src/test/java/bisq/core/offer/OfferMaker.java @@ -36,7 +36,7 @@ public class OfferMaker { public static final Property id = new Property<>(); public static final Instantiator Offer = lookup -> new Offer( - new OfferPayload(lookup.valueOf(id, "1234"), + new FeeTxOfferPayload(lookup.valueOf(id, "1234"), 0L, null, null, diff --git a/core/src/test/java/bisq/core/offer/OfferTest.java b/core/src/test/java/bisq/core/offer/OfferTest.java index 9300c01574e..53cffaa9e7c 100644 --- a/core/src/test/java/bisq/core/offer/OfferTest.java +++ b/core/src/test/java/bisq/core/offer/OfferTest.java @@ -28,7 +28,7 @@ public class OfferTest { @Test public void testHasNoRange() { - OfferPayload payload = mock(OfferPayload.class); + FeeTxOfferPayload payload = mock(FeeTxOfferPayload.class); when(payload.getMinAmount()).thenReturn(1000L); when(payload.getAmount()).thenReturn(1000L); @@ -38,7 +38,7 @@ public void testHasNoRange() { @Test public void testHasRange() { - OfferPayload payload = mock(OfferPayload.class); + FeeTxOfferPayload payload = mock(FeeTxOfferPayload.class); when(payload.getMinAmount()).thenReturn(1000L); when(payload.getAmount()).thenReturn(2000L); diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index 5cc984687a4..01a4e021cd5 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -58,7 +58,7 @@ public void testStartEditOfferForActiveOffer() { doAnswer(invocation -> { ((ResultHandler) invocation.getArgument(1)).handleResult(); return null; - }).when(offerBookService).deactivateOffer(any(OfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); + }).when(offerBookService).deactivateOffer(any(FeeTxOfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); final OpenOffer openOffer = new OpenOffer(make(btcUsdOffer)); @@ -66,7 +66,7 @@ public void testStartEditOfferForActiveOffer() { manager.editOpenOfferStart(openOffer, resultHandler, null); - verify(offerBookService, times(1)).deactivateOffer(any(OfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); + verify(offerBookService, times(1)).deactivateOffer(any(FeeTxOfferPayload.class), any(ResultHandler.class), any(ErrorMessageHandler.class)); assertTrue(startEditOfferSuccessful.get()); diff --git a/core/src/test/java/bisq/core/trade/TradableListTest.java b/core/src/test/java/bisq/core/trade/TradableListTest.java index e6a41bca6ae..7ce55b6e7b1 100644 --- a/core/src/test/java/bisq/core/trade/TradableListTest.java +++ b/core/src/test/java/bisq/core/trade/TradableListTest.java @@ -17,8 +17,8 @@ package bisq.core.trade; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; import org.junit.Test; @@ -32,7 +32,7 @@ public class TradableListTest { @Test public void protoTesting() { - OfferPayload offerPayload = mock(OfferPayload.class, RETURNS_DEEP_STUBS); + FeeTxOfferPayload offerPayload = mock(FeeTxOfferPayload.class, RETURNS_DEEP_STUBS); TradableList openOfferTradableList = new TradableList<>(); protobuf.PersistableEnvelope message = (protobuf.PersistableEnvelope) openOfferTradableList.toProtoMessage(); assertEquals(message.getMessageCase(), TRADABLE_LIST); diff --git a/core/src/test/java/bisq/core/util/ProtoUtilTest.java b/core/src/test/java/bisq/core/util/ProtoUtilTest.java index dee3b4a2490..29e9e51a9c9 100644 --- a/core/src/test/java/bisq/core/util/ProtoUtilTest.java +++ b/core/src/test/java/bisq/core/util/ProtoUtilTest.java @@ -21,7 +21,7 @@ import bisq.common.proto.ProtoUtil; -import protobuf.OfferPayload; +import protobuf.FeeTxOfferPayload; import org.junit.Test; @@ -34,10 +34,10 @@ public class ProtoUtilTest { //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in bisq.common. @Test public void testEnum() { - OfferPayload.Direction direction = OfferPayload.Direction.SELL; - OfferPayload.Direction direction2 = OfferPayload.Direction.BUY; - OfferPayload.Direction realDirection = getDirection(direction); - OfferPayload.Direction realDirection2 = getDirection(direction2); + FeeTxOfferPayload.Direction direction = FeeTxOfferPayload.Direction.SELL; + FeeTxOfferPayload.Direction direction2 = FeeTxOfferPayload.Direction.BUY; + FeeTxOfferPayload.Direction realDirection = getDirection(direction); + FeeTxOfferPayload.Direction realDirection2 = getDirection(direction2); assertEquals("SELL", realDirection.name()); assertEquals("BUY", realDirection2.name()); } @@ -63,7 +63,7 @@ public void testUnknownEnumFix() { } } - public static OfferPayload.Direction getDirection(OfferPayload.Direction direction) { - return OfferPayload.Direction.valueOf(direction.name()); + public static FeeTxOfferPayload.Direction getDirection(FeeTxOfferPayload.Direction direction) { + return FeeTxOfferPayload.Direction.valueOf(direction.name()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index dc7d29c7c08..59625340491 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -28,6 +28,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.TradeCurrency; import bisq.core.offer.CreateOfferService; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; @@ -185,9 +186,12 @@ public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler erro // editedPayload is a merge of the original offerPayload and newOfferPayload // fields which are editable are merged in from newOfferPayload (such as payment account details) // fields which cannot change (most importantly BTC amount) are sourced from the original offerPayload - final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload(); - final OfferPayload newOfferPayload = createAndGetOffer().getOfferPayload(); - final OfferPayload editedPayload = new OfferPayload(offerPayload.getId(), + if (!(openOffer.getOffer().getOfferPayload() instanceof FeeTxOfferPayload)) { + return; + } + final FeeTxOfferPayload offerPayload = (FeeTxOfferPayload) openOffer.getOffer().getOfferPayload(); + final FeeTxOfferPayload newOfferPayload = (FeeTxOfferPayload) createAndGetOffer().getOfferPayload(); + final FeeTxOfferPayload editedPayload = new FeeTxOfferPayload(offerPayload.getId(), offerPayload.getDate(), offerPayload.getOwnerNodeAddress(), offerPayload.getPubKeyRing(), diff --git a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java index 470c86b853b..3e2ab25c700 100644 --- a/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/market/trades/TradesChartsViewModelTest.java @@ -22,7 +22,7 @@ import bisq.core.locale.FiatCurrency; import bisq.core.monetary.Price; -import bisq.core.offer.OfferPayload; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.TradeStatistics3; @@ -60,7 +60,7 @@ public class TradesChartsViewModelTest { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private File dir; - OfferPayload offer = new OfferPayload(null, + FeeTxOfferPayload offer = new FeeTxOfferPayload(null, 0, null, null, diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index dc48c8c5459..55bd1f35705 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -24,8 +24,8 @@ import bisq.core.locale.FiatCurrency; import bisq.core.locale.GlobalSettings; import bisq.core.locale.Res; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.AliPayAccount; import bisq.core.payment.CountryBasedPaymentAccount; @@ -584,7 +584,7 @@ private Offer getOffer(String tradeCurrencyCode, ArrayList acceptedCountryCodes, String bankId, ArrayList acceptedBanks) { - return new Offer(new OfferPayload(null, + return new Offer(new FeeTxOfferPayload(null, 0, null, null, diff --git a/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java b/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java index e84f560f0b4..c9a36e57f54 100644 --- a/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java +++ b/desktop/src/test/java/bisq/desktop/maker/OfferMaker.java @@ -17,6 +17,7 @@ package bisq.desktop.maker; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -40,7 +41,7 @@ public class OfferMaker { public static final Property id = new Property<>(); public static final Instantiator Offer = lookup -> new Offer( - new OfferPayload(lookup.valueOf(id, "1234"), + new FeeTxOfferPayload(lookup.valueOf(id, "1234"), 0L, null, null, diff --git a/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java b/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java index 632b49b82a0..f75c8412ddc 100644 --- a/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java +++ b/desktop/src/test/java/bisq/desktop/util/DisplayUtilsTest.java @@ -2,10 +2,10 @@ import bisq.core.locale.Res; import bisq.core.monetary.Volume; +import bisq.core.offer.FeeTxOfferPayload; import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; -import bisq.core.util.coin.ImmutableCoinFormatter; import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.ImmutableCoinFormatter; import bisq.common.config.Config; @@ -96,7 +96,7 @@ public void testFormatSameAmount() { @Test public void testFormatDifferentAmount() { - OfferPayload offerPayload = mock(OfferPayload.class); + FeeTxOfferPayload offerPayload = mock(FeeTxOfferPayload.class); Offer offer = new Offer(offerPayload); when(offerPayload.getMinAmount()).thenReturn(10000000L); when(offerPayload.getAmount()).thenReturn(20000000L); @@ -106,7 +106,7 @@ public void testFormatDifferentAmount() { @Test public void testFormatAmountWithAlignmenWithDecimals() { - OfferPayload offerPayload = mock(OfferPayload.class); + FeeTxOfferPayload offerPayload = mock(FeeTxOfferPayload.class); Offer offer = new Offer(offerPayload); when(offerPayload.getMinAmount()).thenReturn(10000000L); when(offerPayload.getAmount()).thenReturn(20000000L); @@ -116,7 +116,7 @@ public void testFormatAmountWithAlignmenWithDecimals() { @Test public void testFormatAmountWithAlignmenWithDecimalsNoRange() { - OfferPayload offerPayload = mock(OfferPayload.class); + FeeTxOfferPayload offerPayload = mock(FeeTxOfferPayload.class); Offer offer = new Offer(offerPayload); when(offerPayload.getMinAmount()).thenReturn(10000000L); when(offerPayload.getAmount()).thenReturn(10000000L); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index e9463018e1e..9281a55dcd8 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -529,7 +529,7 @@ message StoragePayload { // TradeStatistics trade_statistics = 5 [deprecated = true]; Removed in v.1.4.0 MailboxStoragePayload mailbox_storage_payload = 6; - OfferPayload offer_payload = 7; + FeeTxOfferPayload fee_tx_offer_payload = 7; TempProposalPayload temp_proposal_payload = 8; RefundAgent refund_agent = 9; } @@ -694,7 +694,7 @@ message Filter { message TradeStatistics2 { string base_currency = 1 [deprecated = true]; string counter_currency = 2 [deprecated = true]; - OfferPayload.Direction direction = 3 [deprecated = true]; + FeeTxOfferPayload.Direction direction = 3 [deprecated = true]; int64 trade_price = 4 [deprecated = true]; int64 trade_amount = 5 [deprecated = true]; int64 trade_date = 6 [deprecated = true]; @@ -729,7 +729,7 @@ message MailboxStoragePayload { map extra_data = 4; } -message OfferPayload { +message FeeTxOfferPayload { enum Direction { PB_ERROR = 0; BUY = 1; @@ -882,7 +882,7 @@ message DisputeResult { /////////////////////////////////////////////////////////////////////////////////////////// message Contract { - OfferPayload offer_payload = 1; + FeeTxOfferPayload fee_tx_offer_payload = 1; int64 trade_amount = 2; int64 trade_price = 3; string taker_fee_tx_id = 4; @@ -1335,7 +1335,7 @@ message Offer { MAKER_OFFLINE = 6; } - OfferPayload offer_payload = 1; + FeeTxOfferPayload fee_tx_offer_payload = 1; } message OpenOffer {