diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 6e6eec11483..79ee123c2b2 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,6 +1,5 @@ \ No newline at end of file diff --git a/common/src/main/java/bisq/common/app/Version.java b/common/src/main/java/bisq/common/app/Version.java index c5275945fdb..5763df44dbb 100644 --- a/common/src/main/java/bisq/common/app/Version.java +++ b/common/src/main/java/bisq/common/app/Version.java @@ -73,7 +73,7 @@ private static int getSubVersion(String version, int index) { // The version no. for the objects sent over the network. A change will break the serialization of old objects. // If objects are used for both network and database the network version is applied. // VERSION = 0.5.0 -> P2P_NETWORK_VERSION = 1 - @SuppressWarnings("ConstantConditions") + // With version 1.2.0 we change to version 2 (new trade protocol) public static final int P2P_NETWORK_VERSION = 1; // The version no. of the serialized data stored to disc. A change will break the serialization of old objects. @@ -84,7 +84,8 @@ private static int getSubVersion(String version, int index) { // A taker will check the version of the offers to see if his version is compatible. // Offers created with the old version will become invalid and have to be canceled. // VERSION = 0.5.0 -> TRADE_PROTOCOL_VERSION = 1 - public static final int TRADE_PROTOCOL_VERSION = 1; + // Version 1.2.0 -> TRADE_PROTOCOL_VERSION = 2 + public static final int TRADE_PROTOCOL_VERSION = 2; private static int p2pMessageVersion; public static final String BSQ_TX_VERSION = "1"; diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index 63385e522dc..e59d12e4170 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -36,9 +36,9 @@ message NetworkEnvelope { CloseConnectionMessage close_connection_message = 15; PrefixedSealedAndSignedMessage prefixed_sealed_and_signed_message = 16; - PayDepositRequest pay_deposit_request = 17; - PublishDepositTxRequest publish_deposit_tx_request = 18; - DepositTxPublishedMessage deposit_tx_published_message = 19; + InputsForDepositTxRequest inputs_for_deposit_tx_request = 17; + InputsForDepositTxResponse inputs_for_deposit_tx_response = 18; + DepositTxMessage deposit_tx_message = 19; CounterCurrencyTransferStartedMessage counter_currency_transfer_started_message = 20; PayoutTxPublishedMessage payout_tx_published_message = 21; @@ -70,6 +70,11 @@ message NetworkEnvelope { BundleOfEnvelopes bundle_of_envelopes = 43; MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 44; MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 45; + + DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 46; + DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 47; + DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 48; + PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 49; } } @@ -144,6 +149,7 @@ message OfferAvailabilityResponse { string uid = 4; NodeAddress arbitrator = 5; NodeAddress mediator = 6; + NodeAddress refund_agent = 7; } message RefreshOfferMessage { @@ -197,7 +203,7 @@ message PrefixedSealedAndSignedMessage { // trade -message PayDepositRequest { +message InputsForDepositTxRequest { string trade_id = 1; NodeAddress sender_node_address = 2; int64 trade_amount = 3; @@ -221,9 +227,11 @@ message PayDepositRequest { string uid = 21; bytes account_age_witness_signature_of_offer_id = 22; int64 current_date = 23; + repeated NodeAddress accepted_refund_agent_node_addresses = 24; + NodeAddress refund_agent_node_address = 25; } -message PublishDepositTxRequest { +message InputsForDepositTxResponse { string trade_id = 1; PaymentAccountPayload maker_payment_account_payload = 2; string maker_account_id = 3; @@ -237,13 +245,42 @@ message PublishDepositTxRequest { string uid = 11; bytes account_age_witness_signature_of_prepared_deposit_tx = 12; int64 current_date = 13; + int64 lock_time = 14; } -message DepositTxPublishedMessage { - string trade_id = 1; - bytes deposit_tx = 2; +message DelayedPayoutTxSignatureRequest { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes delayed_payout_tx = 4; +} + +message DelayedPayoutTxSignatureResponse { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes delayed_payout_tx_signature = 4; +} + +message DepositTxAndDelayedPayoutTxMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes deposit_tx = 4; + bytes delayed_payout_tx = 5; +} + +message DepositTxMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes deposit_tx = 4; +} + +message PeerPublishedDelayedPayoutTxMessage { + string uid = 1; + string trade_id = 2; NodeAddress sender_node_address = 3; - string uid = 4; } message CounterCurrencyTransferStartedMessage { @@ -279,8 +316,8 @@ message MediatedPayoutTxPublishedMessage { message MediatedPayoutTxSignatureMessage { string uid = 1; - bytes tx_signature = 2; string trade_id = 3; + bytes tx_signature = 2; NodeAddress sender_node_address = 4; } @@ -290,6 +327,7 @@ enum SupportType { ARBITRATION = 0; MEDIATION = 1; TRADE = 2; + REFUND = 3; } message OpenNewDisputeMessage { @@ -431,7 +469,7 @@ message Peer { message PubKeyRing { bytes signature_pub_key_bytes = 1; bytes encryption_pub_key_bytes = 2; - reserved 3; // WAS: string pgp_pub_key_as_pem = 3; + reserved 3; // WAS: string pgp_pub_key_as_pem = 3; } message SealedAndSigned { @@ -457,6 +495,7 @@ message StoragePayload { MailboxStoragePayload mailbox_storage_payload = 6; OfferPayload offer_payload = 7; TempProposalPayload temp_proposal_payload = 8; + RefundAgent refund_agent = 9; } } @@ -550,6 +589,18 @@ message Mediator { map extra_data = 9; } +message RefundAgent { + NodeAddress node_address = 1; + repeated string language_codes = 2; + int64 registration_date = 3; + string registration_signature = 4; + bytes registration_pub_key = 5; + PubKeyRing pub_key_ring = 6; + string email_address = 7; + string info = 8; + map extra_data = 9; +} + message Filter { repeated string banned_node_address = 1; repeated string banned_offer_ids = 2; @@ -568,6 +619,7 @@ message Filter { string disable_dao_below_version = 15; string disable_trade_below_version = 16; repeated string mediators = 17; + repeated string refundAgents = 18; } // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older @@ -708,6 +760,8 @@ message Dispute { bool is_closed = 21; DisputeResult dispute_result = 22; string dispute_payout_tx_id = 23; + SupportType support_type = 24; + string mediators_dispute_result = 25; } message Attachment { @@ -774,6 +828,8 @@ message Contract { bytes maker_multi_sig_pub_key = 17; bytes taker_multi_sig_pub_key = 18; NodeAddress mediator_node_address = 19; + int64 lock_time = 20; + NodeAddress refund_agent_node_address = 21; } message RawTransactionInput { @@ -793,6 +849,7 @@ enum AvailabilityResult { NO_MEDIATORS = 7; USER_IGNORED = 8; MISSING_MANDATORY_CAPABILITY = 9; + NO_REFUND_AGENTS = 10; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1074,6 +1131,7 @@ message PersistableEnvelope { UnconfirmedBsqChangeOutputList unconfirmed_bsq_change_output_list = 27; SignedWitnessStore signed_witness_store = 28; MediationDisputeList mediation_dispute_list = 29; + RefundDisputeList refund_dispute_list = 30; } } @@ -1198,6 +1256,7 @@ message OpenOffer { State state = 2; NodeAddress arbitrator_node_address = 3; NodeAddress mediator_node_address = 4; + NodeAddress refund_agent_node_address = 5; } message Tradable { @@ -1220,13 +1279,13 @@ message Trade { MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 5; MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 6; TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 7; - TAKER_PUBLISHED_DEPOSIT_TX = 8; - TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 9; - TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 10; - TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 11; - TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 12; - MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 13; - MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 14; + SELLER_PUBLISHED_DEPOSIT_TX = 8; + SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 9; + SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 10; + SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 11; + SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 12; + BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 13; + BUYER_SAW_DEPOSIT_TX_IN_NETWORK = 14; DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 15; BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 16; BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 17; @@ -1266,6 +1325,9 @@ message Trade { MEDIATION_REQUESTED = 5; MEDIATION_STARTED_BY_PEER = 6; MEDIATION_CLOSED = 7; + REFUND_REQUESTED = 8; + REFUND_REQUEST_STARTED_BY_PEER = 9; + REFUND_REQUEST_CLOSED = 10; } enum TradePeriodState { @@ -1305,6 +1367,11 @@ message Trade { string counter_currency_tx_id = 28; repeated ChatMessage chat_message = 29; MediationResultState mediation_result_state = 30; + int64 lock_time = 31; + string delayed_payout_tx_id = 32; + NodeAddress refund_agent_node_address = 33; + PubKeyRing refund_agent_pub_key_ring = 34; + RefundResultState refund_result_state = 35; } message BuyerAsMakerTrade { @@ -1330,8 +1397,8 @@ message ProcessModel { PubKeyRing pub_key_ring = 4; string take_offer_fee_tx_id = 5; bytes payout_tx_signature = 6; - repeated NodeAddress taker_accepted_arbitrator_node_addresses = 7; - repeated NodeAddress taker_accepted_mediator_node_addresses = 8; + reserved 7; // Not used anymore + reserved 8; // Not used anymore bytes prepared_deposit_tx = 9; repeated RawTransactionInput raw_transaction_inputs = 10; int64 change_output_value = 11; @@ -1376,6 +1443,10 @@ message MediationDisputeList { repeated Dispute dispute = 1; } +message RefundDisputeList { + repeated Dispute dispute = 1; +} + enum MediationResultState { PB_ERROR_MEDIATION_RESULT = 0; UNDEFINED_MEDIATION_RESULT = 1; @@ -1395,6 +1466,12 @@ enum MediationResultState { PAYOUT_TX_SEEN_IN_NETWORK = 15; } +//todo +enum RefundResultState { + PB_ERROR_REFUND_RESULT = 0; + UNDEFINED_REFUND_RESULT = 1; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Preferences /////////////////////////////////////////////////////////////////////////////////////////// @@ -1454,6 +1531,7 @@ message PreferencesPayload { double buyer_security_deposit_as_percent_for_crypto = 52; int32 block_notify_port = 53; int32 css_theme = 54; + bool tac_accepted_v120 = 55; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1474,6 +1552,8 @@ message UserPayload { Mediator registered_mediator = 11; PriceAlertFilter price_alert_filter = 12; repeated MarketAlertFilter market_alert_filters = 13; + repeated RefundAgent accepted_refund_agents = 14; + RefundAgent registered_refund_agent = 15; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java index ef87a6e35f1..f847a3d13e6 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -18,14 +18,6 @@ package bisq.core.account.sign; import bisq.core.account.witness.AccountAgeWitness; -import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.payment.ChargeBackRisk; -import bisq.core.payment.payload.PaymentAccountPayload; -import bisq.core.payment.payload.PaymentMethod; -import bisq.core.support.dispute.Dispute; -import bisq.core.support.dispute.DisputeResult; -import bisq.core.support.dispute.arbitration.ArbitrationManager; -import bisq.core.support.dispute.arbitration.BuyerDataItem; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.network.p2p.P2PService; @@ -34,7 +26,6 @@ import bisq.common.crypto.CryptoException; import bisq.common.crypto.KeyRing; -import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.Sig; import bisq.common.util.Utilities; @@ -57,26 +48,20 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.Stack; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Nullable; - @Slf4j public class SignedWitnessService { - public static final long CHARGEBACK_SAFETY_DAYS = 30; + public static final long SIGNER_AGE_DAYS = 60; + public static final long SIGNER_AGE = SIGNER_AGE_DAYS * ChronoUnit.DAYS.getDuration().toMillis(); private final KeyRing keyRing; private final P2PService p2PService; - private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; - private final ArbitrationManager arbitrationManager; - private final ChargeBackRisk chargeBackRisk; private final Map signedWitnessMap = new HashMap<>(); @@ -88,18 +73,12 @@ public class SignedWitnessService { @Inject public SignedWitnessService(KeyRing keyRing, P2PService p2PService, - AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, SignedWitnessStorageService signedWitnessStorageService, - AppendOnlyDataStoreService appendOnlyDataStoreService, - ArbitrationManager arbitrationManager, - ChargeBackRisk chargeBackRisk) { + AppendOnlyDataStoreService appendOnlyDataStoreService) { this.keyRing = keyRing; this.p2PService = p2PService; - this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; - this.arbitrationManager = arbitrationManager; - this.chargeBackRisk = chargeBackRisk; // We need to add that early (before onAllServicesInitialized) as it will be used at startup. appendOnlyDataStoreService.addService(signedWitnessStorageService); @@ -128,30 +107,46 @@ public void onAllServicesInitialized() { // API /////////////////////////////////////////////////////////////////////////////////////////// - public List getMyWitnessAgeList(PaymentAccountPayload myPaymentAccountPayload) { - AccountAgeWitness accountAgeWitness = accountAgeWitnessService.getMyWitness(myPaymentAccountPayload); - // We do not validate as it would not make sense to cheat one self... + /** + * List of dates as long when accountAgeWitness was signed + */ + public List getVerifiedWitnessDateList(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() + .filter(this::verifySignature) .map(SignedWitness::getDate) .sorted() .collect(Collectors.toList()); } - - public List getVerifiedWitnessAgeList(AccountAgeWitness accountAgeWitness) { - return signedWitnessMap.values().stream() - .filter(e -> Arrays.equals(e.getWitnessHash(), accountAgeWitness.getHash())) - .filter(this::verifySignature) + /** + * List of dates as long when accountAgeWitness was signed + * Not verifying that signatures are correct + */ + public List getWitnessDateList(AccountAgeWitness accountAgeWitness) { + // We do not validate as it would not make sense to cheat one self... + return getSignedWitnessSet(accountAgeWitness).stream() .map(SignedWitness::getDate) .sorted() .collect(Collectors.toList()); } + public boolean isSignedByArbitrator(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .map(SignedWitness::isSignedByArbitrator) + .findAny() + .orElse(false); + } + // Arbitrators sign with EC key public SignedWitness signAccountAgeWitness(Coin tradeAmount, AccountAgeWitness accountAgeWitness, ECKey key, PublicKey peersPubKey) { + if (isValidAccountAgeWitness(accountAgeWitness)) { + log.warn("Arbitrator trying to sign already signed accountagewitness {}", accountAgeWitness.toString()); + return null; + } + String accountAgeWitnessHashAsHex = Utilities.encodeToHex(accountAgeWitness.getHash()); String signatureBase64 = key.signMessage(accountAgeWitnessHashAsHex); SignedWitness signedWitness = new SignedWitness(true, @@ -162,6 +157,7 @@ public SignedWitness signAccountAgeWitness(Coin tradeAmount, new Date().getTime(), tradeAmount.value); publishSignedWitness(signedWitness); + log.info("Arbitrator signed witness {}", signedWitness.toString()); return signedWitness; } @@ -169,6 +165,11 @@ public SignedWitness signAccountAgeWitness(Coin tradeAmount, public SignedWitness signAccountAgeWitness(Coin tradeAmount, AccountAgeWitness accountAgeWitness, PublicKey peersPubKey) throws CryptoException { + if (isValidAccountAgeWitness(accountAgeWitness)) { + log.warn("Trader trying to sign already signed accountagewitness {}", accountAgeWitness.toString()); + return null; + } + byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); SignedWitness signedWitness = new SignedWitness(false, accountAgeWitness.getHash(), @@ -178,6 +179,7 @@ public SignedWitness signAccountAgeWitness(Coin tradeAmount, new Date().getTime(), tradeAmount.value); publishSignedWitness(signedWitness); + log.info("Trader signed witness {}", signedWitness.toString()); return signedWitness; } @@ -220,7 +222,7 @@ private boolean verifySignatureWithDSAKey(SignedWitness signedWitness) { } } - public Set getSignedWitnessSet(AccountAgeWitness accountAgeWitness) { + private Set getSignedWitnessSet(AccountAgeWitness accountAgeWitness) { return signedWitnessMap.values().stream() .filter(e -> Arrays.equals(e.getWitnessHash(), accountAgeWitness.getHash())) .collect(Collectors.toSet()); @@ -244,8 +246,8 @@ public Set getTrustedPeerSignedWitnessSet(AccountAgeWitness accou // We go one level up by using the signer Key to lookup for SignedWitness objects which contain the signerKey as // witnessOwnerPubKey - public Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey, - Stack excluded) { + private Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey, + Stack excluded) { return signedWitnessMap.values().stream() .filter(e -> Arrays.equals(e.getWitnessOwnerPubKey(), ownerPubKey)) .filter(e -> !excluded.contains(new P2PDataStorage.ByteArray(e.getSignerPubKey()))) @@ -254,7 +256,8 @@ public Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey, /** * Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator. - * @param accountAgeWitness + * + * @param accountAgeWitness accountAgeWitness * @return true if accountAgeWitness is valid, false otherwise. */ public boolean isValidAccountAgeWitness(AccountAgeWitness accountAgeWitness) { @@ -272,9 +275,10 @@ public boolean isValidAccountAgeWitness(AccountAgeWitness accountAgeWitness) { /** * Helper to isValidAccountAgeWitness(accountAgeWitness) - * @param signedWitness the signedWitness to validate + * + * @param signedWitness the signedWitness to validate * @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leave. - * @param excludedPubKeys stack to prevent recursive loops + * @param excludedPubKeys stack to prevent recursive loops * @return true if signedWitness is valid, false otherwise. */ private boolean isValidSignedWitnessInternal(SignedWitness signedWitness, @@ -311,7 +315,8 @@ private boolean isValidSignedWitnessInternal(SignedWitness signedWitness, } private boolean verifyDate(SignedWitness signedWitness, long childSignedWitnessDateMillis) { - long childSignedWitnessDateMinusChargebackPeriodMillis = Instant.ofEpochMilli(childSignedWitnessDateMillis).minus(CHARGEBACK_SAFETY_DAYS, ChronoUnit.DAYS).toEpochMilli(); + long childSignedWitnessDateMinusChargebackPeriodMillis = Instant.ofEpochMilli( + childSignedWitnessDateMillis).minus(SIGNER_AGE, ChronoUnit.MILLIS).toEpochMilli(); long signedWitnessDateMillis = signedWitness.getDate(); return signedWitnessDateMillis <= childSignedWitnessDateMinusChargebackPeriodMillis; } @@ -322,49 +327,14 @@ private boolean verifyDate(SignedWitness signedWitness, long childSignedWitnessD @VisibleForTesting void addToMap(SignedWitness signedWitness) { + // TODO: Perhaps filter out all but one signedwitness per accountagewitness signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness); } private void publishSignedWitness(SignedWitness signedWitness) { if (!signedWitnessMap.containsKey(signedWitness.getHashAsByteArray())) { + log.info("broadcast signed witness {}", signedWitness.toString()); p2PService.addPersistableNetworkPayload(signedWitness, false); } } - - // Arbitrator signing - public List getBuyerPaymentAccounts(long safeDate, PaymentMethod paymentMethod) { - return arbitrationManager.getDisputesAsObservableList().stream() - .filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId())) - .filter(this::hasChargebackRisk) - .filter(this::isBuyerWinner) - .map(this::getBuyerData) - .filter(Objects::nonNull) - .filter(buyerDataItem -> buyerDataItem.getAccountAgeWitness().getDate() < safeDate) - .distinct() - .collect(Collectors.toList()); - } - - private boolean hasChargebackRisk(Dispute dispute) { - return chargeBackRisk.hasChargebackRisk(dispute.getContract().getPaymentMethodId(), - dispute.getContract().getOfferPayload().getCurrencyCode()); - } - - private boolean isBuyerWinner(Dispute dispute) { - return dispute.getDisputeResultProperty().get().getWinner() == DisputeResult.Winner.BUYER; - } - - @Nullable - private BuyerDataItem getBuyerData(Dispute dispute) { - PubKeyRing buyerPubKeyRing = dispute.getContract().getBuyerPubKeyRing(); - PaymentAccountPayload buyerPaymentAccountPaload = dispute.getContract().getBuyerPaymentAccountPayload(); - Optional optionalWitness = accountAgeWitnessService - .findWitness(buyerPaymentAccountPaload, buyerPubKeyRing); - return optionalWitness.map(witness -> new BuyerDataItem( - buyerPaymentAccountPaload, - witness, - dispute.getContract().getTradeAmount(), - dispute.getContract().getSellerPubKeyRing().getSignaturePubKey())) - .orElse(null); - } - } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeRestrictions.java b/core/src/main/java/bisq/core/account/witness/AccountAgeRestrictions.java deleted file mode 100644 index 04c6ab2af43..00000000000 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeRestrictions.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.account.witness; - -import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; -import bisq.core.offer.OfferRestrictions; -import bisq.core.payment.PaymentAccount; -import bisq.core.payment.payload.PaymentMethod; -import bisq.core.trade.Trade; - -import bisq.common.util.Utilities; - -import java.util.Date; -import java.util.GregorianCalendar; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class AccountAgeRestrictions { - private static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.MARCH, 1).getTime(); - - public static boolean isMakersAccountAgeImmature(AccountAgeWitnessService accountAgeWitnessService, Offer offer) { - long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getMakersAccountAge(offer, new Date()); - return accountCreationDate > SAFE_ACCOUNT_AGE_DATE; - } - - public static boolean isTradePeersAccountAgeImmature(AccountAgeWitnessService accountAgeWitnessService, Trade trade) { - long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getTradingPeersAccountAge(trade); - return accountCreationDate > SAFE_ACCOUNT_AGE_DATE; - } - - public static boolean isMyAccountAgeImmature(AccountAgeWitnessService accountAgeWitnessService, PaymentAccount myPaymentAccount) { - long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getMyAccountAge(myPaymentAccount.getPaymentAccountPayload()); - return accountCreationDate > SAFE_ACCOUNT_AGE_DATE; - } - - public static long getMyTradeLimitAtCreateOffer(AccountAgeWitnessService accountAgeWitnessService, - PaymentAccount paymentAccount, - String currencyCode, - OfferPayload.Direction direction) { - if (direction == OfferPayload.Direction.BUY && - PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), currencyCode) && - AccountAgeRestrictions.isMyAccountAgeImmature(accountAgeWitnessService, paymentAccount)) { - return OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; - } else { - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode); - } - } - - public static long getMyTradeLimitAtTakeOffer(AccountAgeWitnessService accountAgeWitnessService, - PaymentAccount paymentAccount, - Offer offer, - String currencyCode, - OfferPayload.Direction direction) { - if (direction == OfferPayload.Direction.BUY && - PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), currencyCode) && - AccountAgeRestrictions.isMakersAccountAgeImmature(accountAgeWitnessService, offer)) { - // Taker is seller - return OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; - } else if (direction == OfferPayload.Direction.SELL && - PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod(), currencyCode) && - AccountAgeRestrictions.isMyAccountAgeImmature(accountAgeWitnessService, paymentAccount)) { - // Taker is buyer - return OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; - } else { - // Offers with no chargeback risk or mature buyer accounts - return accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode); - } - } -} diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 74f7d276d3e..8e7ee37291a 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -17,12 +17,20 @@ package bisq.core.account.witness; +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.sign.SignedWitnessService; import bisq.core.locale.CurrencyUtil; import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OfferRestrictions; import bisq.core.payment.AssetAccount; +import bisq.core.payment.ChargeBackRisk; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.TraderDataItem; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; import bisq.core.user.User; @@ -43,6 +51,7 @@ import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; import javax.inject.Inject; @@ -52,10 +61,13 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Random; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -65,8 +77,10 @@ public class AccountAgeWitnessService { private static final Date RELEASE = Utilities.getUTCDate(2017, GregorianCalendar.NOVEMBER, 11); public static final Date FULL_ACTIVATION = Utilities.getUTCDate(2018, GregorianCalendar.FEBRUARY, 15); + private static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 1).getTime(); public enum AccountAge { + UNVERIFIED, LESS_ONE_MONTH, ONE_TO_TWO_MONTHS, TWO_MONTHS_OR_MORE @@ -75,6 +89,8 @@ public enum AccountAge { private final KeyRing keyRing; private final P2PService p2PService; private final User user; + private final SignedWitnessService signedWitnessService; + private final ChargeBackRisk chargeBackRisk; private final Map accountAgeWitnessMap = new HashMap<>(); @@ -85,12 +101,18 @@ public enum AccountAge { @Inject - public AccountAgeWitnessService(KeyRing keyRing, P2PService p2PService, User user, + public AccountAgeWitnessService(KeyRing keyRing, + P2PService p2PService, + User user, + SignedWitnessService signedWitnessService, + ChargeBackRisk chargeBackRisk, AccountAgeWitnessStorageService accountAgeWitnessStorageService, AppendOnlyDataStoreService appendOnlyDataStoreService) { this.keyRing = keyRing; this.p2PService = p2PService; this.user = user; + this.signedWitnessService = signedWitnessService; + this.chargeBackRisk = chargeBackRisk; // We need to add that early (before onAllServicesInitialized) as it will be used at startup. appendOnlyDataStoreService.addService(accountAgeWitnessStorageService); @@ -164,7 +186,8 @@ private AccountAgeWitness getNewWitness(PaymentAccountPayload paymentAccountPayl return new AccountAgeWitness(hash, new Date().getTime()); } - public Optional findWitness(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { + private Optional findWitness(PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing) { byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, pubKeyRing.getSignaturePubKeyBytes())); @@ -172,6 +195,19 @@ public Optional findWitness(PaymentAccountPayload paymentAcco return getWitnessByHash(hash); } + private Optional findWitness(Offer offer) { + final Optional accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex(); + return accountAgeWitnessHash.isPresent() ? + getWitnessByHashAsHex(accountAgeWitnessHash.get()) : + Optional.empty(); + } + + private Optional findTradePeerWitness(Trade trade) { + TradingPeer tradingPeer = trade.getProcessModel().getTradingPeer(); + return (tradingPeer.getPaymentAccountPayload() == null || tradingPeer.getPubKeyRing() == null) ? + Optional.empty() : findWitness(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing()); + } + private Optional getWitnessByHash(byte[] hash) { P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash); @@ -186,19 +222,64 @@ private Optional getWitnessByHashAsHex(String hashAsHex) { return getWitnessByHash(Utilities.decodeFromHex(hashAsHex)); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Witness age + /////////////////////////////////////////////////////////////////////////////////////////// + public long getAccountAge(AccountAgeWitness accountAgeWitness, Date now) { log.debug("getAccountAge now={}, accountAgeWitness.getDate()={}", now.getTime(), accountAgeWitness.getDate()); return now.getTime() - accountAgeWitness.getDate(); } + // Return -1 if no witness found public long getAccountAge(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) { return findWitness(paymentAccountPayload, pubKeyRing) .map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date())) .orElse(-1L); } - public AccountAge getAccountAgeCategory(long accountAge) { - if (accountAge < TimeUnit.DAYS.toMillis(30)) { + /////////////////////////////////////////////////////////////////////////////////////////// + // Signed age + /////////////////////////////////////////////////////////////////////////////////////////// + + // Return -1 if not signed + public long getWitnessSignAge(AccountAgeWitness accountAgeWitness, Date now) { + List dates = signedWitnessService.getVerifiedWitnessDateList(accountAgeWitness); + if (dates.isEmpty()) { + return -1L; + } else { + return now.getTime() - dates.get(0); + } + } + + // Return -1 if not signed + public long getWitnessSignAge(Offer offer, Date now) { + return findWitness(offer) + .map(witness -> getWitnessSignAge(witness, now)) + .orElse(-1L); + } + + // Return -1 if not signed + public long getWitnessSignAge(Trade trade, Date now) { + TradingPeer tradingPeer = trade.getProcessModel().getTradingPeer(); + if (tradingPeer.getPaymentAccountPayload() == null || tradingPeer.getPubKeyRing() == null) { + // unexpected + return -1; + } + + return findWitness(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing()) + .map(witness -> getWitnessSignAge(witness, now)) + .orElse(-1L); + } + + public AccountAge getPeersAccountAgeCategory(long peersAccountAge) { + return getAccountAgeCategory(peersAccountAge); + } + + private AccountAge getAccountAgeCategory(long accountAge) { + if (accountAge < 0) { + return AccountAge.UNVERIFIED; + } else if (accountAge < TimeUnit.DAYS.toMillis(30)) { return AccountAge.LESS_ONE_MONTH; } else if (accountAge < TimeUnit.DAYS.toMillis(60)) { return AccountAge.ONE_TO_TWO_MONTHS; @@ -207,41 +288,72 @@ public AccountAge getAccountAgeCategory(long accountAge) { } } - private long getTradeLimit(Coin maxTradeLimit, String currencyCode, Optional accountAgeWitnessOptional, Date now) { + // Checks trade limit based on time since signing of AccountAgeWitness + private long getTradeLimit(Coin maxTradeLimit, + String currencyCode, + AccountAgeWitness accountAgeWitness, + AccountAge accountAgeCategory, + OfferPayload.Direction direction) { if (CurrencyUtil.isFiatCurrency(currencyCode)) { double factor; - final long accountAge = getAccountAge((accountAgeWitnessOptional.get()), now); - AccountAge accountAgeCategory = accountAgeWitnessOptional - .map(accountAgeWitness -> getAccountAgeCategory(accountAge)) - .orElse(AccountAge.LESS_ONE_MONTH); - - switch (accountAgeCategory) { - case TWO_MONTHS_OR_MORE: - factor = 1; - break; - case ONE_TO_TWO_MONTHS: - factor = 0.5; - break; - case LESS_ONE_MONTH: - default: - factor = 0.25; - break; + long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value; + if (direction == OfferPayload.Direction.BUY) { + switch (accountAgeCategory) { + case TWO_MONTHS_OR_MORE: + factor = 1; + break; + case ONE_TO_TWO_MONTHS: + factor = 0.25; + break; + case LESS_ONE_MONTH: + case UNVERIFIED: + default: + factor = 0; + } + } else { + switch (accountAgeCategory) { + case TWO_MONTHS_OR_MORE: + factor = 1; + break; + case ONE_TO_TWO_MONTHS: + factor = 0.5; + break; + case LESS_ONE_MONTH: + case UNVERIFIED: + factor = 0.25; + break; + default: + factor = 0; + } + } + if (factor > 0) { + limit = MathUtils.roundDoubleToLong((double) maxTradeLimit.value * factor); } - final long limit = MathUtils.roundDoubleToLong((double) maxTradeLimit.value * factor); - log.debug("accountAgeCategory={}, accountAge={}, limit={}, factor={}, accountAgeWitnessHash={}", + log.debug("accountAgeCategory={}, limit={}, factor={}, accountAgeWitnessHash={}", accountAgeCategory, - accountAge / TimeUnit.DAYS.toMillis(1) + " days", Coin.valueOf(limit).toFriendlyString(), factor, - accountAgeWitnessOptional.map(accountAgeWitness -> Utilities.bytesAsHexString(accountAgeWitness.getHash())).orElse("accountAgeWitnessOptional not present")); + Utilities.bytesAsHexString(accountAgeWitness.getHash())); return limit; } else { return maxTradeLimit.value; } } + /////////////////////////////////////////////////////////////////////////////////////////// + // Mature witness checks + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isImmature(AccountAgeWitness accountAgeWitness) { + return accountAgeWitness.getDate() > SAFE_ACCOUNT_AGE_DATE && + getWitnessSignAge(accountAgeWitness, new Date()) < 0; + } + + public boolean isMyAccountAgeImmature(PaymentAccount myPaymentAccount) { + return isImmature(getMyWitness(myPaymentAccount.getPaymentAccountPayload())); + } /////////////////////////////////////////////////////////////////////////////////////////// // My witness @@ -264,44 +376,26 @@ public long getMyAccountAge(PaymentAccountPayload paymentAccountPayload) { return getAccountAge(getMyWitness(paymentAccountPayload), new Date()); } - public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode) { + public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode, OfferPayload.Direction + direction) { if (paymentAccount == null) return 0; - Optional witnessOptional = Optional.of(getMyWitness(paymentAccount.getPaymentAccountPayload())); - return getTradeLimit(paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(currencyCode), - currencyCode, - witnessOptional, - new Date()); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Peers witness - /////////////////////////////////////////////////////////////////////////////////////////// - - // Return -1 if witness data is not found (old versions) - public long getMakersAccountAge(Offer offer, Date peersCurrentDate) { - final Optional accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex(); - final Optional witnessByHashAsHex = accountAgeWitnessHash.isPresent() ? - getWitnessByHashAsHex(accountAgeWitnessHash.get()) : - Optional.empty(); - return witnessByHashAsHex - .map(accountAgeWitness -> getAccountAge(accountAgeWitness, peersCurrentDate)) - .orElse(-1L); - } - - public long getTradingPeersAccountAge(Trade trade) { - TradingPeer tradingPeer = trade.getProcessModel().getTradingPeer(); - if (tradingPeer.getPaymentAccountPayload() == null || tradingPeer.getPubKeyRing() == null) { - // unexpected - return -1; + AccountAgeWitness accountAgeWitness = getMyWitness(paymentAccount.getPaymentAccountPayload()); + Coin maxTradeLimit = paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(currencyCode); + if (!isImmature(accountAgeWitness)) { + return maxTradeLimit.value; } + final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date()); + AccountAge accountAgeCategory = getAccountAgeCategory(accountSignAge); - return getAccountAge(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing()); + return getTradeLimit(maxTradeLimit, + currencyCode, + accountAgeWitness, + accountAgeCategory, + direction); } - /////////////////////////////////////////////////////////////////////////////////////////// // Verification /////////////////////////////////////////////////////////////////////////////////////////// @@ -343,7 +437,8 @@ public boolean verifyAccountAgeWitness(Trade trade, return false; // Check if the peers trade limit is not less than the trade amount - if (!verifyPeersTradeLimit(trade, peersWitness, peersCurrentDate, errorMessageHandler)) { + if (!verifyPeersTradeLimit(trade.getOffer(), trade.getTradeAmount(), peersWitness, peersCurrentDate, + errorMessageHandler)) { log.error("verifyPeersTradeLimit failed: peersPaymentAccountPayload {}", peersPaymentAccountPayload); return false; } @@ -351,12 +446,22 @@ public boolean verifyAccountAgeWitness(Trade trade, return verifySignature(peersPubKeyRing.getSignaturePubKey(), nonce, signature, errorMessageHandler); } + public boolean verifyPeersTradeAmount(Offer offer, + Coin tradeAmount, + ErrorMessageHandler errorMessageHandler) { + checkNotNull(offer); + return findWitness(offer) + .map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler)) + .orElse(false); + } /////////////////////////////////////////////////////////////////////////////////////////// // Package scope verification subroutines /////////////////////////////////////////////////////////////////////////////////////////// - boolean isDateAfterReleaseDate(long witnessDateAsLong, Date ageWitnessReleaseDate, ErrorMessageHandler errorMessageHandler) { + boolean isDateAfterReleaseDate(long witnessDateAsLong, + Date ageWitnessReleaseDate, + ErrorMessageHandler errorMessageHandler) { // Release date minus 1 day as tolerance for not synced clocks Date releaseDateWithTolerance = new Date(ageWitnessReleaseDate.getTime() - TimeUnit.DAYS.toMillis(1)); final Date witnessDate = new Date(witnessDateAsLong); @@ -394,16 +499,23 @@ private boolean verifyWitnessHash(byte[] witnessHash, return result; } - private boolean verifyPeersTradeLimit(Trade trade, + private boolean verifyPeersTradeLimit(Offer offer, + Coin tradeAmount, AccountAgeWitness peersWitness, Date peersCurrentDate, ErrorMessageHandler errorMessageHandler) { - Offer offer = trade.getOffer(); - Coin tradeAmount = checkNotNull(trade.getTradeAmount()); checkNotNull(offer); final String currencyCode = offer.getCurrencyCode(); final Coin defaultMaxTradeLimit = PaymentMethod.getPaymentMethodById(offer.getOfferPayload().getPaymentMethodId()).getMaxTradeLimitAsCoin(currencyCode); - long peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, Optional.of(peersWitness), peersCurrentDate); + long peersCurrentTradeLimit = defaultMaxTradeLimit.value; + if (isImmature(peersWitness)) { + final long accountSignAge = getWitnessSignAge(peersWitness, peersCurrentDate); + AccountAge accountAgeCategory = getPeersAccountAgeCategory(accountSignAge); + OfferPayload.Direction direction = offer.isMyOffer(keyRing) ? + offer.getMirroredDirection() : offer.getDirection(); + peersCurrentTradeLimit = getTradeLimit(defaultMaxTradeLimit, currencyCode, peersWitness, + accountAgeCategory, direction); + } // Makers current trade limit cannot be smaller than that in the offer boolean result = tradeAmount.value <= peersCurrentTradeLimit; if (!result) { @@ -436,4 +548,107 @@ boolean verifySignature(PublicKey peersPublicKey, } return result; } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Witness signing + /////////////////////////////////////////////////////////////////////////////////////////// + + public SignedWitness arbitratorSignAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + ECKey key, + PublicKey peersPubKey) { + return signedWitnessService.signAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey); + } + + public void traderSignPeersAccountAgeWitness(Trade trade) { + AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); + Coin tradeAmount = trade.getTradeAmount(); + checkNotNull(trade.getProcessModel().getTradingPeer().getPubKeyRing(), "Peer must have a keyring"); + PublicKey peersPubKey = trade.getProcessModel().getTradingPeer().getPubKeyRing().getSignaturePubKey(); + checkNotNull(peersWitness, "Not able to find peers witness, unable to sign for trade {}", trade.toString()); + checkNotNull(tradeAmount, "Trade amount must not be null"); + checkNotNull(peersPubKey, "Peers pub key must not be null"); + + try { + signedWitnessService.signAccountAgeWitness(tradeAmount, peersWitness, peersPubKey); + } catch (CryptoException e) { + log.warn("Trader failed to sign witness, exception {}", e.toString()); + } + } + + // Arbitrator signing + public List getTraderPaymentAccounts(long safeDate, PaymentMethod paymentMethod, + List disputes) { + return disputes.stream() + .filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId())) + .filter(this::hasChargebackRisk) + .filter(this::isBuyerWinner) + .flatMap(this::getTraderData) + .filter(traderDataItem -> + !signedWitnessService.isValidAccountAgeWitness(traderDataItem.getAccountAgeWitness())) + .filter(traderDataItem -> traderDataItem.getAccountAgeWitness().getDate() < safeDate) + .distinct() + .collect(Collectors.toList()); + } + + private boolean hasChargebackRisk(Dispute dispute) { + return chargeBackRisk.hasChargebackRisk(dispute.getContract().getPaymentMethodId(), + dispute.getContract().getOfferPayload().getCurrencyCode()); + } + + private boolean isBuyerWinner(Dispute dispute) { + if (!dispute.isClosed() || dispute.getDisputeResultProperty() == null) + return false; + return dispute.getDisputeResultProperty().get().getWinner() == DisputeResult.Winner.BUYER; + } + + private Stream getTraderData(Dispute dispute) { + Coin tradeAmount = dispute.getContract().getTradeAmount(); + + PubKeyRing buyerPubKeyRing = dispute.getContract().getBuyerPubKeyRing(); + PubKeyRing sellerPubKeyRing = dispute.getContract().getSellerPubKeyRing(); + + PaymentAccountPayload buyerPaymentAccountPaload = dispute.getContract().getBuyerPaymentAccountPayload(); + PaymentAccountPayload sellerPaymentAccountPaload = dispute.getContract().getSellerPaymentAccountPayload(); + + TraderDataItem buyerData = findWitness(buyerPaymentAccountPaload, buyerPubKeyRing) + .map(witness -> new TraderDataItem( + buyerPaymentAccountPaload, + witness, + tradeAmount, + sellerPubKeyRing.getSignaturePubKey())) + .orElse(null); + TraderDataItem sellerData = findWitness(sellerPaymentAccountPaload, sellerPubKeyRing) + .map(witness -> new TraderDataItem( + sellerPaymentAccountPaload, + witness, + tradeAmount, + buyerPubKeyRing.getSignaturePubKey())) + .orElse(null); + return Stream.of(buyerData, sellerData); + } + + // Check if my account has a signed witness + public boolean myHasSignedWitness(PaymentAccountPayload paymentAccountPayload) { + return signedWitnessService.isValidAccountAgeWitness(getMyWitness(paymentAccountPayload)); + } + + public boolean hasSignedWitness(Offer offer) { + return findWitness(offer) + .map(signedWitnessService::isValidAccountAgeWitness) + .orElse(false); + } + + public boolean peerHasSignedWitness(Trade trade) { + return findTradePeerWitness(trade) + .map(signedWitnessService::isValidAccountAgeWitness) + .orElse(false); + } + + public boolean accountIsSigner(AccountAgeWitness accountAgeWitness) { + if (signedWitnessService.isSignedByArbitrator(accountAgeWitness)) { + return true; + } + return getWitnessSignAge(accountAgeWitness, new Date()) > SignedWitnessService.SIGNER_AGE; + } } diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 18763753fcf..897899cc223 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -49,6 +49,8 @@ import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.TradeManager; import bisq.core.trade.statistics.AssetTradeActivityCheck; @@ -131,11 +133,13 @@ public interface BisqSetupCompleteListener { private final PriceFeedService priceFeedService; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; private final P2PService p2PService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; + private final RefundManager refundManager; private final TraderChatManager traderChatManager; private final Preferences preferences; private final User user; @@ -213,11 +217,13 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, PriceFeedService priceFeedService, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, P2PService p2PService, TradeManager tradeManager, OpenOfferManager openOfferManager, ArbitrationManager arbitrationManager, MediationManager mediationManager, + RefundManager refundManager, TraderChatManager traderChatManager, Preferences preferences, User user, @@ -257,11 +263,13 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, this.priceFeedService = priceFeedService; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; this.p2PService = p2PService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; + this.refundManager = refundManager; this.traderChatManager = traderChatManager; this.preferences = preferences; this.user = user; @@ -428,10 +436,10 @@ private void maybeReSyncSPVChain() { } private void maybeShowTac() { - if (!preferences.isTacAccepted() && !DevEnv.isDevMode()) { + if (!preferences.isTacAcceptedV120() && !DevEnv.isDevMode()) { if (displayTacHandler != null) displayTacHandler.accept(() -> { - preferences.setTacAccepted(true); + preferences.setTacAcceptedV120(true); step2(); }); } else { @@ -618,6 +626,7 @@ private void initDomainServices() { arbitrationManager.onAllServicesInitialized(); mediationManager.onAllServicesInitialized(); + refundManager.onAllServicesInitialized(); traderChatManager.onAllServicesInitialized(); tradeManager.onAllServicesInitialized(); @@ -631,6 +640,7 @@ private void initDomainServices() { arbitratorManager.onAllServicesInitialized(); mediatorManager.onAllServicesInitialized(); + refundAgentManager.onAllServicesInitialized(); alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> displayAlertIfPresent(newValue, false)); diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index 984211c451e..cf85da0b98d 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -22,6 +22,8 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.closed.ClosedTradableManager; @@ -52,6 +54,7 @@ public class Balances { private final OpenOfferManager openOfferManager; private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; + private final RefundManager refundManager; @Getter private final ObjectProperty availableBalance = new SimpleObjectProperty<>(); @@ -65,17 +68,20 @@ public Balances(TradeManager tradeManager, BtcWalletService btcWalletService, OpenOfferManager openOfferManager, ClosedTradableManager closedTradableManager, - FailedTradesManager failedTradesManager) { + FailedTradesManager failedTradesManager, + RefundManager refundManager) { this.tradeManager = tradeManager; this.btcWalletService = btcWalletService; this.openOfferManager = openOfferManager; this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; + this.refundManager = refundManager; } public void onAllServicesInitialized() { openOfferManager.getObservableList().addListener((ListChangeListener) c -> updateBalance()); tradeManager.getTradableList().addListener((ListChangeListener) change -> updateBalance()); + refundManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> updateBalance()); btcWalletService.addBalanceListener(new BalanceListener() { @Override public void onBalanceChanged(Coin balance, Transaction tx) { diff --git a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java index 9a2e1c76ba5..c0fbfb5b682 100644 --- a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java +++ b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java @@ -126,6 +126,25 @@ private Tuple2 getEstimatedFeeAndTxSize(boolean isTaker, return new Tuple2<>(txFee, size); } + public Tuple2 getEstimatedFeeAndTxSize(Coin amount, + FeeService feeService, + BtcWalletService btcWalletService) { + Coin txFeePerByte = feeService.getTxFeePerByte(); + // We start with min taker fee size of 260 + int estimatedTxSize = TYPICAL_TX_WITH_1_INPUT_SIZE; + try { + estimatedTxSize = getEstimatedTxSize(List.of(amount), estimatedTxSize, txFeePerByte, btcWalletService); + } catch (InsufficientMoneyException e) { + log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + + "if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize); + } + + Coin txFee = txFeePerByte.multiply(estimatedTxSize); + log.info("Fee estimation resulted in a tx size of {} bytes and a tx fee of {} Sat.", estimatedTxSize, txFee.value); + + return new Tuple2<>(txFee, estimatedTxSize); + } + // We start with the initialEstimatedTxSize for a tx with 1 input (260) bytes and get from BitcoinJ a tx back which // contains the required inputs to fund that tx (outputs + miner fee). The miner fee in that case is based on // the assumption that we only need 1 input. Once we receive back the real tx size from the tx BitcoinJ has created diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 0e0ee6f0e9d..4841551cb9a 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -1114,4 +1114,55 @@ private SendRequest getSendRequestForMultipleAddresses(Set fromAddresses protected boolean isDustAttackUtxo(TransactionOutput output) { return output.getValue().value < preferences.getIgnoreDustThreshold(); } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Refund payoutTx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction createRefundPayoutTx(Coin buyerAmount, + Coin sellerAmount, + Coin fee, + String buyerAddressString, + String sellerAddressString) + throws AddressFormatException, InsufficientMoneyException, WalletException, TransactionVerificationException { + Transaction tx = new Transaction(params); + Preconditions.checkArgument(buyerAmount.add(sellerAmount).isPositive(), + "The sellerAmount + buyerAmount must be positive."); + // buyerAmount can be 0 + if (buyerAmount.isPositive()) { + Preconditions.checkArgument(Restrictions.isAboveDust(buyerAmount), + "The buyerAmount is too low (dust limit)."); + + tx.addOutput(buyerAmount, Address.fromBase58(params, buyerAddressString)); + } + // sellerAmount can be 0 + if (sellerAmount.isPositive()) { + Preconditions.checkArgument(Restrictions.isAboveDust(sellerAmount), + "The sellerAmount is too low (dust limit)."); + + tx.addOutput(sellerAmount, Address.fromBase58(params, sellerAddressString)); + } + + SendRequest sendRequest = SendRequest.forTx(tx); + sendRequest.fee = fee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.aesKey = aesKey; + sendRequest.shuffleOutputs = false; + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + sendRequest.changeAddress = getFreshAddressEntry().getAddress(); + + checkNotNull(wallet); + wallet.completeTx(sendRequest); + + Transaction resultTx = sendRequest.tx; + checkWalletConsistency(wallet); + verifyTransaction(resultTx); + + WalletService.printTx("createRefundPayoutTx", resultTx); + + return resultTx; + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index ac18d14e10a..798ae449c5f 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -33,13 +33,11 @@ import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; @@ -72,46 +70,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -// TradeService handles all relevant transactions used in the trade process -/* - To maintain a consistent tx structure we use that structure: - Always buyers in/outputs/keys first then sellers in/outputs/keys the arbitrators outputs/keys. - - Deposit tx: - IN[0] buyer (mandatory) e.g. 0.1 BTC - IN[...] optional additional buyer inputs (normally never used as we pay from trade fee tx and always have 1 output there) - IN[...] seller (mandatory) e.g. 1.1001 BTC - IN[...] optional additional seller inputs (normally never used as we pay from trade fee tx and always have 1 output there) - OUT[0] Multisig output (include tx fee for payout tx) e.g. 1.2001 - OUT[1] OP_RETURN with hash of contract and 0 BTC amount - OUT[...] optional buyer change (normally never used as we pay from trade fee tx and always have 1 output there) - OUT[...] optional seller change (normally never used as we pay from trade fee tx and always have 1 output there) - FEE tx fee 0.0001 BTC - - Payout tx: - IN[0] Multisig output from deposit Tx (signed by buyer and trader) - OUT[0] Buyer payout address - OUT[1] Seller payout address - - We use 0 confirmation transactions to make the trade process practical from usability side. - There is no risk for double spends as the deposit transaction would become invalid if any preceding transaction would have been double spent. - If a preceding transaction in the chain will not make it into the same or earlier block as the deposit transaction the deposit transaction - will be invalid as well. - Though the deposit need 1 confirmation before the buyer starts the Fiat payment. - - We have that chain of transactions: - 1. Deposit from external wallet to our trading wallet: Tx0 (0 conf) - 2. Create offer (or take offer) fee payment from Tx0 output: tx1 (0 conf) - 3. Deposit tx created with inputs from tx1 of both traders: Tx2 (here we wait for 1 conf) - - Fiat transaction will not start before we get at least 1 confirmation for the deposit tx, then we can proceed. - 4. Payout tx with input from MS output and output to both traders: Tx3 (0 conf) - 5. Withdrawal to external wallet from Tx3: Tx4 (0 conf) - - After the payout transaction we also don't have issues with 0 conf or if not both tx (payout, withdrawal) make it into a block. - Worst case is to rebroadcast the transactions (TODO: is not implemented yet). - - */ public class TradeWalletService { private static final Logger log = LoggerFactory.getLogger(TradeWalletService.class); @@ -178,16 +136,7 @@ public Transaction createBtcTradingFeeTx(Address fundingAddress, Coin txFee, String feeReceiverAddresses, boolean doBroadcast, - @Nullable TxBroadcaster.Callback callback) - throws InsufficientMoneyException, AddressFormatException { - log.debug("fundingAddress {}", fundingAddress); - log.debug("reservedForTradeAddress {}", reservedForTradeAddress); - log.debug("changeAddress {}", changeAddress); - log.info("reservedFundsForOffer {}", reservedFundsForOffer.toPlainString()); - log.debug("useSavingsWallet {}", useSavingsWallet); - log.info("tradingFee {}", tradingFee.toPlainString()); - log.info("txFee {}", txFee.toPlainString()); - log.debug("feeReceiverAddresses {}", feeReceiverAddresses); + @Nullable TxBroadcaster.Callback callback) throws InsufficientMoneyException, AddressFormatException { Transaction tradingFeeTx = new Transaction(params); SendRequest sendRequest = null; try { @@ -220,17 +169,18 @@ public Transaction createBtcTradingFeeTx(Address fundingAddress, wallet.completeTx(sendRequest); WalletService.printTx("tradingFeeTx", tradingFeeTx); - if (doBroadcast && callback != null) + if (doBroadcast && callback != null) { broadcastTx(tradingFeeTx, callback); + } return tradingFeeTx; } catch (Throwable t) { - if (wallet != null && sendRequest != null && sendRequest.coinSelector != null) - log.warn("Balance = {}; CoinSelector = {}", - wallet.getBalance(sendRequest.coinSelector), - sendRequest.coinSelector); + if (wallet != null && sendRequest != null && sendRequest.coinSelector != null) { + log.warn("Balance = {}; CoinSelector = {}", wallet.getBalance(sendRequest.coinSelector), sendRequest.coinSelector); + } - log.warn("createBtcTradingFeeTx failed: tradingFeeTx={}, txOutputs={}", tradingFeeTx.toString(), tradingFeeTx.getOutputs()); + log.warn("createBtcTradingFeeTx failed: tradingFeeTx={}, txOutputs={}", tradingFeeTx.toString(), + tradingFeeTx.getOutputs()); throw t; } } @@ -241,17 +191,8 @@ public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, Address changeAddress, Coin reservedFundsForOffer, boolean useSavingsWallet, - Coin txFee) throws - TransactionVerificationException, WalletException, - InsufficientMoneyException, AddressFormatException { - - log.debug("preparedBsqTx {}", preparedBsqTx); - log.debug("fundingAddress {}", fundingAddress); - log.debug("changeAddress {}", changeAddress); - log.debug("reservedFundsForOffer {}", reservedFundsForOffer.toPlainString()); - log.debug("useSavingsWallet {}", useSavingsWallet); - log.debug("txFee {}", txFee.toPlainString()); - + Coin txFee) + throws TransactionVerificationException, WalletException, InsufficientMoneyException, AddressFormatException { // preparedBsqTx has following structure: // inputs [1-n] BSQ inputs // outputs [0-1] BSQ change output @@ -306,7 +247,8 @@ public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, // Sign all BTC inputs for (int i = preparedBsqTxInputsSize; i < resultTx.getInputs().size(); i++) { TransactionInput txIn = resultTx.getInputs().get(i); - checkArgument(txIn.getConnectedOutput() != null && txIn.getConnectedOutput().isMine(wallet), + checkArgument(txIn.getConnectedOutput() != null && + txIn.getConnectedOutput().isMine(wallet), "txIn.getConnectedOutput() is not in our wallet. That must not happen."); WalletService.signTransactionInput(wallet, aesKey, resultTx, txIn, i); WalletService.checkScriptSig(resultTx, txIn, i); @@ -321,9 +263,10 @@ public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, /////////////////////////////////////////////////////////////////////////////////////////// - // Trade + // Deposit tx /////////////////////////////////////////////////////////////////////////////////////////// + // We construct the deposit transaction in the way that the buyer is always the first entry (inputs, outputs, MS keys) and then the seller. // In the creation of the deposit tx the taker/maker roles are the determining roles instead of buyer/seller. // In the payout tx is is the buyer/seller role. We keep the buyer/seller ordering over all transactions to not get confusion with ordering, @@ -341,18 +284,10 @@ public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, * @return A data container holding the inputs, the output value and address * @throws TransactionVerificationException */ - public InputsAndChangeOutput takerCreatesDepositsTxInputs(Transaction takeOfferFeeTx, - Coin inputAmount, - Coin txFee, - Address takersAddress) throws - TransactionVerificationException { - if (log.isDebugEnabled()) { - log.debug("takerCreatesDepositsTxInputs called"); - log.debug("inputAmount {}", inputAmount.toFriendlyString()); - log.debug("txFee {}", txFee.toFriendlyString()); - log.debug("takersAddress {}", takersAddress.toString()); - } - + public InputsAndChangeOutput takerCreatesDepositTxInputs(Transaction takeOfferFeeTx, + Coin inputAmount, + Coin txFee) + throws TransactionVerificationException { // We add the mining fee 2 times to the deposit tx: // 1. Will be spent when publishing the deposit tx (paid by buyer) // 2. Will be added to the MS amount, so when publishing the payout tx the fee is already there and the outputs are not changed by fee reduction @@ -390,14 +325,13 @@ OUT[0] dummyOutputAmount (inputAmount - tx fee) //WalletService.printTx("dummyTX", dummyTX); - List rawTransactionInputList = dummyTX.getInputs().stream() - .map(e -> { - checkNotNull(e.getConnectedOutput(), "e.getConnectedOutput() must not be null"); - checkNotNull(e.getConnectedOutput().getParentTransaction(), "e.getConnectedOutput().getParentTransaction() must not be null"); - checkNotNull(e.getValue(), "e.getValue() must not be null"); - return getRawInputFromTransactionInput(e); - }) - .collect(Collectors.toList()); + List rawTransactionInputList = dummyTX.getInputs().stream().map(e -> { + checkNotNull(e.getConnectedOutput(), "e.getConnectedOutput() must not be null"); + checkNotNull(e.getConnectedOutput().getParentTransaction(), + "e.getConnectedOutput().getParentTransaction() must not be null"); + checkNotNull(e.getValue(), "e.getValue() must not be null"); + return getRawInputFromTransactionInput(e); + }).collect(Collectors.toList()); // TODO changeOutputValue and changeOutputAddress is not used as taker spends exact amount from fee tx. @@ -408,6 +342,54 @@ OUT[0] dummyOutputAmount (inputAmount - tx fee) return new InputsAndChangeOutput(new ArrayList<>(rawTransactionInputList), 0, null); } + public PreparedDepositTxAndMakerInputs sellerAsMakerCreatesDepositTx(byte[] contractHash, + Coin makerInputAmount, + Coin msOutputAmount, + List takerRawTransactionInputs, + long takerChangeOutputValue, + @Nullable String takerChangeAddressString, + Address makerAddress, + Address makerChangeAddress, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { + return makerCreatesDepositTx(false, + contractHash, + makerInputAmount, + msOutputAmount, + takerRawTransactionInputs, + takerChangeOutputValue, + takerChangeAddressString, + makerAddress, + makerChangeAddress, + buyerPubKey, + sellerPubKey); + } + + public PreparedDepositTxAndMakerInputs buyerAsMakerCreatesAndSignsDepositTx(byte[] contractHash, + Coin makerInputAmount, + Coin msOutputAmount, + List takerRawTransactionInputs, + long takerChangeOutputValue, + @Nullable String takerChangeAddressString, + Address makerAddress, + Address makerChangeAddress, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { + return makerCreatesDepositTx(true, + contractHash, + makerInputAmount, + msOutputAmount, + takerRawTransactionInputs, + takerChangeOutputValue, + takerChangeAddressString, + makerAddress, + makerChangeAddress, + buyerPubKey, + sellerPubKey); + } + /** * The maker creates the deposit transaction using the takers input(s) and optional output and signs his input(s). * @@ -422,38 +404,23 @@ OUT[0] dummyOutputAmount (inputAmount - tx fee) * @param makerChangeAddress The maker's change address. * @param buyerPubKey The public key of the buyer. * @param sellerPubKey The public key of the seller. - * @param arbitratorPubKey The public key of the arbitrator. * @return A data container holding the serialized transaction and the maker raw inputs * @throws SigningException * @throws TransactionVerificationException * @throws WalletException */ - public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean makerIsBuyer, - byte[] contractHash, - Coin makerInputAmount, - Coin msOutputAmount, - List takerRawTransactionInputs, - long takerChangeOutputValue, - @Nullable String takerChangeAddressString, - Address makerAddress, - Address makerChangeAddress, - byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey) + private PreparedDepositTxAndMakerInputs makerCreatesDepositTx(boolean makerIsBuyer, + byte[] contractHash, + Coin makerInputAmount, + Coin msOutputAmount, + List takerRawTransactionInputs, + long takerChangeOutputValue, + @Nullable String takerChangeAddressString, + Address makerAddress, + Address makerChangeAddress, + byte[] buyerPubKey, + byte[] sellerPubKey) throws SigningException, TransactionVerificationException, WalletException, AddressFormatException { - log.debug("makerCreatesAndSignsDepositTx called"); - log.debug("makerIsBuyer {}", makerIsBuyer); - log.debug("makerInputAmount {}", makerInputAmount.toFriendlyString()); - log.debug("msOutputAmount {}", msOutputAmount.toFriendlyString()); - log.debug("takerRawInputs {}", takerRawTransactionInputs.toString()); - log.debug("takerChangeOutputValue {}", takerChangeOutputValue); - log.debug("takerChangeAddressString {}", takerChangeAddressString); - log.debug("makerAddress {}", makerAddress); - log.debug("makerChangeAddress {}", makerChangeAddress); - log.debug("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey)); - log.debug("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey)); - log.debug("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey)); - checkArgument(!takerRawTransactionInputs.isEmpty()); // First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx. @@ -461,7 +428,7 @@ public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean mak Transaction dummyTx = new Transaction(params); TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, makerInputAmount, new ECKey().toAddress(params)); dummyTx.addOutput(dummyOutput); - addAvailableInputsAndChangeOutputs(dummyTx, makerAddress, makerChangeAddress, Coin.ZERO); + addAvailableInputsAndChangeOutputs(dummyTx, makerAddress, makerChangeAddress); // Normally we have only 1 input but we support multiple inputs if the user has paid in with several transactions. List makerInputs = dummyTx.getInputs(); TransactionOutput makerOutput = null; @@ -470,8 +437,9 @@ public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean mak checkArgument(dummyTx.getOutputs().size() < 3, "dummyTx.getOutputs().size() >= 3"); // Only save change outputs, the dummy output is ignored (that's why we start with index 1) - if (dummyTx.getOutputs().size() > 1) + if (dummyTx.getOutputs().size() > 1) { makerOutput = dummyTx.getOutput(1); + } // Now we construct the real deposit tx Transaction preparedDepositTx = new Transaction(params); @@ -505,10 +473,11 @@ public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean mak // Add MultiSig output - Script p2SHMultiSigOutputScript = getP2SHMultiSigOutputScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey); // Tx fee for deposit tx will be paid by buyer. - TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount, p2SHMultiSigOutputScript.getProgram()); + TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, preparedDepositTx, msOutputAmount, + p2SHMultiSigOutputScript.getProgram()); preparedDepositTx.addOutput(p2SHMultiSigOutput); // We add the hash ot OP_RETURN with a 0 amount output @@ -517,31 +486,35 @@ public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean mak preparedDepositTx.addOutput(contractHashOutput); TransactionOutput takerTransactionOutput = null; - if (takerChangeOutputValue > 0 && takerChangeAddressString != null) + if (takerChangeOutputValue > 0 && takerChangeAddressString != null) { takerTransactionOutput = new TransactionOutput(params, preparedDepositTx, Coin.valueOf(takerChangeOutputValue), Address.fromBase58(params, takerChangeAddressString)); + } if (makerIsBuyer) { // Add optional buyer outputs - if (makerOutput != null) + if (makerOutput != null) { preparedDepositTx.addOutput(makerOutput); + } // Add optional seller outputs - if (takerTransactionOutput != null) + if (takerTransactionOutput != null) { preparedDepositTx.addOutput(takerTransactionOutput); + } } else { // taker is buyer role // Add optional seller outputs - if (takerTransactionOutput != null) + if (takerTransactionOutput != null) { preparedDepositTx.addOutput(takerTransactionOutput); + } // Add optional buyer outputs - if (makerOutput != null) + if (makerOutput != null) { preparedDepositTx.addOutput(makerOutput); + } } - // Sign inputs int start = makerIsBuyer ? 0 : takerRawTransactionInputs.size(); int end = makerIsBuyer ? makerInputs.size() : preparedDepositTx.getInputs().size(); for (int i = start; i < end; i++) { @@ -550,8 +523,7 @@ public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean mak WalletService.checkScriptSig(preparedDepositTx, input, i); } - WalletService.printTx("prepared depositTx", preparedDepositTx); - + WalletService.printTx("makerCreatesDepositTx", preparedDepositTx); WalletService.verifyTransaction(preparedDepositTx); return new PreparedDepositTxAndMakerInputs(makerRawTransactionInputs, preparedDepositTx.bitcoinSerialize()); @@ -567,40 +539,28 @@ public PreparedDepositTxAndMakerInputs makerCreatesAndSignsDepositTx(boolean mak * @param sellerInputs The connected outputs for all inputs of the seller. * @param buyerPubKey The public key of the buyer. * @param sellerPubKey The public key of the seller. - * @param arbitratorPubKey The public key of the arbitrator. - * @param callback Callback when transaction is broadcasted. * @throws SigningException * @throws TransactionVerificationException * @throws WalletException */ - public Transaction takerSignsAndPublishesDepositTx(boolean takerIsSeller, - byte[] contractHash, - byte[] makersDepositTxSerialized, - List buyerInputs, - List sellerInputs, - byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey, - TxBroadcaster.Callback callback) throws SigningException, TransactionVerificationException, - WalletException { + public Transaction takerSignsDepositTx(boolean takerIsSeller, + byte[] contractHash, + byte[] makersDepositTxSerialized, + List buyerInputs, + List sellerInputs, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws SigningException, TransactionVerificationException, WalletException { Transaction makersDepositTx = new Transaction(params, makersDepositTxSerialized); - log.debug("signAndPublishDepositTx called"); - log.debug("takerIsSeller {}", takerIsSeller); - log.debug("makersDepositTx {}", makersDepositTx.toString()); - log.debug("buyerConnectedOutputsForAllInputs {}", buyerInputs.toString()); - log.debug("sellerConnectedOutputsForAllInputs {}", sellerInputs.toString()); - log.debug("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString()); - log.debug("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString()); - log.debug("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString()); - checkArgument(!buyerInputs.isEmpty()); checkArgument(!sellerInputs.isEmpty()); // Check if maker's Multisig script is identical to the takers - Script p2SHMultiSigOutputScript = getP2SHMultiSigOutputScript(buyerPubKey, sellerPubKey, arbitratorPubKey); - if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(p2SHMultiSigOutputScript)) + Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey); + if (!makersDepositTx.getOutput(0).getScriptPubKey().equals(p2SHMultiSigOutputScript)) { throw new TransactionVerificationException("Maker's p2SHMultiSigOutputScript does not match to takers p2SHMultiSigOutputScript"); + } // The outpoints are not available from the serialized makersDepositTx, so we cannot use that tx directly, but we use it to construct a new // depositTx @@ -609,22 +569,29 @@ public Transaction takerSignsAndPublishesDepositTx(boolean takerIsSeller, if (takerIsSeller) { // Add buyer inputs and apply signature // We grab the signature from the makersDepositTx and apply it to the new tx input - for (int i = 0; i < buyerInputs.size(); i++) - depositTx.addInput(getTransactionInput(depositTx, getScriptProgram(makersDepositTx, i), buyerInputs.get(i))); + for (int i = 0; i < buyerInputs.size(); i++) { + TransactionInput transactionInput = makersDepositTx.getInputs().get(i); + depositTx.addInput(getTransactionInput(depositTx, getMakersScriptSigProgram(transactionInput), buyerInputs.get(i))); + } // Add seller inputs - for (RawTransactionInput rawTransactionInput : sellerInputs) + for (RawTransactionInput rawTransactionInput : sellerInputs) { depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, rawTransactionInput)); + } } else { // taker is buyer // Add buyer inputs and apply signature - for (RawTransactionInput rawTransactionInput : buyerInputs) + for (RawTransactionInput rawTransactionInput : buyerInputs) { depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, rawTransactionInput)); + } // Add seller inputs // We grab the signature from the makersDepositTx and apply it to the new tx input - for (int i = buyerInputs.size(), k = 0; i < makersDepositTx.getInputs().size(); i++, k++) - depositTx.addInput(getTransactionInput(depositTx, getScriptProgram(makersDepositTx, i), sellerInputs.get(k))); + for (int i = buyerInputs.size(), k = 0; i < makersDepositTx.getInputs().size(); i++, k++) { + TransactionInput transactionInput = makersDepositTx.getInputs().get(i); + // We get the deposit tx unsigned if maker is seller + depositTx.addInput(getTransactionInput(depositTx, new byte[]{}, sellerInputs.get(k))); + } } // Check if OP_RETURN output with contract hash matches the one from the maker @@ -633,12 +600,13 @@ public Transaction takerSignsAndPublishesDepositTx(boolean takerIsSeller, log.debug("contractHashOutput {}", contractHashOutput); TransactionOutput makersContractHashOutput = makersDepositTx.getOutputs().get(1); log.debug("makersContractHashOutput {}", makersContractHashOutput); - if (!makersContractHashOutput.getScriptPubKey().equals(contractHashOutput.getScriptPubKey())) + if (!makersContractHashOutput.getScriptPubKey().equals(contractHashOutput.getScriptPubKey())) { throw new TransactionVerificationException("Maker's transaction output for the contract hash is not matching takers version."); + } // Add all outputs from makersDepositTx to depositTx makersDepositTx.getOutputs().forEach(depositTx::addOutput); - //WalletService.printTx("makersDepositTx", makersDepositTx); + WalletService.printTx("makersDepositTx", makersDepositTx); // Sign inputs int start = takerIsSeller ? buyerInputs.size() : 0; @@ -649,17 +617,113 @@ public Transaction takerSignsAndPublishesDepositTx(boolean takerIsSeller, WalletService.checkScriptSig(depositTx, input, i); } - WalletService.printTx("depositTx", depositTx); + WalletService.printTx("takerSignsDepositTx", depositTx); WalletService.verifyTransaction(depositTx); WalletService.checkWalletConsistency(wallet); - broadcastTx(depositTx, callback); - return depositTx; } + public void sellerAsMakerFinalizesDepositTx(Transaction myDepositTx, Transaction takersDepositTx, int numTakersInputs) + throws TransactionVerificationException, AddressFormatException { + + // We add takers signature from his inputs and add it to out tx which was already signed earlier. + for (int i = 0; i < numTakersInputs; i++) { + TransactionInput input = takersDepositTx.getInput(i); + Script scriptSig = input.getScriptSig(); + myDepositTx.getInput(i).setScriptSig(scriptSig); + } + + WalletService.printTx("sellerAsMakerFinalizesDepositTx", myDepositTx); + WalletService.verifyTransaction(myDepositTx); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delayed payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction createDelayedUnsignedPayoutTx(Transaction depositTx, + String donationAddressString, + Coin minerFee, + long lockTime) + throws AddressFormatException, TransactionVerificationException { + TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0); + Transaction delayedPayoutTx = new Transaction(params); + delayedPayoutTx.addInput(p2SHMultiSigOutput); + applyLockTime(lockTime, delayedPayoutTx); + Coin outputAmount = depositTx.getOutputSum().subtract(minerFee); + delayedPayoutTx.addOutput(outputAmount, Address.fromBase58(params, donationAddressString)); + WalletService.printTx("Unsigned delayedPayoutTx ToDonationAddress", delayedPayoutTx); + WalletService.verifyTransaction(delayedPayoutTx); + return delayedPayoutTx; + } + + public byte[] signDelayedPayoutTx(Transaction delayedPayoutTx, + DeterministicKey myMultiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey) + throws AddressFormatException, TransactionVerificationException { + + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + Sha256Hash sigHash = delayedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); + if (myMultiSigKeyPair.isEncrypted()) { + checkNotNull(aesKey); + } + + ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + WalletService.printTx("delayedPayoutTx for sig creation", delayedPayoutTx); + WalletService.verifyTransaction(delayedPayoutTx); + return mySignature.encodeToDER(); + } + + public Transaction finalizeDelayedPayoutTx(Transaction delayedPayoutTx, + byte[] buyerPubKey, + byte[] sellerPubKey, + byte[] buyerSignature, + byte[] sellerSignature) + throws AddressFormatException, TransactionVerificationException, WalletException { + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + ECKey.ECDSASignature buyerECDSASignature = ECKey.ECDSASignature.decodeFromDER(buyerSignature); + ECKey.ECDSASignature sellerECDSASignature = ECKey.ECDSASignature.decodeFromDER(sellerSignature); + TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false); + TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false); + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); + TransactionInput input = delayedPayoutTx.getInput(0); + input.setScriptSig(inputScript); + WalletService.printTx("finalizeDelayedPayoutTx", delayedPayoutTx); + WalletService.verifyTransaction(delayedPayoutTx); + WalletService.checkWalletConsistency(wallet); + WalletService.checkScriptSig(delayedPayoutTx, input, 0); + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.verify(input.getConnectedOutput()); + return delayedPayoutTx; + } + + public boolean verifiesDepositTxAndDelayedPayoutTx(Transaction depositTx, + Transaction delayedPayoutTx) { + // todo add more checks + if (delayedPayoutTx.getLockTime() == 0) { + log.error("Time lock is not set"); + return false; + } + + if (delayedPayoutTx.getInputs().stream().noneMatch(e -> e.getSequenceNumber() == TransactionInput.NO_SEQUENCE - 1)) { + log.error("Sequence number must be 0xFFFFFFFE"); + return false; + } + + return true; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Standard payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + /** * Seller signs payout transaction, buyer has not signed yet. * @@ -671,7 +735,6 @@ public Transaction takerSignsAndPublishesDepositTx(boolean takerIsSeller, * @param multiSigKeyPair DeterministicKey for MultiSig from seller * @param buyerPubKey The public key of the buyer. * @param sellerPubKey The public key of the seller. - * @param arbitratorPubKey The public key of the arbitrator. * @return DER encoded canonical signature * @throws AddressFormatException * @throws TransactionVerificationException @@ -683,38 +746,21 @@ public byte[] buyerSignsPayoutTx(Transaction depositTx, String sellerPayoutAddressString, DeterministicKey multiSigKeyPair, byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey) + byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException { - log.trace("sellerSignsPayoutTx called"); - log.trace("depositTx {}", depositTx.toString()); - log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString); - log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString); - log.trace("multiSigKeyPair (not displayed for security reasons)"); - log.info("buyerPubKey HEX=" + ECKey.fromPublicOnly(buyerPubKey).getPublicKeyAsHex()); - log.info("sellerPubKey HEX=" + ECKey.fromPublicOnly(sellerPubKey).getPublicKeyAsHex()); - log.info("arbitratorPubKey HEX=" + ECKey.fromPublicOnly(arbitratorPubKey).getPublicKeyAsHex()); - Transaction preparedPayoutTx = createPayoutTx(depositTx, - buyerPayoutAmount, - sellerPayoutAmount, - buyerPayoutAddressString, - sellerPayoutAddressString); + Transaction preparedPayoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, + buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); - if (multiSigKeyPair.isEncrypted()) + if (multiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); - + } ECKey.ECDSASignature buyerSignature = multiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); - WalletService.printTx("prepared payoutTx", preparedPayoutTx); - WalletService.verifyTransaction(preparedPayoutTx); - return buyerSignature.encodeToDER(); } @@ -731,7 +777,6 @@ public byte[] buyerSignsPayoutTx(Transaction depositTx, * @param multiSigKeyPair Buyer's keypair for MultiSig * @param buyerPubKey The public key of the buyer. * @param sellerPubKey The public key of the seller. - * @param arbitratorPubKey The public key of the arbitrator. * @return The payout transaction * @throws AddressFormatException * @throws TransactionVerificationException @@ -745,49 +790,27 @@ public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx, String sellerPayoutAddressString, DeterministicKey multiSigKeyPair, byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey) + byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException, WalletException { - log.trace("buyerSignsAndFinalizesPayoutTx called"); - log.trace("depositTx {}", depositTx.toString()); - log.trace("buyerSignature r {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).r.toString()); - log.trace("buyerSignature s {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).s.toString()); - log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString); - log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString); - log.trace("multiSigKeyPair (not displayed for security reasons)"); - log.info("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString()); - log.info("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString()); - log.info("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString()); - - Transaction payoutTx = createPayoutTx(depositTx, - buyerPayoutAmount, - sellerPayoutAmount, - buyerPayoutAddressString, - sellerPayoutAddressString); + Transaction payoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); - if (multiSigKeyPair.isEncrypted()) + if (multiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); - - + } ECKey.ECDSASignature sellerSignature = multiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); - TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), Transaction.SigHash.ALL, false); TransactionSignature sellerTxSig = new TransactionSignature(sellerSignature, Transaction.SigHash.ALL, false); // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (arbitrator, seller, buyer) - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); - + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), + redeemScript); TransactionInput input = payoutTx.getInput(0); input.setScriptSig(inputScript); - WalletService.printTx("payoutTx", payoutTx); - WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); WalletService.checkScriptSig(payoutTx, input, 0); @@ -798,7 +821,7 @@ public Transaction sellerSignsAndFinalizesPayoutTx(Transaction depositTx, /////////////////////////////////////////////////////////////////////////////////////////// - // Mediation + // Mediated payoutTx /////////////////////////////////////////////////////////////////////////////////////////// public byte[] signMediatedPayoutTx(Transaction depositTx, @@ -808,38 +831,20 @@ public byte[] signMediatedPayoutTx(Transaction depositTx, String sellerPayoutAddressString, DeterministicKey myMultiSigKeyPair, byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey) + byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException { - log.trace("signMediatedPayoutTx called"); - log.trace("depositTx {}", depositTx.toString()); - log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString); - log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString); - log.trace("multiSigKeyPair (not displayed for security reasons)"); - log.trace("buyerPubKey HEX=" + ECKey.fromPublicOnly(buyerPubKey).getPublicKeyAsHex()); - log.trace("sellerPubKey HEX=" + ECKey.fromPublicOnly(sellerPubKey).getPublicKeyAsHex()); - log.trace("arbitratorPubKey HEX=" + ECKey.fromPublicOnly(arbitratorPubKey).getPublicKeyAsHex()); - Transaction preparedPayoutTx = createPayoutTx(depositTx, - buyerPayoutAmount, - sellerPayoutAmount, - buyerPayoutAddressString, - sellerPayoutAddressString); + Transaction preparedPayoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); - if (myMultiSigKeyPair.isEncrypted()) + if (myMultiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); - + } ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); - WalletService.printTx("prepared mediated payoutTx for sig creation", preparedPayoutTx); - WalletService.verifyTransaction(preparedPayoutTx); - return mySignature.encodeToDER(); } @@ -852,47 +857,22 @@ public Transaction finalizeMediatedPayoutTx(Transaction depositTx, String sellerPayoutAddressString, DeterministicKey multiSigKeyPair, byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey) + byte[] sellerPubKey) throws AddressFormatException, TransactionVerificationException, WalletException { - log.trace("finalizeMediatedPayoutTx called"); - log.trace("depositTx {}", depositTx.toString()); - log.trace("buyerSignature r {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).r.toString()); - log.trace("buyerSignature s {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).s.toString()); - log.trace("sellerSignature r {}", ECKey.ECDSASignature.decodeFromDER(sellerSignature).r.toString()); - log.trace("sellerSignature s {}", ECKey.ECDSASignature.decodeFromDER(sellerSignature).s.toString()); - log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString); - log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString); - log.trace("multiSigKeyPair (not displayed for security reasons)"); - log.trace("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString()); - log.trace("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString()); - log.trace("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString()); - - Transaction payoutTx = createPayoutTx(depositTx, - buyerPayoutAmount, - sellerPayoutAmount, - buyerPayoutAddressString, - sellerPayoutAddressString); + Transaction payoutTx = createPayoutTx(depositTx, buyerPayoutAmount, sellerPayoutAmount, buyerPayoutAddressString, sellerPayoutAddressString); // MS redeemScript - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); // MS output from prev. tx is index 0 checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); - TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), Transaction.SigHash.ALL, false); TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature), Transaction.SigHash.ALL, false); - // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (arbitrator, seller, buyer) Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); - TransactionInput input = payoutTx.getInput(0); input.setScriptSig(inputScript); - WalletService.printTx("mediated payoutTx", payoutTx); - WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); WalletService.checkScriptSig(payoutTx, input, 0); @@ -903,7 +883,7 @@ public Transaction finalizeMediatedPayoutTx(Transaction depositTx, /////////////////////////////////////////////////////////////////////////////////////////// - // Arbitration + // Arbitrated payoutTx /////////////////////////////////////////////////////////////////////////////////////////// /** @@ -933,39 +913,27 @@ public byte[] arbitratorSignsDisputedPayoutTx(byte[] depositTxSerialized, byte[] arbitratorPubKey) throws AddressFormatException, TransactionVerificationException { Transaction depositTx = new Transaction(params, depositTxSerialized); - log.trace("signDisputedPayoutTx called"); - log.trace("depositTx {}", depositTx.toString()); - log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.trace("buyerAddressString {}", buyerAddressString); - log.trace("sellerAddressString {}", sellerAddressString); - log.trace("arbitratorKeyPair (not displayed for security reasons)"); - log.info("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString()); - log.info("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString()); - log.info("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString()); - // Our MS is index 0 TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0); Transaction preparedPayoutTx = new Transaction(params); preparedPayoutTx.addInput(p2SHMultiSigOutput); - if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) + if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) { preparedPayoutTx.addOutput(buyerPayoutAmount, Address.fromBase58(params, buyerAddressString)); - if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) + } + if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) { preparedPayoutTx.addOutput(sellerPayoutAmount, Address.fromBase58(params, sellerAddressString)); + } // take care of sorting! - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); checkNotNull(arbitratorKeyPair, "arbitratorKeyPair must not be null"); - if (arbitratorKeyPair.isEncrypted()) + if (arbitratorKeyPair.isEncrypted()) { checkNotNull(aesKey); - + } ECKey.ECDSASignature arbitratorSignature = arbitratorKeyPair.sign(sigHash, aesKey).toCanonicalised(); - WalletService.verifyTransaction(preparedPayoutTx); - - //WalletService.printTx("preparedPayoutTx", preparedPayoutTx); - + WalletService.printTx("preparedPayoutTx", preparedPayoutTx); return arbitratorSignature.encodeToDER(); } @@ -999,47 +967,33 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali byte[] arbitratorPubKey) throws AddressFormatException, TransactionVerificationException, WalletException { Transaction depositTx = new Transaction(params, depositTxSerialized); - - log.trace("signAndFinalizeDisputedPayoutTx called"); - log.trace("depositTx {}", depositTx); - log.trace("arbitratorSignature r {}", ECKey.ECDSASignature.decodeFromDER(arbitratorSignature).r.toString()); - log.trace("arbitratorSignature s {}", ECKey.ECDSASignature.decodeFromDER(arbitratorSignature).s.toString()); - log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.trace("buyerAddressString {}", buyerAddressString); - log.trace("sellerAddressString {}", sellerAddressString); - log.trace("tradersMultiSigKeyPair (not displayed for security reasons)"); - log.info("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString()); - log.info("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString()); - log.info("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString()); - - TransactionOutput p2SHMultiSigOutput = depositTx.getOutput(0); Transaction payoutTx = new Transaction(params); payoutTx.addInput(p2SHMultiSigOutput); - if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) + if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) { payoutTx.addOutput(buyerPayoutAmount, Address.fromBase58(params, buyerAddressString)); - if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) + } + if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) { payoutTx.addOutput(sellerPayoutAmount, Address.fromBase58(params, sellerAddressString)); + } // take care of sorting! - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); checkNotNull(tradersMultiSigKeyPair, "tradersMultiSigKeyPair must not be null"); - if (tradersMultiSigKeyPair.isEncrypted()) + if (tradersMultiSigKeyPair.isEncrypted()) { checkNotNull(aesKey); + } ECKey.ECDSASignature tradersSignature = tradersMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); - TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false); TransactionSignature arbitratorTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(arbitratorSignature), Transaction.SigHash.ALL, false); // Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer) - Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(arbitratorTxSig, tradersTxSig), redeemScript); + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(arbitratorTxSig, tradersTxSig), + redeemScript); TransactionInput input = payoutTx.getInput(0); input.setScriptSig(inputScript); - WalletService.printTx("disputed payoutTx", payoutTx); - WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); WalletService.checkScriptSig(payoutTx, input, 0); @@ -1049,49 +1003,38 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali } + /////////////////////////////////////////////////////////////////////////////////////////// + // Emergency payoutTx + /////////////////////////////////////////////////////////////////////////////////////////// + + // Emergency payout tool. Used only in cased when the payput from the arbitrator does not work because some data // in the trade/dispute are messed up. // We keep here arbitratorPayoutAmount just in case (requires cooperation from peer anyway) - public Transaction emergencySignAndPublishPayoutTx(String depositTxHex, - Coin buyerPayoutAmount, - Coin sellerPayoutAmount, - Coin arbitratorPayoutAmount, - Coin txFee, - String buyerAddressString, - String sellerAddressString, - String arbitratorAddressString, - @Nullable String buyerPrivateKeyAsHex, - @Nullable String sellerPrivateKeyAsHex, - String arbitratorPrivateKeyAsHex, - String buyerPubKeyAsHex, - String sellerPubKeyAsHex, - String arbitratorPubKeyAsHex, - String P2SHMultiSigOutputScript, - TxBroadcaster.Callback callback) + public Transaction emergencySignAndPublishPayoutTxFrom2of3MultiSig(String depositTxHex, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + Coin arbitratorPayoutAmount, + Coin txFee, + String buyerAddressString, + String sellerAddressString, + String arbitratorAddressString, + @Nullable String buyerPrivateKeyAsHex, + @Nullable String sellerPrivateKeyAsHex, + String arbitratorPrivateKeyAsHex, + String buyerPubKeyAsHex, + String sellerPubKeyAsHex, + String arbitratorPubKeyAsHex, + TxBroadcaster.Callback callback) throws AddressFormatException, TransactionVerificationException, WalletException { - log.info("signAndPublishPayoutTx called"); - log.info("depositTxHex {}", depositTxHex); - log.info("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); - log.info("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); - log.info("arbitratorPayoutAmount {}", arbitratorPayoutAmount.toFriendlyString()); - log.info("buyerAddressString {}", buyerAddressString); - log.info("sellerAddressString {}", sellerAddressString); - log.info("arbitratorAddressString {}", arbitratorAddressString); - log.info("buyerPrivateKeyAsHex (not displayed for security reasons)"); - log.info("sellerPrivateKeyAsHex (not displayed for security reasons)"); - log.info("arbitratorPrivateKeyAsHex (not displayed for security reasons)"); - log.info("buyerPubKeyAsHex {}", buyerPubKeyAsHex); - log.info("sellerPubKeyAsHex {}", sellerPubKeyAsHex); - log.info("arbitratorPubKeyAsHex {}", arbitratorPubKeyAsHex); - log.info("P2SHMultiSigOutputScript {}", P2SHMultiSigOutputScript); - - checkNotNull((buyerPrivateKeyAsHex != null || sellerPrivateKeyAsHex != null), "either buyerPrivateKeyAsHex or sellerPrivateKeyAsHex must not be null"); + checkNotNull((buyerPrivateKeyAsHex != null || sellerPrivateKeyAsHex != null), + "either buyerPrivateKeyAsHex or sellerPrivateKeyAsHex must not be null"); byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey(); byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey(); - final byte[] arbitratorPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(arbitratorPubKeyAsHex)).getPubKey(); + byte[] arbitratorPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(arbitratorPubKeyAsHex)).getPubKey(); - Script p2SHMultiSigOutputScript = getP2SHMultiSigOutputScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script p2SHMultiSigOutputScript = get2of3MultiSigOutputScript(buyerPubKey, sellerPubKey, arbitratorPubKey); Coin msOutput = buyerPayoutAmount.add(sellerPayoutAmount).add(arbitratorPayoutAmount).add(txFee); TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, null, msOutput, p2SHMultiSigOutputScript.getProgram()); @@ -1102,15 +1045,18 @@ public Transaction emergencySignAndPublishPayoutTx(String depositTxHex, Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex); payoutTx.addInput(new TransactionInput(params, depositTx, p2SHMultiSigOutputScript.getProgram(), new TransactionOutPoint(params, 0, spendTxHash), msOutput)); - if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) + if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) { payoutTx.addOutput(buyerPayoutAmount, Address.fromBase58(params, buyerAddressString)); - if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) + } + if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) { payoutTx.addOutput(sellerPayoutAmount, Address.fromBase58(params, sellerAddressString)); - if (arbitratorPayoutAmount.isGreaterThan(Coin.ZERO)) + } + if (arbitratorPayoutAmount.isGreaterThan(Coin.ZERO)) { payoutTx.addOutput(arbitratorPayoutAmount, Address.fromBase58(params, arbitratorAddressString)); + } // take care of sorting! - Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + Script redeemScript = get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); ECKey.ECDSASignature tradersSignature; @@ -1124,24 +1070,79 @@ public Transaction emergencySignAndPublishPayoutTx(String depositTxHex, checkNotNull(sellerPrivateKey, "sellerPrivateKey must not be null"); tradersSignature = sellerPrivateKey.sign(sigHash, aesKey).toCanonicalised(); } - final ECKey key = ECKey.fromPrivate(Utils.HEX.decode(arbitratorPrivateKeyAsHex)); + ECKey key = ECKey.fromPrivate(Utils.HEX.decode(arbitratorPrivateKeyAsHex)); checkNotNull(key, "key must not be null"); ECKey.ECDSASignature arbitratorSignature = key.sign(sigHash, aesKey).toCanonicalised(); - TransactionSignature tradersTxSig = new TransactionSignature(tradersSignature, Transaction.SigHash.ALL, false); TransactionSignature arbitratorTxSig = new TransactionSignature(arbitratorSignature, Transaction.SigHash.ALL, false); // Take care of order of signatures. See comment below at getMultiSigRedeemScript (sort order needed here: arbitrator, seller, buyer) Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(arbitratorTxSig, tradersTxSig), redeemScript); + TransactionInput input = payoutTx.getInput(0); input.setScriptSig(inputScript); - WalletService.printTx("payoutTx", payoutTx); - WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); - broadcastTx(payoutTx, callback, 20); + return payoutTx; + } + + //todo add window tool for usage + public Transaction emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + Coin txFee, + String buyerAddressString, + String sellerAddressString, + String buyerPrivateKeyAsHex, + String sellerPrivateKeyAsHex, + String buyerPubKeyAsHex, + String sellerPubKeyAsHex, + TxBroadcaster.Callback callback) + throws AddressFormatException, TransactionVerificationException, WalletException { + byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey(); + byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey(); + + Script p2SHMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey); + Coin msOutput = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee); + TransactionOutput p2SHMultiSigOutput = new TransactionOutput(params, null, msOutput, p2SHMultiSigOutputScript.getProgram()); + Transaction depositTx = new Transaction(params); + depositTx.addOutput(p2SHMultiSigOutput); + + Transaction payoutTx = new Transaction(params); + Sha256Hash spendTxHash = Sha256Hash.wrap(depositTxHex); + payoutTx.addInput(new TransactionInput(params, depositTx, p2SHMultiSigOutputScript.getProgram(), new TransactionOutPoint(params, 0, spendTxHash), msOutput)); + + if (buyerPayoutAmount.isGreaterThan(Coin.ZERO)) { + payoutTx.addOutput(buyerPayoutAmount, Address.fromBase58(params, buyerAddressString)); + } + if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) { + payoutTx.addOutput(sellerPayoutAmount, Address.fromBase58(params, sellerAddressString)); + } + + // take care of sorting! + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + Sha256Hash sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + + ECKey buyerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(buyerPrivateKeyAsHex)); + checkNotNull(buyerPrivateKey, "key must not be null"); + ECKey.ECDSASignature buyerECDSASignature = buyerPrivateKey.sign(sigHash, aesKey).toCanonicalised(); + + ECKey sellerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(sellerPrivateKeyAsHex)); + checkNotNull(sellerPrivateKey, "key must not be null"); + ECKey.ECDSASignature sellerECDSASignature = sellerPrivateKey.sign(sigHash, aesKey).toCanonicalised(); + + TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false); + TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false); + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); + + TransactionInput input = payoutTx.getInput(0); + input.setScriptSig(inputScript); + WalletService.printTx("payoutTx", payoutTx); + WalletService.verifyTransaction(payoutTx); + WalletService.checkWalletConsistency(wallet); + broadcastTx(payoutTx, callback, 20); return payoutTx; } @@ -1165,36 +1166,6 @@ public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback, int tim // Misc /////////////////////////////////////////////////////////////////////////////////////////// - /** - * @param transaction The transaction to be added to the wallet - * @return The transaction we added to the wallet, which is different as the one we passed as argument! - * @throws VerificationException - */ - public Transaction addTxToWallet(Transaction transaction) throws VerificationException { - // We need to recreate the transaction otherwise we get a null pointer... - Transaction result = new Transaction(params, transaction.bitcoinSerialize()); - result.getConfidence(Context.get()).setSource(TransactionConfidence.Source.SELF); - - if (wallet != null) - wallet.receivePending(result, null, true); - return result; - } - - /** - * @param serializedTransaction The serialized transaction to be added to the wallet - * @return The transaction we added to the wallet, which is different as the one we passed as argument! - * @throws VerificationException - */ - public Transaction addTxToWallet(byte[] serializedTransaction) throws VerificationException { - // We need to recreate the tx otherwise we get a null pointer... - Transaction transaction = new Transaction(params, serializedTransaction); - transaction.getConfidence(Context.get()).setSource(TransactionConfidence.Source.NETWORK); - - if (wallet != null) - wallet.receivePending(transaction, null, true); - return transaction; - } - /** * @param txId The transaction ID of the transaction we want to lookup * @return Returns local existing wallet transaction @@ -1219,31 +1190,31 @@ public Transaction getClonedTransaction(Transaction tx) { // Private methods /////////////////////////////////////////////////////////////////////////////////////////// - @NotNull private RawTransactionInput getRawInputFromTransactionInput(@NotNull TransactionInput input) { checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); - checkNotNull(input.getConnectedOutput().getParentTransaction(), "input.getConnectedOutput().getParentTransaction() must not be null"); + checkNotNull(input.getConnectedOutput().getParentTransaction(), + "input.getConnectedOutput().getParentTransaction() must not be null"); checkNotNull(input.getValue(), "input.getValue() must not be null"); - return new RawTransactionInput(input.getOutpoint().getIndex(), input.getConnectedOutput().getParentTransaction().bitcoinSerialize(), input.getValue().value); + return new RawTransactionInput(input.getOutpoint().getIndex(), + input.getConnectedOutput().getParentTransaction().bitcoinSerialize(), + input.getValue().value); } - private byte[] getScriptProgram(Transaction makersDepositTx, int i) throws TransactionVerificationException { - byte[] scriptProgram = makersDepositTx.getInputs().get(i).getScriptSig().getProgram(); - if (scriptProgram.length == 0) + private byte[] getMakersScriptSigProgram(TransactionInput transactionInput) throws TransactionVerificationException { + byte[] scriptProgram = transactionInput.getScriptSig().getProgram(); + if (scriptProgram.length == 0) { throw new TransactionVerificationException("Inputs from maker not signed."); + } return scriptProgram; } - @NotNull private TransactionInput getTransactionInput(Transaction depositTx, byte[] scriptProgram, RawTransactionInput rawTransactionInput) { - return new TransactionInput(params, - depositTx, - scriptProgram, - new TransactionOutPoint(params, rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)), + return new TransactionInput(params, depositTx, scriptProgram, new TransactionOutPoint(params, + rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)), Coin.valueOf(rawTransactionInput.value)); } @@ -1257,7 +1228,7 @@ private TransactionInput getTransactionInput(Transaction depositTx, // Furthermore the executed list is reversed to the provided. // Best practice is to provide the list sorted by the least probable successful candidates first (arbitrator is first -> will be last in execution loop, so // avoiding unneeded expensive ECKey.verify calls) - private Script getMultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) { + private Script get2of3MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) { ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey); ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey); ECKey arbitratorKey = ECKey.fromPublicOnly(arbitratorPubKey); @@ -1266,8 +1237,20 @@ private Script getMultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey, return ScriptBuilder.createMultiSigOutputScript(2, keys); } - private Script getP2SHMultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) { - return ScriptBuilder.createP2SHOutputScript(getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey)); + private Script get2of2MultiSigRedeemScript(byte[] buyerPubKey, byte[] sellerPubKey) { + ECKey buyerKey = ECKey.fromPublicOnly(buyerPubKey); + ECKey sellerKey = ECKey.fromPublicOnly(sellerPubKey); + // Take care of sorting! Need to reverse to the order we use normally (buyer, seller) + List keys = ImmutableList.of(sellerKey, buyerKey); + return ScriptBuilder.createMultiSigOutputScript(2, keys); + } + + private Script get2of3MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey, byte[] arbitratorPubKey) { + return ScriptBuilder.createP2SHOutputScript(get2of3MultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey)); + } + + private Script get2of2MultiSigOutputScript(byte[] buyerPubKey, byte[] sellerPubKey) { + return ScriptBuilder.createP2SHOutputScript(get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey)); } private Transaction createPayoutTx(Transaction depositTx, @@ -1288,9 +1271,11 @@ private void signInput(Transaction transaction, TransactionInput input, int inpu Script scriptPubKey = input.getConnectedOutput().getScriptPubKey(); checkNotNull(wallet); ECKey sigKey = input.getOutpoint().getConnectedKey(wallet); - checkNotNull(sigKey, "signInput: sigKey must not be null. input.getOutpoint()=" + input.getOutpoint().toString()); - if (sigKey.isEncrypted()) + checkNotNull(sigKey, "signInput: sigKey must not be null. input.getOutpoint()=" + + input.getOutpoint().toString()); + if (sigKey.isEncrypted()) { checkNotNull(aesKey); + } Sha256Hash hash = transaction.hashForSignature(inputIndex, scriptPubKey, Transaction.SigHash.ALL, false); ECKey.ECDSASignature signature = sigKey.sign(hash, aesKey); TransactionSignature txSig = new TransactionSignature(signature, Transaction.SigHash.ALL, false); @@ -1305,8 +1290,7 @@ private void signInput(Transaction transaction, TransactionInput input, int inpu private void addAvailableInputsAndChangeOutputs(Transaction transaction, Address address, - Address changeAddress, - Coin txFee) throws WalletException { + Address changeAddress) throws WalletException { SendRequest sendRequest = null; try { // Lets let the framework do the work to find the right inputs @@ -1314,7 +1298,7 @@ private void addAvailableInputsAndChangeOutputs(Transaction transaction, sendRequest.shuffleOutputs = false; sendRequest.aesKey = aesKey; // We use a fixed fee - sendRequest.fee = txFee; + sendRequest.fee = Coin.ZERO; sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation) @@ -1327,10 +1311,18 @@ private void addAvailableInputsAndChangeOutputs(Transaction transaction, checkNotNull(wallet, "wallet must not be null"); wallet.completeTx(sendRequest); } catch (Throwable t) { - if (sendRequest != null && sendRequest.tx != null) - log.warn("addAvailableInputsAndChangeOutputs: sendRequest.tx={}, sendRequest.tx.getOutputs()={}", sendRequest.tx, sendRequest.tx.getOutputs()); + if (sendRequest != null && sendRequest.tx != null) { + log.warn("addAvailableInputsAndChangeOutputs: sendRequest.tx={}, sendRequest.tx.getOutputs()={}", + sendRequest.tx, sendRequest.tx.getOutputs()); + } throw new WalletException(t); } } + + private void applyLockTime(long lockTime, Transaction tx) { + checkArgument(!tx.getInputs().isEmpty(), "The tx must have inputs. tx={}", tx); + tx.getInputs().forEach(input -> input.setSequenceNumber(TransactionInput.NO_SEQUENCE - 1)); + tx.setLockTime(lockTime); + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java b/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java index 0576024a736..23d38dd13c1 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java +++ b/core/src/main/java/bisq/core/btc/wallet/TxBroadcaster.java @@ -104,12 +104,12 @@ public static void broadcastTx(Wallet wallet, PeerGroup peerGroup, Transaction t } // We decided the least risky scenario is to commit the tx to the wallet and broadcast it later. - // If it's a bsq tx WalletManager.publishAndCommitBsqTx() should have commited the tx to both bsq and btc + // If it's a bsq tx WalletManager.publishAndCommitBsqTx() should have committed the tx to both bsq and btc // wallets so the next line causes no effect. // If it's a btc tx, the next line adds the tx to the wallet. wallet.maybeCommitTx(tx); - Futures.addCallback(peerGroup.broadcastTransaction(tx).future(), new FutureCallback() { + Futures.addCallback(peerGroup.broadcastTransaction(tx).future(), new FutureCallback<>() { @Override public void onSuccess(@Nullable Transaction result) { // We expect that there is still a timeout in our map, otherwise the timeout got triggered @@ -119,7 +119,7 @@ public void onSuccess(@Nullable Transaction result) { // before the caller is finished. UserThread.execute(() -> callback.onSuccess(tx)); } else { - log.warn("We got an onSuccess callback for a broadcast which already triggered the timeout.", txId); + log.warn("We got an onSuccess callback for a broadcast which already triggered the timeout. txId={}", txId); } } diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index 62822c4a0ad..d5749bae08b 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -34,6 +34,7 @@ import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Context; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.NetworkParameters; @@ -43,6 +44,7 @@ import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.VerificationException; import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypter; @@ -102,6 +104,7 @@ public abstract class WalletService { protected final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>(); protected final CopyOnWriteArraySet txConfidenceListeners = new CopyOnWriteArraySet<>(); protected final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); + @Getter protected Wallet wallet; @Getter protected KeyParameter aesKey; @@ -223,7 +226,9 @@ public static void checkAllScriptSignaturesForTx(Transaction transaction) throws } } - public static void checkScriptSig(Transaction transaction, TransactionInput input, int inputIndex) throws TransactionVerificationException { + public static void checkScriptSig(Transaction transaction, + TransactionInput input, + int inputIndex) throws TransactionVerificationException { try { checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); input.getScriptSig().correctlySpends(transaction, inputIndex, input.getConnectedOutput().getScriptPubKey(), Script.ALL_VERIFY_FLAGS); @@ -245,7 +250,11 @@ public static void removeSignatures(Transaction transaction) { // Sign tx /////////////////////////////////////////////////////////////////////////////////////////// - public static void signTransactionInput(Wallet wallet, KeyParameter aesKey, Transaction tx, TransactionInput txIn, int index) { + public static void signTransactionInput(Wallet wallet, + KeyParameter aesKey, + Transaction tx, + TransactionInput txIn, + int index) { KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(wallet, aesKey); if (txIn.getConnectedOutput() != null) { try { @@ -475,7 +484,10 @@ public boolean isAddressUnused(Address address) { // Empty complete Wallet /////////////////////////////////////////////////////////////////////////////////////////// - public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) + public void emptyWallet(String toAddress, + KeyParameter aesKey, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) throws InsufficientMoneyException, AddressFormatException { SendRequest sendRequest = SendRequest.emptyWallet(Address.fromBase58(params, toAddress)); sendRequest.fee = Coin.ZERO; @@ -675,6 +687,46 @@ public static String getAddressStringFromOutput(TransactionOutput output) { } + /** + * @param serializedTransaction The serialized transaction to be added to the wallet + * @return The transaction we added to the wallet, which is different as the one we passed as argument! + * @throws VerificationException + */ + public static Transaction maybeAddTxToWallet(byte[] serializedTransaction, + Wallet wallet, + TransactionConfidence.Source source) throws VerificationException { + Transaction tx = new Transaction(wallet.getParams(), serializedTransaction); + Transaction walletTransaction = wallet.getTransaction(tx.getHash()); + log.error("maybeAddTxToWallet id={}, is walletTransaction==null? {}", tx.getHashAsString(), walletTransaction == null); + + if (walletTransaction == null) { + // We need to recreate the transaction otherwise we get a null pointer... + tx.getConfidence(Context.get()).setSource(source); + //wallet.maybeCommitTx(tx); + wallet.receivePending(tx, null, true); + return tx; + } else { + return walletTransaction; + } + } + + public static Transaction maybeAddNetworkTxToWallet(byte[] serializedTransaction, + Wallet wallet) throws VerificationException { + return maybeAddTxToWallet(serializedTransaction, wallet, TransactionConfidence.Source.NETWORK); + } + + public static Transaction maybeAddSelfTxToWallet(Transaction transaction, + Wallet wallet) throws VerificationException { + return maybeAddTxToWallet(transaction, wallet, TransactionConfidence.Source.SELF); + } + + public static Transaction maybeAddTxToWallet(Transaction transaction, + Wallet wallet, + TransactionConfidence.Source source) throws VerificationException { + return maybeAddTxToWallet(transaction.bitcoinSerialize(), wallet, source); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // bisqWalletEventListener /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 8d92fa17839..134dbf52b10 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -98,6 +98,10 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { @Nullable private final List mediators; + // added in v1.2.0 + @Nullable + private final List refundAgents; + public Filter(List bannedOfferIds, List bannedNodeAddress, List bannedPaymentAccounts, @@ -111,7 +115,8 @@ public Filter(List bannedOfferIds, boolean disableDao, @Nullable String disableDaoBelowVersion, @Nullable String disableTradeBelowVersion, - @Nullable List mediators) { + @Nullable List mediators, + @Nullable List refundAgents) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -126,6 +131,7 @@ public Filter(List bannedOfferIds, this.disableDaoBelowVersion = disableDaoBelowVersion; this.disableTradeBelowVersion = disableTradeBelowVersion; this.mediators = mediators; + this.refundAgents = refundAgents; } @@ -150,7 +156,8 @@ public Filter(List bannedOfferIds, String signatureAsBase64, byte[] ownerPubKeyBytes, @Nullable Map extraDataMap, - @Nullable List mediators) { + @Nullable List mediators, + @Nullable List refundAgents) { this(bannedOfferIds, bannedNodeAddress, bannedPaymentAccounts, @@ -164,7 +171,8 @@ public Filter(List bannedOfferIds, disableDao, disableDaoBelowVersion, disableTradeBelowVersion, - mediators); + mediators, + refundAgents); this.signatureAsBase64 = signatureAsBase64; this.ownerPubKeyBytes = ownerPubKeyBytes; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); @@ -198,6 +206,7 @@ public protobuf.StoragePayload toProtoMessage() { Optional.ofNullable(disableTradeBelowVersion).ifPresent(builder::setDisableTradeBelowVersion); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); Optional.ofNullable(mediators).ifPresent(builder::addAllMediators); + Optional.ofNullable(refundAgents).ifPresent(builder::addAllRefundAgents); return protobuf.StoragePayload.newBuilder().setFilter(builder).build(); } @@ -221,7 +230,8 @@ public static Filter fromProto(protobuf.Filter proto) { proto.getSignatureAsBase64(), proto.getOwnerPubKeyBytes().toByteArray(), CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), - CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList())); + CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList()), + CollectionUtils.isEmpty(proto.getRefundAgentsList()) ? null : new ArrayList<>(proto.getRefundAgentsList())); } diff --git a/core/src/main/java/bisq/core/offer/AvailabilityResult.java b/core/src/main/java/bisq/core/offer/AvailabilityResult.java index 95895687037..2d3d749ff24 100644 --- a/core/src/main/java/bisq/core/offer/AvailabilityResult.java +++ b/core/src/main/java/bisq/core/offer/AvailabilityResult.java @@ -26,5 +26,6 @@ public enum AvailabilityResult { NO_ARBITRATORS, NO_MEDIATORS, USER_IGNORED, - MISSING_MANDATORY_CAPABILITY + MISSING_MANDATORY_CAPABILITY, + NO_REFUND_AGENTS } diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index d99955d5327..5ebe152453c 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -28,7 +28,6 @@ import bisq.network.p2p.storage.payload.ProtectedStorageEntry; import bisq.common.UserThread; -import bisq.common.app.Capability; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import bisq.common.storage.JsonFileManager; @@ -93,10 +92,8 @@ public void onAdded(ProtectedStorageEntry data) { if (data.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); - if (showOffer(offer)) { - offer.setPriceFeedService(priceFeedService); - listener.onAdded(offer); - } + offer.setPriceFeedService(priceFeedService); + listener.onAdded(offer); } }); } @@ -135,11 +132,6 @@ public void onRemoved(Offer offer) { } } - private boolean showOffer(Offer offer) { - return !OfferRestrictions.requiresUpdate() || - OfferRestrictions.hasOfferMandatoryCapability(offer, Capability.MEDIATION); - } - /////////////////////////////////////////////////////////////////////////////////////////// // API @@ -208,7 +200,6 @@ public List getOffers() { offer.setPriceFeedService(priceFeedService); return offer; }) - .filter(this::showOffer) .collect(Collectors.toList()); } diff --git a/core/src/main/java/bisq/core/offer/OfferRestrictions.java b/core/src/main/java/bisq/core/offer/OfferRestrictions.java index d7b64580bd5..856b7a6e206 100644 --- a/core/src/main/java/bisq/core/offer/OfferRestrictions.java +++ b/core/src/main/java/bisq/core/offer/OfferRestrictions.java @@ -17,9 +17,6 @@ package bisq.core.offer; -import bisq.core.payment.payload.PaymentMethod; -import bisq.core.trade.Trade; - import bisq.common.app.Capabilities; import bisq.common.app.Capability; import bisq.common.util.Utilities; @@ -41,38 +38,6 @@ static boolean requiresUpdate() { public static Coin TOLERATED_SMALL_TRADE_AMOUNT = Coin.parseCoin("0.01"); - public static boolean isOfferRisky(Offer offer) { - return offer != null && - offer.isBuyOffer() && - PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()) && - isMinTradeAmountRisky(offer); - } - - public static boolean isSellOfferRisky(Offer offer) { - return offer != null && - PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()) && - isMinTradeAmountRisky(offer); - } - - public static boolean isTradeRisky(Trade trade) { - if (trade == null) - return false; - - Offer offer = trade.getOffer(); - return offer != null && - PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()) && - trade.getTradeAmount() != null && - isAmountRisky(trade.getTradeAmount()); - } - - public static boolean isMinTradeAmountRisky(Offer offer) { - return isAmountRisky(offer.getMinAmount()); - } - - private static boolean isAmountRisky(Coin amount) { - return amount.isGreaterThan(TOLERATED_SMALL_TRADE_AMOUNT); - } - static boolean hasOfferMandatoryCapability(Offer offer, Capability mandatoryCapability) { Map extraDataMap = offer.getOfferPayload().getExtraDataMap(); if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) { diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index bffe39c8890..a8c9406d682 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -32,7 +32,6 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.Preferences; -import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; import bisq.core.util.CoinUtil; @@ -326,7 +325,8 @@ public static Optional getFeeInUserFiatCurrency(Coin makerFee, boolean i public static Map getExtraDataMap(AccountAgeWitnessService accountAgeWitnessService, ReferralIdService referralIdService, PaymentAccount paymentAccount, - String currencyCode) { + String currencyCode, + Preferences preferences) { Map extraDataMap = new HashMap<>(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 2193b30752c..ced8cce9834 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -65,6 +65,12 @@ public enum State { @Nullable private NodeAddress mediatorNodeAddress; + // Added v1.2.0 + @Getter + @Setter + @Nullable + private NodeAddress refundAgentNodeAddress; + transient private Storage> storage; public OpenOffer(Offer offer, Storage> storage) { @@ -80,11 +86,13 @@ public OpenOffer(Offer offer, Storage> storage) { private OpenOffer(Offer offer, State state, @Nullable NodeAddress arbitratorNodeAddress, - @Nullable NodeAddress mediatorNodeAddress) { + @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress) { this.offer = offer; this.state = state; this.arbitratorNodeAddress = arbitratorNodeAddress; this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -98,6 +106,7 @@ public protobuf.Tradable toProtoMessage() { Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage())); Optional.ofNullable(mediatorNodeAddress).ifPresent(nodeAddress -> builder.setMediatorNodeAddress(nodeAddress.toProtoMessage())); + Optional.ofNullable(refundAgentNodeAddress).ifPresent(nodeAddress -> builder.setRefundAgentNodeAddress(nodeAddress.toProtoMessage())); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } @@ -106,7 +115,8 @@ public static Tradable fromProto(protobuf.OpenOffer proto) { return new OpenOffer(Offer.fromProto(proto.getOffer()), ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, - proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null); + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null); } @@ -175,6 +185,7 @@ public String toString() { ",\n state=" + state + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + "\n}"; } } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 6754512c6e1..f0226ee6a52 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -29,6 +29,7 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradableList; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.handlers.TransactionResultHandler; @@ -51,6 +52,7 @@ import bisq.common.UserThread; import bisq.common.app.Capabilities; import bisq.common.app.Capability; +import bisq.common.app.Version; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.ErrorMessageHandler; @@ -104,6 +106,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private final TradeStatisticsManager tradeStatisticsManager; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; private final Storage> openOfferTradableListStorage; private final Map offersToBeEdited = new HashMap<>(); private boolean stopped; @@ -129,6 +132,7 @@ public OpenOfferManager(KeyRing keyRing, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, Storage> storage) { this.keyRing = keyRing; this.user = user; @@ -143,6 +147,7 @@ public OpenOfferManager(KeyRing keyRing, this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; openOfferTradableListStorage = storage; @@ -577,6 +582,7 @@ private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, No AvailabilityResult availabilityResult; NodeAddress arbitratorNodeAddress = null; NodeAddress mediatorNodeAddress = null; + NodeAddress refundAgentNodeAddress = null; if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); if (openOffer.getState() == OpenOffer.State.AVAILABLE) { @@ -584,41 +590,29 @@ private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, No if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { availabilityResult = AvailabilityResult.AVAILABLE; - List acceptedArbitrators = user.getAcceptedArbitratorAddresses(); - if (acceptedArbitrators != null && !acceptedArbitrators.isEmpty()) { - arbitratorNodeAddress = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager).getNodeAddress(); - openOffer.setArbitratorNodeAddress(arbitratorNodeAddress); - - mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); - openOffer.setMediatorNodeAddress(mediatorNodeAddress); - Capabilities supportedCapabilities = request.getSupportedCapabilities(); - if (!OfferRestrictions.requiresUpdate() || - (supportedCapabilities != null && - Capabilities.hasMandatoryCapability(supportedCapabilities, Capability.MEDIATION))) { - try { - // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference - // in trade price between the peers. Also here poor connectivity might cause market price API connection - // losses and therefore an outdated market price. - offer.checkTradePriceTolerance(request.getTakersTradePrice()); - } catch (TradePriceOutOfToleranceException e) { - log.warn("Trade price check failed because takers price is outside out tolerance."); - availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; - } catch (MarketPriceNotAvailableException e) { - log.warn(e.getMessage()); - availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; - } catch (Throwable e) { - log.warn("Trade price check failed. " + e.getMessage()); - availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; - } - } else { - log.warn("Taker has not mandatory capability MEDIATION"); - // Because an old peer has not AvailabilityResult.MISSING_MANDATORY_CAPABILITY and we - // have not set the UNDEFINED fallback in AvailabilityResult the user will get a null value. - availabilityResult = AvailabilityResult.MISSING_MANDATORY_CAPABILITY; - } - } else { - log.warn("acceptedArbitrators is null or empty: acceptedArbitrators=" + acceptedArbitrators); - availabilityResult = AvailabilityResult.NO_ARBITRATORS; + arbitratorNodeAddress = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager).getNodeAddress(); + openOffer.setArbitratorNodeAddress(arbitratorNodeAddress); + + mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); + openOffer.setMediatorNodeAddress(mediatorNodeAddress); + + refundAgentNodeAddress = DisputeAgentSelection.getLeastUsedRefundAgent(tradeStatisticsManager, refundAgentManager).getNodeAddress(); + openOffer.setRefundAgentNodeAddress(refundAgentNodeAddress); + + try { + // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference + // in trade price between the peers. Also here poor connectivity might cause market price API connection + // losses and therefore an outdated market price. + offer.checkTradePriceTolerance(request.getTakersTradePrice()); + } catch (TradePriceOutOfToleranceException e) { + log.warn("Trade price check failed because takers price is outside out tolerance."); + availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; + } catch (MarketPriceNotAvailableException e) { + log.warn(e.getMessage()); + availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; + } catch (Throwable e) { + log.warn("Trade price check failed. " + e.getMessage()); + availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; } } else { availabilityResult = AvailabilityResult.USER_IGNORED; @@ -634,7 +628,8 @@ private void handleOfferAvailabilityRequest(OfferAvailabilityRequest request, No OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, availabilityResult, arbitratorNodeAddress, - mediatorNodeAddress); + mediatorNodeAddress, + refundAgentNodeAddress); log.info("Send {} with offerId {} and uid {} to peer {}", offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), offerAvailabilityResponse.getUid(), peer); @@ -722,7 +717,8 @@ private void maybeUpdatePersistedOffers() { // We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and want to rewrite a // persisted offer after the user has updated to 1.1.6 so their offer will be accepted by the network. - if (!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION)) { + if (originalOfferPayload.getProtocolVersion() < Version.TRADE_PROTOCOL_VERSION || + !OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION)) { // We rewrite our offer with the additional capabilities entry Map originalExtraDataMap = originalOfferPayload.getExtraDataMap(); @@ -735,6 +731,9 @@ private void maybeUpdatePersistedOffers() { // We overwrite any entry with our current capabilities updatedExtraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList()); + // We update the trade protocol version + int protocolVersion = Version.TRADE_PROTOCOL_VERSION; + OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), originalOfferPayload.getDate(), originalOfferPayload.getOwnerNodeAddress(), @@ -772,7 +771,7 @@ private void maybeUpdatePersistedOffers() { originalOfferPayload.isPrivateOffer(), originalOfferPayload.getHashOfChallenge(), updatedExtraDataMap, - originalOfferPayload.getProtocolVersion()); + protocolVersion); // Save states from original data to use the for updated Offer.State originalOfferState = originalOffer.getState(); diff --git a/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java index bce44e6cbc6..2639261d6f4 100644 --- a/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java +++ b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java @@ -57,6 +57,13 @@ public static T getLeastUsedMediator(TradeStatisticsMan TradeStatistics2.MEDIATOR_ADDRESS); } + public static T getLeastUsedRefundAgent(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager) { + return getLeastUsedDisputeAgent(tradeStatisticsManager, + disputeAgentManager, + TradeStatistics2.REFUND_AGENT_ADDRESS); + } + private static T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager, DisputeAgentManager disputeAgentManager, String extraMapKey) { diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java index 2b55c18813a..aad97d33f42 100644 --- a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java @@ -60,6 +60,12 @@ public class OfferAvailabilityModel implements Model { @Getter private NodeAddress selectedMediator; + // Added in v1.2.0 + @Nullable + @Setter + @Getter + private NodeAddress selectedRefundAgent; + public OfferAvailabilityModel(Offer offer, PubKeyRing pubKeyRing, diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java index 1690ddc34ae..6862661b391 100644 --- a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java +++ b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java @@ -57,12 +57,16 @@ protected void run() { offer.setState(Offer.State.AVAILABLE); model.setSelectedArbitrator(offerAvailabilityResponse.getArbitrator()); + NodeAddress mediator = offerAvailabilityResponse.getMediator(); if (mediator == null) { // We do not get a mediator from old clients so we need to handle the null case. mediator = DisputeAgentSelection.getLeastUsedMediator(model.getTradeStatisticsManager(), model.getMediatorManager()).getNodeAddress(); } model.setSelectedMediator(mediator); + + model.setSelectedRefundAgent(offerAvailabilityResponse.getRefundAgent()); + complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java index f71713c7a01..1f5161f6f2e 100644 --- a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java @@ -49,17 +49,23 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup @Nullable private final NodeAddress mediator; + // Added v1.2.0 + @Nullable + private final NodeAddress refundAgent; + public OfferAvailabilityResponse(String offerId, AvailabilityResult availabilityResult, NodeAddress arbitrator, - NodeAddress mediator) { + NodeAddress mediator, + NodeAddress refundAgent) { this(offerId, availabilityResult, Capabilities.app, Version.getP2PMessageVersion(), UUID.randomUUID().toString(), arbitrator, - mediator); + mediator, + refundAgent); } @@ -73,12 +79,14 @@ private OfferAvailabilityResponse(String offerId, int messageVersion, @Nullable String uid, NodeAddress arbitrator, - @Nullable NodeAddress mediator) { + @Nullable NodeAddress mediator, + @Nullable NodeAddress refundAgent) { super(messageVersion, offerId, uid); this.availabilityResult = availabilityResult; this.supportedCapabilities = supportedCapabilities; this.arbitrator = arbitrator; this.mediator = mediator; + this.refundAgent = refundAgent; } @Override @@ -91,6 +99,7 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); Optional.ofNullable(mediator).ifPresent(e -> builder.setMediator(mediator.toProtoMessage())); + Optional.ofNullable(refundAgent).ifPresent(e -> builder.setRefundAgent(refundAgent.toProtoMessage())); return getNetworkEnvelopeBuilder() .setOfferAvailabilityResponse(builder) @@ -104,6 +113,7 @@ public static OfferAvailabilityResponse fromProto(protobuf.OfferAvailabilityResp messageVersion, proto.getUid().isEmpty() ? null : proto.getUid(), NodeAddress.fromProto(proto.getArbitrator()), - proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null); + proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null, + proto.hasRefundAgent() ? NodeAddress.fromProto(proto.getRefundAgent()) : null); } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java index b4bba4f4e76..c4d5e545371 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -17,11 +17,9 @@ package bisq.core.payment; -import bisq.core.account.witness.AccountAgeRestrictions; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; import bisq.core.offer.Offer; -import bisq.core.offer.OfferRestrictions; import bisq.core.payment.payload.PaymentMethod; import javafx.collections.FXCollections; @@ -41,57 +39,8 @@ @Slf4j public class PaymentAccountUtil { - public static boolean isRiskyBuyOfferWithImmatureAccountAge(Offer offer, AccountAgeWitnessService accountAgeWitnessService) { - return OfferRestrictions.isOfferRisky(offer) && - AccountAgeRestrictions.isMakersAccountAgeImmature(accountAgeWitnessService, offer); - } - - public static boolean isSellOfferAndAllTakerPaymentAccountsForOfferImmature(Offer offer, - Collection takerPaymentAccounts, - AccountAgeWitnessService accountAgeWitnessService) { - if (offer.isBuyOffer()) { - return false; - } - - if (!OfferRestrictions.isSellOfferRisky(offer)) { - return false; - } - - for (PaymentAccount takerPaymentAccount : takerPaymentAccounts) { - if (isTakerAccountForOfferMature(offer, takerPaymentAccount, accountAgeWitnessService)) - return false; - } - return true; - } - - private static boolean isTakerAccountForOfferMature(Offer offer, - PaymentAccount takerPaymentAccount, - AccountAgeWitnessService accountAgeWitnessService) { - return !PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()) || - !OfferRestrictions.isMinTradeAmountRisky(offer) || - (isTakerPaymentAccountValidForOffer(offer, takerPaymentAccount) && - !AccountAgeRestrictions.isMyAccountAgeImmature(accountAgeWitnessService, takerPaymentAccount)); - } - - public static boolean hasMakerAnyMatureAccountForBuyOffer(Collection makerPaymentAccounts, - AccountAgeWitnessService accountAgeWitnessService) { - for (PaymentAccount makerPaymentAccount : makerPaymentAccounts) { - if (hasMyMatureAccountForBuyOffer(makerPaymentAccount, accountAgeWitnessService)) - return true; - } - return false; - } - - private static boolean hasMyMatureAccountForBuyOffer(PaymentAccount myPaymentAccount, - AccountAgeWitnessService accountAgeWitnessService) { - if (myPaymentAccount.selectedTradeCurrency == null) - return false; - return !PaymentMethod.hasChargebackRisk(myPaymentAccount.getPaymentMethod(), - myPaymentAccount.selectedTradeCurrency.getCode()) || - !AccountAgeRestrictions.isMyAccountAgeImmature(accountAgeWitnessService, myPaymentAccount); - } - - public static boolean isAnyTakerPaymentAccountValidForOffer(Offer offer, Collection takerPaymentAccounts) { + public static boolean isAnyTakerPaymentAccountValidForOffer(Offer offer, + Collection takerPaymentAccounts) { for (PaymentAccount takerPaymentAccount : takerPaymentAccounts) { if (isTakerPaymentAccountValidForOffer(offer, takerPaymentAccount)) return true; @@ -105,11 +54,21 @@ public static ObservableList getPossiblePaymentAccounts(Offer of ObservableList result = FXCollections.observableArrayList(); result.addAll(paymentAccounts.stream() .filter(paymentAccount -> isTakerPaymentAccountValidForOffer(offer, paymentAccount)) - .filter(paymentAccount -> offer.isBuyOffer() || isTakerAccountForOfferMature(offer, paymentAccount, accountAgeWitnessService)) + .filter(paymentAccount -> isAmountValidForOffer(offer, paymentAccount, accountAgeWitnessService)) .collect(Collectors.toList())); return result; } + // Return true if paymentAccount can take this offer + public static boolean isAmountValidForOffer(Offer offer, + PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService) { + boolean hasChargebackRisk = PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode()); + boolean hasValidAccountAgeWitness = accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection()) > offer.getAmount().value; + return !hasChargebackRisk || hasValidAccountAgeWitness; + } + // TODO might be used to show more details if we get payment methods updates with diff. limits public static String getInfoForMismatchingPaymentMethodLimits(Offer offer, PaymentAccount paymentAccount) { // dont translate atm as it is not used so far in the UI just for logs diff --git a/core/src/main/java/bisq/core/payment/PaymentAccounts.java b/core/src/main/java/bisq/core/payment/PaymentAccounts.java index e6fe3a32bb4..3a04d637d42 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccounts.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccounts.java @@ -37,17 +37,17 @@ class PaymentAccounts { private static final Logger log = LoggerFactory.getLogger(PaymentAccounts.class); private final Set accounts; - private final AccountAgeWitnessService service; + private final AccountAgeWitnessService accountAgeWitnessService; private final BiFunction validator; - PaymentAccounts(Set accounts, AccountAgeWitnessService service) { - this(accounts, service, PaymentAccountUtil::isTakerPaymentAccountValidForOffer); + PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService) { + this(accounts, accountAgeWitnessService, PaymentAccountUtil::isTakerPaymentAccountValidForOffer); } - PaymentAccounts(Set accounts, AccountAgeWitnessService service, + PaymentAccounts(Set accounts, AccountAgeWitnessService accountAgeWitnessService, BiFunction validator) { this.accounts = accounts; - this.service = service; + this.accountAgeWitnessService = accountAgeWitnessService; this.validator = validator; } @@ -61,7 +61,7 @@ PaymentAccount getOldestPaymentAccountForOffer(Offer offer) { } private List sortValidAccounts(Offer offer) { - Comparator comparator = this::compareByAge; + Comparator comparator = this::compareBySignAgeOrMatureAccount; return accounts.stream() .filter(account -> validator.apply(offer, account)) .sorted(comparator.reversed()) @@ -78,7 +78,7 @@ private void logAccounts(List accounts) { StringBuilder message = new StringBuilder("Valid accounts: \n"); for (PaymentAccount account : accounts) { String accountName = account.getAccountName(); - String witnessHex = service.getMyWitnessHashAsHex(account.getPaymentAccountPayload()); + String witnessHex = accountAgeWitnessService.getMyWitnessHashAsHex(account.getPaymentAccountPayload()); message.append("name = ") .append(accountName) @@ -91,15 +91,24 @@ private void logAccounts(List accounts) { } } - private int compareByAge(PaymentAccount left, PaymentAccount right) { - AccountAgeWitness leftWitness = service.getMyWitness(left.getPaymentAccountPayload()); - AccountAgeWitness rightWitness = service.getMyWitness(right.getPaymentAccountPayload()); + // Accounts created before + private int compareBySignAgeOrMatureAccount(PaymentAccount left, PaymentAccount right) { + // Mature accounts count as infinite sign age + if (!accountAgeWitnessService.isMyAccountAgeImmature(left)) { + return accountAgeWitnessService.isMyAccountAgeImmature(right) ? 1 : 0; + } + if (!accountAgeWitnessService.isMyAccountAgeImmature(right)) { + return -1; + } + + AccountAgeWitness leftWitness = accountAgeWitnessService.getMyWitness(left.getPaymentAccountPayload()); + AccountAgeWitness rightWitness = accountAgeWitnessService.getMyWitness(right.getPaymentAccountPayload()); Date now = new Date(); - long leftAge = service.getAccountAge(leftWitness, now); - long rightAge = service.getAccountAge(rightWitness, now); + long leftSignAge = accountAgeWitnessService.getWitnessSignAge(leftWitness, now); + long rightSignAge = accountAgeWitnessService.getWitnessSignAge(rightWitness, now); - return Long.compare(leftAge, rightAge); + return Long.compare(leftSignAge, rightSignAge); } } 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 9d84408420b..91791a5ba39 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -44,14 +44,19 @@ import bisq.core.support.dispute.messages.DisputeResultMessage; import bisq.core.support.dispute.messages.OpenNewDisputeMessage; import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; -import bisq.core.trade.messages.DepositTxPublishedMessage; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; -import bisq.core.trade.messages.PayDepositRequest; import bisq.core.trade.messages.PayoutTxPublishedMessage; -import bisq.core.trade.messages.PublishDepositTxRequest; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.statistics.TradeStatistics; import bisq.network.p2p.AckMessage; @@ -89,7 +94,6 @@ @Slf4j @Singleton public class CoreNetworkProtoResolver extends CoreProtoResolver implements NetworkProtoResolver { - @Inject public CoreNetworkProtoResolver() { } @@ -134,16 +138,28 @@ public NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws Protobuf case PREFIXED_SEALED_AND_SIGNED_MESSAGE: return PrefixedSealedAndSignedMessage.fromProto(proto.getPrefixedSealedAndSignedMessage(), messageVersion); - case PAY_DEPOSIT_REQUEST: - return PayDepositRequest.fromProto(proto.getPayDepositRequest(), this, messageVersion); - case DEPOSIT_TX_PUBLISHED_MESSAGE: - return DepositTxPublishedMessage.fromProto(proto.getDepositTxPublishedMessage(), messageVersion); - case PUBLISH_DEPOSIT_TX_REQUEST: - return PublishDepositTxRequest.fromProto(proto.getPublishDepositTxRequest(), this, messageVersion); + // trade protocol messages + case INPUTS_FOR_DEPOSIT_TX_REQUEST: + return InputsForDepositTxRequest.fromProto(proto.getInputsForDepositTxRequest(), this, messageVersion); + case INPUTS_FOR_DEPOSIT_TX_RESPONSE: + return InputsForDepositTxResponse.fromProto(proto.getInputsForDepositTxResponse(), this, messageVersion); + case DEPOSIT_TX_MESSAGE: + return DepositTxMessage.fromProto(proto.getDepositTxMessage(), messageVersion); + case DELAYED_PAYOUT_TX_SIGNATURE_REQUEST: + return DelayedPayoutTxSignatureRequest.fromProto(proto.getDelayedPayoutTxSignatureRequest(), messageVersion); + case DELAYED_PAYOUT_TX_SIGNATURE_RESPONSE: + return DelayedPayoutTxSignatureResponse.fromProto(proto.getDelayedPayoutTxSignatureResponse(), messageVersion); + case DEPOSIT_TX_AND_DELAYED_PAYOUT_TX_MESSAGE: + return DepositTxAndDelayedPayoutTxMessage.fromProto(proto.getDepositTxAndDelayedPayoutTxMessage(), messageVersion); + case COUNTER_CURRENCY_TRANSFER_STARTED_MESSAGE: return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), messageVersion); + case PAYOUT_TX_PUBLISHED_MESSAGE: return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion); + case PEER_PUBLISHED_DELAYED_PAYOUT_TX_MESSAGE: + return PeerPublishedDelayedPayoutTxMessage.fromProto(proto.getPeerPublishedDelayedPayoutTxMessage(), messageVersion); + case MEDIATED_PAYOUT_TX_SIGNATURE_MESSAGE: return MediatedPayoutTxSignatureMessage.fromProto(proto.getMediatedPayoutTxSignatureMessage(), messageVersion); case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE: @@ -236,6 +252,8 @@ public NetworkPayload fromProto(protobuf.StoragePayload proto) { return Arbitrator.fromProto(proto.getArbitrator()); case MEDIATOR: return Mediator.fromProto(proto.getMediator()); + case REFUND_AGENT: + return RefundAgent.fromProto(proto.getRefundAgent()); case FILTER: return Filter.fromProto(proto.getFilter()); case TRADE_STATISTICS: diff --git a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java index 796572b0c3f..321b236408f 100644 --- a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java @@ -37,6 +37,7 @@ import bisq.core.proto.CoreProtoResolver; import bisq.core.support.dispute.arbitration.ArbitrationDisputeList; import bisq.core.support.dispute.mediation.MediationDisputeList; +import bisq.core.support.dispute.refund.RefundDisputeList; import bisq.core.trade.TradableList; import bisq.core.trade.statistics.TradeStatistics2Store; import bisq.core.user.PreferencesPayload; @@ -110,6 +111,10 @@ public PersistableEnvelope fromProto(protobuf.PersistableEnvelope proto) { return MediationDisputeList.fromProto(proto.getMediationDisputeList(), this, new Storage<>(storageDir, this, corruptedDatabaseFilesHandler)); + case REFUND_DISPUTE_LIST: + return RefundDisputeList.fromProto(proto.getRefundDisputeList(), + this, + new Storage<>(storageDir, this, corruptedDatabaseFilesHandler)); case PREFERENCES_PAYLOAD: return PreferencesPayload.fromProto(proto.getPreferencesPayload(), this); case USER_PAYLOAD: diff --git a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java index 757f1f09df7..09181eebf73 100644 --- a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java +++ b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java @@ -37,7 +37,8 @@ static void setSupportedCapabilities(BisqEnvironment bisqEnvironment) { Capability.BLIND_VOTE, Capability.DAO_STATE, Capability.BUNDLE_OF_ENVELOPES, - Capability.MEDIATION + Capability.MEDIATION, + Capability.SIGNED_ACCOUNT_AGE_WITNESS ); if (BisqEnvironment.isDaoActivated(bisqEnvironment)) { diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index 1a068e82a8a..f5a0e39c6b3 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -29,6 +29,7 @@ import bisq.core.offer.OpenOfferManager; import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService; import bisq.core.support.dispute.mediation.MediationDisputeListService; +import bisq.core.support.dispute.refund.RefundDisputeListService; import bisq.core.trade.TradeManager; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; @@ -63,6 +64,7 @@ public static List getPersistedDataHosts(Injector injector) { persistedDataHosts.add(injector.getInstance(FailedTradesManager.class)); persistedDataHosts.add(injector.getInstance(ArbitrationDisputeListService.class)); persistedDataHosts.add(injector.getInstance(MediationDisputeListService.class)); + persistedDataHosts.add(injector.getInstance(RefundDisputeListService.class)); persistedDataHosts.add(injector.getInstance(P2PService.class)); if (injector.getInstance(Key.get(Boolean.class, Names.named(DaoOptionKeys.DAO_ACTIVATED)))) { diff --git a/core/src/main/java/bisq/core/support/SupportType.java b/core/src/main/java/bisq/core/support/SupportType.java index 4d13c7848ec..cd10cc024ff 100644 --- a/core/src/main/java/bisq/core/support/SupportType.java +++ b/core/src/main/java/bisq/core/support/SupportType.java @@ -22,7 +22,8 @@ public enum SupportType { ARBITRATION, // Need to be at index 0 to be the fall back for old clients MEDIATION, - TRADE; + TRADE, + REFUND; public static SupportType fromProto( protobuf.SupportType type) { diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index 3348bbf963f..abe76911ada 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -48,6 +48,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @@ -80,7 +81,7 @@ public final class Dispute implements NetworkPayload { private final String makerContractSignature; @Nullable private final String takerContractSignature; - private final PubKeyRing agentPubKeyRing; // arbitrator or mediator + private final PubKeyRing agentPubKeyRing; // dispute agent private final boolean isSupportTicket; private final ObservableList chatMessages = FXCollections.observableArrayList(); private BooleanProperty isClosedProperty = new SimpleBooleanProperty(); @@ -92,6 +93,13 @@ public final class Dispute implements NetworkPayload { transient private Storage storage; + // Added v1.2.0 + private SupportType supportType; + // Only used at refundAgent so that he knows how the mediator resolved the case + @Setter + @Nullable + private String mediatorsDisputeResult; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -114,7 +122,8 @@ public Dispute(Storage storage, @Nullable String makerContractSignature, @Nullable String takerContractSignature, PubKeyRing agentPubKeyRing, - boolean isSupportTicket) { + boolean isSupportTicket, + SupportType supportType) { this(tradeId, traderId, disputeOpenerIsBuyer, @@ -131,7 +140,8 @@ public Dispute(Storage storage, makerContractSignature, takerContractSignature, agentPubKeyRing, - isSupportTicket); + isSupportTicket, + supportType); this.storage = storage; openingDate = new Date().getTime(); } @@ -157,7 +167,8 @@ public Dispute(String tradeId, @Nullable String makerContractSignature, @Nullable String takerContractSignature, PubKeyRing agentPubKeyRing, - boolean isSupportTicket) { + boolean isSupportTicket, + SupportType supportType) { this.tradeId = tradeId; this.traderId = traderId; this.disputeOpenerIsBuyer = disputeOpenerIsBuyer; @@ -175,6 +186,7 @@ public Dispute(String tradeId, this.takerContractSignature = takerContractSignature; this.agentPubKeyRing = agentPubKeyRing; this.isSupportTicket = isSupportTicket; + this.supportType = supportType; id = tradeId + "_" + traderId; } @@ -210,6 +222,8 @@ public protobuf.Dispute toProtoMessage() { Optional.ofNullable(makerContractSignature).ifPresent(builder::setMakerContractSignature); Optional.ofNullable(takerContractSignature).ifPresent(builder::setTakerContractSignature); Optional.ofNullable(disputeResultProperty.get()).ifPresent(result -> builder.setDisputeResult(disputeResultProperty.get().toProtoMessage())); + Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType))); + Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); return builder.build(); } @@ -230,7 +244,8 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature()), ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature()), PubKeyRing.fromProto(proto.getAgentPubKeyRing()), - proto.getIsSupportTicket()); + proto.getIsSupportTicket(), + SupportType.fromProto(proto.getSupportType())); dispute.chatMessages.addAll(proto.getChatMessageList().stream() .map(ChatMessage::fromPayloadProto) @@ -241,6 +256,7 @@ public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver corePr if (proto.hasDisputeResult()) dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult())); dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId()); + dispute.setMediatorsDisputeResult(proto.getMediatorsDisputeResult()); return dispute; } @@ -258,10 +274,6 @@ public void addAndPersistChatMessage(ChatMessage chatMessage) { } } - public boolean isMediationDispute() { - return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION; - } - /////////////////////////////////////////////////////////////////////////////////////////// // Setters 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 7049dbcc431..e7ae907394d 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -60,7 +60,7 @@ @Slf4j public abstract class DisputeManager> extends SupportManager { protected final TradeWalletService tradeWalletService; - protected final BtcWalletService walletService; + protected final BtcWalletService btcWalletService; protected final TradeManager tradeManager; protected final ClosedTradableManager closedTradableManager; protected final OpenOfferManager openOfferManager; @@ -74,7 +74,7 @@ public abstract class DisputeManager findOwnDispute(String tradeId) { // Message handler /////////////////////////////////////////////////////////////////////////////////////////// - // arbitrator receives that from trader who opens dispute + // dispute agent receives that from trader who opens dispute protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { T disputeList = getDisputeList(); if (disputeList == null) { @@ -270,15 +272,29 @@ protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessa } // We use the ChatMessage not the openNewDisputeMessage for the ACK - ObservableList messages = openNewDisputeMessage.getDispute().getChatMessages(); + ObservableList messages = dispute.getChatMessages(); if (!messages.isEmpty()) { - ChatMessage msg = messages.get(0); + ChatMessage chatMessage = messages.get(0); PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing(); - sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); + sendAckMessage(chatMessage, sendersPubKeyRing, errorMessage == null, errorMessage); + } + + // In case of refundAgent we add a message with the mediatorsDisputeSummary. Only visible for refundAgent. + if (dispute.getMediatorsDisputeResult() != null) { + String mediatorsDisputeResult = Res.get("support.mediatorsDisputeSummary", dispute.getMediatorsDisputeResult()); + ChatMessage mediatorsDisputeResultMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + mediatorsDisputeResult, + p2PService.getAddress()); + mediatorsDisputeResultMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(mediatorsDisputeResultMessage); } } - // not dispute requester receives that from arbitrator + // not dispute requester receives that from dispute agent protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { T disputeList = getDisputeList(); if (disputeList == null) { @@ -345,17 +361,18 @@ public void sendOpenNewDisputeMessage(Dispute dispute, Optional storedDisputeOptional = findDispute(dispute); if (!storedDisputeOptional.isPresent() || reOpen) { - String disputeInfo = getDisputeInfo(dispute.isMediationDispute()); + String disputeInfo = getDisputeInfo(dispute); String sysMsg = dispute.isSupportTicket() ? Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) : Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); + String message = Res.get("support.systemMsg", sysMsg); ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), pubKeyRing.hashCode(), false, - Res.get("support.systemMsg", sysMsg), + message, p2PService.getAddress()); chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); @@ -368,11 +385,14 @@ public void sendOpenNewDisputeMessage(Dispute dispute, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType()); - log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "chatMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), + agentNodeAddress, + openNewDisputeMessage.getTradeId(), + openNewDisputeMessage.getUid(), chatMessage.getUid()); + p2PService.sendEncryptedMailboxMessage(agentNodeAddress, dispute.getAgentPubKeyRing(), openNewDisputeMessage, @@ -432,7 +452,7 @@ public void onFault(String errorMessage) { } } - // arbitrator sends that to trading peer when he received openDispute request + // dispute agent sends that to trading peer when he received openDispute request private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, Contract contractFromOpener, PubKeyRing pubKeyRing) { @@ -459,10 +479,11 @@ private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, disputeFromOpener.getMakerContractSignature(), disputeFromOpener.getTakerContractSignature(), disputeFromOpener.getAgentPubKeyRing(), - disputeFromOpener.isSupportTicket()); + disputeFromOpener.isSupportTicket(), + disputeFromOpener.getSupportType()); Optional storedDisputeOptional = findDispute(dispute); if (!storedDisputeOptional.isPresent()) { - String disputeInfo = getDisputeInfo(dispute.isMediationDispute()); + String disputeInfo = getDisputeInfo(dispute); String sysMsg = dispute.isSupportTicket() ? Res.get("support.peerOpenedTicket", disputeInfo) : Res.get("support.peerOpenedDispute", disputeInfo); @@ -485,11 +506,12 @@ private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, p2PService.getAddress(), UUID.randomUUID().toString(), getSupportType()); - log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "chatMessage.uid={}", + + log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, chatMessage.uid={}", peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), chatMessage.getUid()); + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, peersPubKeyRing, peerOpenedDisputeMessage, @@ -546,7 +568,7 @@ public void onFault(String errorMessage) { } } - // arbitrator send result to trader + // dispute agent send result to trader public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { T disputeList = getDisputeList(); if (disputeList == null) { @@ -690,12 +712,4 @@ public Optional findDispute(String tradeId) { .filter(e -> e.getTradeId().equals(tradeId)) .findAny(); } - - private String getDisputeInfo(boolean isMediationDispute) { - String role = isMediationDispute ? Res.get("shared.mediator").toLowerCase() : - Res.get("shared.arbitrator2").toLowerCase(); - String link = isMediationDispute ? "https://docs.bisq.network/trading-rules.html#mediation" : - "https://bisq.network/docs/exchange/arbitration-system"; - return Res.get("support.initialInfo", role, role, link); - } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index cbf859e2a24..53adb7cd998 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -24,6 +24,8 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletService; +import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.support.SupportType; @@ -140,6 +142,13 @@ public void cleanupDisputes() { disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED)); } + @Override + protected String getDisputeInfo(Dispute dispute) { + String role = Res.get("shared.arbitrator2").toLowerCase(); + String link = "https://bisq.network/docs/exchange/arbitration-system"; + return Res.get("support.initialInfo", role, role, link); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Message handler @@ -152,7 +161,7 @@ public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { ChatMessage chatMessage = disputeResult.getChatMessage(); checkNotNull(chatMessage, "chatMessage must not be null"); if (Arrays.equals(disputeResult.getArbitratorPubKey(), - walletService.getArbitratorAddressEntry().getPubKey())) { + btcWalletService.getArbitratorAddressEntry().getPubKey())) { log.error("Arbitrator received disputeResultMessage. That must never happen."); return; } @@ -230,7 +239,7 @@ else if (publisher == DisputeResult.Winner.SELLER) if (payoutTx == null) { if (dispute.getDepositTxSerialized() != null) { byte[] multiSigPubKey = isBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey(); - DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, multiSigPubKey); + DeterministicKey multiSigKeyPair = btcWalletService.getMultiSigKeyPair(tradeId, multiSigPubKey); Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx( dispute.getDepositTxSerialized(), disputeResult.getArbitratorSignature(), @@ -243,7 +252,7 @@ else if (publisher == DisputeResult.Winner.SELLER) contract.getSellerMultiSigPubKey(), disputeResult.getArbitratorPubKey() ); - Transaction committedDisputedPayoutTx = tradeWalletService.addTxToWallet(signedDisputedPayoutTx); + Transaction committedDisputedPayoutTx = WalletService.maybeAddSelfTxToWallet(signedDisputedPayoutTx, btcWalletService.getWallet()); tradeWalletService.broadcastTx(committedDisputedPayoutTx, new TxBroadcaster.Callback() { @Override public void onSuccess(Transaction transaction) { @@ -328,9 +337,11 @@ private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerP PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); cleanupRetryMap(uid); - Transaction walletTx = tradeWalletService.addTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction()); - dispute.setDisputePayoutTxId(walletTx.getHashAsString()); - BtcWalletService.printTx("Disputed payoutTx received from peer", walletTx); + + Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet()); + + dispute.setDisputePayoutTxId(committedDisputePayoutTx.getHashAsString()); + BtcWalletService.printTx("Disputed payoutTx received from peer", committedDisputePayoutTx); // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/BuyerDataItem.java b/core/src/main/java/bisq/core/support/dispute/arbitration/TraderDataItem.java similarity index 80% rename from core/src/main/java/bisq/core/support/dispute/arbitration/BuyerDataItem.java rename to core/src/main/java/bisq/core/support/dispute/arbitration/TraderDataItem.java index 7962a4e074f..1ae4cc16462 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/BuyerDataItem.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/TraderDataItem.java @@ -30,20 +30,20 @@ // TODO consider to move to signed witness domain @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) -public class BuyerDataItem { +public class TraderDataItem { private final PaymentAccountPayload paymentAccountPayload; @EqualsAndHashCode.Include private final AccountAgeWitness accountAgeWitness; private final Coin tradeAmount; - private final PublicKey sellerPubKey; + private final PublicKey peersPubKey; - public BuyerDataItem(PaymentAccountPayload paymentAccountPayload, - AccountAgeWitness accountAgeWitness, - Coin tradeAmount, - PublicKey sellerPubKey) { + public TraderDataItem(PaymentAccountPayload paymentAccountPayload, + AccountAgeWitness accountAgeWitness, + Coin tradeAmount, + PublicKey peersPubKey) { this.paymentAccountPayload = paymentAccountPayload; this.accountAgeWitness = accountAgeWitness; this.tradeAmount = tradeAmount; - this.sellerPubKey = sellerPubKey; + this.peersPubKey = peersPubKey; } } diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java index 705a17ff5ef..7ad44e76509 100644 --- a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -20,6 +20,7 @@ import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.support.SupportType; @@ -46,15 +47,12 @@ import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; import com.google.inject.Inject; import com.google.inject.Singleton; -import java.util.Date; -import java.util.GregorianCalendar; import java.util.Optional; import lombok.extern.slf4j.Slf4j; @@ -66,14 +64,6 @@ @Singleton public final class MediationManager extends DisputeManager { - // The date when mediation is activated - private static final Date MEDIATION_ACTIVATED_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 26); - - public static boolean isMediationActivated() { - return new Date().after(MEDIATION_ACTIVATED_DATE); - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -141,6 +131,13 @@ public void cleanupDisputes() { }); } + @Override + protected String getDisputeInfo(Dispute dispute) { + String role = Res.get("shared.mediator").toLowerCase(); + String link = "https://docs.bisq.network/trading-rules.html#mediation"; + return Res.get("support.initialInfo", role, role, link); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Message handler diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeList.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeList.java new file mode 100644 index 00000000000..5b2ba07b5c1 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeList.java @@ -0,0 +1,83 @@ +/* + * 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.support.dispute.refund; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; + +import bisq.common.proto.ProtoUtil; +import bisq.common.storage.Storage; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ToString +/* + * Holds a List of refund dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public final class RefundDisputeList extends DisputeList { + + RefundDisputeList(Storage storage) { + super(storage); + } + + @Override + public void readPersisted() { + // We need to use DisputeList as file name to not lose existing disputes which are stored in the DisputeList file + RefundDisputeList persisted = storage.initAndGetPersisted(this, "RefundDisputeList", 50); + if (persisted != null) { + list.addAll(persisted.getList()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private RefundDisputeList(Storage storage, List list) { + super(storage, list); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(new ArrayList<>(list)))).build(); + } + + public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto, + CoreProtoResolver coreProtoResolver, + Storage storage) { + List list = proto.getDisputeList().stream() + .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) + .collect(Collectors.toList()); + list.forEach(e -> e.setStorage(storage)); + return new RefundDisputeList(storage, list); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeListService.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeListService.java new file mode 100644 index 00000000000..afdac5c9f3b --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundDisputeListService.java @@ -0,0 +1,48 @@ +/* + * 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.support.dispute.refund; + +import bisq.core.support.dispute.DisputeListService; + +import bisq.common.storage.Storage; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public final class RefundDisputeListService extends DisputeListService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public RefundDisputeListService(Storage storage) { + super(storage); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected RefundDisputeList getConcreteDisputeList() { + return new RefundDisputeList(storage); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java new file mode 100644 index 00000000000..34dd6711383 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -0,0 +1,206 @@ +/* + * 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.support.dispute.refund; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class RefundManager extends DisputeManager { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public RefundManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + PubKeyRing pubKeyRing, + RefundDisputeListService refundDisputeListService) { + super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, + openOfferManager, pubKeyRing, refundDisputeListService); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.REFUND; + } + + @Override + public void dispatchMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) { + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + } else if (message instanceof PeerOpenedDisputeMessage) { + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + } else if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + onDisputeResultMessage((DisputeResultMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + @Override + protected Trade.DisputeState getDisputeState_StartedByPeer() { + return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.REFUND_MESSAGE; + } + + @Override + public void cleanupDisputes() { + disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED)); + } + + @Override + protected String getDisputeInfo(Dispute dispute) { + String role = Res.get("shared.refundAgent").toLowerCase(); + String link = "https://bisq.network/docs/exchange/refundAgent"; //todo create link + return Res.get("support.initialInfo", role, role, link); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + // We get that message at both peers. The dispute object is in context of the trader + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); + String tradeId = disputeResult.getTradeId(); + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + Optional disputeOptional = findDispute(disputeResult); + String uid = disputeResultMessage.getUid(); + if (!disputeOptional.isPresent()) { + log.warn("We got a dispute result msg but we don't have a matching dispute. " + + "That might happen when we get the disputeResultMessage before the dispute was created. " + + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 2 sec. to be sure the comm. msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + cleanupRetryMap(uid); + if (!dispute.getChatMessages().contains(chatMessage)) { + dispute.addAndPersistChatMessage(chatMessage); + } else { + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); + } + dispute.setIsClosed(true); + + if (dispute.disputeResultProperty().get() != null) { + log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + + "again because the first close did not succeed. TradeId = " + tradeId); + } + + dispute.setDisputeResult(disputeResult); + + Optional tradeOptional = tradeManager.getTradeById(tradeId); + if (tradeOptional.isPresent()) { + Trade trade = tradeOptional.get(); + if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED || + trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) { + trade.setDisputeState(Trade.DisputeState.REFUND_REQUEST_CLOSED); + } + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); + + // set state after payout as we call swapTradeEntryToAvailableEntry + if (tradeManager.getTradeById(tradeId).isPresent()) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public NodeAddress getAgentNodeAddress(Dispute dispute) { + return dispute.getContract().getRefundAgentNodeAddress(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundResultState.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundResultState.java new file mode 100644 index 00000000000..1664b733bc4 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundResultState.java @@ -0,0 +1,33 @@ +/* + * 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.support.dispute.refund; + +import bisq.common.proto.ProtoUtil; + +// todo +public enum RefundResultState { + UNDEFINED_REFUND_RESULT; + + public static RefundResultState fromProto(protobuf.RefundResultState refundResultState) { + return ProtoUtil.enumFromProto(RefundResultState.class, refundResultState.name()); + } + + public static protobuf.RefundResultState toProtoMessage(RefundResultState refundResultState) { + return protobuf.RefundResultState.valueOf(refundResultState.name()); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundSession.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundSession.java new file mode 100644 index 00000000000..b5e9d7e5cc9 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundSession.java @@ -0,0 +1,33 @@ +/* + * 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.support.dispute.refund; + +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class RefundSession extends DisputeSession { + + public RefundSession(@Nullable Dispute dispute, boolean isTrader) { + super(dispute, isTrader); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgent.java b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgent.java new file mode 100644 index 00000000000..e4a4a47d643 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgent.java @@ -0,0 +1,109 @@ +/* + * 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.support.dispute.refund.refundagent; + +import bisq.core.support.dispute.agent.DisputeAgent; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.ProtoUtil; + +import com.google.protobuf.ByteString; + +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode(callSuper = true) +@Slf4j +@Getter +public final class RefundAgent extends DisputeAgent { + + public RefundAgent(NodeAddress nodeAddress, + PubKeyRing pubKeyRing, + List languageCodes, + long registrationDate, + byte[] registrationPubKey, + String registrationSignature, + @Nullable String emailAddress, + @Nullable String info, + @Nullable Map extraDataMap) { + + super(nodeAddress, + pubKeyRing, + languageCodes, + registrationDate, + registrationPubKey, + registrationSignature, + emailAddress, + info, + extraDataMap); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public protobuf.StoragePayload toProtoMessage() { + protobuf.RefundAgent.Builder builder = protobuf.RefundAgent.newBuilder() + .setNodeAddress(nodeAddress.toProtoMessage()) + .setPubKeyRing(pubKeyRing.toProtoMessage()) + .addAllLanguageCodes(languageCodes) + .setRegistrationDate(registrationDate) + .setRegistrationPubKey(ByteString.copyFrom(registrationPubKey)) + .setRegistrationSignature(registrationSignature); + Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress); + Optional.ofNullable(info).ifPresent(builder::setInfo); + Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + return protobuf.StoragePayload.newBuilder().setRefundAgent(builder).build(); + } + + public static RefundAgent fromProto(protobuf.RefundAgent proto) { + return new RefundAgent(NodeAddress.fromProto(proto.getNodeAddress()), + PubKeyRing.fromProto(proto.getPubKeyRing()), + new ArrayList<>(proto.getLanguageCodesList()), + proto.getRegistrationDate(), + proto.getRegistrationPubKey().toByteArray(), + proto.getRegistrationSignature(), + ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()), + ProtoUtil.stringOrNullFromProto(proto.getInfo()), + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + + @Override + public String toString() { + return "RefundAgent{} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentManager.java b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentManager.java new file mode 100644 index 00000000000..d13560f3dfc --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentManager.java @@ -0,0 +1,105 @@ +/* + * 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.support.dispute.refund.refundagent; + +import bisq.core.app.AppOptionKeys; +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class RefundAgentManager extends DisputeAgentManager { + + @Inject + public RefundAgentManager(KeyRing keyRing, + RefundAgentService refundAgentService, + User user, + FilterManager filterManager, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(keyRing, refundAgentService, user, filterManager, useDevPrivilegeKeys); + } + + @Override + protected List getPubKeyList() { + return List.of("02a25798e256b800d7ea71c31098ac9a47cb20892176afdfeb051f5ded382d44af", + "0360455d3cffe00ef73cc1284c84eedacc8c5c3374c43f4aac8ffb95f5130b9ef5", + "03b0513afbb531bc4551b379eba027feddd33c92b5990fd477b0fa6eff90a5b7db", + "03533fd75fda29c351298e50b8ea696656dcb8ce4e263d10618c6901a50450bf0e", + "028124436482aa4c61a4bc4097d60c80b09f4285413be3b023a37a0164cbd5d818", + "0384fcf883116d8e9469720ed7808cc4141f6dc6a5ed23d76dd48f2f5f255590d7", + "029bd318ecee4e212ff06a4396770d600d72e9e0c6532142a428bdb401491e9721", + "02e375b4b24d0a858953f7f94666667554d41f78000b9c8a301294223688b29011", + "0232c088ae7c070de89d2b6c8d485b34bf0e3b2a964a2c6622f39ca501260c23f7", + "033e047f74f2aa1ce41e8c85731f97ab83d448d65dc8518ab3df4474a5d53a3d19", + "02f52a8cf373c8cbddb318e523b7f111168bf753fdfb6f8aa81f88c950ede3a5ce", + "039784029922c54bcd0f0e7f14530f586053a5f4e596e86b3474cd7404657088ae", + "037969f9d5ab2cc609104c6e61323df55428f8f108c11aab7c7b5f953081d39304", + "031bd37475b8c5615ac46d6816e791c59d806d72a0bc6739ae94e5fe4545c7f8a6", + "021bb92c636feacf5b082313eb071a63dfcd26501a48b3cd248e35438e5afb7daf"); + + + } + + @Override + protected boolean isExpectedInstance(ProtectedStorageEntry data) { + return data.getProtectedStoragePayload() instanceof RefundAgent; + } + + @Override + protected void addAcceptedDisputeAgentToUser(RefundAgent disputeAgent) { + user.addAcceptedRefundAgent(disputeAgent); + } + + @Override + protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { + user.removeAcceptedRefundAgent((RefundAgent) data.getProtectedStoragePayload()); + } + + @Override + protected List getAcceptedDisputeAgentsFromUser() { + return user.getAcceptedRefundAgents(); + } + + @Override + protected void clearAcceptedDisputeAgentsAtUser() { + user.clearAcceptedRefundAgents(); + } + + @Override + protected RefundAgent getRegisteredDisputeAgentFromUser() { + return user.getRegisteredRefundAgent(); + } + + @Override + protected void setRegisteredDisputeAgentAtUser(RefundAgent disputeAgent) { + user.setRegisteredRefundAgent(disputeAgent); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentService.java b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentService.java new file mode 100644 index 00000000000..ab67223e984 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/refund/refundagent/RefundAgentService.java @@ -0,0 +1,61 @@ +/* + * 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.support.dispute.refund.refundagent; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentService; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import com.google.inject.Singleton; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Singleton +public class RefundAgentService extends DisputeAgentService { + @Inject + public RefundAgentService(P2PService p2PService, FilterManager filterManager) { + super(p2PService, filterManager); + } + + @Override + protected Set getDisputeAgentSet(List bannedDisputeAgents) { + return p2PService.getDataMap().values().stream() + .filter(data -> data.getProtectedStoragePayload() instanceof RefundAgent) + .map(data -> (RefundAgent) data.getProtectedStoragePayload()) + .filter(a -> bannedDisputeAgents == null || + !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) + .collect(Collectors.toSet()); + } + + @Override + protected List getDisputeAgentsFromFilter() { + return filterManager.getFilter() != null ? filterManager.getFilter().getRefundAgents() : new ArrayList<>(); + } + + public Map getRefundAgents() { + return super.getDisputeAgents(); + } +} diff --git a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java index 17eeca7a7a4..e6cecc1e4f9 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java @@ -20,7 +20,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.proto.CoreProtoResolver; -import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.protocol.BuyerAsMakerProtocol; import bisq.core.trade.protocol.MakerProtocol; @@ -48,6 +48,7 @@ public BuyerAsMakerTrade(Offer offer, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -56,6 +57,7 @@ public BuyerAsMakerTrade(Offer offer, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } @@ -84,6 +86,7 @@ public static Tradable fromProto(protobuf.BuyerAsMakerTrade buyerAsMakerTradePro proto.getIsCurrencyForTakerFeeBtc(), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, storage, btcWalletService); @@ -107,7 +110,7 @@ protected void createTradeProtocol() { } @Override - public void handleTakeOfferRequest(TradeMessage message, + public void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress taker, ErrorMessageHandler errorMessageHandler) { ((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler); diff --git a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java index c4c2c1e7768..d9b5f86c290 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java @@ -51,6 +51,7 @@ public BuyerAsTakerTrade(Offer offer, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -62,6 +63,7 @@ public BuyerAsTakerTrade(Offer offer, tradingPeerNodeAddress, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } @@ -94,6 +96,7 @@ public static Tradable fromProto(protobuf.BuyerAsTakerTrade buyerAsTakerTradePro proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null, proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, storage, btcWalletService), proto, diff --git a/core/src/main/java/bisq/core/trade/BuyerTrade.java b/core/src/main/java/bisq/core/trade/BuyerTrade.java index 4c534b30f05..4112252e6d6 100644 --- a/core/src/main/java/bisq/core/trade/BuyerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerTrade.java @@ -47,6 +47,7 @@ public abstract class BuyerTrade extends Trade { NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -58,6 +59,7 @@ public abstract class BuyerTrade extends Trade { tradingPeerNodeAddress, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } @@ -68,6 +70,7 @@ public abstract class BuyerTrade extends Trade { boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -76,6 +79,7 @@ public abstract class BuyerTrade extends Trade { isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java index 6ec2e573949..43bd345813b 100644 --- a/core/src/main/java/bisq/core/trade/Contract.java +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -73,6 +73,10 @@ public final class Contract implements NetworkPayload { @JsonExclude private final byte[] takerMultiSigPubKey; + // Added in v1.2.0 + private long lockTime; + private final NodeAddress refundAgentNodeAddress; + public Contract(OfferPayload offerPayload, long tradeAmount, long tradePrice, @@ -91,7 +95,9 @@ public Contract(OfferPayload offerPayload, String makerPayoutAddressString, String takerPayoutAddressString, byte[] makerMultiSigPubKey, - byte[] takerMultiSigPubKey) { + byte[] takerMultiSigPubKey, + long lockTime, + NodeAddress refundAgentNodeAddress) { this.offerPayload = offerPayload; this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; @@ -111,6 +117,8 @@ public Contract(OfferPayload offerPayload, this.takerPayoutAddressString = takerPayoutAddressString; this.makerMultiSigPubKey = makerMultiSigPubKey; this.takerMultiSigPubKey = takerMultiSigPubKey; + this.lockTime = lockTime; + this.refundAgentNodeAddress = refundAgentNodeAddress; String makerPaymentMethodId = makerPaymentAccountPayload.getPaymentMethodId(); String takerPaymentMethodId = takerPaymentAccountPayload.getPaymentMethodId(); @@ -128,7 +136,6 @@ public Contract(OfferPayload offerPayload, // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - @Nullable public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) { return new Contract(OfferPayload.fromProto(proto.getOfferPayload()), proto.getTradeAmount(), @@ -148,7 +155,9 @@ public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver core proto.getMakerPayoutAddressString(), proto.getTakerPayoutAddressString(), proto.getMakerMultiSigPubKey().toByteArray(), - proto.getTakerMultiSigPubKey().toByteArray()); + proto.getTakerMultiSigPubKey().toByteArray(), + proto.getLockTime(), + NodeAddress.fromProto(proto.getRefundAgentNodeAddress())); } @Override @@ -173,6 +182,8 @@ public protobuf.Contract toProtoMessage() { .setTakerPayoutAddressString(takerPayoutAddressString) .setMakerMultiSigPubKey(ByteString.copyFrom(makerMultiSigPubKey)) .setTakerMultiSigPubKey(ByteString.copyFrom(takerMultiSigPubKey)) + .setLockTime(lockTime) + .setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage()) .build(); } @@ -291,6 +302,7 @@ public String toString() { ",\n sellerNodeAddress=" + sellerNodeAddress + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + ",\n isBuyerMakerAndSellerTaker=" + isBuyerMakerAndSellerTaker + ",\n makerAccountId='" + makerAccountId + '\'' + ",\n takerAccountId='" + takerAccountId + '\'' + @@ -304,6 +316,7 @@ public String toString() { ",\n takerMultiSigPubKey=" + Utilities.bytesAsHexString(takerMultiSigPubKey) + ",\n buyerMultiSigPubKey=" + Utilities.bytesAsHexString(getBuyerMultiSigPubKey()) + ",\n sellerMultiSigPubKey=" + Utilities.bytesAsHexString(getSellerMultiSigPubKey()) + + ",\n lockTime=" + lockTime + "\n}"; } } diff --git a/core/src/main/java/bisq/core/trade/MakerTrade.java b/core/src/main/java/bisq/core/trade/MakerTrade.java index ffb5b672892..5a2fd7dd8d3 100644 --- a/core/src/main/java/bisq/core/trade/MakerTrade.java +++ b/core/src/main/java/bisq/core/trade/MakerTrade.java @@ -17,12 +17,12 @@ package bisq.core.trade; -import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; public interface MakerTrade { - void handleTakeOfferRequest(TradeMessage message, NodeAddress peerNodeAddress, ErrorMessageHandler errorMessageHandler); + void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress peerNodeAddress, ErrorMessageHandler errorMessageHandler); } diff --git a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java index 08fb2f46ca2..5e9b5883f9d 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java @@ -20,7 +20,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.proto.CoreProtoResolver; -import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.protocol.MakerProtocol; import bisq.core.trade.protocol.SellerAsMakerProtocol; @@ -48,9 +48,18 @@ public SellerAsMakerTrade(Offer offer, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, storage, btcWalletService); + super(offer, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + refundAgentNodeAddress, + storage, + btcWalletService); } @@ -78,6 +87,7 @@ public static Tradable fromProto(protobuf.SellerAsMakerTrade sellerAsMakerTradeP proto.getIsCurrencyForTakerFeeBtc(), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, storage, btcWalletService); @@ -101,7 +111,7 @@ protected void createTradeProtocol() { } @Override - public void handleTakeOfferRequest(TradeMessage message, NodeAddress taker, ErrorMessageHandler errorMessageHandler) { + public void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress taker, ErrorMessageHandler errorMessageHandler) { ((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler); } } diff --git a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java index 0d4aeb6b847..79debd8137c 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java @@ -51,6 +51,7 @@ public SellerAsTakerTrade(Offer offer, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -62,6 +63,7 @@ public SellerAsTakerTrade(Offer offer, tradingPeerNodeAddress, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } @@ -94,6 +96,7 @@ public static Tradable fromProto(protobuf.SellerAsTakerTrade sellerAsTakerTradeP proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null, proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, + proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null, storage, btcWalletService), proto, diff --git a/core/src/main/java/bisq/core/trade/SellerTrade.java b/core/src/main/java/bisq/core/trade/SellerTrade.java index 629dc5adb52..68eed3a1e08 100644 --- a/core/src/main/java/bisq/core/trade/SellerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerTrade.java @@ -46,6 +46,7 @@ public abstract class SellerTrade extends Trade { NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -57,6 +58,7 @@ public abstract class SellerTrade extends Trade { tradingPeerNodeAddress, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } @@ -67,6 +69,7 @@ public abstract class SellerTrade extends Trade { boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { super(offer, @@ -75,6 +78,7 @@ public abstract class SellerTrade extends Trade { isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index a86ea93fa7b..938b225e437 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -21,6 +21,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.monetary.Price; @@ -34,6 +35,8 @@ import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.RefundResultState; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.protocol.ProcessModel; import bisq.core.trade.protocol.TradeProtocol; @@ -114,7 +117,7 @@ public enum State { // maker perspective MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), - MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), + MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), //todo remove MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.TAKER_FEE_PUBLISHED), // taker perspective @@ -122,21 +125,21 @@ public enum State { // #################### Phase DEPOSIT_PAID - TAKER_PUBLISHED_DEPOSIT_TX(Phase.DEPOSIT_PUBLISHED), + SELLER_PUBLISHED_DEPOSIT_TX(Phase.DEPOSIT_PUBLISHED), // DEPOSIT_TX_PUBLISHED_MSG - // taker perspective - TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), - TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), - TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), - TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + // seller perspective + SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), - // maker perspective - MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), + // buyer perspective + BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PUBLISHED), - // Alternatively the maker could have seen the deposit tx earlier before he received the DEPOSIT_TX_PUBLISHED_MSG - MAKER_SAW_DEPOSIT_TX_IN_NETWORK(Phase.DEPOSIT_PUBLISHED), + // Alternatively the buyer could have seen the deposit tx earlier before he received the DEPOSIT_TX_PUBLISHED_MSG + BUYER_SAW_DEPOSIT_TX_IN_NETWORK(Phase.DEPOSIT_PUBLISHED), // #################### Phase DEPOSIT_CONFIRMED @@ -221,7 +224,12 @@ public enum DisputeState { // mediation MEDIATION_REQUESTED, MEDIATION_STARTED_BY_PEER, - MEDIATION_CLOSED; + MEDIATION_CLOSED, + + // refund + REFUND_REQUESTED, + REFUND_REQUEST_STARTED_BY_PEER, + REFUND_REQUEST_CLOSED; public static Trade.DisputeState fromProto(protobuf.Trade.DisputeState disputeState) { return ProtoUtil.enumFromProto(Trade.DisputeState.class, disputeState.name()); @@ -368,9 +376,14 @@ public static protobuf.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodSt @Getter transient protected TradeProtocol tradeProtocol; @Nullable - transient private Transaction payoutTx; - @Nullable transient private Transaction depositTx; + + // Added in v1.2.0 + @Nullable + transient private Transaction delayedPayoutTx; + + @Nullable + transient private Transaction payoutTx; @Nullable transient private Coin tradeAmount; @@ -378,12 +391,33 @@ public static protobuf.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodSt transient private ObjectProperty tradeVolumeProperty; final transient private Set decryptedMessageWithPubKeySet = new HashSet<>(); - //Added in v1.1.6 + // Added in v1.1.6 @Getter @Nullable private MediationResultState mediationResultState = MediationResultState.UNDEFINED_MEDIATION_RESULT; transient final private ObjectProperty mediationResultStateProperty = new SimpleObjectProperty<>(mediationResultState); + // Added in v1.2.0 + @Getter + @Setter + private long lockTime; + @Nullable + @Getter + @Setter + private String delayedPayoutTxId; + @Nullable + @Getter + @Setter + private NodeAddress refundAgentNodeAddress; + @Nullable + @Getter + @Setter + private PubKeyRing refundAgentPubKeyRing; + @Getter + @Nullable + private RefundResultState refundResultState = RefundResultState.UNDEFINED_REFUND_RESULT; + transient final private ObjectProperty refundResultStateProperty = new SimpleObjectProperty<>(refundResultState); + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization @@ -396,6 +430,7 @@ protected Trade(Offer offer, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { this.offer = offer; @@ -406,6 +441,7 @@ protected Trade(Offer offer, this.btcWalletService = btcWalletService; this.arbitratorNodeAddress = arbitratorNodeAddress; this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; txFeeAsLong = txFee.value; takerFeeAsLong = takerFee.value; @@ -425,6 +461,7 @@ protected Trade(Offer offer, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, @Nullable NodeAddress mediatorNodeAddress, + @Nullable NodeAddress refundAgentNodeAddress, Storage storage, BtcWalletService btcWalletService) { @@ -434,6 +471,7 @@ protected Trade(Offer offer, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, + refundAgentNodeAddress, storage, btcWalletService); this.tradePrice = tradePrice; @@ -463,7 +501,8 @@ public Message toProtoMessage() { .setTradePeriodState(Trade.TradePeriodState.toProtoMessage(tradePeriodState)) .addAllChatMessage(chatMessages.stream() .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) - .collect(Collectors.toList())); + .collect(Collectors.toList())) + .setLockTime(lockTime); Optional.ofNullable(takerFeeTxId).ifPresent(builder::setTakerFeeTxId); Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId); @@ -476,13 +515,17 @@ public Message toProtoMessage() { Optional.ofNullable(makerContractSignature).ifPresent(builder::setMakerContractSignature); Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())); Optional.ofNullable(mediatorNodeAddress).ifPresent(e -> builder.setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage())); + Optional.ofNullable(refundAgentNodeAddress).ifPresent(e -> builder.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())); Optional.ofNullable(arbitratorBtcPubKey).ifPresent(e -> builder.setArbitratorBtcPubKey(ByteString.copyFrom(arbitratorBtcPubKey))); Optional.ofNullable(takerPaymentAccountId).ifPresent(builder::setTakerPaymentAccountId); Optional.ofNullable(errorMessage).ifPresent(builder::setErrorMessage); Optional.ofNullable(arbitratorPubKeyRing).ifPresent(e -> builder.setArbitratorPubKeyRing(arbitratorPubKeyRing.toProtoMessage())); Optional.ofNullable(mediatorPubKeyRing).ifPresent(e -> builder.setMediatorPubKeyRing(mediatorPubKeyRing.toProtoMessage())); + Optional.ofNullable(refundAgentPubKeyRing).ifPresent(e -> builder.setRefundAgentPubKeyRing(refundAgentPubKeyRing.toProtoMessage())); Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); + Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); + Optional.ofNullable(delayedPayoutTxId).ifPresent(e -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); return builder.build(); } @@ -502,13 +545,19 @@ public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolv trade.setMakerContractSignature(ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature())); trade.setArbitratorNodeAddress(proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); trade.setMediatorNodeAddress(proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null); + trade.setRefundAgentNodeAddress(proto.hasRefundAgentNodeAddress() ? NodeAddress.fromProto(proto.getRefundAgentNodeAddress()) : null); trade.setArbitratorBtcPubKey(ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorBtcPubKey())); trade.setTakerPaymentAccountId(ProtoUtil.stringOrNullFromProto(proto.getTakerPaymentAccountId())); trade.setErrorMessage(ProtoUtil.stringOrNullFromProto(proto.getErrorMessage())); trade.setArbitratorPubKeyRing(proto.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()) : null); trade.setMediatorPubKeyRing(proto.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(proto.getMediatorPubKeyRing()) : null); + trade.setRefundAgentPubKeyRing(proto.hasRefundAgentPubKeyRing() ? PubKeyRing.fromProto(proto.getRefundAgentPubKeyRing()) : null); trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState())); + trade.setRefundResultState(RefundResultState.fromProto(proto.getRefundResultState())); + String delayedPayoutTxId = proto.getDelayedPayoutTxId(); + trade.setDelayedPayoutTxId(delayedPayoutTxId.isEmpty() ? null : delayedPayoutTxId); + trade.setLockTime(proto.getLockTime()); trade.chatMessages.addAll(proto.getChatMessageList().stream() .map(ChatMessage::fromPayloadProto) @@ -531,6 +580,7 @@ public void init(P2PService p2PService, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, TradeWalletService tradeWalletService, + DaoFacade daoFacade, TradeManager tradeManager, OpenOfferManager openOfferManager, ReferralIdService referralIdService, @@ -540,6 +590,7 @@ public void init(P2PService p2PService, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, KeyRing keyRing, boolean useSavingsWallet, Coin fundsNeededForTrade) { @@ -550,6 +601,7 @@ public void init(P2PService p2PService, btcWalletService, bsqWalletService, tradeWalletService, + daoFacade, referralIdService, user, filterManager, @@ -557,6 +609,7 @@ public void init(P2PService p2PService, tradeStatisticsManager, arbitratorManager, mediatorManager, + refundAgentManager, keyRing, useSavingsWallet, fundsNeededForTrade); @@ -572,6 +625,11 @@ public void init(P2PService p2PService, persist(); }); + refundAgentManager.getDisputeAgentByNodeAddress(refundAgentNodeAddress).ifPresent(refundAgent -> { + refundAgentPubKeyRing = refundAgent.getPubKeyRing(); + persist(); + }); + createTradeProtocol(); // If we have already received a msg we apply it. @@ -590,10 +648,10 @@ public void init(P2PService p2PService, // The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet. void updateDepositTxFromWallet() { if (getDepositTx() != null) - setDepositTx(processModel.getTradeWalletService().getWalletTx(getDepositTx().getHash())); + applyDepositTx(processModel.getTradeWalletService().getWalletTx(getDepositTx().getHash())); } - public void setDepositTx(Transaction tx) { + public void applyDepositTx(Transaction tx) { log.debug("setDepositTx " + tx); this.depositTx = tx; depositTxId = depositTx.getHashAsString(); @@ -608,6 +666,19 @@ public Transaction getDepositTx() { return depositTx; } + public void applyDelayedPayoutTx(Transaction delayedPayoutTx) { + this.delayedPayoutTx = delayedPayoutTx; + delayedPayoutTxId = delayedPayoutTx.getHashAsString(); + persist(); + } + + @Nullable + public Transaction getDelayedPayoutTx() { + if (delayedPayoutTx == null) + delayedPayoutTx = delayedPayoutTxId != null ? btcWalletService.getTransaction(delayedPayoutTxId) : null; + return delayedPayoutTx; + } + // We don't need to persist the msg as if we dont apply it it will not be removed from the P2P network and we // will received it again at next startup. Such might happen in edge cases when the user shuts down after we // received the msb but before the init is called. @@ -703,6 +774,14 @@ public void setMediationResultState(MediationResultState mediationResultState) { persist(); } + public void setRefundResultState(RefundResultState refundResultState) { + boolean changed = this.refundResultState != refundResultState; + this.refundResultState = refundResultState; + refundResultStateProperty.set(refundResultState); + if (changed) + persist(); + } + public void setTradePeriodState(TradePeriodState tradePeriodState) { boolean changed = this.tradePeriodState != tradePeriodState; @@ -821,7 +900,12 @@ public boolean isDepositPublished() { } public boolean isFundsLockedIn() { - return isDepositPublished() && !isPayoutPublished() && disputeState != DisputeState.DISPUTE_CLOSED; + return isDepositPublished() && + !isPayoutPublished() && + disputeState != DisputeState.DISPUTE_CLOSED && + disputeState != DisputeState.REFUND_REQUESTED && + disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER && + disputeState != DisputeState.REFUND_REQUEST_CLOSED; } public boolean isDepositConfirmed() { @@ -832,7 +916,6 @@ public boolean isFiatSent() { return getState().getPhase().ordinal() >= Phase.FIAT_SENT.ordinal(); } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean isFiatReceived() { return getState().getPhase().ordinal() >= Phase.FIAT_RECEIVED.ordinal(); } @@ -861,6 +944,10 @@ public ReadOnlyObjectProperty mediationResultStateProperty return mediationResultStateProperty; } + public ReadOnlyObjectProperty refundResultStateProperty() { + return refundResultStateProperty; + } + public ReadOnlyObjectProperty tradePeriodStateProperty() { return tradePeriodStateProperty; } @@ -987,7 +1074,7 @@ public String toString() { ",\n isCurrencyForTakerFeeBtc=" + isCurrencyForTakerFeeBtc + ",\n txFeeAsLong=" + txFeeAsLong + ",\n takerFeeAsLong=" + takerFeeAsLong + - ",\n takeOfferDate=" + getTakeOfferDate() + + ",\n takeOfferDate=" + takeOfferDate + ",\n processModel=" + processModel + ",\n takerFeeTxId='" + takerFeeTxId + '\'' + ",\n depositTxId='" + depositTxId + '\'' + @@ -1004,10 +1091,14 @@ public String toString() { ",\n takerContractSignature='" + takerContractSignature + '\'' + ",\n makerContractSignature='" + makerContractSignature + '\'' + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + - ",\n mediatorNodeAddress=" + mediatorNodeAddress + ",\n arbitratorBtcPubKey=" + Utilities.bytesAsHexString(arbitratorBtcPubKey) + + ",\n arbitratorPubKeyRing=" + arbitratorPubKeyRing + + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + ",\n mediatorPubKeyRing=" + mediatorPubKeyRing + ",\n takerPaymentAccountId='" + takerPaymentAccountId + '\'' + ",\n errorMessage='" + errorMessage + '\'' + + ",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' + + ",\n chatMessages=" + chatMessages + ",\n txFee=" + txFee + ",\n takerFee=" + takerFee + ",\n storage=" + storage + @@ -1018,15 +1109,21 @@ public String toString() { ",\n tradePeriodStateProperty=" + tradePeriodStateProperty + ",\n errorMessageProperty=" + errorMessageProperty + ",\n tradeProtocol=" + tradeProtocol + - ",\n payoutTx=" + payoutTx + ",\n depositTx=" + depositTx + + ",\n delayedPayoutTx=" + delayedPayoutTx + + ",\n payoutTx=" + payoutTx + ",\n tradeAmount=" + tradeAmount + ",\n tradeAmountProperty=" + tradeAmountProperty + ",\n tradeVolumeProperty=" + tradeVolumeProperty + ",\n decryptedMessageWithPubKeySet=" + decryptedMessageWithPubKeySet + - ",\n arbitratorPubKeyRing=" + arbitratorPubKeyRing + - ",\n mediatorPubKeyRing=" + mediatorPubKeyRing + - ",\n chatMessages=" + chatMessages + + ",\n mediationResultState=" + mediationResultState + + ",\n mediationResultStateProperty=" + mediationResultStateProperty + + ",\n lockTime=" + lockTime + + ",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + + ",\n refundAgentPubKeyRing=" + refundAgentPubKeyRing + + ",\n refundResultState=" + refundResultState + + ",\n refundResultStateProperty=" + refundResultStateProperty + "\n}"; } } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index d3e57398885..d3612d95165 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -19,10 +19,14 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.exceptions.AddressEntryException; +import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletService; +import bisq.core.dao.DaoFacade; import bisq.core.filter.FilterManager; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -32,10 +36,12 @@ import bisq.core.provider.price.PriceFeedService; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.handlers.TradeResultHandler; -import bisq.core.trade.messages.PayDepositRequest; +import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.statistics.ReferralIdService; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -47,6 +53,7 @@ import bisq.network.p2p.BootstrapListener; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; import bisq.common.ClockWatcher; import bisq.common.UserThread; @@ -82,6 +89,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -116,6 +124,8 @@ public class TradeManager implements PersistedDataHost { private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + private final DaoFacade daoFacade; private final ClockWatcher clockWatcher; private final Storage> tradableListStorage; @@ -150,6 +160,8 @@ public TradeManager(User user, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, ClockWatcher clockWatcher, Storage> storage) { this.user = user; @@ -168,6 +180,8 @@ public TradeManager(User user, this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.daoFacade = daoFacade; this.clockWatcher = clockWatcher; tradableListStorage = storage; @@ -176,8 +190,8 @@ public TradeManager(User user, NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); // Handler for incoming initial network_messages from taker - if (networkEnvelope instanceof PayDepositRequest) { - handlePayDepositRequest((PayDepositRequest) networkEnvelope, peerNodeAddress); + if (networkEnvelope instanceof InputsForDepositTxRequest) { + handlePayDepositRequest((InputsForDepositTxRequest) networkEnvelope, peerNodeAddress); } }); @@ -307,18 +321,18 @@ private void cleanUpAddressEntries() { }); } - private void handlePayDepositRequest(PayDepositRequest payDepositRequest, NodeAddress peer) { + private void handlePayDepositRequest(InputsForDepositTxRequest inputsForDepositTxRequest, NodeAddress peer) { log.info("Received PayDepositRequest from {} with tradeId {} and uid {}", - peer, payDepositRequest.getTradeId(), payDepositRequest.getUid()); + peer, inputsForDepositTxRequest.getTradeId(), inputsForDepositTxRequest.getUid()); try { - Validator.nonEmptyStringOf(payDepositRequest.getTradeId()); + Validator.nonEmptyStringOf(inputsForDepositTxRequest.getTradeId()); } catch (Throwable t) { - log.warn("Invalid requestDepositTxInputsMessage " + payDepositRequest.toString()); + log.warn("Invalid requestDepositTxInputsMessage " + inputsForDepositTxRequest.toString()); return; } - Optional openOfferOptional = openOfferManager.getOpenOfferById(payDepositRequest.getTradeId()); + Optional openOfferOptional = openOfferManager.getOpenOfferById(inputsForDepositTxRequest.getTradeId()); if (openOfferOptional.isPresent() && openOfferOptional.get().getState() == OpenOffer.State.AVAILABLE) { OpenOffer openOffer = openOfferOptional.get(); Offer offer = openOffer.getOffer(); @@ -326,26 +340,28 @@ private void handlePayDepositRequest(PayDepositRequest payDepositRequest, NodeAd Trade trade; if (offer.isBuyOffer()) trade = new BuyerAsMakerTrade(offer, - Coin.valueOf(payDepositRequest.getTxFee()), - Coin.valueOf(payDepositRequest.getTakerFee()), - payDepositRequest.isCurrencyForTakerFeeBtc(), + Coin.valueOf(inputsForDepositTxRequest.getTxFee()), + Coin.valueOf(inputsForDepositTxRequest.getTakerFee()), + inputsForDepositTxRequest.isCurrencyForTakerFeeBtc(), openOffer.getArbitratorNodeAddress(), openOffer.getMediatorNodeAddress(), + openOffer.getRefundAgentNodeAddress(), tradableListStorage, btcWalletService); else trade = new SellerAsMakerTrade(offer, - Coin.valueOf(payDepositRequest.getTxFee()), - Coin.valueOf(payDepositRequest.getTakerFee()), - payDepositRequest.isCurrencyForTakerFeeBtc(), + Coin.valueOf(inputsForDepositTxRequest.getTxFee()), + Coin.valueOf(inputsForDepositTxRequest.getTakerFee()), + inputsForDepositTxRequest.isCurrencyForTakerFeeBtc(), openOffer.getArbitratorNodeAddress(), openOffer.getMediatorNodeAddress(), + openOffer.getRefundAgentNodeAddress(), tradableListStorage, btcWalletService); initTrade(trade, trade.getProcessModel().isUseSavingsWallet(), trade.getProcessModel().getFundsNeededForTradeAsLong()); tradableList.add(trade); - ((MakerTrade) trade).handleTakeOfferRequest(payDepositRequest, peer, errorMessage -> { + ((MakerTrade) trade).handleTakeOfferRequest(inputsForDepositTxRequest, peer, errorMessage -> { if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); @@ -362,6 +378,7 @@ private void initTrade(Trade trade, boolean useSavingsWallet, Coin fundsNeededFo btcWalletService, bsqWalletService, tradeWalletService, + daoFacade, this, openOfferManager, referralIdService, @@ -371,6 +388,7 @@ private void initTrade(Trade trade, boolean useSavingsWallet, Coin fundsNeededFo tradeStatisticsManager, arbitratorManager, mediatorManager, + refundAgentManager, keyRing, useSavingsWallet, fundsNeededForTrade); @@ -453,6 +471,7 @@ private void createTrade(Coin amount, model.getPeerNodeAddress(), model.getSelectedArbitrator(), model.getSelectedMediator(), + model.getSelectedRefundAgent(), tradableListStorage, btcWalletService); else @@ -465,6 +484,7 @@ private void createTrade(Coin amount, model.getPeerNodeAddress(), model.getSelectedArbitrator(), model.getSelectedMediator(), + model.getSelectedRefundAgent(), tradableListStorage, btcWalletService); @@ -567,6 +587,70 @@ public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) } } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Publish delayed payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public void publishDelayedPayoutTx(String tradeId, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + getTradeById(tradeId).ifPresent(trade -> { + Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + if (delayedPayoutTx != null) { + // We have spent the funds from the deposit tx with the delayedPayoutTx + btcWalletService.swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.MULTI_SIG); + // We might receive funds on AddressEntry.Context.TRADE_PAYOUT so we don't swap that + + Transaction committedDelayedPayoutTx = WalletService.maybeAddSelfTxToWallet(delayedPayoutTx, btcWalletService.getWallet()); + + tradeWalletService.broadcastTx(committedDelayedPayoutTx, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("publishDelayedPayoutTx onSuccess " + transaction); + NodeAddress tradingPeerNodeAddress = trade.getTradingPeerNodeAddress(); + PeerPublishedDelayedPayoutTxMessage msg = new PeerPublishedDelayedPayoutTxMessage(UUID.randomUUID().toString(), + tradeId, + tradingPeerNodeAddress); + p2PService.sendEncryptedMailboxMessage( + tradingPeerNodeAddress, + trade.getProcessModel().getTradingPeer().getPubKeyRing(), + msg, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + resultHandler.handleResult(); + log.info("SendMailboxMessageListener onArrived tradeId={} at peer {}", + tradeId, tradingPeerNodeAddress); + } + + @Override + public void onStoredInMailbox() { + resultHandler.handleResult(); + log.info("SendMailboxMessageListener onStoredInMailbox tradeId={} at peer {}", + tradeId, tradingPeerNodeAddress); + } + + @Override + public void onFault(String errorMessage) { + log.error("SendMailboxMessageListener onFault tradeId={} at peer {}", + tradeId, tradingPeerNodeAddress); + errorMessageHandler.handleErrorMessage(errorMessage); + } + } + ); + } + + @Override + public void onFailure(TxBroadcastException exception) { + log.error("publishDelayedPayoutTx onFailure", exception); + errorMessageHandler.handleErrorMessage(exception.toString()); + } + }); + } + }); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureRequest.java b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureRequest.java new file mode 100644 index 00000000000..0d1182001b4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureRequest.java @@ -0,0 +1,89 @@ +/* + * 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.trade.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class DelayedPayoutTxSignatureRequest extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final byte[] delayedPayoutTx; + + public DelayedPayoutTxSignatureRequest(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTx) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + delayedPayoutTx); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DelayedPayoutTxSignatureRequest(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTx) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.delayedPayoutTx = delayedPayoutTx; + } + + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDelayedPayoutTxSignatureRequest(protobuf.DelayedPayoutTxSignatureRequest.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx))) + .build(); + } + + public static DelayedPayoutTxSignatureRequest fromProto(protobuf.DelayedPayoutTxSignatureRequest proto, int messageVersion) { + return new DelayedPayoutTxSignatureRequest(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDelayedPayoutTx().toByteArray()); + } + + @Override + public String toString() { + return "DelayedPayoutTxSignatureRequest{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n delayedPayoutTx=" + Utilities.bytesAsHexString(delayedPayoutTx) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureResponse.java b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureResponse.java new file mode 100644 index 00000000000..639d5edb72a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DelayedPayoutTxSignatureResponse.java @@ -0,0 +1,90 @@ +/* + * 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.trade.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class DelayedPayoutTxSignatureResponse extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final byte[] delayedPayoutTxSignature; + + public DelayedPayoutTxSignatureResponse(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTxSignature) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + delayedPayoutTxSignature); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DelayedPayoutTxSignatureResponse(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] delayedPayoutTxSignature) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.delayedPayoutTxSignature = delayedPayoutTxSignature; + } + + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDelayedPayoutTxSignatureResponse(protobuf.DelayedPayoutTxSignatureResponse.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDelayedPayoutTxSignature(ByteString.copyFrom(delayedPayoutTxSignature)) + ) + .build(); + } + + public static DelayedPayoutTxSignatureResponse fromProto(protobuf.DelayedPayoutTxSignatureResponse proto, int messageVersion) { + return new DelayedPayoutTxSignatureResponse(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDelayedPayoutTxSignature().toByteArray()); + } + + @Override + public String toString() { + return "DelayedPayoutTxSignatureResponse{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n delayedPayoutTxSignature=" + Utilities.bytesAsHexString(delayedPayoutTxSignature) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DepositTxAndDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/messages/DepositTxAndDelayedPayoutTxMessage.java new file mode 100644 index 00000000000..6d90bb1f2de --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/DepositTxAndDelayedPayoutTxMessage.java @@ -0,0 +1,98 @@ +/* + * 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.trade.messages; + +import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +// It is the last message in the take offer phase. We use MailboxMessage instead of DirectMessage to add more tolerance +// in case of network issues and as the message does not trigger further protocol execution. +@EqualsAndHashCode(callSuper = true) +@Value +public final class DepositTxAndDelayedPayoutTxMessage extends TradeMessage implements MailboxMessage { + private final NodeAddress senderNodeAddress; + private final byte[] depositTx; + private final byte[] delayedPayoutTx; + + public DepositTxAndDelayedPayoutTxMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTx, + byte[] delayedPayoutTx) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + depositTx, + delayedPayoutTx); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private DepositTxAndDelayedPayoutTxMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTx, + byte[] delayedPayoutTx) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.depositTx = depositTx; + this.delayedPayoutTx = delayedPayoutTx; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setDepositTxAndDelayedPayoutTxMessage(protobuf.DepositTxAndDelayedPayoutTxMessage.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setDepositTx(ByteString.copyFrom(depositTx)) + .setDelayedPayoutTx(ByteString.copyFrom(delayedPayoutTx))) + .build(); + } + + public static DepositTxAndDelayedPayoutTxMessage fromProto(protobuf.DepositTxAndDelayedPayoutTxMessage proto, int messageVersion) { + return new DepositTxAndDelayedPayoutTxMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDepositTx().toByteArray(), + proto.getDelayedPayoutTx().toByteArray()); + } + + @Override + public String toString() { + return "DepositTxAndDelayedPayoutTxMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n depositTx=" + Utilities.bytesAsHexString(depositTx) + + ",\n delayedPayoutTx=" + Utilities.bytesAsHexString(delayedPayoutTx) + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/DepositTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/messages/DepositTxMessage.java similarity index 58% rename from core/src/main/java/bisq/core/trade/messages/DepositTxPublishedMessage.java rename to core/src/main/java/bisq/core/trade/messages/DepositTxMessage.java index e5fd07021e0..4350afd2266 100644 --- a/core/src/main/java/bisq/core/trade/messages/DepositTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/DepositTxMessage.java @@ -17,7 +17,7 @@ package bisq.core.trade.messages; -import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.DirectMessage; import bisq.network.p2p.NodeAddress; import bisq.common.app.Version; @@ -28,64 +28,63 @@ import lombok.EqualsAndHashCode; import lombok.Value; +// It is the last message in the take offer phase. We use MailboxMessage instead of DirectMessage to add more tolerance +// in case of network issues and as the message does not trigger further protocol execution. @EqualsAndHashCode(callSuper = true) @Value -public final class DepositTxPublishedMessage extends TradeMessage implements MailboxMessage { - private final byte[] depositTx; +public final class DepositTxMessage extends TradeMessage implements DirectMessage { private final NodeAddress senderNodeAddress; + private final byte[] depositTx; - public DepositTxPublishedMessage(String tradeId, - byte[] depositTx, - NodeAddress senderNodeAddress, - String uid) { - this(tradeId, - depositTx, - senderNodeAddress, + public DepositTxMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTx) { + this(Version.getP2PMessageVersion(), uid, - Version.getP2PMessageVersion()); + tradeId, + senderNodeAddress, + depositTx); } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private DepositTxPublishedMessage(String tradeId, - byte[] depositTx, - NodeAddress senderNodeAddress, - String uid, - int messageVersion) { + private DepositTxMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] depositTx) { super(messageVersion, tradeId, uid); - this.depositTx = depositTx; this.senderNodeAddress = senderNodeAddress; + this.depositTx = depositTx; } - @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { return getNetworkEnvelopeBuilder() - .setDepositTxPublishedMessage(protobuf.DepositTxPublishedMessage.newBuilder() + .setDepositTxMessage(protobuf.DepositTxMessage.newBuilder() + .setUid(uid) .setTradeId(tradeId) - .setDepositTx(ByteString.copyFrom(depositTx)) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid)) + .setDepositTx(ByteString.copyFrom(depositTx))) .build(); } - public static DepositTxPublishedMessage fromProto(protobuf.DepositTxPublishedMessage proto, int messageVersion) { - return new DepositTxPublishedMessage(proto.getTradeId(), - proto.getDepositTx().toByteArray(), - NodeAddress.fromProto(proto.getSenderNodeAddress()), + public static DepositTxMessage fromProto(protobuf.DepositTxMessage proto, int messageVersion) { + return new DepositTxMessage(messageVersion, proto.getUid(), - messageVersion); + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getDepositTx().toByteArray()); } - @Override public String toString() { - return "DepositTxPublishedMessage{" + - "\n depositTx=" + Utilities.bytesAsHexString(depositTx) + - ",\n senderNodeAddress=" + senderNodeAddress + - ",\n uid='" + uid + '\'' + + return "DepositTxMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + ",\n depositTx=" + Utilities.bytesAsHexString(depositTx) + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxRequest.java similarity index 72% rename from core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java rename to core/src/main/java/bisq/core/trade/messages/InputsForDepositTxRequest.java index 05447f26a65..defe3165080 100644 --- a/core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxRequest.java @@ -21,6 +21,7 @@ import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.proto.CoreProtoResolver; +import bisq.network.p2p.DirectMessage; import bisq.network.p2p.NodeAddress; import bisq.common.crypto.PubKeyRing; @@ -29,7 +30,6 @@ import com.google.protobuf.ByteString; -import java.util.Date; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -41,7 +41,7 @@ @EqualsAndHashCode(callSuper = true) @Value -public final class PayDepositRequest extends TradeMessage { +public final class InputsForDepositTxRequest extends TradeMessage implements DirectMessage { private final NodeAddress senderNodeAddress; private final long tradeAmount; private final long tradePrice; @@ -60,38 +60,42 @@ public final class PayDepositRequest extends TradeMessage { private final String takerFeeTxId; private final List acceptedArbitratorNodeAddresses; private final List acceptedMediatorNodeAddresses; + private final List acceptedRefundAgentNodeAddresses; private final NodeAddress arbitratorNodeAddress; private final NodeAddress mediatorNodeAddress; + private final NodeAddress refundAgentNodeAddress; // added in v 0.6. can be null if we trade with an older peer @Nullable private final byte[] accountAgeWitnessSignatureOfOfferId; private final long currentDate; - public PayDepositRequest(String tradeId, - NodeAddress senderNodeAddress, - long tradeAmount, - long tradePrice, - long txFee, - long takerFee, - boolean isCurrencyForTakerFeeBtc, - List rawTransactionInputs, - long changeOutputValue, - @Nullable String changeOutputAddress, - byte[] takerMultiSigPubKey, - String takerPayoutAddressString, - PubKeyRing takerPubKeyRing, - PaymentAccountPayload takerPaymentAccountPayload, - String takerAccountId, - String takerFeeTxId, - List acceptedArbitratorNodeAddresses, - List acceptedMediatorNodeAddresses, - NodeAddress arbitratorNodeAddress, - NodeAddress mediatorNodeAddress, - String uid, - int messageVersion, - @Nullable byte[] accountAgeWitnessSignatureOfOfferId, - long currentDate) { + public InputsForDepositTxRequest(String tradeId, + NodeAddress senderNodeAddress, + long tradeAmount, + long tradePrice, + long txFee, + long takerFee, + boolean isCurrencyForTakerFeeBtc, + List rawTransactionInputs, + long changeOutputValue, + @Nullable String changeOutputAddress, + byte[] takerMultiSigPubKey, + String takerPayoutAddressString, + PubKeyRing takerPubKeyRing, + PaymentAccountPayload takerPaymentAccountPayload, + String takerAccountId, + String takerFeeTxId, + List acceptedArbitratorNodeAddresses, + List acceptedMediatorNodeAddresses, + List acceptedRefundAgentNodeAddresses, + NodeAddress arbitratorNodeAddress, + NodeAddress mediatorNodeAddress, + NodeAddress refundAgentNodeAddress, + String uid, + int messageVersion, + @Nullable byte[] accountAgeWitnessSignatureOfOfferId, + long currentDate) { super(messageVersion, tradeId, uid); this.senderNodeAddress = senderNodeAddress; this.tradeAmount = tradeAmount; @@ -110,8 +114,10 @@ public PayDepositRequest(String tradeId, this.takerFeeTxId = takerFeeTxId; this.acceptedArbitratorNodeAddresses = acceptedArbitratorNodeAddresses; this.acceptedMediatorNodeAddresses = acceptedMediatorNodeAddresses; + this.acceptedRefundAgentNodeAddresses = acceptedRefundAgentNodeAddresses; this.arbitratorNodeAddress = arbitratorNodeAddress; this.mediatorNodeAddress = mediatorNodeAddress; + this.refundAgentNodeAddress = refundAgentNodeAddress; this.accountAgeWitnessSignatureOfOfferId = accountAgeWitnessSignatureOfOfferId; this.currentDate = currentDate; } @@ -123,7 +129,7 @@ public PayDepositRequest(String tradeId, @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.PayDepositRequest.Builder builder = protobuf.PayDepositRequest.newBuilder() + protobuf.InputsForDepositTxRequest.Builder builder = protobuf.InputsForDepositTxRequest.newBuilder() .setTradeId(tradeId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setTradeAmount(tradeAmount) @@ -144,20 +150,23 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { .map(NodeAddress::toProtoMessage).collect(Collectors.toList())) .addAllAcceptedMediatorNodeAddresses(acceptedMediatorNodeAddresses.stream() .map(NodeAddress::toProtoMessage).collect(Collectors.toList())) + .addAllAcceptedRefundAgentNodeAddresses(acceptedRefundAgentNodeAddresses.stream() + .map(NodeAddress::toProtoMessage).collect(Collectors.toList())) .setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()) .setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage()) + .setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage()) .setUid(uid); Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); - return getNetworkEnvelopeBuilder().setPayDepositRequest(builder).build(); + return getNetworkEnvelopeBuilder().setInputsForDepositTxRequest(builder).build(); } - public static PayDepositRequest fromProto(protobuf.PayDepositRequest proto, - CoreProtoResolver coreProtoResolver, - int messageVersion) { + public static InputsForDepositTxRequest fromProto(protobuf.InputsForDepositTxRequest proto, + CoreProtoResolver coreProtoResolver, + int messageVersion) { List rawTransactionInputs = proto.getRawTransactionInputsList().stream() .map(rawTransactionInput -> new RawTransactionInput(rawTransactionInput.getIndex(), rawTransactionInput.getParentTransaction().toByteArray(), rawTransactionInput.getValue())) @@ -166,8 +175,10 @@ public static PayDepositRequest fromProto(protobuf.PayDepositRequest proto, .map(NodeAddress::fromProto).collect(Collectors.toList()); List acceptedMediatorNodeAddresses = proto.getAcceptedMediatorNodeAddressesList().stream() .map(NodeAddress::fromProto).collect(Collectors.toList()); + List acceptedRefundAgentNodeAddresses = proto.getAcceptedRefundAgentNodeAddressesList().stream() + .map(NodeAddress::fromProto).collect(Collectors.toList()); - return new PayDepositRequest(proto.getTradeId(), + return new InputsForDepositTxRequest(proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getTradeAmount(), proto.getTradePrice(), @@ -185,8 +196,10 @@ public static PayDepositRequest fromProto(protobuf.PayDepositRequest proto, proto.getTakerFeeTxId(), acceptedArbitratorNodeAddresses, acceptedMediatorNodeAddresses, + acceptedRefundAgentNodeAddresses, NodeAddress.fromProto(proto.getArbitratorNodeAddress()), NodeAddress.fromProto(proto.getMediatorNodeAddress()), + NodeAddress.fromProto(proto.getRefundAgentNodeAddress()), proto.getUid(), messageVersion, ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfOfferId()), @@ -195,7 +208,7 @@ public static PayDepositRequest fromProto(protobuf.PayDepositRequest proto, @Override public String toString() { - return "PayDepositRequest{" + + return "InputsForDepositTxRequest{" + "\n senderNodeAddress=" + senderNodeAddress + ",\n tradeAmount=" + tradeAmount + ",\n tradePrice=" + tradePrice + @@ -213,11 +226,12 @@ public String toString() { ",\n takerFeeTxId='" + takerFeeTxId + '\'' + ",\n acceptedArbitratorNodeAddresses=" + acceptedArbitratorNodeAddresses + ",\n acceptedMediatorNodeAddresses=" + acceptedMediatorNodeAddresses + + ",\n acceptedRefundAgentNodeAddresses=" + acceptedRefundAgentNodeAddresses + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + ",\n mediatorNodeAddress=" + mediatorNodeAddress + - ",\n uid='" + uid + '\'' + + ",\n refundAgentNodeAddress=" + refundAgentNodeAddress + ",\n accountAgeWitnessSignatureOfOfferId=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfOfferId) + - ",\n currentDate=" + new Date(currentDate) + + ",\n currentDate=" + currentDate + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PublishDepositTxRequest.java b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxResponse.java similarity index 69% rename from core/src/main/java/bisq/core/trade/messages/PublishDepositTxRequest.java rename to core/src/main/java/bisq/core/trade/messages/InputsForDepositTxResponse.java index 7e115979fbf..994be36e5bb 100644 --- a/core/src/main/java/bisq/core/trade/messages/PublishDepositTxRequest.java +++ b/core/src/main/java/bisq/core/trade/messages/InputsForDepositTxResponse.java @@ -21,7 +21,7 @@ import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.proto.CoreProtoResolver; -import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.DirectMessage; import bisq.network.p2p.NodeAddress; import bisq.common.app.Version; @@ -40,12 +40,9 @@ import javax.annotation.Nullable; -// We use a MailboxMessage here because the taker has paid already the trade fee and it could be that -// we lost connection to him but we are complete on our side. So even if the peer is offline he can -// continue later to complete the deposit tx. @EqualsAndHashCode(callSuper = true) @Value -public final class PublishDepositTxRequest extends TradeMessage implements MailboxMessage { +public final class InputsForDepositTxResponse extends TradeMessage implements DirectMessage { private final PaymentAccountPayload makerPaymentAccountPayload; private final String makerAccountId; private final byte[] makerMultiSigPubKey; @@ -60,20 +57,22 @@ public final class PublishDepositTxRequest extends TradeMessage implements Mailb @Nullable private final byte[] accountAgeWitnessSignatureOfPreparedDepositTx; private final long currentDate; - - public PublishDepositTxRequest(String tradeId, - PaymentAccountPayload makerPaymentAccountPayload, - String makerAccountId, - byte[] makerMultiSigPubKey, - String makerContractAsJson, - String makerContractSignature, - String makerPayoutAddressString, - byte[] preparedDepositTx, - List makerInputs, - NodeAddress senderNodeAddress, - String uid, - @Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx, - long currentDate) { + private final long lockTime; + + public InputsForDepositTxResponse(String tradeId, + PaymentAccountPayload makerPaymentAccountPayload, + String makerAccountId, + byte[] makerMultiSigPubKey, + String makerContractAsJson, + String makerContractSignature, + String makerPayoutAddressString, + byte[] preparedDepositTx, + List makerInputs, + NodeAddress senderNodeAddress, + String uid, + @Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx, + long currentDate, + long lockTime) { this(tradeId, makerPaymentAccountPayload, makerAccountId, @@ -87,7 +86,8 @@ public PublishDepositTxRequest(String tradeId, uid, Version.getP2PMessageVersion(), accountAgeWitnessSignatureOfPreparedDepositTx, - currentDate); + currentDate, + lockTime); } @@ -95,20 +95,21 @@ public PublishDepositTxRequest(String tradeId, // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private PublishDepositTxRequest(String tradeId, - PaymentAccountPayload makerPaymentAccountPayload, - String makerAccountId, - byte[] makerMultiSigPubKey, - String makerContractAsJson, - String makerContractSignature, - String makerPayoutAddressString, - byte[] preparedDepositTx, - List makerInputs, - NodeAddress senderNodeAddress, - String uid, - int messageVersion, - @Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx, - long currentDate) { + private InputsForDepositTxResponse(String tradeId, + PaymentAccountPayload makerPaymentAccountPayload, + String makerAccountId, + byte[] makerMultiSigPubKey, + String makerContractAsJson, + String makerContractSignature, + String makerPayoutAddressString, + byte[] preparedDepositTx, + List makerInputs, + NodeAddress senderNodeAddress, + String uid, + int messageVersion, + @Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx, + long currentDate, + long lockTime) { super(messageVersion, tradeId, uid); this.makerPaymentAccountPayload = makerPaymentAccountPayload; this.makerAccountId = makerAccountId; @@ -121,11 +122,12 @@ private PublishDepositTxRequest(String tradeId, this.senderNodeAddress = senderNodeAddress; this.accountAgeWitnessSignatureOfPreparedDepositTx = accountAgeWitnessSignatureOfPreparedDepositTx; this.currentDate = currentDate; + this.lockTime = lockTime; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - final protobuf.PublishDepositTxRequest.Builder builder = protobuf.PublishDepositTxRequest.newBuilder() + final protobuf.InputsForDepositTxResponse.Builder builder = protobuf.InputsForDepositTxResponse.newBuilder() .setTradeId(tradeId) .setMakerPaymentAccountPayload((protobuf.PaymentAccountPayload) makerPaymentAccountPayload.toProtoMessage()) .setMakerAccountId(makerAccountId) @@ -136,22 +138,23 @@ public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { .setPreparedDepositTx(ByteString.copyFrom(preparedDepositTx)) .addAllMakerInputs(makerInputs.stream().map(RawTransactionInput::toProtoMessage).collect(Collectors.toList())) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid); + .setUid(uid) + .setLockTime(lockTime); Optional.ofNullable(accountAgeWitnessSignatureOfPreparedDepositTx).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfPreparedDepositTx(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); return getNetworkEnvelopeBuilder() - .setPublishDepositTxRequest(builder) + .setInputsForDepositTxResponse(builder) .build(); } - public static PublishDepositTxRequest fromProto(protobuf.PublishDepositTxRequest proto, CoreProtoResolver coreProtoResolver, int messageVersion) { + public static InputsForDepositTxResponse fromProto(protobuf.InputsForDepositTxResponse proto, CoreProtoResolver coreProtoResolver, int messageVersion) { List makerInputs = proto.getMakerInputsList().stream() .map(RawTransactionInput::fromProto) .collect(Collectors.toList()); - return new PublishDepositTxRequest(proto.getTradeId(), + return new InputsForDepositTxResponse(proto.getTradeId(), coreProtoResolver.fromProto(proto.getMakerPaymentAccountPayload()), proto.getMakerAccountId(), proto.getMakerMultiSigPubKey().toByteArray(), @@ -164,13 +167,14 @@ public static PublishDepositTxRequest fromProto(protobuf.PublishDepositTxRequest proto.getUid(), messageVersion, ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignatureOfPreparedDepositTx()), - proto.getCurrentDate()); + proto.getCurrentDate(), + proto.getLockTime()); } @Override public String toString() { - return "PublishDepositTxRequest{" + + return "InputsForDepositTxResponse{" + "\n makerPaymentAccountPayload=" + makerPaymentAccountPayload + ",\n makerAccountId='" + makerAccountId + '\'' + ",\n makerMultiSigPubKey=" + Utilities.bytesAsHexString(makerMultiSigPubKey) + @@ -183,6 +187,7 @@ public String toString() { ",\n uid='" + uid + '\'' + ",\n accountAgeWitnessSignatureOfPreparedDepositTx=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfPreparedDepositTx) + ",\n currentDate=" + new Date(currentDate) + + ",\n lockTime=" + lockTime + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/trade/messages/PeerPublishedDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/messages/PeerPublishedDelayedPayoutTxMessage.java new file mode 100644 index 00000000000..9447f9494f9 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/PeerPublishedDelayedPayoutTxMessage.java @@ -0,0 +1,77 @@ +/* + * 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.trade.messages; + +import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class PeerPublishedDelayedPayoutTxMessage extends TradeMessage implements MailboxMessage { + private final NodeAddress senderNodeAddress; + + public PeerPublishedDelayedPayoutTxMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private PeerPublishedDelayedPayoutTxMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.PeerPublishedDelayedPayoutTxMessage.Builder builder = protobuf.PeerPublishedDelayedPayoutTxMessage.newBuilder(); + builder.setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()); + return getNetworkEnvelopeBuilder().setPeerPublishedDelayedPayoutTxMessage(builder).build(); + } + + public static PeerPublishedDelayedPayoutTxMessage fromProto(protobuf.PeerPublishedDelayedPayoutTxMessage proto, int messageVersion) { + return new PeerPublishedDelayedPayoutTxMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress())); + } + + @Override + public String toString() { + return "PeerPublishedDelayedPayoutTxMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/TradeMessage.java b/core/src/main/java/bisq/core/trade/messages/TradeMessage.java index 5b386e2e1e5..e90cbb02657 100644 --- a/core/src/main/java/bisq/core/trade/messages/TradeMessage.java +++ b/core/src/main/java/bisq/core/trade/messages/TradeMessage.java @@ -17,7 +17,6 @@ package bisq.core.trade.messages; -import bisq.network.p2p.DirectMessage; import bisq.network.p2p.UidMessage; import bisq.common.proto.network.NetworkEnvelope; @@ -29,7 +28,7 @@ @EqualsAndHashCode(callSuper = true) @Getter @ToString -public abstract class TradeMessage extends NetworkEnvelope implements DirectMessage, UidMessage { +public abstract class TradeMessage extends NetworkEnvelope implements UidMessage { protected final String tradeId; protected final String uid; diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index dd68fd16f01..236e6d47b09 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -19,38 +19,40 @@ import bisq.core.trade.BuyerAsMakerTrade; import bisq.core.trade.Trade; -import bisq.core.trade.messages.DepositTxPublishedMessage; -import bisq.core.trade.messages.PayDepositRequest; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.PublishTradeStatistics; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener; import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerVerifiesDelayedPayoutTx; import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerCreatesAndSignsDepositTx; -import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerSignPayoutTx; +import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerSendsInputsForDepositTxResponse; import bisq.core.trade.protocol.tasks.maker.MakerCreateAndSignContract; -import bisq.core.trade.protocol.tasks.maker.MakerProcessDepositTxPublishedMessage; -import bisq.core.trade.protocol.tasks.maker.MakerProcessPayDepositRequest; -import bisq.core.trade.protocol.tasks.maker.MakerSendPublishDepositTxRequest; -import bisq.core.trade.protocol.tasks.maker.MakerSetupDepositTxListener; +import bisq.core.trade.protocol.tasks.maker.MakerProcessesInputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerAccount; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; import bisq.core.util.Validator; -import bisq.network.p2p.MailboxMessage; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.proto.network.NetworkEnvelope; import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; - @Slf4j public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol, MakerProtocol { private final BuyerAsMakerTrade buyerAsMakerTrade; @@ -68,10 +70,10 @@ public BuyerAsMakerProtocol(BuyerAsMakerTrade trade) { Trade.Phase phase = trade.getState().getPhase(); if (phase == Trade.Phase.TAKER_FEE_PUBLISHED) { TradeTaskRunner taskRunner = new TradeTaskRunner(trade, - () -> handleTaskRunnerSuccess("MakerSetupDepositTxListener"), + () -> handleTaskRunnerSuccess("BuyerSetupDepositTxListener"), this::handleTaskRunnerFault); - taskRunner.addTasks(MakerSetupDepositTxListener.class); + taskRunner.addTasks(BuyerSetupDepositTxListener.class); taskRunner.run(); } else if (trade.isFiatSent() && !trade.isPayoutPublished()) { TradeTaskRunner taskRunner = new TradeTaskRunner(trade, @@ -89,23 +91,13 @@ public BuyerAsMakerProtocol(BuyerAsMakerTrade trade) { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) { - this.trade = trade; - - if (networkEnvelope instanceof MailboxMessage) { - MailboxMessage mailboxMessage = (MailboxMessage) networkEnvelope; - NodeAddress peerNodeAddress = mailboxMessage.getSenderNodeAddress(); - if (networkEnvelope instanceof TradeMessage) { - TradeMessage tradeMessage = (TradeMessage) networkEnvelope; - log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", - tradeMessage.getClass().getSimpleName(), peerNodeAddress, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof DepositTxPublishedMessage) - handle((DepositTxPublishedMessage) tradeMessage, peerNodeAddress); - else if (tradeMessage instanceof PayoutTxPublishedMessage) - handle((PayoutTxPublishedMessage) tradeMessage, peerNodeAddress); - else - log.error("We received an unhandled tradeMessage" + tradeMessage.toString()); - } + public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) { + super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress); + + if (tradeMessage instanceof DepositTxAndDelayedPayoutTxMessage) { + handle((DepositTxAndDelayedPayoutTxMessage) tradeMessage, peerNodeAddress); + } else if (tradeMessage instanceof PayoutTxPublishedMessage) { + handle((PayoutTxPublishedMessage) tradeMessage, peerNodeAddress); } } @@ -115,11 +107,10 @@ else if (tradeMessage instanceof PayoutTxPublishedMessage) /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void handleTakeOfferRequest(TradeMessage tradeMessage, + public void handleTakeOfferRequest(InputsForDepositTxRequest tradeMessage, NodeAddress peerNodeAddress, ErrorMessageHandler errorMessageHandler) { Validator.checkTradeId(processModel.getOfferId(), tradeMessage); - checkArgument(tradeMessage instanceof PayDepositRequest); processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(peerNodeAddress); @@ -130,15 +121,16 @@ public void handleTakeOfferRequest(TradeMessage tradeMessage, handleTaskRunnerFault(errorMessage); }); taskRunner.addTasks( - MakerProcessPayDepositRequest.class, + MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, MakerVerifyTakerAccount.class, VerifyPeersAccountAgeWitness.class, MakerVerifyTakerFeePayment.class, + MakerSetsLockTime.class, MakerCreateAndSignContract.class, BuyerAsMakerCreatesAndSignsDepositTx.class, - MakerSetupDepositTxListener.class, - MakerSendPublishDepositTxRequest.class + BuyerSetupDepositTxListener.class, + BuyerAsMakerSendsInputsForDepositTxResponse.class ); // We don't use a timeout here because if the DepositTxPublishedMessage does not arrive we // get the deposit tx set at MakerSetupDepositTxListener once it is seen in the bitcoin network @@ -150,19 +142,35 @@ public void handleTakeOfferRequest(TradeMessage tradeMessage, // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// - private void handle(DepositTxPublishedMessage tradeMessage, NodeAddress peerNodeAddress) { + private void handle(DelayedPayoutTxSignatureRequest tradeMessage, NodeAddress peerNodeAddress) { processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(peerNodeAddress); TradeTaskRunner taskRunner = new TradeTaskRunner(buyerAsMakerTrade, () -> { - handleTaskRunnerSuccess(tradeMessage, "handle DepositTxPublishedMessage"); + handleTaskRunnerSuccess(tradeMessage, "handle DelayedPayoutTxSignatureRequest"); }, errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); taskRunner.addTasks( - MakerProcessDepositTxPublishedMessage.class, - MakerVerifyTakerAccount.class, - MakerVerifyTakerFeePayment.class, + BuyerProcessDelayedPayoutTxSignatureRequest.class, + BuyerSignsDelayedPayoutTx.class, + BuyerSendsDelayedPayoutTxSignatureResponse.class + ); + taskRunner.run(); + } + + private void handle(DepositTxAndDelayedPayoutTxMessage tradeMessage, NodeAddress peerNodeAddress) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(peerNodeAddress); + + TradeTaskRunner taskRunner = new TradeTaskRunner(buyerAsMakerTrade, + () -> { + handleTaskRunnerSuccess(tradeMessage, "handle DepositTxAndDelayedPayoutTxMessage"); + }, + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + taskRunner.addTasks( + BuyerProcessDepositTxAndDelayedPayoutTxMessage.class, + BuyerVerifiesDelayedPayoutTx.class, PublishTradeStatistics.class ); taskRunner.run(); @@ -191,7 +199,7 @@ public void onFiatPaymentStarted(ResultHandler resultHandler, ErrorMessageHandle ApplyFilter.class, MakerVerifyTakerAccount.class, MakerVerifyTakerFeePayment.class, - BuyerAsMakerSignPayoutTx.class, + BuyerSignPayoutTx.class, BuyerSendCounterCurrencyTransferStartedMessage.class, BuyerSetupPayoutTxListener.class ); @@ -232,8 +240,10 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof DepositTxPublishedMessage) { - handle((DepositTxPublishedMessage) tradeMessage, sender); + if (tradeMessage instanceof DelayedPayoutTxSignatureRequest) { + handle((DelayedPayoutTxSignatureRequest) tradeMessage, sender); + } else if (tradeMessage instanceof DepositTxAndDelayedPayoutTxMessage) { + handle((DepositTxAndDelayedPayoutTxMessage) tradeMessage, sender); } else if (tradeMessage instanceof PayoutTxPublishedMessage) { handle((PayoutTxPublishedMessage) tradeMessage, sender); } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index 36b868cd90a..6047236501b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -18,38 +18,47 @@ package bisq.core.trade.protocol; +import bisq.core.offer.Offer; import bisq.core.trade.BuyerAsTakerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; +import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.messages.PayoutTxPublishedMessage; -import bisq.core.trade.messages.PublishDepositTxRequest; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.PublishTradeStatistics; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener; import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; -import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerSignPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.buyer.BuyerVerifiesDelayedPayoutTx; import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerCreatesDepositTxInputs; -import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignAndPublishDepositTx; +import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSendsDepositTxMessage; +import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignsDepositTx; import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerProcessPublishDepositTxRequest; +import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerSendDepositTxPublishedMessage; -import bisq.core.trade.protocol.tasks.taker.TakerSendPayDepositRequest; +import bisq.core.trade.protocol.tasks.taker.TakerSendInputsForDepositTxRequest; import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerAccount; import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; -import bisq.network.p2p.MailboxMessage; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.proto.network.NetworkEnvelope; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol, TakerProtocol { private final BuyerAsTakerTrade buyerAsTakerTrade; @@ -64,9 +73,18 @@ public BuyerAsTakerProtocol(BuyerAsTakerTrade trade) { this.buyerAsTakerTrade = trade; - processModel.getTradingPeer().setPubKeyRing(trade.getOffer().getPubKeyRing()); + Offer offer = checkNotNull(trade.getOffer()); + processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing()); + + Trade.Phase phase = trade.getState().getPhase(); + if (phase == Trade.Phase.TAKER_FEE_PUBLISHED) { + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> handleTaskRunnerSuccess("BuyerSetupDepositTxListener"), + this::handleTaskRunnerFault); - if (trade.isFiatSent() && !trade.isPayoutPublished()) { + taskRunner.addTasks(BuyerSetupDepositTxListener.class); + taskRunner.run(); + } else if (trade.isFiatSent() && !trade.isPayoutPublished()) { TradeTaskRunner taskRunner = new TradeTaskRunner(trade, () -> handleTaskRunnerSuccess("BuyerSetupPayoutTxListener"), this::handleTaskRunnerFault); @@ -82,22 +100,13 @@ public BuyerAsTakerProtocol(BuyerAsTakerTrade trade) { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) { - this.trade = trade; - - if (networkEnvelope instanceof MailboxMessage) { - final NodeAddress peerNodeAddress = ((MailboxMessage) networkEnvelope).getSenderNodeAddress(); - if (networkEnvelope instanceof TradeMessage) { - TradeMessage tradeMessage = (TradeMessage) networkEnvelope; - log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", - tradeMessage.getClass().getSimpleName(), peerNodeAddress, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof PublishDepositTxRequest) - handle((PublishDepositTxRequest) tradeMessage, peerNodeAddress); - else if (tradeMessage instanceof PayoutTxPublishedMessage) { - handle((PayoutTxPublishedMessage) tradeMessage, peerNodeAddress); - } else - log.error("We received an unhandled tradeMessage" + tradeMessage.toString()); - } + public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) { + super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress); + + if (tradeMessage instanceof DepositTxAndDelayedPayoutTxMessage) { + handle((DepositTxAndDelayedPayoutTxMessage) tradeMessage, peerNodeAddress); + } else if (tradeMessage instanceof PayoutTxPublishedMessage) { + handle((PayoutTxPublishedMessage) tradeMessage, peerNodeAddress); } } @@ -117,7 +126,7 @@ public void takeAvailableOffer() { TakerVerifyMakerFeePayment.class, CreateTakerFeeTx.class, BuyerAsTakerCreatesDepositTxInputs.class, - TakerSendPayDepositRequest.class + TakerSendInputsForDepositTxRequest.class ); //TODO if peer does get an error he does not respond and all we get is the timeout now knowing why it failed. @@ -131,7 +140,7 @@ public void takeAvailableOffer() { // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// - private void handle(PublishDepositTxRequest tradeMessage, NodeAddress sender) { + private void handle(InputsForDepositTxResponse tradeMessage, NodeAddress sender) { processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(sender); @@ -142,15 +151,50 @@ private void handle(PublishDepositTxRequest tradeMessage, NodeAddress sender) { }, errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); taskRunner.addTasks( - TakerProcessPublishDepositTxRequest.class, + TakerProcessesInputsForDepositTxResponse.class, ApplyFilter.class, TakerVerifyMakerAccount.class, VerifyPeersAccountAgeWitness.class, TakerVerifyMakerFeePayment.class, TakerVerifyAndSignContract.class, TakerPublishFeeTx.class, - BuyerAsTakerSignAndPublishDepositTx.class, - TakerSendDepositTxPublishedMessage.class, + BuyerAsTakerSignsDepositTx.class, + BuyerSetupDepositTxListener.class, + BuyerAsTakerSendsDepositTxMessage.class + ); + taskRunner.run(); + } + + private void handle(DelayedPayoutTxSignatureRequest tradeMessage, NodeAddress sender) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(buyerAsTakerTrade, + () -> { + handleTaskRunnerSuccess(tradeMessage, "handle DelayedPayoutTxSignatureRequest"); + }, + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + taskRunner.addTasks( + BuyerProcessDelayedPayoutTxSignatureRequest.class, + BuyerSignsDelayedPayoutTx.class, + BuyerSendsDelayedPayoutTxSignatureResponse.class + ); + taskRunner.run(); + } + + + private void handle(DepositTxAndDelayedPayoutTxMessage tradeMessage, NodeAddress peerNodeAddress) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(peerNodeAddress); + + TradeTaskRunner taskRunner = new TradeTaskRunner(buyerAsTakerTrade, + () -> { + handleTaskRunnerSuccess(tradeMessage, "handle DepositTxAndDelayedPayoutTxMessage"); + }, + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + taskRunner.addTasks( + BuyerProcessDepositTxAndDelayedPayoutTxMessage.class, + BuyerVerifiesDelayedPayoutTx.class, PublishTradeStatistics.class ); taskRunner.run(); @@ -180,7 +224,7 @@ public void onFiatPaymentStarted(ResultHandler resultHandler, ErrorMessageHandle ApplyFilter.class, TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, - BuyerAsMakerSignPayoutTx.class, + BuyerSignPayoutTx.class, BuyerSendCounterCurrencyTransferStartedMessage.class, BuyerSetupPayoutTxListener.class ); @@ -221,8 +265,12 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof PublishDepositTxRequest) { - handle((PublishDepositTxRequest) tradeMessage, sender); + if (tradeMessage instanceof InputsForDepositTxResponse) { + handle((InputsForDepositTxResponse) tradeMessage, sender); + } else if (tradeMessage instanceof DelayedPayoutTxSignatureRequest) { + handle((DelayedPayoutTxSignatureRequest) tradeMessage, sender); + } else if (tradeMessage instanceof DepositTxAndDelayedPayoutTxMessage) { + handle((DepositTxAndDelayedPayoutTxMessage) tradeMessage, sender); } else if (tradeMessage instanceof PayoutTxPublishedMessage) { handle((PayoutTxPublishedMessage) tradeMessage, sender); } diff --git a/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java index a95c78bfeb2..d460289e3fc 100644 --- a/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java @@ -18,12 +18,12 @@ package bisq.core.trade.protocol; -import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; public interface MakerProtocol { - void handleTakeOfferRequest(TradeMessage message, NodeAddress taker, ErrorMessageHandler errorMessageHandler); + void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress taker, ErrorMessageHandler errorMessageHandler); } diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java index f2b180926aa..9e25fa1a0f8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -22,6 +22,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; import bisq.core.filter.FilterManager; import bisq.core.network.MessageState; import bisq.core.offer.Offer; @@ -31,6 +32,7 @@ import bisq.core.proto.CoreProtoResolver; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; @@ -69,6 +71,10 @@ import javax.annotation.Nullable; +// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not +// persist them. +//todo clean up older fields as well to make most transient + @Getter @Slf4j public class ProcessModel implements Model, PersistablePayload { @@ -78,6 +84,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private BtcWalletService btcWalletService; transient private BsqWalletService bsqWalletService; transient private TradeWalletService tradeWalletService; + transient private DaoFacade daoFacade; transient private Offer offer; transient private User user; transient private FilterManager filterManager; @@ -85,6 +92,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private TradeStatisticsManager tradeStatisticsManager; transient private ArbitratorManager arbitratorManager; transient private MediatorManager mediatorManager; + transient private RefundAgentManager refundAgentManager; transient private KeyRing keyRing; transient private P2PService p2PService; transient private ReferralIdService referralIdService; @@ -96,32 +104,29 @@ public class ProcessModel implements Model, PersistablePayload { @Setter transient private DecryptedMessageWithPubKey decryptedMessageWithPubKey; - - // Persistable Immutable (only set by PB) + // Added in v1.2.0 @Setter - private TradingPeer tradingPeer = new TradingPeer(); + @Nullable + transient private byte[] delayedPayoutTxSignature; @Setter + @Nullable + transient private Transaction preparedDelayedPayoutTx; + + // Persistable Immutable (private setter only used by PB method) + private TradingPeer tradingPeer = new TradingPeer(); private String offerId; - @Setter private String accountId; - @Setter private PubKeyRing pubKeyRing; // Persistable Mutable @Nullable - @Setter + @Setter() private String takeOfferFeeTxId; @Nullable @Setter private byte[] payoutTxSignature; @Nullable @Setter - private List takerAcceptedArbitratorNodeAddresses; - @Nullable - @Setter - private List takerAcceptedMediatorNodeAddresses; - @Nullable - @Setter private byte[] preparedDepositTx; @Nullable @Setter @@ -182,14 +187,13 @@ public protobuf.ProcessModel toProtoMessage() { Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId); Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature))); - Optional.ofNullable(takerAcceptedArbitratorNodeAddresses).ifPresent(e -> builder.addAllTakerAcceptedArbitratorNodeAddresses(ProtoUtil.collectionToProto(takerAcceptedArbitratorNodeAddresses))); - Optional.ofNullable(takerAcceptedMediatorNodeAddresses).ifPresent(e -> builder.addAllTakerAcceptedMediatorNodeAddresses(ProtoUtil.collectionToProto(takerAcceptedMediatorNodeAddresses))); Optional.ofNullable(preparedDepositTx).ifPresent(e -> builder.setPreparedDepositTx(ByteString.copyFrom(preparedDepositTx))); Optional.ofNullable(rawTransactionInputs).ifPresent(e -> builder.addAllRawTransactionInputs(ProtoUtil.collectionToProto(rawTransactionInputs))); Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); Optional.ofNullable(myMultiSigPubKey).ifPresent(e -> builder.setMyMultiSigPubKey(ByteString.copyFrom(myMultiSigPubKey))); Optional.ofNullable(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage())); Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); + return builder.build(); } @@ -208,14 +212,6 @@ public static ProcessModel fromProto(protobuf.ProcessModel proto, CoreProtoResol // nullable processModel.setTakeOfferFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakeOfferFeeTxId())); processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); - List takerAcceptedArbitratorNodeAddresses = proto.getTakerAcceptedArbitratorNodeAddressesList().isEmpty() ? - null : proto.getTakerAcceptedArbitratorNodeAddressesList().stream() - .map(NodeAddress::fromProto).collect(Collectors.toList()); - List takerAcceptedMediatorNodeAddresses = proto.getTakerAcceptedMediatorNodeAddressesList().isEmpty() ? - null : proto.getTakerAcceptedMediatorNodeAddressesList().stream() - .map(NodeAddress::fromProto).collect(Collectors.toList()); - processModel.setTakerAcceptedArbitratorNodeAddresses(takerAcceptedArbitratorNodeAddresses); - processModel.setTakerAcceptedMediatorNodeAddresses(takerAcceptedMediatorNodeAddresses); processModel.setPreparedDepositTx(ProtoUtil.byteArrayOrNullFromProto(proto.getPreparedDepositTx())); List rawTransactionInputs = proto.getRawTransactionInputsList().isEmpty() ? null : proto.getRawTransactionInputsList().stream() @@ -243,6 +239,7 @@ public void onAllServicesInitialized(Offer offer, BtcWalletService walletService, BsqWalletService bsqWalletService, TradeWalletService tradeWalletService, + DaoFacade daoFacade, ReferralIdService referralIdService, User user, FilterManager filterManager, @@ -250,6 +247,7 @@ public void onAllServicesInitialized(Offer offer, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, KeyRing keyRing, boolean useSavingsWallet, Coin fundsNeededForTrade) { @@ -259,6 +257,7 @@ public void onAllServicesInitialized(Offer offer, this.btcWalletService = walletService; this.bsqWalletService = bsqWalletService; this.tradeWalletService = tradeWalletService; + this.daoFacade = daoFacade; this.referralIdService = referralIdService; this.user = user; this.filterManager = filterManager; @@ -266,6 +265,7 @@ public void onAllServicesInitialized(Offer offer, this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; this.keyRing = keyRing; this.p2PService = p2PService; this.useSavingsWallet = useSavingsWallet; @@ -339,4 +339,20 @@ public void setPaymentStartedAckMessage(AckMessage ackMessage) { public void setPaymentStartedMessageState(MessageState paymentStartedMessageStateProperty) { this.paymentStartedMessageStateProperty.set(paymentStartedMessageStateProperty); } + + private void setTradingPeer(TradingPeer tradingPeer) { + this.tradingPeer = tradingPeer; + } + + private void setOfferId(String offerId) { + this.offerId = offerId; + } + + private void setAccountId(String accountId) { + this.accountId = accountId; + } + + private void setPubKeyRing(PubKeyRing pubKeyRing) { + this.pubKeyRing = pubKeyRing; + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index 60894b26336..f5090965295 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -21,38 +21,42 @@ import bisq.core.trade.SellerAsMakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; -import bisq.core.trade.messages.DepositTxPublishedMessage; -import bisq.core.trade.messages.PayDepositRequest; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.PublishTradeStatistics; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import bisq.core.trade.protocol.tasks.maker.MakerCreateAndSignContract; -import bisq.core.trade.protocol.tasks.maker.MakerProcessDepositTxPublishedMessage; -import bisq.core.trade.protocol.tasks.maker.MakerProcessPayDepositRequest; -import bisq.core.trade.protocol.tasks.maker.MakerSendPublishDepositTxRequest; -import bisq.core.trade.protocol.tasks.maker.MakerSetupDepositTxListener; +import bisq.core.trade.protocol.tasks.maker.MakerProcessesInputsForDepositTxRequest; +import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerAccount; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; import bisq.core.trade.protocol.tasks.seller.SellerBroadcastPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerFinalizesDelayedPayoutTx; import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerProcessDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.seller.SellerPublishesDepositTx; +import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerSendsDepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx; -import bisq.core.trade.protocol.tasks.seller.SellerVerifiesPeersAccountAge; -import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesAndSignsDepositTx; +import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerSendsInputsForDepositTxResponse; import bisq.core.util.Validator; -import bisq.network.p2p.MailboxMessage; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.proto.network.NetworkEnvelope; import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkArgument; - @Slf4j public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtocol, MakerProtocol { private final SellerAsMakerTrade sellerAsMakerTrade; @@ -73,7 +77,6 @@ public SellerAsMakerProtocol(SellerAsMakerTrade trade) { () -> handleTaskRunnerSuccess("MakerSetupDepositTxListener"), this::handleTaskRunnerFault); - taskRunner.addTasks(MakerSetupDepositTxListener.class); taskRunner.run(); } } @@ -84,23 +87,11 @@ public SellerAsMakerProtocol(SellerAsMakerTrade trade) { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) { - this.trade = trade; - - if (networkEnvelope instanceof MailboxMessage) { - NodeAddress peerNodeAddress = ((MailboxMessage) networkEnvelope).getSenderNodeAddress(); - if (networkEnvelope instanceof TradeMessage) { - TradeMessage tradeMessage = (TradeMessage) networkEnvelope; - log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", - tradeMessage.getClass().getSimpleName(), peerNodeAddress, tradeMessage.getTradeId(), tradeMessage.getUid()); - - if (tradeMessage instanceof DepositTxPublishedMessage) - handle((DepositTxPublishedMessage) tradeMessage, peerNodeAddress); - else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) - handle((CounterCurrencyTransferStartedMessage) tradeMessage, peerNodeAddress); - else - log.error("We received an unhandled tradeMessage" + tradeMessage.toString()); - } + public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) { + super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress); + + if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { + handle((CounterCurrencyTransferStartedMessage) tradeMessage, peerNodeAddress); } } @@ -110,11 +101,10 @@ else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void handleTakeOfferRequest(TradeMessage tradeMessage, + public void handleTakeOfferRequest(InputsForDepositTxRequest tradeMessage, NodeAddress sender, ErrorMessageHandler errorMessageHandler) { Validator.checkTradeId(processModel.getOfferId(), tradeMessage); - checkArgument(tradeMessage instanceof PayDepositRequest); processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(sender); @@ -126,20 +116,17 @@ public void handleTakeOfferRequest(TradeMessage tradeMessage, }); taskRunner.addTasks( - MakerProcessPayDepositRequest.class, + MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, MakerVerifyTakerAccount.class, VerifyPeersAccountAgeWitness.class, - SellerVerifiesPeersAccountAge.class, MakerVerifyTakerFeePayment.class, + MakerSetsLockTime.class, MakerCreateAndSignContract.class, - SellerAsMakerCreatesAndSignsDepositTx.class, - MakerSetupDepositTxListener.class, - MakerSendPublishDepositTxRequest.class + SellerAsMakerCreatesUnsignedDepositTx.class, + SellerAsMakerSendsInputsForDepositTxResponse.class ); - // We don't start a timeout because if we don't receive the peers DepositTxPublishedMessage we still - // will get set the deposit tx in MakerSetupDepositTxListener once seen in the network taskRunner.run(); } @@ -148,7 +135,7 @@ public void handleTakeOfferRequest(TradeMessage tradeMessage, // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// - protected void handle(DepositTxPublishedMessage tradeMessage, NodeAddress sender) { + protected void handle(DepositTxMessage tradeMessage, NodeAddress sender) { processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(sender); @@ -159,10 +146,32 @@ protected void handle(DepositTxPublishedMessage tradeMessage, NodeAddress sender errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); taskRunner.addTasks( - MakerProcessDepositTxPublishedMessage.class, - PublishTradeStatistics.class, - MakerVerifyTakerAccount.class, - MakerVerifyTakerFeePayment.class + SellerAsMakerProcessDepositTxMessage.class, + SellerAsMakerFinalizesDepositTx.class, + SellerCreatesDelayedPayoutTx.class, + SellerSendDelayedPayoutTxSignatureRequest.class + ); + taskRunner.run(); + } + + private void handle(DelayedPayoutTxSignatureResponse tradeMessage, NodeAddress sender) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(sellerAsMakerTrade, + () -> { + stopTimeout(); + handleTaskRunnerSuccess(tradeMessage, "PublishDepositTxRequest"); + }, + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + + taskRunner.addTasks( + SellerProcessDelayedPayoutTxSignatureResponse.class, + SellerSignsDelayedPayoutTx.class, + SellerFinalizesDelayedPayoutTx.class, + SellerPublishesDepositTx.class, + SellerSendsDepositTxAndDelayedPayoutTxMessage.class, + PublishTradeStatistics.class ); taskRunner.run(); } @@ -255,8 +264,10 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof DepositTxPublishedMessage) { - handle((DepositTxPublishedMessage) tradeMessage, sender); + if (tradeMessage instanceof DepositTxMessage) { + handle((DepositTxMessage) tradeMessage, sender); + } else if (tradeMessage instanceof DelayedPayoutTxSignatureResponse) { + handle((DelayedPayoutTxSignatureResponse) tradeMessage, sender); } else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { handle((CounterCurrencyTransferStartedMessage) tradeMessage, sender); } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index d3071b1c8b6..fa4c4d3d0c5 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -18,39 +18,46 @@ package bisq.core.trade.protocol; +import bisq.core.offer.Offer; import bisq.core.trade.SellerAsTakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; -import bisq.core.trade.messages.PublishDepositTxRequest; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.PublishTradeStatistics; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import bisq.core.trade.protocol.tasks.seller.SellerBroadcastPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx; +import bisq.core.trade.protocol.tasks.seller.SellerFinalizesDelayedPayoutTx; import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerProcessDelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.seller.SellerPublishesDepositTx; +import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerSendsDepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx; -import bisq.core.trade.protocol.tasks.seller.SellerVerifiesPeersAccountAge; +import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx; import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs; -import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignAndPublishDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx; import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerProcessPublishDepositTxRequest; +import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerSendDepositTxPublishedMessage; -import bisq.core.trade.protocol.tasks.taker.TakerSendPayDepositRequest; +import bisq.core.trade.protocol.tasks.taker.TakerSendInputsForDepositTxRequest; import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerAccount; import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; -import bisq.network.p2p.MailboxMessage; import bisq.network.p2p.NodeAddress; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.proto.network.NetworkEnvelope; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtocol, TakerProtocol { private final SellerAsTakerTrade sellerAsTakerTrade; @@ -65,7 +72,8 @@ public SellerAsTakerProtocol(SellerAsTakerTrade trade) { this.sellerAsTakerTrade = trade; - processModel.getTradingPeer().setPubKeyRing(trade.getOffer().getPubKeyRing()); + Offer offer = checkNotNull(trade.getOffer()); + processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing()); } @@ -74,22 +82,11 @@ public SellerAsTakerProtocol(SellerAsTakerTrade trade) { /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) { - this.trade = trade; - - if (networkEnvelope instanceof MailboxMessage) { - NodeAddress peerNodeAddress = ((MailboxMessage) networkEnvelope).getSenderNodeAddress(); - if (networkEnvelope instanceof TradeMessage) { - TradeMessage tradeMessage = (TradeMessage) networkEnvelope; - log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", - tradeMessage.getClass().getSimpleName(), peerNodeAddress, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof PublishDepositTxRequest) - handle((PublishDepositTxRequest) tradeMessage, peerNodeAddress); - else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) - handle((CounterCurrencyTransferStartedMessage) tradeMessage, peerNodeAddress); - else - log.error("We received an unhandled tradeMessage" + tradeMessage.toString()); - } + public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) { + super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress); + + if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { + handle((CounterCurrencyTransferStartedMessage) tradeMessage, peerNodeAddress); } } @@ -109,11 +106,9 @@ public void takeAvailableOffer() { TakerVerifyMakerFeePayment.class, CreateTakerFeeTx.class, SellerAsTakerCreatesDepositTxInputs.class, - TakerSendPayDepositRequest.class + TakerSendInputsForDepositTxRequest.class ); - //TODO if peer does get an error he does not respond and all we get is the timeout now knowing why it failed. - // We should add an error message the peer sends us in such cases. startTimeout(); taskRunner.run(); } @@ -123,7 +118,7 @@ public void takeAvailableOffer() { // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// - private void handle(PublishDepositTxRequest tradeMessage, NodeAddress sender) { + private void handle(InputsForDepositTxResponse tradeMessage, NodeAddress sender) { processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(sender); @@ -135,16 +130,37 @@ private void handle(PublishDepositTxRequest tradeMessage, NodeAddress sender) { errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); taskRunner.addTasks( - TakerProcessPublishDepositTxRequest.class, + TakerProcessesInputsForDepositTxResponse.class, ApplyFilter.class, TakerVerifyMakerAccount.class, VerifyPeersAccountAgeWitness.class, - SellerVerifiesPeersAccountAge.class, TakerVerifyMakerFeePayment.class, TakerVerifyAndSignContract.class, TakerPublishFeeTx.class, - SellerAsTakerSignAndPublishDepositTx.class, - TakerSendDepositTxPublishedMessage.class, + SellerAsTakerSignsDepositTx.class, + SellerCreatesDelayedPayoutTx.class, + SellerSendDelayedPayoutTxSignatureRequest.class + ); + taskRunner.run(); + } + + private void handle(DelayedPayoutTxSignatureResponse tradeMessage, NodeAddress sender) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(sellerAsTakerTrade, + () -> { + stopTimeout(); + handleTaskRunnerSuccess(tradeMessage, "PublishDepositTxRequest"); + }, + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + + taskRunner.addTasks( + SellerProcessDelayedPayoutTxSignatureResponse.class, + SellerSignsDelayedPayoutTx.class, + SellerFinalizesDelayedPayoutTx.class, + SellerPublishesDepositTx.class, + SellerSendsDepositTxAndDelayedPayoutTxMessage.class, PublishTradeStatistics.class ); taskRunner.run(); @@ -238,8 +254,10 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); - if (tradeMessage instanceof PublishDepositTxRequest) { - handle((PublishDepositTxRequest) tradeMessage, sender); + if (tradeMessage instanceof InputsForDepositTxResponse) { + handle((InputsForDepositTxResponse) tradeMessage, sender); + } else if (tradeMessage instanceof DelayedPayoutTxSignatureResponse) { + handle((DelayedPayoutTxSignatureResponse) tradeMessage, sender); } else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { handle((CounterCurrencyTransferStartedMessage) tradeMessage, sender); } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index eee4da8371d..cc2a59c3187 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -21,11 +21,13 @@ import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; -import bisq.core.trade.messages.PayDepositRequest; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.ProcessPeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.mediation.BroadcastMediatedPayoutTx; import bisq.core.trade.protocol.tasks.mediation.FinalizeMediatedPayoutTx; import bisq.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutSignatureMessage; @@ -59,6 +61,7 @@ import javax.annotation.Nullable; import static bisq.core.util.Validator.nonEmptyStringOf; +import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public abstract class TradeProtocol { @@ -80,13 +83,13 @@ public TradeProtocol(Trade trade) { PublicKey signaturePubKey = decryptedMessageWithPubKey.getSignaturePubKey(); if (tradingPeerPubKeyRing != null && signaturePubKey.equals(tradingPeerPubKeyRing.getSignaturePubKey())) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - log.trace("handleNewMessage: message = {} from {}", networkEnvelope.getClass().getSimpleName(), peersNodeAddress); if (networkEnvelope instanceof TradeMessage) { TradeMessage tradeMessage = (TradeMessage) networkEnvelope; nonEmptyStringOf(tradeMessage.getTradeId()); - if (tradeMessage.getTradeId().equals(processModel.getOfferId())) + if (tradeMessage.getTradeId().equals(processModel.getOfferId())) { doHandleDecryptedMessage(tradeMessage, peersNodeAddress); + } } else if (networkEnvelope instanceof AckMessage) { AckMessage ackMessage = (AckMessage) networkEnvelope; if (ackMessage.getSourceType() == AckMessageSourceType.TRADE_MESSAGE && @@ -110,7 +113,7 @@ public TradeProtocol(Trade trade) { stateChangeListener = (observable, oldValue, newValue) -> { if (newValue.getPhase() == Trade.Phase.TAKER_FEE_PUBLISHED && trade instanceof MakerTrade) - processModel.getOpenOfferManager().closeOpenOffer(trade.getOffer()); + processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(trade.getOffer())); }; trade.stateProperty().addListener(stateChangeListener); } @@ -206,6 +209,26 @@ protected void handle(MediatedPayoutTxPublishedMessage tradeMessage, NodeAddress } + /////////////////////////////////////////////////////////////////////////////////////////// + // Peer has published the delayed payout tx + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handle(PeerPublishedDelayedPayoutTxMessage tradeMessage, NodeAddress sender) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> handleTaskRunnerSuccess(tradeMessage, "PeerPublishedDelayedPayoutTxMessage"), + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + + taskRunner.addTasks( + //todo + ProcessPeerPublishedDelayedPayoutTxMessage.class + ); + taskRunner.run(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Dispatcher /////////////////////////////////////////////////////////////////////////////////////////// @@ -215,6 +238,8 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s handle((MediatedPayoutTxSignatureMessage) tradeMessage, sender); } else if (tradeMessage instanceof MediatedPayoutTxPublishedMessage) { handle((MediatedPayoutTxPublishedMessage) tradeMessage, sender); + } else if (tradeMessage instanceof PeerPublishedDelayedPayoutTxMessage) { + handle((PeerPublishedDelayedPayoutTxMessage) tradeMessage, sender); } } @@ -225,10 +250,6 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s public void completed() { cleanup(); - - // We only removed earlier the listener here, but then we migth have dangling trades after faults... - // so lets remove it at cleanup - //processModel.getP2PService().removeDecryptedDirectMessageListener(decryptedDirectMessageListener); } private void cleanup() { @@ -241,28 +262,33 @@ private void cleanup() { public void applyMailboxMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Trade trade) { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - log.debug("applyMailboxMessage {}", networkEnvelope); if (processModel.getTradingPeer().getPubKeyRing() != null && decryptedMessageWithPubKey.getSignaturePubKey().equals(processModel.getTradingPeer().getPubKeyRing().getSignaturePubKey())) { processModel.setDecryptedMessageWithPubKey(decryptedMessageWithPubKey); - doApplyMailboxMessage(networkEnvelope, trade); - // This is just a quick fix for the missing handling of the mediation MailboxMessages. - // With the new trade protocol that will be refactored further with using doApplyMailboxMessage... if (networkEnvelope instanceof MailboxMessage && networkEnvelope instanceof TradeMessage) { - NodeAddress sender = ((MailboxMessage) networkEnvelope).getSenderNodeAddress(); - if (networkEnvelope instanceof MediatedPayoutTxSignatureMessage) { - handle((MediatedPayoutTxSignatureMessage) networkEnvelope, sender); - } else if (networkEnvelope instanceof MediatedPayoutTxPublishedMessage) { - handle((MediatedPayoutTxPublishedMessage) networkEnvelope, sender); - } + this.trade = trade; + TradeMessage tradeMessage = (TradeMessage) networkEnvelope; + NodeAddress peerNodeAddress = ((MailboxMessage) networkEnvelope).getSenderNodeAddress(); + doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress); } } else { log.error("SignaturePubKey in message does not match the SignaturePubKey we have stored to that trading peer."); } } - protected abstract void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade); + protected void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) { + log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", + tradeMessage.getClass().getSimpleName(), peerNodeAddress, tradeMessage.getTradeId(), tradeMessage.getUid()); + + if (tradeMessage instanceof MediatedPayoutTxSignatureMessage) { + handle((MediatedPayoutTxSignatureMessage) tradeMessage, peerNodeAddress); + } else if (tradeMessage instanceof MediatedPayoutTxPublishedMessage) { + handle((MediatedPayoutTxPublishedMessage) tradeMessage, peerNodeAddress); + } else if (tradeMessage instanceof PeerPublishedDelayedPayoutTxMessage) { + handle((PeerPublishedDelayedPayoutTxMessage) tradeMessage, peerNodeAddress); + } + } protected void startTimeout() { stopTimeout(); @@ -318,7 +344,7 @@ private void sendAckMessage(@Nullable TradeMessage tradeMessage, boolean result, sourceUid = ((MailboxMessage) tradeMessage).getUid(); } else { // For direct msg we don't have a mandatory uid so we need to cast to get it - if (tradeMessage instanceof PayDepositRequest) { + if (tradeMessage instanceof InputsForDepositTxRequest) { sourceUid = tradeMessage.getUid(); } } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java index 94398caf791..3a2af777055 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java @@ -38,10 +38,23 @@ import javax.annotation.Nullable; +// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not +// persist them. +//todo clean up older fields as well to make most transient @Slf4j @Getter @Setter public final class TradingPeer implements PersistablePayload { + // Transient/Mutable + // Added in v1.2.0 + @Setter + @Nullable + transient private byte[] delayedPayoutTxSignature; + @Setter + @Nullable + transient private byte[] preparedDepositTx; + + // Persistable mutable @Nullable private String accountId; @Nullable @@ -75,6 +88,7 @@ public final class TradingPeer implements PersistablePayload { @Nullable private byte[] mediatedPayoutTxSignature; + public TradingPeer() { } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPeerPublishedDelayedPayoutTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPeerPublishedDelayedPayoutTxMessage.java new file mode 100644 index 00000000000..d0dc9300530 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ProcessPeerPublishedDelayedPayoutTxMessage.java @@ -0,0 +1,64 @@ +/* + * 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.trade.protocol.tasks; + +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ProcessPeerPublishedDelayedPayoutTxMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public ProcessPeerPublishedDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + PeerPublishedDelayedPayoutTxMessage message = (PeerPublishedDelayedPayoutTxMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + processModel.removeMailboxMessageAfterProcessing(trade); + + // We add the tx to our wallet. + Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx()); + WalletService.maybeAddSelfTxToWallet(delayedPayoutTx, processModel.getBtcWalletService().getWallet()); + + // todo trade.setState + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java index 61dbe0ff531..20b85539398 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java @@ -54,10 +54,10 @@ protected void run() { runInterceptHook(); if (!trade.isPayoutPublished()) { BtcWalletService walletService = processModel.getBtcWalletService(); - final String id = processModel.getOffer().getId(); + String id = processModel.getOffer().getId(); Address address = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT).getAddress(); - final TransactionConfidence confidence = walletService.getConfidenceForAddress(address); + TransactionConfidence confidence = walletService.getConfidenceForAddress(address); if (isInNetwork(confidence)) { applyConfidence(confidence); } else { diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDelayedPayoutTxSignatureRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDelayedPayoutTxSignatureRequest.java new file mode 100644 index 00000000000..c9fe2e8b7ad --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDelayedPayoutTxSignatureRequest.java @@ -0,0 +1,59 @@ +/* + * 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.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerProcessDelayedPayoutTxSignatureRequest extends TradeTask { + @SuppressWarnings({"unused"}) + public BuyerProcessDelayedPayoutTxSignatureRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + DelayedPayoutTxSignatureRequest message = (DelayedPayoutTxSignatureRequest) processModel.getTradeMessage(); + checkNotNull(message); + Validator.checkTradeId(processModel.getOfferId(), message); + byte[] delayedPayoutTxAsBytes = checkNotNull(message.getDelayedPayoutTx()); + Transaction preparedDelayedPayoutTx = processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxAsBytes); + processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java similarity index 61% rename from core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java index c000c49e3f5..7c0e3c7b3b2 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessDepositTxAndDelayedPayoutTxMessage.java @@ -15,12 +15,13 @@ * along with Bisq. If not, see . */ -package bisq.core.trade.protocol.tasks.maker; +package bisq.core.trade.protocol.tasks.buyer; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; import bisq.core.trade.Trade; -import bisq.core.trade.messages.DepositTxPublishedMessage; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.util.Validator; @@ -34,9 +35,9 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class MakerProcessDepositTxPublishedMessage extends TradeTask { +public class BuyerProcessDepositTxAndDelayedPayoutTxMessage extends TradeTask { @SuppressWarnings({"unused"}) - public MakerProcessDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + public BuyerProcessDepositTxAndDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -44,18 +45,24 @@ public MakerProcessDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade protected void run() { try { runInterceptHook(); - log.debug("current trade state " + trade.getState()); - DepositTxPublishedMessage message = (DepositTxPublishedMessage) processModel.getTradeMessage(); - Validator.checkTradeId(processModel.getOfferId(), message); + DepositTxAndDelayedPayoutTxMessage message = (DepositTxAndDelayedPayoutTxMessage) processModel.getTradeMessage(); checkNotNull(message); + Validator.checkTradeId(processModel.getOfferId(), message); checkArgument(message.getDepositTx() != null); // To access tx confidence we need to add that tx into our wallet. - Transaction txFromSerializedTx = processModel.getBtcWalletService().getTxFromSerializedTx(message.getDepositTx()); + Transaction depositTx = processModel.getBtcWalletService().getTxFromSerializedTx(message.getDepositTx()); // update with full tx - Transaction walletTx = processModel.getTradeWalletService().addTxToWallet(txFromSerializedTx); - trade.setDepositTx(walletTx); - BtcWalletService.printTx("depositTx received from peer", walletTx); + Transaction committedDepositTx = WalletService.maybeAddSelfTxToWallet(depositTx, processModel.getBtcWalletService().getWallet()); + trade.applyDepositTx(committedDepositTx); + BtcWalletService.printTx("depositTx received from peer", committedDepositTx); + + // To access tx confidence we need to add that tx into our wallet. + Transaction delayedPayoutTx = processModel.getBtcWalletService().getTxFromSerializedTx(message.getDelayedPayoutTx()); + trade.applyDelayedPayoutTx(delayedPayoutTx); + BtcWalletService.printTx("delayedPayoutTx received from peer", delayedPayoutTx); + + WalletService.maybeAddSelfTxToWallet(delayedPayoutTx, processModel.getBtcWalletService().getWallet()); // update to the latest peer address of our peer if the message is correct trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); @@ -63,8 +70,8 @@ protected void run() { processModel.removeMailboxMessageAfterProcessing(trade); // If we got already the confirmation we don't want to apply an earlier state - if (trade.getState() != Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK) - trade.setState(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG); + if (trade.getState() != Trade.State.BUYER_SAW_DEPOSIT_TX_IN_NETWORK) + trade.setState(Trade.State.BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG); processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.RESERVED_FOR_TRADE); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java index 6fc5f2d18c7..245dd40aacb 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java @@ -19,6 +19,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; import bisq.core.trade.Trade; import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.TradeTask; @@ -54,9 +55,9 @@ protected void run() { trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); if (trade.getPayoutTx() == null) { - Transaction walletTx = processModel.getTradeWalletService().addTxToWallet(message.getPayoutTx()); - trade.setPayoutTx(walletTx); - BtcWalletService.printTx("payoutTx received from peer", walletTx); + Transaction committedPayoutTx = WalletService.maybeAddNetworkTxToWallet(message.getPayoutTx(), processModel.getBtcWalletService().getWallet()); + trade.setPayoutTx(committedPayoutTx); + BtcWalletService.printTx("payoutTx received from peer", committedPayoutTx); trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG); processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.MULTI_SIG); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsDelayedPayoutTxSignatureResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsDelayedPayoutTxSignatureResponse.java new file mode 100644 index 00000000000..887c416a546 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendsDelayedPayoutTxSignatureResponse.java @@ -0,0 +1,85 @@ +/* + * 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.trade.protocol.tasks.buyer; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerSendsDelayedPayoutTxSignatureResponse extends TradeTask { + @SuppressWarnings({"unused"}) + public BuyerSendsDelayedPayoutTxSignatureResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + byte[] delayedPayoutTxSignature = checkNotNull(processModel.getDelayedPayoutTxSignature()); + DelayedPayoutTxSignatureResponse message = new DelayedPayoutTxSignatureResponse(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + delayedPayoutTxSignature); + + // todo trade.setState + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + // todo trade.setState + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + // todo trade.setState + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(errorMessage); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupDepositTxListener.java similarity index 94% rename from core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupDepositTxListener.java index a858124c5a1..454761b4aec 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupDepositTxListener.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.core.trade.protocol.tasks.maker; +package bisq.core.trade.protocol.tasks.buyer; import bisq.core.btc.listeners.AddressConfidenceListener; import bisq.core.btc.model.AddressEntry; @@ -39,13 +39,13 @@ import static com.google.common.base.Preconditions.checkArgument; @Slf4j -public class MakerSetupDepositTxListener extends TradeTask { +public class BuyerSetupDepositTxListener extends TradeTask { // Use instance fields to not get eaten up by the GC private Subscription tradeStateSubscription; private AddressConfidenceListener confidenceListener; @SuppressWarnings({"unused"}) - public MakerSetupDepositTxListener(TaskRunner taskHandler, Trade trade) { + public BuyerSetupDepositTxListener(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -94,9 +94,9 @@ public void onTransactionConfidenceChanged(TransactionConfidence confidence) { private void applyConfidence(TransactionConfidence confidence) { if (trade.getDepositTx() == null) { Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash()); - trade.setDepositTx(walletTx); + trade.applyDepositTx(walletTx); BtcWalletService.printTx("depositTx received from network", walletTx); - trade.setState(Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK); + trade.setState(Trade.State.BUYER_SAW_DEPOSIT_TX_IN_NETWORK); } else { log.info("We got the deposit tx already set from MakerProcessDepositTxPublishedMessage. tradeId={}, state={}", trade.getId(), trade.getState()); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignPayoutTx.java similarity index 89% rename from core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignPayoutTx.java index 4140ce364dd..b90a91ec22d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignPayoutTx.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.core.trade.protocol.tasks.buyer_as_maker; +package bisq.core.trade.protocol.tasks.buyer; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; @@ -38,10 +38,10 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class BuyerAsMakerSignPayoutTx extends TradeTask { +public class BuyerSignPayoutTx extends TradeTask { @SuppressWarnings({"unused"}) - public BuyerAsMakerSignPayoutTx(TaskRunner taskHandler, Trade trade) { + public BuyerSignPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -69,7 +69,7 @@ protected void run() { checkArgument(Arrays.equals(buyerMultiSigPubKey, walletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); - final byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); byte[] payoutTxSignature = processModel.getTradeWalletService().buyerSignsPayoutTx( trade.getDepositTx(), @@ -79,8 +79,7 @@ protected void run() { sellerPayoutAddressString, buyerMultiSigKeyPair, buyerMultiSigPubKey, - sellerMultiSigPubKey, - trade.getArbitratorBtcPubKey()); + sellerMultiSigPubKey); processModel.setPayoutTxSignature(payoutTxSignature); complete(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java new file mode 100644 index 00000000000..0d0bb585ae4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSignsDelayedPayoutTx.java @@ -0,0 +1,68 @@ +/* + * 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.trade.protocol.tasks.buyer; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class BuyerSignsDelayedPayoutTx extends TradeTask { + @SuppressWarnings({"unused"}) + public BuyerSignsDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + byte[] buyerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + DeterministicKey myMultiSigKeyPair = btcWalletService.getMultiSigKeyPair(id, buyerMultiSigPubKey); + + checkArgument(Arrays.equals(buyerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + byte[] sellerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx(preparedDelayedPayoutTx, myMultiSigKeyPair, buyerMultiSigPubKey, sellerMultiSigPubKey); + processModel.setDelayedPayoutTxSignature(delayedPayoutTxSignature); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesDelayedPayoutTx.java similarity index 50% rename from core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesDelayedPayoutTx.java index 3f2637328a4..3ec30a67786 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesDelayedPayoutTx.java @@ -15,22 +15,23 @@ * along with Bisq. If not, see . */ -package bisq.core.trade.protocol.tasks.seller; +package bisq.core.trade.protocol.tasks.buyer; -import bisq.core.account.witness.AccountAgeRestrictions; -import bisq.core.offer.OfferRestrictions; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.common.taskrunner.TaskRunner; +import org.bitcoinj.core.Transaction; + import lombok.extern.slf4j.Slf4j; -@Slf4j -public class SellerVerifiesPeersAccountAge extends TradeTask { +import static com.google.common.base.Preconditions.checkNotNull; +@Slf4j +public class BuyerVerifiesDelayedPayoutTx extends TradeTask { @SuppressWarnings({"unused"}) - public SellerVerifiesPeersAccountAge(TaskRunner taskHandler, Trade trade) { + public BuyerVerifiesDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -39,19 +40,12 @@ protected void run() { try { runInterceptHook(); - boolean isTradeRisky = OfferRestrictions.isTradeRisky(trade); - boolean isTradePeersAccountAgeImmature = AccountAgeRestrictions.isTradePeersAccountAgeImmature( - processModel.getAccountAgeWitnessService(), trade); - log.debug("SellerVerifiesPeersAccountAge isOfferRisky={} isTradePeersAccountAgeImmature={}", - isTradeRisky, isTradePeersAccountAgeImmature); - if (isTradeRisky && - isTradePeersAccountAgeImmature) { - failed("Violation of security restrictions:\n" + - " - The peer's account was created after March 1st 2019\n" + - " - The trade amount is above 0.01 BTC\n" + - " - The payment method for that offer is considered risky for bank chargebacks\n"); - } else { + Transaction depositTx = checkNotNull(trade.getDepositTx()); + Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx()); + if (processModel.getTradeWalletService().verifiesDepositTxAndDelayedPayoutTx(depositTx, delayedPayoutTx)) { complete(); + } else { + failed("DelayedPayoutTx is not spending correctly depositTx"); } } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java index d49995560b2..f7d2e8c8176 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java @@ -57,52 +57,40 @@ protected void run() { BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); TradingPeer tradingPeer = processModel.getTradingPeer(); - final Offer offer = trade.getOffer(); + Offer offer = checkNotNull(trade.getOffer()); // params - final boolean makerIsBuyer = true; - - final byte[] contractHash = Hash.getSha256Hash(trade.getContractAsJson()); + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); trade.setContractHash(contractHash); log.debug("\n\n------------------------------------------------------------\n" + "Contract as json\n" + trade.getContractAsJson() + "\n------------------------------------------------------------\n"); - final Coin makerInputAmount = offer.getBuyerSecurityDeposit(); + Coin makerInputAmount = offer.getBuyerSecurityDeposit(); Optional addressEntryOptional = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); checkArgument(addressEntryOptional.isPresent(), "addressEntryOptional must be present"); AddressEntry makerMultiSigAddressEntry = addressEntryOptional.get(); makerMultiSigAddressEntry.setCoinLockedInMultiSig(makerInputAmount); walletService.saveAddressEntryList(); - final Coin msOutputAmount = makerInputAmount + Coin msOutputAmount = makerInputAmount .add(trade.getTxFee()) .add(offer.getSellerSecurityDeposit()) .add(trade.getTradeAmount()); - final List takerRawTransactionInputs = tradingPeer.getRawTransactionInputs(); - - final long takerChangeOutputValue = tradingPeer.getChangeOutputValue(); - - final String takerChangeAddressString = tradingPeer.getChangeOutputAddress(); - - final Address makerAddress = walletService.getOrCreateAddressEntry(id, - AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); - - final Address makerChangeAddress = walletService.getFreshAddressEntry().getAddress(); - - final byte[] buyerPubKey = processModel.getMyMultiSigPubKey(); + List takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs()); + long takerChangeOutputValue = tradingPeer.getChangeOutputValue(); + String takerChangeAddressString = tradingPeer.getChangeOutputAddress(); + Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + Address makerChangeAddress = walletService.getFreshAddressEntry().getAddress(); + byte[] buyerPubKey = processModel.getMyMultiSigPubKey(); + byte[] sellerPubKey = tradingPeer.getMultiSigPubKey(); checkArgument(Arrays.equals(buyerPubKey, makerMultiSigAddressEntry.getPubKey()), "buyerPubKey from AddressEntry must match the one from the trade data. trade id =" + id); - final byte[] sellerPubKey = tradingPeer.getMultiSigPubKey(); - - final byte[] arbitratorBtcPubKey = trade.getArbitratorBtcPubKey(); - - PreparedDepositTxAndMakerInputs result = processModel.getTradeWalletService().makerCreatesAndSignsDepositTx( - makerIsBuyer, + PreparedDepositTxAndMakerInputs result = processModel.getTradeWalletService().buyerAsMakerCreatesAndSignsDepositTx( contractHash, makerInputAmount, msOutputAmount, @@ -112,8 +100,7 @@ protected void run() { makerAddress, makerChangeAddress, buyerPubKey, - sellerPubKey, - arbitratorBtcPubKey); + sellerPubKey); processModel.setPreparedDepositTx(result.depositTransaction); processModel.setRawTransactionInputs(result.rawMakerInputs); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSendsInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSendsInputsForDepositTxResponse.java new file mode 100644 index 00000000000..029a8213dcd --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSendsInputsForDepositTxResponse.java @@ -0,0 +1,38 @@ +/* + * 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.trade.protocol.tasks.buyer_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.maker.MakerSendsInputsForDepositTxResponse; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BuyerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInputsForDepositTxResponse { + @SuppressWarnings({"unused"}) + public BuyerAsMakerSendsInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected byte[] getPreparedDepositTx() { + return processModel.getPreparedDepositTx(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java index 79b18574dd9..ad0efc70bf6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java @@ -17,19 +17,18 @@ package bisq.core.trade.protocol.tasks.buyer_as_taker; -import bisq.core.btc.model.AddressEntry; import bisq.core.btc.model.InputsAndChangeOutput; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.common.taskrunner.TaskRunner; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j public class BuyerAsTakerCreatesDepositTxInputs extends TradeTask { @@ -47,15 +46,15 @@ protected void run() { Coin bsqTakerFee = trade.isCurrencyForTakerFeeBtc() ? Coin.ZERO : trade.getTakerFee(); Coin txFee = trade.getTxFee(); - Coin takerInputAmount = trade.getOffer().getBuyerSecurityDeposit().add(txFee).add(txFee).subtract(bsqTakerFee); - BtcWalletService walletService = processModel.getBtcWalletService(); - Address takersAddress = walletService.getOrCreateAddressEntry(processModel.getOffer().getId(), - AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); - InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs( + Coin takerInputAmount = checkNotNull(trade.getOffer()).getBuyerSecurityDeposit() + .add(txFee) + .add(txFee) + .subtract(bsqTakerFee); + Coin fee = txFee.subtract(bsqTakerFee); + InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositTxInputs( processModel.getTakeOfferFeeTx(), takerInputAmount, - txFee.subtract(bsqTakerFee), - takersAddress); + fee); processModel.setRawTransactionInputs(result.rawTransactionInputs); processModel.setChangeOutputValue(result.changeOutputValue); processModel.setChangeOutputAddress(result.changeOutputAddress); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSendsDepositTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSendsDepositTxMessage.java new file mode 100644 index 00000000000..f0391f91ebf --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSendsDepositTxMessage.java @@ -0,0 +1,87 @@ +/* + * 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.trade.protocol.tasks.buyer_as_taker; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class BuyerAsTakerSendsDepositTxMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public BuyerAsTakerSendsDepositTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + if (trade.getDepositTx() != null) { + DepositTxMessage message = new DepositTxMessage(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + trade.getDepositTx().bitcoinSerialize()); + + // todo trade.setState + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + // todo trade.setState + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + + // todo trade.setState + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(); + } + } + ); + } else { + log.error("trade.getDepositTx() = " + trade.getDepositTx()); + failed("DepositTx is null"); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignsDepositTx.java similarity index 55% rename from core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignsDepositTx.java index 125e347578c..f5cdcb37606 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignsDepositTx.java @@ -17,11 +17,9 @@ package bisq.core.trade.protocol.tasks.buyer_as_taker; -import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.model.RawTransactionInput; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.tasks.TradeTask; @@ -42,10 +40,10 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class BuyerAsTakerSignAndPublishDepositTx extends TradeTask { +public class BuyerAsTakerSignsDepositTx extends TradeTask { @SuppressWarnings({"unused"}) - public BuyerAsTakerSignAndPublishDepositTx(TaskRunner taskHandler, Trade trade) { + public BuyerAsTakerSignsDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -60,7 +58,7 @@ protected void run() { + "\n------------------------------------------------------------\n"); - byte[] contractHash = Hash.getSha256Hash(trade.getContractAsJson()); + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); trade.setContractHash(contractHash); List buyerInputs = checkNotNull(processModel.getRawTransactionInputs(), "buyerInputs must not be null"); BtcWalletService walletService = processModel.getBtcWalletService(); @@ -79,49 +77,19 @@ protected void run() { checkArgument(Arrays.equals(buyerMultiSigPubKey, buyerMultiSigAddressEntry.getPubKey()), "buyerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); - Transaction depositTx = processModel.getTradeWalletService().takerSignsAndPublishesDepositTx( + List sellerInputs = checkNotNull(tradingPeer.getRawTransactionInputs()); + byte[] sellerMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx( false, contractHash, processModel.getPreparedDepositTx(), buyerInputs, - tradingPeer.getRawTransactionInputs(), + sellerInputs, buyerMultiSigPubKey, - tradingPeer.getMultiSigPubKey(), - trade.getArbitratorBtcPubKey(), - new TxBroadcaster.Callback() { - @Override - public void onSuccess(Transaction transaction) { - if (!completed) { - // We set the depositTx before we change the state as the state change triggers code - // which expected the tx to be available. That case will usually never happen as the - // callback is called after the method call has returned but in some test scenarios - // with regtest we run into such issues, thus fixing it to make it more stict seems - // reasonable. - trade.setDepositTx(transaction); - log.trace("takerSignsAndPublishesDepositTx succeeded " + transaction); - trade.setState(Trade.State.TAKER_PUBLISHED_DEPOSIT_TX); - walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE); - - complete(); - } else { - log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); - } - } - - @Override - public void onFailure(TxBroadcastException exception) { - if (!completed) { - failed(exception); - } else { - log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); - } - } - }); - if (trade.getDepositTx() == null) { - // We set the deposit tx in case we get the onFailure called. We cannot set it in the onFailure - // callback as the tx is returned by the method call where the callback is used as an argument. - trade.setDepositTx(depositTx); - } + sellerMultiSigPubKey); + trade.applyDepositTx(depositTx); + + complete(); } catch (Throwable t) { failed(t); } 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 417a4be432c..9f963e0afac 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 @@ -55,7 +55,7 @@ protected void run() { TradingPeer taker = processModel.getTradingPeer(); PaymentAccountPayload makerPaymentAccountPayload = processModel.getPaymentAccountPayload(trade); checkNotNull(makerPaymentAccountPayload, "makerPaymentAccountPayload must not be null"); - PaymentAccountPayload takerPaymentAccountPayload = taker.getPaymentAccountPayload(); + PaymentAccountPayload takerPaymentAccountPayload = checkNotNull(taker.getPaymentAccountPayload()); boolean isBuyerMakerAndSellerTaker = trade instanceof BuyerAsMakerTrade; NodeAddress buyerNodeAddress = isBuyerMakerAndSellerTaker ? @@ -91,7 +91,9 @@ protected void run() { takerAddressEntry.getAddressString(), taker.getPayoutAddressString(), makerMultiSigPubKey, - taker.getMultiSigPubKey() + taker.getMultiSigPubKey(), + trade.getLockTime(), + trade.getRefundAgentNodeAddress() ); String contractAsJson = Utilities.objectToJson(contract); String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessesInputsForDepositTxRequest.java similarity index 63% rename from core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessesInputsForDepositTxRequest.java index 3a8088905ce..d449accd164 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessesInputsForDepositTxRequest.java @@ -22,7 +22,7 @@ import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayDepositRequest; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.user.User; @@ -43,9 +43,9 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class MakerProcessPayDepositRequest extends TradeTask { +public class MakerProcessesInputsForDepositTxRequest extends TradeTask { @SuppressWarnings({"unused"}) - public MakerProcessPayDepositRequest(TaskRunner taskHandler, Trade trade) { + public MakerProcessesInputsForDepositTxRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -54,37 +54,35 @@ protected void run() { try { runInterceptHook(); log.debug("current trade state " + trade.getState()); - PayDepositRequest payDepositRequest = (PayDepositRequest) processModel.getTradeMessage(); - checkNotNull(payDepositRequest); - checkTradeId(processModel.getOfferId(), payDepositRequest); + InputsForDepositTxRequest inputsForDepositTxRequest = (InputsForDepositTxRequest) processModel.getTradeMessage(); + checkNotNull(inputsForDepositTxRequest); + checkTradeId(processModel.getOfferId(), inputsForDepositTxRequest); final TradingPeer tradingPeer = processModel.getTradingPeer(); - tradingPeer.setPaymentAccountPayload(checkNotNull(payDepositRequest.getTakerPaymentAccountPayload())); - tradingPeer.setRawTransactionInputs(checkNotNull(payDepositRequest.getRawTransactionInputs())); - checkArgument(payDepositRequest.getRawTransactionInputs().size() > 0); - - tradingPeer.setChangeOutputValue(payDepositRequest.getChangeOutputValue()); - tradingPeer.setChangeOutputAddress(payDepositRequest.getChangeOutputAddress()); - - tradingPeer.setMultiSigPubKey(checkNotNull(payDepositRequest.getTakerMultiSigPubKey())); - tradingPeer.setPayoutAddressString(nonEmptyStringOf(payDepositRequest.getTakerPayoutAddressString())); - tradingPeer.setPubKeyRing(checkNotNull(payDepositRequest.getTakerPubKeyRing())); - - tradingPeer.setAccountId(nonEmptyStringOf(payDepositRequest.getTakerAccountId())); - trade.setTakerFeeTxId(nonEmptyStringOf(payDepositRequest.getTakerFeeTxId())); - processModel.setTakerAcceptedArbitratorNodeAddresses(checkNotNull(payDepositRequest.getAcceptedArbitratorNodeAddresses())); - processModel.setTakerAcceptedMediatorNodeAddresses(checkNotNull(payDepositRequest.getAcceptedMediatorNodeAddresses())); - if (payDepositRequest.getAcceptedArbitratorNodeAddresses().isEmpty()) + tradingPeer.setPaymentAccountPayload(checkNotNull(inputsForDepositTxRequest.getTakerPaymentAccountPayload())); + tradingPeer.setRawTransactionInputs(checkNotNull(inputsForDepositTxRequest.getRawTransactionInputs())); + checkArgument(inputsForDepositTxRequest.getRawTransactionInputs().size() > 0); + + tradingPeer.setChangeOutputValue(inputsForDepositTxRequest.getChangeOutputValue()); + tradingPeer.setChangeOutputAddress(inputsForDepositTxRequest.getChangeOutputAddress()); + + tradingPeer.setMultiSigPubKey(checkNotNull(inputsForDepositTxRequest.getTakerMultiSigPubKey())); + tradingPeer.setPayoutAddressString(nonEmptyStringOf(inputsForDepositTxRequest.getTakerPayoutAddressString())); + tradingPeer.setPubKeyRing(checkNotNull(inputsForDepositTxRequest.getTakerPubKeyRing())); + + tradingPeer.setAccountId(nonEmptyStringOf(inputsForDepositTxRequest.getTakerAccountId())); + trade.setTakerFeeTxId(nonEmptyStringOf(inputsForDepositTxRequest.getTakerFeeTxId())); + if (inputsForDepositTxRequest.getAcceptedArbitratorNodeAddresses().isEmpty()) failed("acceptedArbitratorNodeAddresses must not be empty"); // Taker has to sign offerId (he cannot manipulate that - so we avoid to have a challenge protocol for passing the nonce we want to get signed) tradingPeer.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); - tradingPeer.setAccountAgeWitnessSignature(payDepositRequest.getAccountAgeWitnessSignatureOfOfferId()); - tradingPeer.setCurrentDate(payDepositRequest.getCurrentDate()); + tradingPeer.setAccountAgeWitnessSignature(inputsForDepositTxRequest.getAccountAgeWitnessSignatureOfOfferId()); + tradingPeer.setCurrentDate(inputsForDepositTxRequest.getCurrentDate()); User user = checkNotNull(processModel.getUser(), "User must not be null"); - NodeAddress arbitratorNodeAddress = checkNotNull(payDepositRequest.getArbitratorNodeAddress(), + NodeAddress arbitratorNodeAddress = checkNotNull(inputsForDepositTxRequest.getArbitratorNodeAddress(), "payDepositRequest.getArbitratorNodeAddress() must not be null"); trade.setArbitratorNodeAddress(arbitratorNodeAddress); Arbitrator arbitrator = checkNotNull(user.getAcceptedArbitratorByAddress(arbitratorNodeAddress), @@ -94,7 +92,7 @@ protected void run() { trade.setArbitratorPubKeyRing(checkNotNull(arbitrator.getPubKeyRing(), "arbitrator.getPubKeyRing() must not be null")); - NodeAddress mediatorNodeAddress = checkNotNull(payDepositRequest.getMediatorNodeAddress(), + NodeAddress mediatorNodeAddress = checkNotNull(inputsForDepositTxRequest.getMediatorNodeAddress(), "payDepositRequest.getMediatorNodeAddress() must not be null"); trade.setMediatorNodeAddress(mediatorNodeAddress); Mediator mediator = checkNotNull(user.getAcceptedMediatorByAddress(mediatorNodeAddress), @@ -104,7 +102,7 @@ protected void run() { Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); try { - long takersTradePrice = payDepositRequest.getTradePrice(); + long takersTradePrice = inputsForDepositTxRequest.getTradePrice(); offer.checkTradePriceTolerance(takersTradePrice); trade.setTradePrice(takersTradePrice); } catch (TradePriceOutOfToleranceException e) { @@ -113,15 +111,13 @@ protected void run() { failed(e2); } - checkArgument(payDepositRequest.getTradeAmount() > 0); - trade.setTradeAmount(Coin.valueOf(payDepositRequest.getTradeAmount())); + checkArgument(inputsForDepositTxRequest.getTradeAmount() > 0); + trade.setTradeAmount(Coin.valueOf(inputsForDepositTxRequest.getTradeAmount())); trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); trade.persist(); - processModel.removeMailboxMessageAfterProcessing(trade); - complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendsInputsForDepositTxResponse.java similarity index 79% rename from core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendsInputsForDepositTxResponse.java index 8bb029f140f..2cd81721ee7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendsInputsForDepositTxResponse.java @@ -21,11 +21,11 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PublishDepositTxRequest; +import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.SendMailboxMessageListener; +import bisq.network.p2p.SendDirectMessageListener; import bisq.common.crypto.Sig; import bisq.common.taskrunner.TaskRunner; @@ -41,12 +41,14 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class MakerSendPublishDepositTxRequest extends TradeTask { +public abstract class MakerSendsInputsForDepositTxResponse extends TradeTask { @SuppressWarnings({"unused"}) - public MakerSendPublishDepositTxRequest(TaskRunner taskHandler, Trade trade) { + public MakerSendsInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } + protected abstract byte[] getPreparedDepositTx(); + @Override protected void run() { try { @@ -62,15 +64,16 @@ protected void run() { addressEntryOptional.get().getPubKey()), "makerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); - final byte[] preparedDepositTx = processModel.getPreparedDepositTx(); + byte[] preparedDepositTx = getPreparedDepositTx(); // Maker has to use preparedDepositTx as nonce. // He cannot manipulate the preparedDepositTx - so we avoid to have a challenge protocol for passing the nonce we want to get signed. - final PaymentAccountPayload paymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade), "processModel.getPaymentAccountPayload(trade) must not be null"); + PaymentAccountPayload paymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade), + "processModel.getPaymentAccountPayload(trade) must not be null"); byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), preparedDepositTx); - PublishDepositTxRequest message = new PublishDepositTxRequest( + InputsForDepositTxResponse message = new InputsForDepositTxResponse( processModel.getOfferId(), paymentAccountPayload, processModel.getAccountId(), @@ -83,18 +86,19 @@ protected void run() { processModel.getMyNodeAddress(), UUID.randomUUID().toString(), sig, - new Date().getTime()); + new Date().getTime(), + trade.getLockTime()); trade.setState(Trade.State.MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST); NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); log.info("Send {} to peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - processModel.getP2PService().sendEncryptedMailboxMessage( + processModel.getP2PService().sendEncryptedDirectMessage( peersNodeAddress, processModel.getTradingPeer().getPubKeyRing(), message, - new SendMailboxMessageListener() { + new SendDirectMessageListener() { @Override public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, uid={}", @@ -103,14 +107,6 @@ public void onArrived() { complete(); } - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - trade.setState(Trade.State.MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST); - complete(); - } - @Override public void onFault(String errorMessage) { log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java new file mode 100644 index 00000000000..bea1d452e3c --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetsLockTime.java @@ -0,0 +1,54 @@ +/* + * 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.trade.protocol.tasks.maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.Random; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MakerSetsLockTime extends TradeTask { + @SuppressWarnings({"unused"}) + public MakerSetsLockTime(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // 20-30 days + int delay = 144 * 20 + new Random().nextInt(144 * 10); + long lockTime = processModel.getBtcWalletService().getBestChainHeight() + delay; + log.info("lockTime={}, delay={}", lockTime, delay); + trade.setLockTime(lockTime); + //todo for dev testing + trade.setLockTime(processModel.getBtcWalletService().getBestChainHeight() + 5); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java index b88a39ff05e..96d8db5b4ec 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java @@ -104,8 +104,7 @@ protected void run() { sellerPayoutAddressString, multiSigKeyPair, buyerMultiSigPubKey, - sellerMultiSigPubKey, - trade.getArbitratorBtcPubKey() + sellerMultiSigPubKey ); trade.setPayoutTx(transaction); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java index cb94c85d5a2..e6c60eb4a10 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java @@ -19,6 +19,7 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.trade.Trade; import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; @@ -55,9 +56,9 @@ protected void run() { trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); if (trade.getPayoutTx() == null) { - Transaction walletTx = processModel.getTradeWalletService().addTxToWallet(message.getPayoutTx()); - trade.setPayoutTx(walletTx); - BtcWalletService.printTx("payoutTx received from peer", walletTx); + Transaction committedMediatedPayoutTx = WalletService.maybeAddNetworkTxToWallet(message.getPayoutTx(), processModel.getBtcWalletService().getWallet()); + trade.setPayoutTx(committedMediatedPayoutTx); + BtcWalletService.printTx("MediatedPayoutTx received from peer", committedMediatedPayoutTx); trade.setMediationResultState(MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java index 59c27e83177..5a48240f7d4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java @@ -97,8 +97,7 @@ protected void run() { sellerPayoutAddressString, myMultiSigKeyPair, buyerMultiSigPubKey, - sellerMultiSigPubKey, - trade.getArbitratorBtcPubKey()); + sellerMultiSigPubKey); processModel.setMediatedPayoutTxSignature(mediatedPayoutTxSignature); complete(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerCreatesDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerCreatesDelayedPayoutTx.java new file mode 100644 index 00000000000..6bddc5d9d64 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerCreatesDelayedPayoutTx.java @@ -0,0 +1,65 @@ +/* + * 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.trade.protocol.tasks.seller; + +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.governance.param.Param; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerCreatesDelayedPayoutTx extends TradeTask { + + @SuppressWarnings({"unused"}) + public SellerCreatesDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + String donationAddressString = processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS); + Coin minerFee = trade.getTxFee(); + TradeWalletService tradeWalletService = processModel.getTradeWalletService(); + Transaction depositTx = checkNotNull(trade.getDepositTx()); + + long lockTime = trade.getLockTime(); + Transaction preparedDelayedPayoutTx = tradeWalletService.createDelayedUnsignedPayoutTx(depositTx, + donationAddressString, + minerFee, + lockTime); + + processModel.setPreparedDelayedPayoutTx(preparedDelayedPayoutTx); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerFinalizesDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerFinalizesDelayedPayoutTx.java new file mode 100644 index 00000000000..f615bad0c44 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerFinalizesDelayedPayoutTx.java @@ -0,0 +1,76 @@ +/* + * 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.trade.protocol.tasks.seller; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerFinalizesDelayedPayoutTx extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerFinalizesDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + byte[] buyerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + checkArgument(Arrays.equals(sellerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "sellerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + + byte[] buyerSignature = processModel.getTradingPeer().getDelayedPayoutTxSignature(); + byte[] sellerSignature = processModel.getDelayedPayoutTxSignature(); + + Transaction signedDelayedPayoutTx = processModel.getTradeWalletService().finalizeDelayedPayoutTx(preparedDelayedPayoutTx, + buyerMultiSigPubKey, + sellerMultiSigPubKey, + buyerSignature, + sellerSignature); + + trade.applyDelayedPayoutTx(signedDelayedPayoutTx); + WalletService.maybeAddSelfTxToWallet(signedDelayedPayoutTx, processModel.getBtcWalletService().getWallet()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessDelayedPayoutTxSignatureResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessDelayedPayoutTxSignatureResponse.java new file mode 100644 index 00000000000..e2e3d7cd37f --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessDelayedPayoutTxSignatureResponse.java @@ -0,0 +1,59 @@ +/* + * 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.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.util.Validator.checkTradeId; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerProcessDelayedPayoutTxSignatureResponse extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerProcessDelayedPayoutTxSignatureResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + DelayedPayoutTxSignatureResponse delayedPayoutTxSignatureResponse = (DelayedPayoutTxSignatureResponse) processModel.getTradeMessage(); + checkNotNull(delayedPayoutTxSignatureResponse); + checkTradeId(processModel.getOfferId(), delayedPayoutTxSignatureResponse); + + byte[] delayedPayoutTxSignature = checkNotNull(delayedPayoutTxSignatureResponse.getDelayedPayoutTxSignature()); + processModel.getTradingPeer().setDelayedPayoutTxSignature(delayedPayoutTxSignature); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + // todo trade.setState + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesDepositTx.java new file mode 100644 index 00000000000..be9b3fc1bd3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerPublishesDepositTx.java @@ -0,0 +1,76 @@ +/* + * 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.trade.protocol.tasks.seller; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SellerPublishesDepositTx extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerPublishesDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + processModel.getTradeWalletService().broadcastTx(trade.getDepositTx(), + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + if (!completed) { + trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX); + + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(processModel.getOffer().getId(), AddressEntry.Context.RESERVED_FOR_TRADE); + + complete(); + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + } + + @Override + public void onFailure(TxBroadcastException exception) { + if (!completed) { + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + }); + } catch (Throwable t) { + Contract contract = trade.getContract(); + if (contract != null) + contract.printDiff(processModel.getTradingPeer().getContractAsJson()); + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendDelayedPayoutTxSignatureRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendDelayedPayoutTxSignatureRequest.java new file mode 100644 index 00000000000..187c274304b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendDelayedPayoutTxSignatureRequest.java @@ -0,0 +1,88 @@ +/* + * 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.trade.protocol.tasks.seller; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerSendDelayedPayoutTxSignatureRequest extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerSendDelayedPayoutTxSignatureRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx(), + "processModel.getPreparedDelayedPayoutTx() must not be null"); + DelayedPayoutTxSignatureRequest message = new DelayedPayoutTxSignatureRequest(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + preparedDelayedPayoutTx.bitcoinSerialize()); + + // todo trade.setState + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + // todo trade.setState + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + // todo trade.setState + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsDepositTxAndDelayedPayoutTxMessage.java similarity index 73% rename from core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsDepositTxAndDelayedPayoutTxMessage.java index ce9034b98dc..077bca13636 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendsDepositTxAndDelayedPayoutTxMessage.java @@ -15,10 +15,10 @@ * along with Bisq. If not, see . */ -package bisq.core.trade.protocol.tasks.taker; +package bisq.core.trade.protocol.tasks.seller; import bisq.core.trade.Trade; -import bisq.core.trade.messages.DepositTxPublishedMessage; +import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.network.p2p.NodeAddress; @@ -26,14 +26,18 @@ import bisq.common.taskrunner.TaskRunner; +import org.bitcoinj.core.Transaction; + import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j -public class TakerSendDepositTxPublishedMessage extends TradeTask { +public class SellerSendsDepositTxAndDelayedPayoutTxMessage extends TradeTask { @SuppressWarnings({"unused"}) - public TakerSendDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + public SellerSendsDepositTxAndDelayedPayoutTxMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -42,12 +46,14 @@ protected void run() { try { runInterceptHook(); if (trade.getDepositTx() != null) { - final String id = processModel.getOfferId(); - DepositTxPublishedMessage message = new DepositTxPublishedMessage(processModel.getOfferId(), - trade.getDepositTx().bitcoinSerialize(), + Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx()); + Transaction depositTx = checkNotNull(trade.getDepositTx()); + DepositTxAndDelayedPayoutTxMessage message = new DepositTxAndDelayedPayoutTxMessage(UUID.randomUUID().toString(), + processModel.getOfferId(), processModel.getMyNodeAddress(), - UUID.randomUUID().toString()); - trade.setState(Trade.State.TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG); + depositTx.bitcoinSerialize(), + delayedPayoutTx.bitcoinSerialize()); + trade.setState(Trade.State.SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG); NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); log.info("Send {} to peer {}. tradeId={}, uid={}", @@ -61,7 +67,7 @@ protected void run() { public void onArrived() { log.info("{} arrived at peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - trade.setState(Trade.State.TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG); + trade.setState(Trade.State.SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG); complete(); } @@ -69,7 +75,8 @@ public void onArrived() { public void onStoredInMailbox() { log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - trade.setState(Trade.State.TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG); + + trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG); complete(); } @@ -77,7 +84,7 @@ public void onStoredInMailbox() { public void onFault(String errorMessage) { log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); - trade.setState(Trade.State.TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG); + trade.setState(Trade.State.SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG); appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); failed(); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java index 80b3e4c6340..d6868db4238 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java @@ -60,7 +60,7 @@ protected void run() { final byte[] buyerSignature = tradingPeer.getSignature(); - Coin buyerPayoutAmount = offer.getBuyerSecurityDeposit().add(trade.getTradeAmount()); + Coin buyerPayoutAmount = checkNotNull(offer.getBuyerSecurityDeposit()).add(trade.getTradeAmount()); Coin sellerPayoutAmount = offer.getSellerSecurityDeposit(); final String buyerPayoutAddressString = tradingPeer.getPayoutAddressString(); @@ -78,7 +78,7 @@ protected void run() { DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(id, sellerMultiSigPubKey); Transaction transaction = processModel.getTradeWalletService().sellerSignsAndFinalizesPayoutTx( - trade.getDepositTx(), + checkNotNull(trade.getDepositTx()), buyerSignature, buyerPayoutAmount, sellerPayoutAmount, @@ -86,8 +86,7 @@ protected void run() { sellerPayoutAddressString, multiSigKeyPair, buyerMultiSigPubKey, - sellerMultiSigPubKey, - trade.getArbitratorBtcPubKey() + sellerMultiSigPubKey ); trade.setPayoutTx(transaction); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java new file mode 100644 index 00000000000..b8b8e155c2d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignsDelayedPayoutTx.java @@ -0,0 +1,73 @@ +/* + * 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.trade.protocol.tasks.seller; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerSignsDelayedPayoutTx extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerSignsDelayedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction preparedDelayedPayoutTx = checkNotNull(processModel.getPreparedDelayedPayoutTx()); + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + String id = processModel.getOffer().getId(); + + byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey(); + DeterministicKey myMultiSigKeyPair = btcWalletService.getMultiSigKeyPair(id, sellerMultiSigPubKey); + + checkArgument(Arrays.equals(sellerMultiSigPubKey, + btcWalletService.getOrCreateAddressEntry(id, AddressEntry.Context.MULTI_SIG).getPubKey()), + "sellerMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + id); + byte[] buyerMultiSigPubKey = processModel.getTradingPeer().getMultiSigPubKey(); + + byte[] delayedPayoutTxSignature = processModel.getTradeWalletService().signDelayedPayoutTx(preparedDelayedPayoutTx, + myMultiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey); + + processModel.setDelayedPayoutTxSignature(delayedPayoutTxSignature); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java similarity index 72% rename from core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java index 28f3005163d..3f7cbdbef02 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesUnsignedDepositTx.java @@ -42,9 +42,9 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class SellerAsMakerCreatesAndSignsDepositTx extends TradeTask { +public class SellerAsMakerCreatesUnsignedDepositTx extends TradeTask { @SuppressWarnings({"unused"}) - public SellerAsMakerCreatesAndSignsDepositTx(TaskRunner taskHandler, Trade trade) { + public SellerAsMakerCreatesUnsignedDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -57,44 +57,39 @@ protected void run() { BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); TradingPeer tradingPeer = processModel.getTradingPeer(); - final Offer offer = trade.getOffer(); + Offer offer = checkNotNull(trade.getOffer()); // params - final boolean makerIsBuyer = false; - - final byte[] contractHash = Hash.getSha256Hash(trade.getContractAsJson()); + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); trade.setContractHash(contractHash); log.debug("\n\n------------------------------------------------------------\n" + "Contract as json\n" + trade.getContractAsJson() + "\n------------------------------------------------------------\n"); - final Coin makerInputAmount = offer.getSellerSecurityDeposit().add(trade.getTradeAmount()); + Coin makerInputAmount = offer.getSellerSecurityDeposit().add(trade.getTradeAmount()); Optional addressEntryOptional = walletService.getAddressEntry(id, AddressEntry.Context.MULTI_SIG); checkArgument(addressEntryOptional.isPresent(), "addressEntryOptional must be present"); AddressEntry makerMultiSigAddressEntry = addressEntryOptional.get(); makerMultiSigAddressEntry.setCoinLockedInMultiSig(makerInputAmount); walletService.saveAddressEntryList(); - final Coin msOutputAmount = makerInputAmount + Coin msOutputAmount = makerInputAmount .add(trade.getTxFee()) .add(offer.getBuyerSecurityDeposit()); - final List takerRawTransactionInputs = tradingPeer.getRawTransactionInputs(); - final long takerChangeOutputValue = tradingPeer.getChangeOutputValue(); - final String takerChangeAddressString = tradingPeer.getChangeOutputAddress(); - final Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); - final Address makerChangeAddress = walletService.getFreshAddressEntry().getAddress(); - final byte[] buyerPubKey = tradingPeer.getMultiSigPubKey(); - final byte[] sellerPubKey = processModel.getMyMultiSigPubKey(); + List takerRawTransactionInputs = checkNotNull(tradingPeer.getRawTransactionInputs()); + long takerChangeOutputValue = tradingPeer.getChangeOutputValue(); + String takerChangeAddressString = tradingPeer.getChangeOutputAddress(); + Address makerAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + Address makerChangeAddress = walletService.getFreshAddressEntry().getAddress(); + byte[] buyerPubKey = tradingPeer.getMultiSigPubKey(); + byte[] sellerPubKey = processModel.getMyMultiSigPubKey(); checkArgument(Arrays.equals(sellerPubKey, makerMultiSigAddressEntry.getPubKey()), "sellerPubKey from AddressEntry must match the one from the trade data. trade id =" + id); - final byte[] arbitratorBtcPubKey = trade.getArbitratorBtcPubKey(); - - PreparedDepositTxAndMakerInputs result = processModel.getTradeWalletService().makerCreatesAndSignsDepositTx( - makerIsBuyer, + PreparedDepositTxAndMakerInputs result = processModel.getTradeWalletService().sellerAsMakerCreatesDepositTx( contractHash, makerInputAmount, msOutputAmount, @@ -104,8 +99,7 @@ protected void run() { makerAddress, makerChangeAddress, buyerPubKey, - sellerPubKey, - arbitratorBtcPubKey); + sellerPubKey); processModel.setPreparedDepositTx(result.depositTransaction); processModel.setRawTransactionInputs(result.rawMakerInputs); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerFinalizesDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerFinalizesDepositTx.java new file mode 100644 index 00000000000..ba490a92f3d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerFinalizesDepositTx.java @@ -0,0 +1,57 @@ +/* + * 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.trade.protocol.tasks.seller_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsMakerFinalizesDepositTx extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerAsMakerFinalizesDepositTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + byte[] takersRawPreparedDepositTx = checkNotNull(processModel.getTradingPeer().getPreparedDepositTx()); + byte[] myRawPreparedDepositTx = checkNotNull(processModel.getPreparedDepositTx()); + Transaction takersDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(takersRawPreparedDepositTx); + Transaction myDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(myRawPreparedDepositTx); + int numTakersInputs = checkNotNull(processModel.getTradingPeer().getRawTransactionInputs()).size(); + processModel.getTradeWalletService().sellerAsMakerFinalizesDepositTx(myDepositTx, takersDepositTx, numTakersInputs); + + trade.applyDepositTx(myDepositTx); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerProcessDepositTxMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerProcessDepositTxMessage.java new file mode 100644 index 00000000000..b5962c989fa --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerProcessDepositTxMessage.java @@ -0,0 +1,56 @@ +/* + * 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.trade.protocol.tasks.seller_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositTxMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SellerAsMakerProcessDepositTxMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public SellerAsMakerProcessDepositTxMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + DepositTxMessage message = (DepositTxMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + checkNotNull(message.getDepositTx()); + + processModel.getTradingPeer().setPreparedDepositTx(message.getDepositTx()); + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerSendsInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerSendsInputsForDepositTxResponse.java new file mode 100644 index 00000000000..f1fb5162809 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerSendsInputsForDepositTxResponse.java @@ -0,0 +1,47 @@ +/* + * 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.trade.protocol.tasks.seller_as_maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.maker.MakerSendsInputsForDepositTxResponse; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.script.Script; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SellerAsMakerSendsInputsForDepositTxResponse extends MakerSendsInputsForDepositTxResponse { + @SuppressWarnings({"unused"}) + public SellerAsMakerSendsInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected byte[] getPreparedDepositTx() { + Transaction preparedDepositTx = processModel.getBtcWalletService().getTxFromSerializedTx(processModel.getPreparedDepositTx()); + preparedDepositTx.getInputs().forEach(input -> { + // Remove signature before sending to peer as we don't want to risk that buyer could publish deposit tx + // before we have received his signature for the delayed payout tx. + input.setScriptSig(new Script(new byte[]{})); + }); + return preparedDepositTx.bitcoinSerialize(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java index 6298250566e..9b4b88a0ba4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java @@ -17,19 +17,18 @@ package bisq.core.trade.protocol.tasks.seller_as_taker; -import bisq.core.btc.model.AddressEntry; import bisq.core.btc.model.InputsAndChangeOutput; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.common.taskrunner.TaskRunner; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j public class SellerAsTakerCreatesDepositTxInputs extends TradeTask { @SuppressWarnings({"unused"}) @@ -43,17 +42,14 @@ protected void run() { runInterceptHook(); if (trade.getTradeAmount() != null) { Coin txFee = trade.getTxFee(); - Coin takerInputAmount = trade.getOffer().getSellerSecurityDeposit() - .add(txFee).add(txFee).add(trade.getTradeAmount()); - - BtcWalletService walletService = processModel.getBtcWalletService(); - Address takersAddress = walletService.getOrCreateAddressEntry(processModel.getOffer().getId(), - AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); - InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs( + Coin takerInputAmount = checkNotNull(trade.getOffer()).getSellerSecurityDeposit() + .add(txFee) + .add(txFee) + .add(trade.getTradeAmount()); + InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositTxInputs( processModel.getTakeOfferFeeTx(), takerInputAmount, - txFee, - takersAddress); + txFee); processModel.setRawTransactionInputs(result.rawTransactionInputs); processModel.setChangeOutputValue(result.changeOutputValue); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java similarity index 56% rename from core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java index 9bce2944109..6a27e1221c8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignsDepositTx.java @@ -17,11 +17,9 @@ package bisq.core.trade.protocol.tasks.seller_as_taker; -import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.model.RawTransactionInput; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; @@ -43,9 +41,9 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class SellerAsTakerSignAndPublishDepositTx extends TradeTask { +public class SellerAsTakerSignsDepositTx extends TradeTask { @SuppressWarnings({"unused"}) - public SellerAsTakerSignAndPublishDepositTx(TaskRunner taskHandler, Trade trade) { + public SellerAsTakerSignsDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -58,7 +56,7 @@ protected void run() { + trade.getContractAsJson() + "\n------------------------------------------------------------\n"); - byte[] contractHash = Hash.getSha256Hash(trade.getContractAsJson()); + byte[] contractHash = Hash.getSha256Hash(checkNotNull(trade.getContractAsJson())); trade.setContractHash(contractHash); List sellerInputs = checkNotNull(processModel.getRawTransactionInputs(), "sellerInputs must not be null"); @@ -80,51 +78,20 @@ protected void run() { TradingPeer tradingPeer = processModel.getTradingPeer(); - Transaction depositTx = processModel.getTradeWalletService().takerSignsAndPublishesDepositTx( + Transaction depositTx = processModel.getTradeWalletService().takerSignsDepositTx( true, contractHash, processModel.getPreparedDepositTx(), - tradingPeer.getRawTransactionInputs(), + checkNotNull(tradingPeer.getRawTransactionInputs()), sellerInputs, tradingPeer.getMultiSigPubKey(), - sellerMultiSigPubKey, - trade.getArbitratorBtcPubKey(), - new TxBroadcaster.Callback() { - @Override - public void onSuccess(Transaction transaction) { - if (!completed) { - // We set the depositTx before we change the state as the state change triggers code - // which expected the tx to be available. That case will usually never happen as the - // callback is called after the method call has returned but in some test scenarios - // with regtest we run into such issues, thus fixing it to make it more stict seems - // reasonable. - trade.setDepositTx(transaction); - log.trace("takerSignsAndPublishesDepositTx succeeded " + transaction); - trade.setState(Trade.State.TAKER_PUBLISHED_DEPOSIT_TX); - walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE); - - complete(); - } else { - log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); - } - } - - @Override - public void onFailure(TxBroadcastException exception) { - if (!completed) { - failed(exception); - } else { - log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); - } - } - }); - if (trade.getDepositTx() == null) { - // We set the deposit tx in case we get the onFailure called. We cannot set it in the onFailure - // callback as the tx is returned by the method call where the callback is used as an argument. - trade.setDepositTx(depositTx); - } + sellerMultiSigPubKey); + + trade.applyDepositTx(depositTx); + + complete(); } catch (Throwable t) { - final Contract contract = trade.getContract(); + Contract contract = trade.getContract(); if (contract != null) contract.printDiff(processModel.getTradingPeer().getContractAsJson()); failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java deleted file mode 100644 index 0160397d04e..00000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.trade.protocol.tasks.taker; - -import bisq.core.trade.Trade; -import bisq.core.trade.messages.PublishDepositTxRequest; -import bisq.core.trade.protocol.TradingPeer; -import bisq.core.trade.protocol.tasks.TradeTask; - -import bisq.common.taskrunner.TaskRunner; - -import lombok.extern.slf4j.Slf4j; - -import static bisq.core.util.Validator.checkTradeId; -import static bisq.core.util.Validator.nonEmptyStringOf; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -@Slf4j -public class TakerProcessPublishDepositTxRequest extends TradeTask { - @SuppressWarnings({"unused"}) - public TakerProcessPublishDepositTxRequest(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - log.debug("current trade state " + trade.getState()); - PublishDepositTxRequest publishDepositTxRequest = (PublishDepositTxRequest) processModel.getTradeMessage(); - checkTradeId(processModel.getOfferId(), publishDepositTxRequest); - checkNotNull(publishDepositTxRequest); - - final TradingPeer tradingPeer = processModel.getTradingPeer(); - tradingPeer.setPaymentAccountPayload(checkNotNull(publishDepositTxRequest.getMakerPaymentAccountPayload())); - tradingPeer.setAccountId(nonEmptyStringOf(publishDepositTxRequest.getMakerAccountId())); - tradingPeer.setMultiSigPubKey(checkNotNull(publishDepositTxRequest.getMakerMultiSigPubKey())); - tradingPeer.setContractAsJson(nonEmptyStringOf(publishDepositTxRequest.getMakerContractAsJson())); - tradingPeer.setContractSignature(nonEmptyStringOf(publishDepositTxRequest.getMakerContractSignature())); - tradingPeer.setPayoutAddressString(nonEmptyStringOf(publishDepositTxRequest.getMakerPayoutAddressString())); - tradingPeer.setRawTransactionInputs(checkNotNull(publishDepositTxRequest.getMakerInputs())); - final byte[] preparedDepositTx = publishDepositTxRequest.getPreparedDepositTx(); - processModel.setPreparedDepositTx(checkNotNull(preparedDepositTx)); - - // Maker has to sign preparedDepositTx. He cannot manipulate the preparedDepositTx - so we avoid to have a - // challenge protocol for passing the nonce we want to get signed. - tradingPeer.setAccountAgeWitnessNonce(publishDepositTxRequest.getPreparedDepositTx()); - tradingPeer.setAccountAgeWitnessSignature(publishDepositTxRequest.getAccountAgeWitnessSignatureOfPreparedDepositTx()); - - tradingPeer.setCurrentDate(publishDepositTxRequest.getCurrentDate()); - - checkArgument(publishDepositTxRequest.getMakerInputs().size() > 0); - - // update to the latest peer address of our peer if the message is correct - trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); - trade.setState(Trade.State.TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST); - - complete(); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java new file mode 100644 index 00000000000..ba1b80b5308 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessesInputsForDepositTxResponse.java @@ -0,0 +1,84 @@ +/* + * 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.trade.protocol.tasks.taker; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.InputsForDepositTxResponse; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.util.Validator.checkTradeId; +import static bisq.core.util.Validator.nonEmptyStringOf; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class TakerProcessesInputsForDepositTxResponse extends TradeTask { + @SuppressWarnings({"unused"}) + public TakerProcessesInputsForDepositTxResponse(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + InputsForDepositTxResponse inputsForDepositTxResponse = (InputsForDepositTxResponse) processModel.getTradeMessage(); + checkTradeId(processModel.getOfferId(), inputsForDepositTxResponse); + checkNotNull(inputsForDepositTxResponse); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + tradingPeer.setPaymentAccountPayload(checkNotNull(inputsForDepositTxResponse.getMakerPaymentAccountPayload())); + tradingPeer.setAccountId(nonEmptyStringOf(inputsForDepositTxResponse.getMakerAccountId())); + tradingPeer.setMultiSigPubKey(checkNotNull(inputsForDepositTxResponse.getMakerMultiSigPubKey())); + tradingPeer.setContractAsJson(nonEmptyStringOf(inputsForDepositTxResponse.getMakerContractAsJson())); + tradingPeer.setContractSignature(nonEmptyStringOf(inputsForDepositTxResponse.getMakerContractSignature())); + tradingPeer.setPayoutAddressString(nonEmptyStringOf(inputsForDepositTxResponse.getMakerPayoutAddressString())); + tradingPeer.setRawTransactionInputs(checkNotNull(inputsForDepositTxResponse.getMakerInputs())); + byte[] preparedDepositTx = inputsForDepositTxResponse.getPreparedDepositTx(); + processModel.setPreparedDepositTx(checkNotNull(preparedDepositTx)); + long lockTime = inputsForDepositTxResponse.getLockTime(); + //todo for dev testing deactivated + //checkArgument(lockTime >= processModel.getBtcWalletService().getBestChainHeight() + 144 * 20); + trade.setLockTime(lockTime); + log.info("lockTime={}, delay={}", lockTime, (processModel.getBtcWalletService().getBestChainHeight() - lockTime)); + + // Maker has to sign preparedDepositTx. He cannot manipulate the preparedDepositTx - so we avoid to have a + // challenge protocol for passing the nonce we want to get signed. + tradingPeer.setAccountAgeWitnessNonce(inputsForDepositTxResponse.getPreparedDepositTx()); + tradingPeer.setAccountAgeWitnessSignature(inputsForDepositTxResponse.getAccountAgeWitnessSignatureOfPreparedDepositTx()); + + tradingPeer.setCurrentDate(inputsForDepositTxResponse.getCurrentDate()); + + checkArgument(inputsForDepositTxResponse.getMakerInputs().size() > 0); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + trade.setState(Trade.State.TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendInputsForDepositTxRequest.java similarity index 87% rename from core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendInputsForDepositTxRequest.java index 58f8591d47d..f9d0651383a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendInputsForDepositTxRequest.java @@ -21,7 +21,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.trade.Trade; -import bisq.core.trade.messages.PayDepositRequest; +import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.user.User; @@ -45,9 +45,9 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class TakerSendPayDepositRequest extends TradeTask { +public class TakerSendInputsForDepositTxRequest extends TradeTask { @SuppressWarnings({"unused"}) - public TakerSendPayDepositRequest(TaskRunner taskHandler, Trade trade) { + public TakerSendInputsForDepositTxRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -61,8 +61,10 @@ protected void run() { checkNotNull(user, "User must not be null"); final List acceptedArbitratorAddresses = user.getAcceptedArbitratorAddresses(); final List acceptedMediatorAddresses = user.getAcceptedMediatorAddresses(); - checkNotNull(acceptedArbitratorAddresses, "acceptedArbitratorAddresses must not be null"); + final List acceptedRefundAgentAddresses = user.getAcceptedRefundAgentAddresses(); + // We don't check for arbitrators as they should vanish soon checkNotNull(acceptedMediatorAddresses, "acceptedMediatorAddresses must not be null"); + // We also don't check for refund agents yet as we don't want to restict us too much. They are not mandatory. BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); @@ -85,7 +87,7 @@ protected void run() { final PaymentAccountPayload paymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade), "processModel.getPaymentAccountPayload(trade) must not be null"); byte[] sig = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), offerId.getBytes(Charsets.UTF_8)); - PayDepositRequest message = new PayDepositRequest( + InputsForDepositTxRequest message = new InputsForDepositTxRequest( offerId, processModel.getMyNodeAddress(), trade.getTradeAmount().value, @@ -102,10 +104,12 @@ protected void run() { paymentAccountPayload, processModel.getAccountId(), trade.getTakerFeeTxId(), - new ArrayList<>(acceptedArbitratorAddresses), + acceptedArbitratorAddresses == null ? new ArrayList<>() : new ArrayList<>(acceptedArbitratorAddresses), new ArrayList<>(acceptedMediatorAddresses), + acceptedRefundAgentAddresses == null ? new ArrayList<>() : new ArrayList<>(acceptedRefundAgentAddresses), trade.getArbitratorNodeAddress(), trade.getMediatorNodeAddress(), + trade.getRefundAgentNodeAddress(), UUID.randomUUID().toString(), Version.getP2PMessageVersion(), sig, 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 879a9ac6e9b..fb4ddc662ef 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 @@ -56,8 +56,8 @@ protected void run() { checkNotNull(trade.getTakerFeeTxId(), "TakeOfferFeeTxId must not be null"); TradingPeer maker = processModel.getTradingPeer(); - PaymentAccountPayload makerPaymentAccountPayload = maker.getPaymentAccountPayload(); - PaymentAccountPayload takerPaymentAccountPayload = processModel.getPaymentAccountPayload(trade); + PaymentAccountPayload makerPaymentAccountPayload = checkNotNull(maker.getPaymentAccountPayload()); + PaymentAccountPayload takerPaymentAccountPayload = checkNotNull(processModel.getPaymentAccountPayload(trade)); boolean isBuyerMakerAndSellerTaker = trade instanceof SellerAsTakerTrade; NodeAddress buyerNodeAddress = isBuyerMakerAndSellerTaker ? processModel.getTempTradingPeerNodeAddress() : processModel.getMyNodeAddress(); @@ -97,7 +97,9 @@ protected void run() { maker.getPayoutAddressString(), takerPayoutAddressString, maker.getMultiSigPubKey(), - takerMultiSigPubKey + takerMultiSigPubKey, + trade.getLockTime(), + trade.getRefundAgentNodeAddress() ); String contractAsJson = Utilities.objectToJson(contract); log.trace("Contract as json:{}", contractAsJson); 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 1210de64c86..2cd22ad92b4 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -63,6 +63,7 @@ public final class TradeStatistics2 implements LazyProcessedPayload, PersistableNetworkPayload, PersistableEnvelope { public static final String ARBITRATOR_ADDRESS = "arbAddr"; public static final String MEDIATOR_ADDRESS = "medAddr"; + public static final String REFUND_AGENT_ADDRESS = "refAddr"; private final OfferPayload.Direction direction; private final String baseCurrency; diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 4d575029d24..6109da9d71e 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -394,6 +394,11 @@ public void setTacAccepted(boolean tacAccepted) { persist(); } + public void setTacAcceptedV120(boolean tacAccepted) { + prefPayload.setTacAcceptedV120(tacAccepted); + persist(); + } + private void persist() { if (initialReadDone) storage.queueUpForSave(prefPayload); @@ -951,5 +956,7 @@ private interface ExcludesDelegateMethods { String getRpcPw(); int getBlockNotifyPort(); + + void setTacAcceptedV120(boolean tacAccepted); } } diff --git a/core/src/main/java/bisq/core/user/PreferencesPayload.java b/core/src/main/java/bisq/core/user/PreferencesPayload.java index 998378b61da..1cddc321787 100644 --- a/core/src/main/java/bisq/core/user/PreferencesPayload.java +++ b/core/src/main/java/bisq/core/user/PreferencesPayload.java @@ -125,6 +125,7 @@ public final class PreferencesPayload implements PersistableEnvelope { private int ignoreDustThreshold = 600; private double buyerSecurityDepositAsPercentForCrypto = getDefaultBuyerSecurityDepositAsPercent(new CryptoCurrencyAccount()); private int blockNotifyPort; + private boolean tacAcceptedV120; /////////////////////////////////////////////////////////////////////////////////////////// @@ -185,7 +186,8 @@ public Message toProtoMessage() { .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) .setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto) - .setBlockNotifyPort(blockNotifyPort); + .setBlockNotifyPort(blockNotifyPort) + .setTacAcceptedV120(tacAcceptedV120); Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory); Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage())); Optional.ofNullable(offerBookChartScreenCurrencyCode).ifPresent(builder::setOfferBookChartScreenCurrencyCode); @@ -271,7 +273,8 @@ public static PersistableEnvelope fromProto(protobuf.PreferencesPayload proto, C proto.getBuyerSecurityDepositAsPercent(), proto.getIgnoreDustThreshold(), proto.getBuyerSecurityDepositAsPercentForCrypto(), - proto.getBlockNotifyPort()); + proto.getBlockNotifyPort(), + proto.getTacAcceptedV120()); } } diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index d197a802c98..75891395d22 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -26,6 +26,7 @@ import bisq.core.payment.PaymentAccount; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.network.p2p.NodeAddress; @@ -44,7 +45,6 @@ import javafx.collections.ObservableSet; import javafx.collections.SetChangeListener; -import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -126,13 +126,6 @@ public void persist() { // API /////////////////////////////////////////////////////////////////////////////////////////// - /* public Optional getPaymentAccountForCurrency(TradeCurrency tradeCurrency) { - return getPaymentAccounts().stream() - .flatMap(e -> e.getTradeCurrencies().stream()) - .filter(e -> e.equals(tradeCurrency)) - .findFirst(); - }*/ - @Nullable public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); @@ -159,6 +152,19 @@ public Mediator getAcceptedMediatorByAddress(NodeAddress nodeAddress) { } } + @Nullable + public RefundAgent getAcceptedRefundAgentByAddress(NodeAddress nodeAddress) { + final List acceptedRefundAgents = userPayload.getAcceptedRefundAgents(); + if (acceptedRefundAgents != null) { + Optional refundAgentOptional = acceptedRefundAgents.stream() + .filter(e -> e.getNodeAddress().equals(nodeAddress)) + .findFirst(); + return refundAgentOptional.orElse(null); + } else { + return null; + } + } + @Nullable public PaymentAccount findFirstPaymentAccountWithCurrency(TradeCurrency tradeCurrency) { if (userPayload.getPaymentAccounts() != null) { @@ -174,21 +180,6 @@ public PaymentAccount findFirstPaymentAccountWithCurrency(TradeCurrency tradeCur } } - public boolean hasMatchingLanguage(Arbitrator arbitrator) { - final List codes = userPayload.getAcceptedLanguageLocaleCodes(); - if (arbitrator != null && codes != null) { - for (String acceptedCode : codes) { - for (String itemCode : arbitrator.getLanguageCodes()) { - if (acceptedCode.equals(itemCode)) - return true; - } - } - return false; - } else { - return false; - } - } - public boolean hasPaymentAccountForCurrency(TradeCurrency tradeCurrency) { return findFirstPaymentAccountWithCurrency(tradeCurrency) != null; } @@ -222,10 +213,10 @@ public void removePaymentAccount(PaymentAccount paymentAccount) { persist(); } - public boolean addAcceptedLanguageLocale(String localeCode) { - final List codes = userPayload.getAcceptedLanguageLocaleCodes(); - if (codes != null && !codes.contains(localeCode)) { - boolean changed = codes.add(localeCode); + public boolean addAcceptedArbitrator(Arbitrator arbitrator) { + final List arbitrators = userPayload.getAcceptedArbitrators(); + if (arbitrators != null && !arbitrators.contains(arbitrator) && !isMyOwnRegisteredArbitrator(arbitrator)) { + boolean changed = arbitrators.add(arbitrator); if (changed) persist(); return changed; @@ -234,23 +225,18 @@ public boolean addAcceptedLanguageLocale(String localeCode) { } } - public boolean removeAcceptedLanguageLocale(String languageLocaleCode) { - boolean changed = userPayload.getAcceptedLanguageLocaleCodes() != null && - userPayload.getAcceptedLanguageLocaleCodes().remove(languageLocaleCode); - if (changed) - persist(); - return changed; - } - - public boolean addAcceptedArbitrator(Arbitrator arbitrator) { - final List arbitrators = userPayload.getAcceptedArbitrators(); - if (arbitrators != null && !arbitrators.contains(arbitrator) && !isMyOwnRegisteredArbitrator(arbitrator)) { - boolean changed = arbitrators.add(arbitrator); + public void removeAcceptedArbitrator(Arbitrator arbitrator) { + if (userPayload.getAcceptedArbitrators() != null) { + boolean changed = userPayload.getAcceptedArbitrators().remove(arbitrator); if (changed) persist(); - return changed; - } else { - return false; + } + } + + public void clearAcceptedArbitrators() { + if (userPayload.getAcceptedArbitrators() != null) { + userPayload.getAcceptedArbitrators().clear(); + persist(); } } @@ -266,33 +252,44 @@ public boolean addAcceptedMediator(Mediator mediator) { } } - - public void removeAcceptedArbitrator(Arbitrator arbitrator) { - if (userPayload.getAcceptedArbitrators() != null) { - boolean changed = userPayload.getAcceptedArbitrators().remove(arbitrator); + public void removeAcceptedMediator(Mediator mediator) { + if (userPayload.getAcceptedMediators() != null) { + boolean changed = userPayload.getAcceptedMediators().remove(mediator); if (changed) persist(); } } - public void clearAcceptedArbitrators() { - if (userPayload.getAcceptedArbitrators() != null) { - userPayload.getAcceptedArbitrators().clear(); + public void clearAcceptedMediators() { + if (userPayload.getAcceptedMediators() != null) { + userPayload.getAcceptedMediators().clear(); persist(); } } - public void removeAcceptedMediator(Mediator mediator) { - if (userPayload.getAcceptedMediators() != null) { - boolean changed = userPayload.getAcceptedMediators().remove(mediator); + public boolean addAcceptedRefundAgent(RefundAgent refundAgent) { + final List refundAgents = userPayload.getAcceptedRefundAgents(); + if (refundAgents != null && !refundAgents.contains(refundAgent) && !isMyOwnRegisteredRefundAgent(refundAgent)) { + boolean changed = refundAgents.add(refundAgent); if (changed) persist(); + return changed; + } else { + return false; } } - public void clearAcceptedMediators() { - if (userPayload.getAcceptedMediators() != null) { - userPayload.getAcceptedMediators().clear(); + public void removeAcceptedRefundAgent(RefundAgent refundAgent) { + if (userPayload.getAcceptedRefundAgents() != null) { + boolean changed = userPayload.getAcceptedRefundAgents().remove(refundAgent); + if (changed) + persist(); + } + } + + public void clearAcceptedRefundAgents() { + if (userPayload.getAcceptedRefundAgents() != null) { + userPayload.getAcceptedRefundAgents().clear(); persist(); } } @@ -317,6 +314,11 @@ public void setRegisteredMediator(@Nullable Mediator mediator) { persist(); } + public void setRegisteredRefundAgent(@Nullable RefundAgent refundAgent) { + userPayload.setRegisteredRefundAgent(refundAgent); + persist(); + } + public void setDevelopersFilter(@Nullable Filter developersFilter) { userPayload.setDevelopersFilter(developersFilter); persist(); @@ -385,6 +387,12 @@ public ObservableSet getPaymentAccountsAsObservable() { return paymentAccountsAsObservable; } + + /** + * If this user is an arbitrator it returns the registered arbitrator. + * + * @return The arbitrator registered for this user + */ @Nullable public Arbitrator getRegisteredArbitrator() { return userPayload.getRegisteredArbitrator(); @@ -395,20 +403,41 @@ public Mediator getRegisteredMediator() { return userPayload.getRegisteredMediator(); } + @Nullable + public RefundAgent getRegisteredRefundAgent() { + return userPayload.getRegisteredRefundAgent(); + } + + //TODO @Nullable public List getAcceptedArbitrators() { return userPayload.getAcceptedArbitrators(); } + @Nullable + public List getAcceptedMediators() { + return userPayload.getAcceptedMediators(); + } + + @Nullable + public List getAcceptedRefundAgents() { + return userPayload.getAcceptedRefundAgents(); + } + @Nullable public List getAcceptedArbitratorAddresses() { return userPayload.getAcceptedArbitrators() != null ? userPayload.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList()) : null; } @Nullable - public List getAcceptedMediators() { - return userPayload.getAcceptedMediators(); + public List getAcceptedMediatorAddresses() { + return userPayload.getAcceptedMediators() != null ? userPayload.getAcceptedMediators().stream().map(Mediator::getNodeAddress).collect(Collectors.toList()) : null; + } + + @Nullable + public List getAcceptedRefundAgentAddresses() { + return userPayload.getAcceptedRefundAgents() != null ? userPayload.getAcceptedRefundAgents().stream().map(RefundAgent::getNodeAddress).collect(Collectors.toList()) : null; } public boolean hasAcceptedArbitrators() { @@ -419,13 +448,8 @@ public boolean hasAcceptedMediators() { return getAcceptedMediators() != null && !getAcceptedMediators().isEmpty(); } - @Nullable - public List getAcceptedMediatorAddresses() { - return userPayload.getAcceptedMediators() != null ? userPayload.getAcceptedMediators().stream().map(Mediator::getNodeAddress).collect(Collectors.toList()) : null; - } - - public List getAcceptedLanguageLocaleCodes() { - return userPayload.getAcceptedLanguageLocaleCodes() != null ? userPayload.getAcceptedLanguageLocaleCodes() : new ArrayList<>(); + public boolean hasAcceptedRefundAgents() { + return getAcceptedRefundAgents() != null && !getAcceptedRefundAgents().isEmpty(); } @Nullable @@ -451,6 +475,10 @@ public boolean isMyOwnRegisteredMediator(Mediator mediator) { return mediator.equals(userPayload.getRegisteredMediator()); } + public boolean isMyOwnRegisteredRefundAgent(RefundAgent refundAgent) { + return refundAgent.equals(userPayload.getRegisteredRefundAgent()); + } + public List getMarketAlertFilters() { return userPayload.getMarketAlertFilters(); } diff --git a/core/src/main/java/bisq/core/user/UserPayload.java b/core/src/main/java/bisq/core/user/UserPayload.java index 878a711df90..1edd8f37da3 100644 --- a/core/src/main/java/bisq/core/user/UserPayload.java +++ b/core/src/main/java/bisq/core/user/UserPayload.java @@ -25,6 +25,7 @@ import bisq.core.proto.CoreProtoResolver; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.common.proto.ProtoUtil; import bisq.common.proto.persistable.PersistableEnvelope; @@ -73,6 +74,12 @@ public class UserPayload implements PersistableEnvelope { @Nullable private List marketAlertFilters = new ArrayList<>(); + // Added v1.2.0 + @Nullable + private RefundAgent registeredRefundAgent; + @Nullable + private List acceptedRefundAgents = new ArrayList<>(); + public UserPayload() { } @@ -105,6 +112,12 @@ public protobuf.PersistableEnvelope toProtoMessage() { Optional.ofNullable(priceAlertFilter).ifPresent(priceAlertFilter -> builder.setPriceAlertFilter(priceAlertFilter.toProtoMessage())); Optional.ofNullable(marketAlertFilters) .ifPresent(e -> builder.addAllMarketAlertFilters(ProtoUtil.collectionToProto(marketAlertFilters))); + + Optional.ofNullable(registeredRefundAgent) + .ifPresent(registeredRefundAgent -> builder.setRegisteredRefundAgent(registeredRefundAgent.toProtoMessage().getRefundAgent())); + Optional.ofNullable(acceptedRefundAgents) + .ifPresent(e -> builder.addAllAcceptedRefundAgents(ProtoUtil.collectionToProto(acceptedRefundAgents, + message -> ((protobuf.StoragePayload) message).getRefundAgent()))); return protobuf.PersistableEnvelope.newBuilder().setUserPayload(builder).build(); } @@ -130,6 +143,11 @@ public static UserPayload fromProto(protobuf.UserPayload proto, CoreProtoResolve PriceAlertFilter.fromProto(proto.getPriceAlertFilter()), proto.getMarketAlertFiltersList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getMarketAlertFiltersList().stream() .map(e -> MarketAlertFilter.fromProto(e, coreProtoResolver)) - .collect(Collectors.toSet()))); + .collect(Collectors.toSet())), + proto.hasRegisteredRefundAgent() ? RefundAgent.fromProto(proto.getRegisteredRefundAgent()) : null, + proto.getAcceptedRefundAgentsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedRefundAgentsList().stream() + .map(RefundAgent::fromProto) + .collect(Collectors.toList())) + ); } } diff --git a/core/src/main/java/bisq/core/util/BSFormatter.java b/core/src/main/java/bisq/core/util/BSFormatter.java index 98ed6f0d5ab..00eb5e23a56 100644 --- a/core/src/main/java/bisq/core/util/BSFormatter.java +++ b/core/src/main/java/bisq/core/util/BSFormatter.java @@ -43,6 +43,7 @@ import java.text.DateFormat; import java.text.DecimalFormat; +import java.text.SimpleDateFormat; import java.math.BigDecimal; @@ -356,6 +357,13 @@ public static String formatDateTime(Date date, DateFormat dateFormatter, DateFor } } + public static String getDateFromBlockHeight(long blockHeight) { + long now = new Date().getTime(); + SimpleDateFormat dateFormatter = new SimpleDateFormat("dd MMM", Locale.getDefault()); + SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm", Locale.getDefault()); + return BSFormatter.formatDateTime(new Date(now + blockHeight * 10 * 60 * 1000L), dateFormatter, timeFormatter); + } + public static String formatToPercentWithSymbol(double value) { return formatToPercent(value) + "%"; } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 4fe68119f48..bb724a2e47f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -210,6 +210,7 @@ shared.selectedArbitrator=Selected arbitrator shared.selectedMediator=Selected mediator shared.mediator=Mediator shared.arbitrator2=Arbitrator +shared.refundAgent=Refund agent #################################################################### # UI views @@ -329,6 +330,11 @@ offerbook.offerersAcceptedBankSeats=Accepted seat of bank countries (taker):\n { offerbook.availableOffers=Available offers offerbook.filterByCurrency=Filter by currency offerbook.filterByPaymentMethod=Filter by payment method +offerbook.timeSinceSigning=Time since signing +offerbook.timeSinceSigning.help=By trading with a payment account that was verified by an arbitrator or a peer, your account gets signed as well.\n\ + 30 days later the initial limit of 0.01 BTC gets lifted and after 90 days your account can sign other peers as well. +offerbook.timeSinceSigning.notSigned=Not signed yet +shared.notSigned=This account hasn't been signed yet. offerbook.nrOffers=No. of offers: {0} offerbook.volume={0} (min - max) @@ -364,6 +370,8 @@ offerbook.warning.sellOfferAndAnyTakerPaymentAccountForOfferMature=This offer ca - The minimum trade amount is above 0.01 BTC\n\ - The payment method for this offer is considered risky for bank chargebacks\n\n{0} +offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions + offerbook.warning.newVersionAnnouncement=We needed to deploy this restriction as a short-term measure for enhanced security.\n\n\ The next software release will provide more robust protection tools so that offers with this risk profile can be traded again. @@ -705,9 +713,11 @@ portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} portfolio.pending.step3_seller.onPaymentReceived.fiat=The trade ID (\"reason for payment\" text) of the transaction is: \"{0}\"\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.name=Please also verify that the sender's name in your bank statement matches that one from the trade contract:\nSender's name: {0}\n\nIf the name is not the same as the one displayed here, please don't confirm but open a dispute by entering \"alt + o\" or \"option + o\".\n\n -portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded. +portfolio.pending.step3_seller.onPaymentReceived.note=Please note, that as soon you have confirmed the receipt, the locked trade amount will be released to the BTC buyer and the security deposit will be refunded.\n\n portfolio.pending.step3_seller.onPaymentReceived.confirm.headline=Confirm that you have received the payment portfolio.pending.step3_seller.onPaymentReceived.confirm.yes=Yes, I have received the payment +portfolio.pending.step3_seller.onPaymentReceived.signer=By confirming receipt of payment you verify that the \ + counterparty has acted according to the trade protocol. portfolio.pending.step5_buyer.groupTitle=Summary of completed trade portfolio.pending.step5_buyer.tradeFee=Trade fee @@ -716,6 +726,8 @@ portfolio.pending.step5_buyer.takersMiningFee=Total mining fees portfolio.pending.step5_buyer.refunded=Refunded security deposit portfolio.pending.step5_buyer.withdrawBTC=Withdraw your bitcoin portfolio.pending.step5_buyer.amount=Amount to withdraw +portfolio.pending.step5_buyer.signer=By withdrawing your bitcoins you verify that the \ + counterparty has acted according to the trade protocol. portfolio.pending.step5_buyer.withdrawToAddress=Withdraw to address portfolio.pending.step5_buyer.moveToBisqWallet=Move funds to Bisq wallet portfolio.pending.step5_buyer.withdrawExternal=Withdraw to external wallet @@ -754,6 +766,8 @@ portfolio.pending.openSupportTicket.msg=Please use this function only in emergen \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and \ handled by a mediator or arbitrator. +portfolio.pending.timeLockNotOver=You have to wait until ≈{0} ({1} more blocks) before you can open an arbitration dispute. + portfolio.pending.notification=Notification portfolio.pending.support.headline.getHelp=Need help? @@ -767,15 +781,12 @@ portfolio.pending.support.button.getHelp=Get support portfolio.pending.support.popup.info=If your issue with the trade remains unsolved, you can open a support \ ticket to request help from a mediator. If you have not received the payment, please wait until the trade period is over.\n\n\ Are you sure you want to open a support ticket? -portfolio.pending.support.popup.info.arbitrator=If your issue with the trade remains unsolved, you can open a support \ - ticket to request help from an arbitrator. If you have not received the payment, please wait until the trade period is over.\n\n\ - Are you sure you want to open a support ticket? portfolio.pending.support.popup.button=Open support ticket portfolio.pending.support.headline.halfPeriodOver=Check payment portfolio.pending.support.headline.periodOver=Trade period is over -portfolio.pending.arbitrationRequested=Arbitration requested portfolio.pending.mediationRequested=Mediation requested +portfolio.pending.refundRequested=Refund requested portfolio.pending.openSupport=Open support ticket portfolio.pending.supportTicketOpened=Support ticket opened portfolio.pending.requestSupport=Request support @@ -806,10 +817,14 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll You can accept or reject this suggested payout.\n\n\ By accepting it, you sign the proposed payout transaction. \ If your trade peer also accepts and signs, the payout will be completed, and the trade is closed.\n\n\ - If one or both parties reject the suggestion, a dispute with an arbitrator will be opened. \ - The arbitrator will investigate the case again and do a payout based on their findings.\n\n\ - Please note that arbitrators are not always online and may take longer to respond than mediators. \ - It can take up to 5 business days for them to respond to messages. + If one or both parties reject the suggestion, they have to wait until {2} (block {3}) and can afterwards open a \ + second dispute round with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\ + If the arbitrator comes to the same conclusion for the payout as the mediator the trader who opened the arbitration \ + request will lose part of their payout to cover the costs for the arbitrator's effort. Finding a consensus with the \ + other trader about the mediator's suggestion is the preferred model and requesting arbitration should be only used if \ + the other peer is not reacting or if a trader is convinced that the mediator did not made a fair payout suggestion.\n\n\ + Please read about the details about the new arbitration model at:\n\ + https://docs.bisq.network/trading-rules.html#arbitration portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.closed.completed=Completed @@ -878,6 +893,9 @@ funds.tx.multiSigDeposit=Multisig deposit: {0} funds.tx.multiSigPayout=Multisig payout: {0} funds.tx.disputePayout=Dispute payout: {0} funds.tx.disputeLost=Lost dispute case: {0} +funds.tx.collateralForRefund=Collateral for refund: {0} +funds.tx.timeLockedPayoutTx=Time locked payout tx: {0} +funds.tx.refund=Refund from arbitration: {0} funds.tx.unknown=Unknown reason: {0} funds.tx.noFundsFromDispute=No refund from dispute funds.tx.receivedFunds=Received funds @@ -905,6 +923,7 @@ funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount support.tab.mediation.support=Mediation support.tab.arbitration.support=Arbitration +support.tab.refund.support=Refund support.tab.ArbitratorsSupportTickets={0}'s tickets support.filter=Filter list support.filter.prompt=Enter trade ID, date, onion address or account data @@ -973,6 +992,8 @@ support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nBisq version support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nBisq version: {1} support.peerOpenedTicket=Your trading peer has requested support due to technical problems.\n\n{0} support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0} +support.mediatorsDisputeSummary=System message:\nMediator''s dispute summary:\n{0} +support.mediatorsAddress=Mediator''s node address: {0} #################################################################### @@ -1104,6 +1125,7 @@ setting.about.subsystems.val=Network version: {0}; P2P message version: {1}; Loc account.tab.arbitratorRegistration=Arbitrator registration account.tab.mediatorRegistration=Mediator registration +account.tab.refundAgentRegistration=Refund agent registration account.tab.account=Account account.info.headline=Welcome to your Bisq Account account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\n\ @@ -2206,6 +2228,14 @@ Summary notes:\n{3} disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\ Open ongoing trade and accept or reject the suggested mediation disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! +disputeSummaryWindow.close.txDetails.headline=Publish refund transaction +disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n +disputeSummaryWindow.close.txDetails=Spending: {0}\n\ + {1}{2}\ + Transaction fee: {3} ({4} satoshis/byte)\n\ + Transaction size: {5} Kb\n\n\ + Are you sure you want to publish that transaction? emptyWalletWindow.headline={0} emergency wallet tool emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ @@ -2232,6 +2262,7 @@ filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) filterWindow.arbitrators=Filtered arbitrators (comma sep. onion addresses) filterWindow.mediators=Filtered mediators (comma sep. onion addresses) +filterWindow.refundAgents=Filtered refund agents (comma sep. onion addresses) filterWindow.seedNode=Filtered seed nodes (comma sep. onion addresses) filterWindow.priceRelayNode=Filtered price relay nodes (comma sep. onion addresses) filterWindow.btcNode=Filtered Bitcoin nodes (comma sep. addresses + port) @@ -2483,6 +2514,22 @@ popup.dao.launch.cheaperFees.title=Cheaper fees # suppress inspection "TrailingSpacesInProperty" popup.dao.launch.cheaperFees=Get a 90% discount on trading fees when you use BSQ. Save money and support the project at the same time!\n\n +popup.accountSigning.selectAccounts.headline=Select payment accounts +popup.accountSigning.selectAccounts.description=Based on the payment method and point of time all payment accounts that are connected to a dispute where a payout to the buyer occurred will be selected for you to sign. +popup.accountSigning.selectAccounts.datePicker=Select point of time until which accounts will be signed + +popup.accountSigning.confirmSelectedAccounts.headline=Confirm selected payment accounts +popup.accountSigning.confirmSelectedAccounts.description=Based on your input, {0} payment accounts will be selected. +popup.accountSigning.confirmSelectedAccounts.button=Confirm payment accounts +popup.accountSigning.signAccounts.headline=Confirm signing of payment accounts +popup.accountSigning.signAccounts.description=Based on your selection, {0} payment accounts will be signed. +popup.accountSigning.signAccounts.button=Sign payment accounts +popup.accountSigning.signAccounts.ECKey=Enter private arbitrator key +popup.accountSigning.signAccounts.ECKey.error=Bad arbitrator ECKey + +popup.accountSigning.success.headline=Congratulations +popup.accountSigning.success.description=All {0} payment accounts were successfully signed! + #################################################################### # Notifications #################################################################### @@ -2749,6 +2796,7 @@ payment.accepted.banks=Accepted banks (ID) payment.mobile=Mobile no. payment.postal.address=Postal address payment.national.account.id.AR=CBU number +shared.accountSigningState=Account signing status #new payment.altcoin.address.dyn={0} address @@ -2757,7 +2805,7 @@ payment.accountNr=Account number payment.emailOrMobile=Email or mobile nr payment.useCustomAccountName=Use custom account name payment.maxPeriod=Max. allowed trade period -payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. trade limit: {1} / Account age: {2} +payment.maxPeriodAndLimit=Max. trade duration: {0} / Max. buy: {1} / Max. sell: {2} / Account age: {3} payment.maxPeriodAndLimitCrypto=Max. trade duration: {0} / Max. trade limit: {1} payment.currencyWithSymbol=Currency: {0} payment.nameOfAcceptedBank=Name of accepted bank diff --git a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java index 941a8996c3c..67bafddde1e 100644 --- a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java @@ -70,6 +70,10 @@ public class SignedWitnessServiceTest { private long tradeAmount1; private long tradeAmount2; private long tradeAmount3; + private long SIGN_AGE_1 = SignedWitnessService.SIGNER_AGE_DAYS * 3 + 5; + private long SIGN_AGE_2 = SignedWitnessService.SIGNER_AGE_DAYS * 2 + 4; + private long SIGN_AGE_3 = SignedWitnessService.SIGNER_AGE_DAYS + 3; + @Before public void setup() throws Exception { @@ -77,13 +81,13 @@ public void setup() throws Exception { ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); ArbitrationManager arbitrationManager = mock(ArbitrationManager.class); when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); - signedWitnessService = new SignedWitnessService(null, null, null, arbitratorManager, null, appendOnlyDataStoreService, arbitrationManager, null); + signedWitnessService = new SignedWitnessService(null, null, arbitratorManager, null, appendOnlyDataStoreService); account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); - long account1CreationTime = getTodayMinusNDays(96); - long account2CreationTime = getTodayMinusNDays(66); - long account3CreationTime = getTodayMinusNDays(36); + long account1CreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); + long account2CreationTime = getTodayMinusNDays(SIGN_AGE_2 + 1); + long account3CreationTime = getTodayMinusNDays(SIGN_AGE_3 + 1); aew1 = new AccountAgeWitness(account1DataHash, account1CreationTime); aew2 = new AccountAgeWitness(account2DataHash, account2CreationTime); aew3 = new AccountAgeWitness(account3DataHash, account3CreationTime); @@ -94,9 +98,9 @@ public void setup() throws Exception { signature1 = arbitrator1Key.signMessage(Utilities.encodeToHex(account1DataHash)).getBytes(Charsets.UTF_8); signature2 = Sig.sign(peer1KeyPair.getPrivate(), Utilities.encodeToHex(account2DataHash).getBytes(Charsets.UTF_8)); signature3 = Sig.sign(peer2KeyPair.getPrivate(), Utilities.encodeToHex(account3DataHash).getBytes(Charsets.UTF_8)); - date1 = getTodayMinusNDays(95); - date2 = getTodayMinusNDays(64); - date3 = getTodayMinusNDays(33); + date1 = getTodayMinusNDays(SIGN_AGE_1); + date2 = getTodayMinusNDays(SIGN_AGE_2); + date3 = getTodayMinusNDays(SIGN_AGE_3); signer1PubKey = arbitrator1Key.getPubKey(); signer2PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); signer3PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); @@ -159,7 +163,7 @@ public void testIsValidAccountAgeWitnessPeerSignatureProblem() { @Test public void testIsValidAccountAgeWitnessDateTooSoonProblem() { - date3 = getTodayMinusNDays(63); + date3 = getTodayMinusNDays(SIGN_AGE_2 - 1); SignedWitness sw1 = new SignedWitness(true, account1DataHash, signature1, signer1PubKey, witnessOwner1PubKey, date1, tradeAmount1); SignedWitness sw2 = new SignedWitness(false, account2DataHash, signature2, signer2PubKey, witnessOwner2PubKey, date2, tradeAmount2); @@ -197,9 +201,9 @@ public void testIsValidAccountAgeWitnessEndlessLoop() throws Exception { byte[] account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); byte[] account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); byte[] account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); - long account1CreationTime = getTodayMinusNDays(96); - long account2CreationTime = getTodayMinusNDays(66); - long account3CreationTime = getTodayMinusNDays(36); + long account1CreationTime = getTodayMinusNDays(SIGN_AGE_1 + 1); + long account2CreationTime = getTodayMinusNDays(SIGN_AGE_2 + 1); + long account3CreationTime = getTodayMinusNDays(SIGN_AGE_3 + 1); AccountAgeWitness aew1 = new AccountAgeWitness(account1DataHash, account1CreationTime); AccountAgeWitness aew2 = new AccountAgeWitness(account2DataHash, account2CreationTime); AccountAgeWitness aew3 = new AccountAgeWitness(account3DataHash, account3CreationTime); @@ -223,9 +227,9 @@ public void testIsValidAccountAgeWitnessEndlessLoop() throws Exception { byte[] witnessOwner1PubKey = Sig.getPublicKeyBytes(peer1KeyPair.getPublic()); byte[] witnessOwner2PubKey = Sig.getPublicKeyBytes(peer2KeyPair.getPublic()); byte[] witnessOwner3PubKey = Sig.getPublicKeyBytes(peer3KeyPair.getPublic()); - long date1 = getTodayMinusNDays(95); - long date2 = getTodayMinusNDays(64); - long date3 = getTodayMinusNDays(33); + long date1 = getTodayMinusNDays(SIGN_AGE_1); + long date2 = getTodayMinusNDays(SIGN_AGE_2); + long date3 = getTodayMinusNDays(SIGN_AGE_3); long tradeAmount1 = 1000; long tradeAmount2 = 1001; @@ -251,7 +255,7 @@ public void testIsValidAccountAgeWitnessLongLoop() throws Exception { int iterations = 1002; for (int i = 0; i < iterations; i++) { byte[] accountDataHash = org.bitcoinj.core.Utils.sha256hash160(String.valueOf(i).getBytes(Charsets.UTF_8)); - long accountCreationTime = getTodayMinusNDays((iterations - i) * (SignedWitnessService.CHARGEBACK_SAFETY_DAYS + 1)); + long accountCreationTime = getTodayMinusNDays((iterations - i) * (SignedWitnessService.SIGNER_AGE_DAYS + 1)); aew = new AccountAgeWitness(accountDataHash, accountCreationTime); String accountDataHashAsHexString = Utilities.encodeToHex(accountDataHash); byte[] signature; @@ -270,7 +274,7 @@ public void testIsValidAccountAgeWitnessLongLoop() throws Exception { signerPubKey = Sig.getPublicKeyBytes(signerKeyPair.getPublic()); } byte[] witnessOwnerPubKey = Sig.getPublicKeyBytes(signedKeyPair.getPublic()); - long date = getTodayMinusNDays((iterations - i) * (SignedWitnessService.CHARGEBACK_SAFETY_DAYS + 1)); + long date = getTodayMinusNDays((iterations - i) * (SignedWitnessService.SIGNER_AGE_DAYS + 1)); SignedWitness sw = new SignedWitness(i == 0, accountDataHash, signature, signerPubKey, witnessOwnerPubKey, date, tradeAmount1); signedWitnessService.addToMap(sw); } 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 c5f28113f55..22e7cd16dd6 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -17,6 +17,9 @@ package bisq.core.account.witness; +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.payment.ChargeBackRisk; + import bisq.common.crypto.CryptoException; import bisq.common.crypto.Sig; @@ -38,6 +41,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; // Restricted default Java security policy on Travis does not allow long keys, so test fails. // Using Utilities.removeCryptographyRestrictions(); did not work. @@ -49,7 +53,9 @@ public class AccountAgeWitnessServiceTest { @Before public void setup() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { - service = new AccountAgeWitnessService(null, null, null, null, null); + SignedWitnessService signedWitnessService = mock(SignedWitnessService.class); + ChargeBackRisk chargeBackRisk = mock(ChargeBackRisk.class); + service = new AccountAgeWitnessService(null, null, null, signedWitnessService, chargeBackRisk, null, null); keypair = Sig.generateKeyPair(); publicKey = keypair.getPublic(); } diff --git a/core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java b/core/src/test/java/bisq/core/arbitration/TraderDataItemTest.java similarity index 64% rename from core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java rename to core/src/test/java/bisq/core/arbitration/TraderDataItemTest.java index 61dc33b8348..7909a6fdf98 100644 --- a/core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java +++ b/core/src/test/java/bisq/core/arbitration/TraderDataItemTest.java @@ -1,7 +1,7 @@ package bisq.core.arbitration; import bisq.core.account.witness.AccountAgeWitness; -import bisq.core.support.dispute.arbitration.BuyerDataItem; +import bisq.core.support.dispute.arbitration.TraderDataItem; import bisq.core.payment.payload.PaymentAccountPayload; import org.bitcoinj.core.Coin; @@ -31,10 +31,10 @@ * You should have received a copy of the GNU Affero General Public License * along with Bisq. If not, see . */ -public class BuyerDataItemTest { - private BuyerDataItem buyerDataItem1; - private BuyerDataItem buyerDataItem2; - private BuyerDataItem buyerDataItem3; +public class TraderDataItemTest { + private TraderDataItem traderDataItem1; + private TraderDataItem traderDataItem2; + private TraderDataItem traderDataItem3; private AccountAgeWitness accountAgeWitness1; private AccountAgeWitness accountAgeWitness2; private byte[] hash1 = "1".getBytes(); @@ -44,24 +44,24 @@ public class BuyerDataItemTest { public void setup() { accountAgeWitness1 = new AccountAgeWitness(hash1, 123); accountAgeWitness2 = new AccountAgeWitness(hash2, 124); - buyerDataItem1 = new BuyerDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, Coin.valueOf(546), + traderDataItem1 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, Coin.valueOf(546), mock(PublicKey.class)); - buyerDataItem2 = new BuyerDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, Coin.valueOf(547), + traderDataItem2 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness1, Coin.valueOf(547), mock(PublicKey.class)); - buyerDataItem3 = new BuyerDataItem(mock(PaymentAccountPayload.class), accountAgeWitness2, Coin.valueOf(548), + traderDataItem3 = new TraderDataItem(mock(PaymentAccountPayload.class), accountAgeWitness2, Coin.valueOf(548), mock(PublicKey.class)); } @Test public void testEquals() { - assertEquals(buyerDataItem1, buyerDataItem2); - assertNotEquals(buyerDataItem1, buyerDataItem3); - assertNotEquals(buyerDataItem2, buyerDataItem3); + assertEquals(traderDataItem1, traderDataItem2); + assertNotEquals(traderDataItem1, traderDataItem3); + assertNotEquals(traderDataItem2, traderDataItem3); } @Test public void testHashCode() { - assertEquals(buyerDataItem1.hashCode(), buyerDataItem2.hashCode()); - assertNotEquals(buyerDataItem1.hashCode(), buyerDataItem3.hashCode()); + assertEquals(traderDataItem1.hashCode(), traderDataItem2.hashCode()); + assertNotEquals(traderDataItem1.hashCode(), traderDataItem3.hashCode()); } } diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index 9fcca9a4def..5fead0386be 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -40,7 +40,7 @@ public void testStartEditOfferForActiveOffer() { final OpenOfferManager manager = new OpenOfferManager(null, null, p2PService, null, null, null, offerBookService, null, null, null, - null, null, null, + null, null, null, null, new Storage>(null, null, corruptedDatabaseFilesHandler)); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); @@ -76,7 +76,7 @@ public void testStartEditOfferForDeactivatedOffer() { final OpenOfferManager manager = new OpenOfferManager(null, null, p2PService, null, null, null, offerBookService, null, null, null, - null, null, null, + null, null, null, null, new Storage>(null, null, corruptedDatabaseFilesHandler)); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); @@ -104,7 +104,7 @@ public void testStartEditOfferForOfferThatIsCurrentlyEdited() { final OpenOfferManager manager = new OpenOfferManager(null, null, p2PService, null, null, null, offerBookService, null, null, null, - null, null, null, + null, null, null, null, new Storage>(null, null, corruptedDatabaseFilesHandler)); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); diff --git a/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java b/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java index 8402ebab474..b869045683b 100644 --- a/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java +++ b/core/src/test/java/bisq/core/payment/PaymentAccountsTest.java @@ -22,15 +22,10 @@ import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentAccountPayload; -import com.google.common.collect.Sets; - import java.util.Collections; -import java.util.Set; -import java.util.function.BiFunction; import org.junit.Test; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -46,22 +41,22 @@ public void testGetOldestPaymentAccountForOfferWhenNoValidAccounts() { assertNull(actual); } - @Test - public void testGetOldestPaymentAccountForOffer() { - AccountAgeWitnessService service = mock(AccountAgeWitnessService.class); - - PaymentAccount oldest = createAccountWithAge(service, 3); - Set accounts = Sets.newHashSet( - oldest, - createAccountWithAge(service, 2), - createAccountWithAge(service, 1)); - - BiFunction dummyValidator = (offer, account) -> true; - PaymentAccounts testedEntity = new PaymentAccounts(accounts, service, dummyValidator); - - PaymentAccount actual = testedEntity.getOldestPaymentAccountForOffer(mock(Offer.class)); - assertEquals(oldest, actual); - } +// @Test +// public void testGetOldestPaymentAccountForOffer() { +// AccountAgeWitnessService service = mock(AccountAgeWitnessService.class); +// +// PaymentAccount oldest = createAccountWithAge(service, 3); +// Set accounts = Sets.newHashSet( +// oldest, +// createAccountWithAge(service, 2), +// createAccountWithAge(service, 1)); +// +// BiFunction dummyValidator = (offer, account) -> true; +// PaymentAccounts testedEntity = new PaymentAccounts(accounts, service, dummyValidator); +// +// PaymentAccount actual = testedEntity.getOldestPaymentAccountForOffer(mock(Offer.class)); +// assertEquals(oldest, actual); +// } private static PaymentAccount createAccountWithAge(AccountAgeWitnessService service, long age) { PaymentAccountPayload payload = mock(PaymentAccountPayload.class); diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index 9062f5209cf..96616e5afdf 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -57,6 +57,7 @@ public void testRoundtripFull() { "string", new byte[]{10, 0, 0}, null, + Lists.newArrayList(), Lists.newArrayList())); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 53caec8ed05..a0b23994556 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -2040,3 +2040,12 @@ textfield */ .status-icon { -fx-text-fill: -fx-faint-focus-color; } + +/******************************************************************************************************************** + * * + * Popover * + * * + ********************************************************************************************************************/ +.popover > .content { + -fx-padding: 10; +} diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java index 0a1c12781ec..906a751e2cb 100644 --- a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java @@ -28,8 +28,6 @@ import javafx.scene.control.TableColumn; import javafx.scene.layout.HBox; -import javafx.geometry.Insets; - import java.util.concurrent.TimeUnit; public class AutoTooltipTableColumn extends TableColumn { @@ -66,7 +64,6 @@ public void setTitleWithHelpText(String title, String help) { final Label helpLabel = new Label(help); helpLabel.setMaxWidth(300); helpLabel.setWrapText(true); - helpLabel.setPadding(new Insets(10)); showInfoPopOver(helpLabel); }); helpIcon.setOnMouseExited(e -> { diff --git a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java index fe418e76d1f..2b033827f54 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java @@ -35,25 +35,41 @@ public class InfoAutoTooltipLabel extends AutoTooltipLabel { + public static final int DEFAULT_WIDTH = 300; private Node textIcon; private Boolean hidePopover; private PopOver infoPopover; + private ContentDisplay contentDisplay; public InfoAutoTooltipLabel(String text, GlyphIcons icon, ContentDisplay contentDisplay, String info) { super(text); + this.contentDisplay = contentDisplay; - textIcon = getIcon(icon); - addIcon(contentDisplay, info, 300); + setIcon(icon); + positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH); } public InfoAutoTooltipLabel(String text, AwesomeIcon icon, ContentDisplay contentDisplay, String info, double width) { super(text); + setIcon(icon); + positionAndActivateIcon(contentDisplay, info, width); + } + + public void setIcon(GlyphIcons icon) { + textIcon = getIcon(icon); + } + + public void setIcon(GlyphIcons icon, String info) { + setIcon(icon); + positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH); + } + + public void setIcon(AwesomeIcon icon) { textIcon = getIcon(icon); - addIcon(contentDisplay, info, width); } - private void addIcon(ContentDisplay contentDisplay, String info, double width) { + private void positionAndActivateIcon(ContentDisplay contentDisplay, String info, double width) { textIcon.setOpacity(0.4); textIcon.setOnMouseEntered(e -> { diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java index 54d3725e618..172c0296dd8 100644 --- a/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIcon.java @@ -152,7 +152,7 @@ private PeerInfoIcon(NodeAddress nodeAddress, // outer circle Color ringColor; if (isFiatCurrency) { - switch (accountAgeWitnessService.getAccountAgeCategory(peersAccountAge)) { + switch (accountAgeWitnessService.getPeersAccountAgeCategory(peersAccountAge)) { case TWO_MONTHS_OR_MORE: ringColor = Color.rgb(0, 225, 0); // > 2 months green break; @@ -160,9 +160,12 @@ private PeerInfoIcon(NodeAddress nodeAddress, ringColor = Color.rgb(0, 139, 205); // 1-2 months blue break; case LESS_ONE_MONTH: - default: ringColor = Color.rgb(255, 140, 0); //< 1 month orange break; + case UNVERIFIED: + default: + ringColor = Color.rgb(255, 0, 0); // not signed, red + break; } @@ -240,7 +243,18 @@ private PeerInfoIcon(NodeAddress nodeAddress, getChildren().addAll(outerBackground, innerBackground, avatarImageView, tagPane, numTradesPane); - addMouseListener(numTrades, privateNotificationManager, offer, preferences, formatter, useDevPrivilegeKeys, isFiatCurrency, peersAccountAge); + //TODO sqrrm: We need these states in here: + // - signed by arbitrator + // - signed by peer + // - signed by peer and limit lifted + // - signed by peer and able to sign + // - not signing necessary for this payment account + // - signing required and not signed + // Additionally we need to have some enum or so how the account signing took place. + // e.g. if in the future we'll also offer the "pay with two different accounts"-signing + String accountSigningState = Res.get("shared.notSigned"); + + addMouseListener(numTrades, privateNotificationManager, offer, preferences, formatter, useDevPrivilegeKeys, isFiatCurrency, peersAccountAge, accountSigningState); } private long getPeersAccountAge(@Nullable Trade trade, @Nullable Offer offer) { @@ -251,11 +265,11 @@ private long getPeersAccountAge(@Nullable Trade trade, @Nullable Offer offer) { return -1; } - return accountAgeWitnessService.getTradingPeersAccountAge(trade); + return accountAgeWitnessService.getWitnessSignAge(trade, new Date()); } else { checkNotNull(offer, "Offer must not be null if trade is null."); - return accountAgeWitnessService.getMakersAccountAge(offer, new Date()); + return accountAgeWitnessService.getWitnessSignAge(offer, new Date()); } } @@ -265,17 +279,17 @@ protected void addMouseListener(int numTrades, Preferences preferences, BSFormatter formatter, boolean useDevPrivilegeKeys, - boolean isFiatCurrency, - long makersAccountAge) { + boolean isFiatCurrency, long makersAccountAge, String accountSigningState) { final String accountAgeTagEditor = isFiatCurrency ? makersAccountAge > -1 ? DisplayUtils.formatAccountAge(makersAccountAge) : Res.get("peerInfo.unknownAge") : null; + setOnMouseClicked(e -> new PeerInfoWithTagEditor(privateNotificationManager, offer, preferences, useDevPrivilegeKeys) .fullAddress(fullAddress) .numTrades(numTrades) - .accountAge(accountAgeTagEditor) + .accountAge(accountAgeTagEditor).accountSigningState(accountSigningState) .position(localToScene(new Point2D(0, 0))) .onSave(newTag -> { preferences.setTagForPeer(fullAddress, newTag); diff --git a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java index c07bc5ff328..d72b2568e45 100644 --- a/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java +++ b/desktop/src/main/java/bisq/desktop/components/PeerInfoIconSmall.java @@ -38,8 +38,7 @@ protected void addMouseListener(int numTrades, Offer offer, Preferences preferences, BSFormatter formatter, boolean useDevPrivilegeKeys, - boolean isFiatCurrency, - long makersAccountAge) { + boolean isFiatCurrency, long makersAccountAge, String accountSigningState) { } @Override diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java index 1ac3618a0f2..dd3c0ef8230 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java @@ -32,6 +32,7 @@ import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; import bisq.core.payment.AssetAccount; import bisq.core.payment.PaymentAccount; import bisq.core.util.BSFormatter; @@ -178,15 +179,40 @@ else if (!paymentAccount.getTradeCurrencies().isEmpty()) final String limitationsText = paymentAccount instanceof AssetAccount ? Res.get("payment.maxPeriodAndLimitCrypto", getTimeText(hours), - formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrency.getCode())))) + formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( + paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.BUY)))) : Res.get("payment.maxPeriodAndLimit", getTimeText(hours), - formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrency.getCode()))), + formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( + paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.BUY))), + formatter.formatCoinWithCode(Coin.valueOf(accountAgeWitnessService.getMyTradeLimit( + paymentAccount, tradeCurrency.getCode(), OfferPayload.Direction.SELL))), DisplayUtils.formatAccountAge(accountAge)); - if (isDisplayForm) + if (isDisplayForm) { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.limitations"), limitationsText); + + String accountSigningStateText; + + if (accountAgeWitnessService.myHasSignedWitness(paymentAccount.getPaymentAccountPayload())) { + //TODO sqrrm: We need four states in here: + // - signed by arbitrator + // - signed by peer + // - signed by peer and limit lifted + // - signed by peer and able to sign + // Additionally we need to have some enum or so how the account signing took place. + // e.g. if in the future we'll also offer the "pay with two different accounts"-signing + accountSigningStateText = "This account was verified and signed by an arbitrator or peer / Time since signing: 3 days"; + } else { + //TODO sqrrm: Here we need two states: + // - not signing necessary for this payment account + // - signing required and not signed + accountSigningStateText = Res.get("shared.notSigned"); + } + + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.accountSigningState"), accountSigningStateText); + } else addTopLabelTextField(gridPane, ++gridRow, Res.get("payment.limitations"), limitationsText); diff --git a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java index 0c9dc8c8e22..e13272a1d1c 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java @@ -32,6 +32,7 @@ import bisq.desktop.main.account.content.seedwords.SeedWordsView; import bisq.desktop.main.account.register.arbitrator.ArbitratorRegistrationView; import bisq.desktop.main.account.register.mediator.MediatorRegistrationView; +import bisq.desktop.main.account.register.refundagent.RefundAgentRegistrationView; import bisq.desktop.main.overlays.popups.Popup; import bisq.core.locale.Res; @@ -73,8 +74,10 @@ public class AccountView extends ActivatableView { private Tab selectedTab; private Tab arbitratorRegistrationTab; private Tab mediatorRegistrationTab; + private Tab refundAgentRegistrationTab; private ArbitratorRegistrationView arbitratorRegistrationView; private MediatorRegistrationView mediatorRegistrationView; + private RefundAgentRegistrationView refundAgentRegistrationView; private Scene scene; private EventHandler keyEventEventHandler; private ListChangeListener tabListChangeListener; @@ -103,6 +106,8 @@ public void initialize() { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } else if (mediatorRegistrationTab == null && viewPath.get(2).equals(MediatorRegistrationView.class)) { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else if (refundAgentRegistrationTab == null && viewPath.get(2).equals(RefundAgentRegistrationView.class)) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } else { loadView(viewPath.tip()); } @@ -116,6 +121,9 @@ public void initialize() { if (mediatorRegistrationTab != null) { root.getTabs().remove(mediatorRegistrationTab); } + if (refundAgentRegistrationTab != null) { + root.getTabs().remove(refundAgentRegistrationTab); + } arbitratorRegistrationTab = new Tab(Res.get("account.tab.arbitratorRegistration").toUpperCase()); arbitratorRegistrationTab.setClosable(true); root.getTabs().add(arbitratorRegistrationTab); @@ -124,10 +132,24 @@ public void initialize() { if (arbitratorRegistrationTab != null) { root.getTabs().remove(arbitratorRegistrationTab); } + if (refundAgentRegistrationTab != null) { + root.getTabs().remove(refundAgentRegistrationTab); + } mediatorRegistrationTab = new Tab(Res.get("account.tab.mediatorRegistration").toUpperCase()); mediatorRegistrationTab.setClosable(true); root.getTabs().add(mediatorRegistrationTab); navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.N, event) && refundAgentRegistrationTab == null) { + if (arbitratorRegistrationTab != null) { + root.getTabs().remove(arbitratorRegistrationTab); + } + if (mediatorRegistrationTab != null) { + root.getTabs().remove(mediatorRegistrationTab); + } + refundAgentRegistrationTab = new Tab(Res.get("account.tab.refundAgentRegistration").toUpperCase()); + refundAgentRegistrationTab.setClosable(true); + root.getTabs().add(refundAgentRegistrationTab); + navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); } }; @@ -136,6 +158,8 @@ public void initialize() { navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); } else if (mediatorRegistrationTab != null && selectedTab != mediatorRegistrationTab) { navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); + } else if (refundAgentRegistrationTab != null && selectedTab != refundAgentRegistrationTab) { + navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); } else if (newValue == fiatAccountsTab && selectedTab != fiatAccountsTab) { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } else if (newValue == altcoinAccountsTab && selectedTab != altcoinAccountsTab) { @@ -159,6 +183,9 @@ public void initialize() { if (removedTabs.size() == 1 && removedTabs.get(0).equals(mediatorRegistrationTab)) onMediatorRegistrationTabRemoved(); + + if (removedTabs.size() == 1 && removedTabs.get(0).equals(refundAgentRegistrationTab)) + onRefundAgentRegistrationTabRemoved(); }; } @@ -172,6 +199,11 @@ private void onMediatorRegistrationTabRemoved() { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } + private void onRefundAgentRegistrationTabRemoved() { + refundAgentRegistrationTab = null; + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + @Override protected void activate() { navigation.addListener(navigationListener); @@ -188,6 +220,8 @@ protected void activate() { navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); else if (mediatorRegistrationTab != null) navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); + else if (refundAgentRegistrationTab != null) + navigation.navigateTo(MainView.class, AccountView.class, RefundAgentRegistrationView.class); else if (root.getSelectionModel().getSelectedItem() == fiatAccountsTab) navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); else if (root.getSelectionModel().getSelectedItem() == altcoinAccountsTab) @@ -240,6 +274,12 @@ private void loadView(Class viewClass) { mediatorRegistrationView = (MediatorRegistrationView) view; mediatorRegistrationView.onTabSelection(true); } + } else if (view instanceof RefundAgentRegistrationView) { + if (refundAgentRegistrationTab != null) { + selectedTab = refundAgentRegistrationTab; + refundAgentRegistrationView = (RefundAgentRegistrationView) view; + refundAgentRegistrationView.onTabSelection(true); + } } else if (view instanceof FiatAccountsView) { selectedTab = fiatAccountsTab; } else if (view instanceof AltCoinAccountsView) { diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java index d4a051dd9fc..834229fb560 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java @@ -4,16 +4,22 @@ import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.InfoAutoTooltipLabel; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.ImageUtil; +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Res; import bisq.core.payment.PaymentAccount; import bisq.common.UserThread; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; @@ -31,11 +37,14 @@ public abstract class PaymentAccountsView extends ActivatableViewAndModel { protected ListView paymentAccountsListView; - protected ChangeListener paymentAccountChangeListener; + private ChangeListener paymentAccountChangeListener; protected Button addAccountButton, exportButton, importButton; + SignedWitnessService signedWitnessService; + protected AccountAgeWitnessService accountAgeWitnessService; - public PaymentAccountsView(M model) { + public PaymentAccountsView(M model, AccountAgeWitnessService accountAgeWitnessService) { super(model); + this.accountAgeWitnessService = accountAgeWitnessService; } @Override @@ -84,11 +93,11 @@ protected void onDeleteAccount(PaymentAccount paymentAccount) { } protected void setPaymentAccountsCellFactory() { - paymentAccountsListView.setCellFactory(new Callback, ListCell>() { + paymentAccountsListView.setCellFactory(new Callback<>() { @Override public ListCell call(ListView list) { - return new ListCell() { - final Label label = new AutoTooltipLabel(); + return new ListCell<>() { + final InfoAutoTooltipLabel label = new InfoAutoTooltipLabel("", MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, ContentDisplay.RIGHT, ""); final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); final Button removeButton = new AutoTooltipButton("", icon); final AnchorPane pane = new AnchorPane(label, removeButton); @@ -104,6 +113,22 @@ public void updateItem(final PaymentAccount item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { label.setText(item.getAccountName()); + + if (accountAgeWitnessService.myHasSignedWitness(item.paymentAccountPayload)) { + //TODO sqrrm: We need four states in here: + // - signed by arbitrator + // - signed by peer + // - signed by peer and limit lifted + // - signed by peer and able to sign + // Additionally we need to have some enum or so how the account signing took place. + // e.g. if in the future we'll also offer the "pay with two different accounts"-signing + label.setIcon(MaterialDesignIcon.APPROVAL, "This account was verified and signed by an arbitrator or peer."); + } else { + //TODO sqrrm: Here we need two states: + // - not signing necessary for this payment account + // - signing required and not signed + label.setIcon(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, Res.get("shared.notSigned")); + } removeButton.setOnAction(e -> onDeleteAccount(item)); setGraphic(pane); } else { diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java index ba0048bccdf..ae03f56d923 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java @@ -72,7 +72,6 @@ public class AltCoinAccountsView extends PaymentAccountsView paymentMethodComboBox; private PaymentMethodForm paymentMethodForm; @@ -173,7 +172,7 @@ public FiatAccountsView(FiatAccountsViewModel model, AdvancedCashValidator advancedCashValidator, AccountAgeWitnessService accountAgeWitnessService, BSFormatter formatter) { - super(model); + super(model, accountAgeWitnessService); this.ibanValidator = ibanValidator; this.bicValidator = bicValidator; @@ -195,7 +194,6 @@ public FiatAccountsView(FiatAccountsViewModel model, this.f2FValidator = f2FValidator; this.promptPayValidator = promptPayValidator; this.advancedCashValidator = advancedCashValidator; - this.accountAgeWitnessService = accountAgeWitnessService; this.formatter = formatter; } @@ -343,8 +341,7 @@ protected void addNewAccount() { removeAccountRows(); addAccountButton.setDisable(true); accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE); - paymentMethodComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("shared.paymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); - paymentMethodComboBox.setPromptText(Res.get("shared.selectPaymentMethod")); + paymentMethodComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); paymentMethodComboBox.setVisibleRowCount(11); paymentMethodComboBox.setPrefWidth(250); List list = PaymentMethod.getPaymentMethods().stream() diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml new file mode 100644 index 00000000000..3ca8ce3c3b2 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java new file mode 100644 index 00000000000..af42e6599d0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationView.java @@ -0,0 +1,45 @@ +/* + * 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.desktop.main.account.register.refundagent; + + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.account.register.AgentRegistrationView; + +import bisq.core.app.AppOptionKeys; +import bisq.core.locale.Res; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class RefundAgentRegistrationView extends AgentRegistrationView { + + @Inject + public RefundAgentRegistrationView(RefundAgentRegistrationViewModel model, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(model, useDevPrivilegeKeys); + } + + @Override + protected String getRole() { + return Res.get("shared.refundAgent"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java new file mode 100644 index 00000000000..f4b60f98bbe --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/refundagent/RefundAgentRegistrationViewModel.java @@ -0,0 +1,68 @@ +/* + * 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.desktop.main.account.register.refundagent; + + +import bisq.desktop.main.account.register.AgentRegistrationViewModel; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Date; + +public class RefundAgentRegistrationViewModel extends AgentRegistrationViewModel { + + @Inject + public RefundAgentRegistrationViewModel(RefundAgentManager arbitratorManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + super(arbitratorManager, user, p2PService, walletService, keyRing); + } + + @Override + protected RefundAgent getDisputeAgent(String registrationSignature, + String emailAddress) { + return new RefundAgent( + p2PService.getAddress(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date().getTime(), + registrationKey.getPubKey(), + registrationSignature, + emailAddress, + null, + null + ); + } + + @Override + protected RefundAgent getRegisteredDisputeAgentFromUser() { + return user.getRegisteredRefundAgent(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java index 7bd2b24707c..7f9008b256f 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -29,29 +29,27 @@ import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.PublishTradeStatistics; import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage; +import bisq.core.trade.protocol.tasks.buyer.BuyerSetupDepositTxListener; import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener; +import bisq.core.trade.protocol.tasks.buyer.BuyerSignPayoutTx; import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerCreatesAndSignsDepositTx; -import bisq.core.trade.protocol.tasks.buyer_as_maker.BuyerAsMakerSignPayoutTx; import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerCreatesDepositTxInputs; -import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignAndPublishDepositTx; +import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignsDepositTx; import bisq.core.trade.protocol.tasks.maker.MakerCreateAndSignContract; -import bisq.core.trade.protocol.tasks.maker.MakerProcessDepositTxPublishedMessage; -import bisq.core.trade.protocol.tasks.maker.MakerProcessPayDepositRequest; -import bisq.core.trade.protocol.tasks.maker.MakerSendPublishDepositTxRequest; -import bisq.core.trade.protocol.tasks.maker.MakerSetupDepositTxListener; +import bisq.core.trade.protocol.tasks.maker.MakerProcessesInputsForDepositTxRequest; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerAccount; import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment; import bisq.core.trade.protocol.tasks.seller.SellerBroadcastPayoutTx; import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage; import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.seller.SellerSendsDepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx; -import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesAndSignsDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx; import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs; -import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignAndPublishDepositTx; +import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx; import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerProcessPublishDepositTxRequest; -import bisq.core.trade.protocol.tasks.taker.TakerSendDepositTxPublishedMessage; -import bisq.core.trade.protocol.tasks.taker.TakerSendPayDepositRequest; +import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; +import bisq.core.trade.protocol.tasks.taker.TakerSendInputsForDepositTxRequest; import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerAccount; import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment; @@ -76,6 +74,7 @@ import static bisq.desktop.util.FormBuilder.addTopLabelComboBox; +// Not maintained anymore with new trade protocol, but leave it...If used needs to be adopted to current protocol. @FxmlView public class DebugView extends InitializableView { @@ -105,16 +104,14 @@ public void initialize() { addGroup("BuyerAsMakerProtocol", FXCollections.observableArrayList(Arrays.asList( - MakerProcessPayDepositRequest.class, + MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, MakerVerifyTakerAccount.class, MakerVerifyTakerFeePayment.class, MakerCreateAndSignContract.class, BuyerAsMakerCreatesAndSignsDepositTx.class, - MakerSetupDepositTxListener.class, - MakerSendPublishDepositTxRequest.class, + BuyerSetupDepositTxListener.class, - MakerProcessDepositTxPublishedMessage.class, MakerVerifyTakerAccount.class, MakerVerifyTakerFeePayment.class, PublishTradeStatistics.class, @@ -122,7 +119,7 @@ public void initialize() { ApplyFilter.class, MakerVerifyTakerAccount.class, MakerVerifyTakerFeePayment.class, - BuyerAsMakerSignPayoutTx.class, + BuyerSignPayoutTx.class, BuyerSendCounterCurrencyTransferStartedMessage.class, BuyerSetupPayoutTxListener.class) )); @@ -132,15 +129,15 @@ public void initialize() { TakerVerifyMakerFeePayment.class, CreateTakerFeeTx.class, SellerAsTakerCreatesDepositTxInputs.class, - TakerSendPayDepositRequest.class, + TakerSendInputsForDepositTxRequest.class, - TakerProcessPublishDepositTxRequest.class, + TakerProcessesInputsForDepositTxResponse.class, ApplyFilter.class, TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, TakerVerifyAndSignContract.class, - SellerAsTakerSignAndPublishDepositTx.class, - TakerSendDepositTxPublishedMessage.class, + SellerAsTakerSignsDepositTx.class, + SellerSendsDepositTxAndDelayedPayoutTxMessage.class, SellerProcessCounterCurrencyTransferStartedMessage.class, TakerVerifyMakerAccount.class, @@ -159,35 +156,33 @@ public void initialize() { TakerVerifyMakerFeePayment.class, CreateTakerFeeTx.class, BuyerAsTakerCreatesDepositTxInputs.class, - TakerSendPayDepositRequest.class, + TakerSendInputsForDepositTxRequest.class, - TakerProcessPublishDepositTxRequest.class, + TakerProcessesInputsForDepositTxResponse.class, ApplyFilter.class, TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, TakerVerifyAndSignContract.class, - BuyerAsTakerSignAndPublishDepositTx.class, - TakerSendDepositTxPublishedMessage.class, + BuyerAsTakerSignsDepositTx.class, + SellerSendsDepositTxAndDelayedPayoutTxMessage.class, ApplyFilter.class, TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, - BuyerAsMakerSignPayoutTx.class, + BuyerSignPayoutTx.class, BuyerSendCounterCurrencyTransferStartedMessage.class, BuyerSetupPayoutTxListener.class) )); addGroup("SellerAsMakerProtocol", FXCollections.observableArrayList(Arrays.asList( - MakerProcessPayDepositRequest.class, + MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, MakerVerifyTakerAccount.class, MakerVerifyTakerFeePayment.class, MakerCreateAndSignContract.class, - SellerAsMakerCreatesAndSignsDepositTx.class, - MakerSetupDepositTxListener.class, - MakerSendPublishDepositTxRequest.class, + SellerAsMakerCreatesUnsignedDepositTx.class, + BuyerSetupDepositTxListener.class, - MakerProcessDepositTxPublishedMessage.class, PublishTradeStatistics.class, MakerVerifyTakerAccount.class, MakerVerifyTakerFeePayment.class, diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/DisplayedTransactionsFactory.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/DisplayedTransactionsFactory.java index 4f2e44fc9a4..049b8efe6b4 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/DisplayedTransactionsFactory.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/DisplayedTransactionsFactory.java @@ -30,7 +30,8 @@ public class DisplayedTransactionsFactory { private final TransactionAwareTradableFactory transactionAwareTradableFactory; @Inject - DisplayedTransactionsFactory(BtcWalletService btcWalletService, TradableRepository tradableRepository, + DisplayedTransactionsFactory(BtcWalletService btcWalletService, + TradableRepository tradableRepository, TransactionListItemFactory transactionListItemFactory, TransactionAwareTradableFactory transactionAwareTradableFactory) { this.btcWalletService = btcWalletService; diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java index 691901fc1b1..f81fece8374 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java @@ -38,8 +38,10 @@ public class TradableRepository { private final FailedTradesManager failedTradesManager; @Inject - TradableRepository(OpenOfferManager openOfferManager, TradeManager tradeManager, - ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager) { + TradableRepository(OpenOfferManager openOfferManager, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + FailedTradesManager failedTradesManager) { this.openOfferManager = openOfferManager; this.tradeManager = tradeManager; this.closedTradableManager = closedTradableManager; diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java index 490bbafb52c..86b7699e2e1 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java @@ -17,28 +17,46 @@ package bisq.desktop.main.funds.transactions; -import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.OpenOffer; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; +import bisq.common.crypto.PubKeyRing; + import javax.inject.Inject; import javax.inject.Singleton; + @Singleton public class TransactionAwareTradableFactory { private final ArbitrationManager arbitrationManager; + private final RefundManager refundManager; + private final BtcWalletService btcWalletService; + private final PubKeyRing pubKeyRing; @Inject - TransactionAwareTradableFactory(ArbitrationManager arbitrationManager) { + TransactionAwareTradableFactory(ArbitrationManager arbitrationManager, + RefundManager refundManager, + BtcWalletService btcWalletService, + PubKeyRing pubKeyRing) { this.arbitrationManager = arbitrationManager; + this.refundManager = refundManager; + this.btcWalletService = btcWalletService; + this.pubKeyRing = pubKeyRing; } TransactionAwareTradable create(Tradable delegate) { if (delegate instanceof OpenOffer) { return new TransactionAwareOpenOffer((OpenOffer) delegate); } else if (delegate instanceof Trade) { - return new TransactionAwareTrade((Trade) delegate, arbitrationManager); + return new TransactionAwareTrade((Trade) delegate, + arbitrationManager, + refundManager, + btcWalletService, + pubKeyRing); } else { return new DummyTransactionAwareTradable(delegate); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java index 07c4bd7d801..f37a4057490 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java @@ -17,63 +17,90 @@ package bisq.desktop.main.funds.transactions; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.arbitration.ArbitrationManager; -import bisq.core.offer.Offer; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.trade.Contract; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; +import bisq.common.crypto.PubKeyRing; + +import org.bitcoinj.core.Address; import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; import javafx.collections.ObservableList; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j class TransactionAwareTrade implements TransactionAwareTradable { - private final Trade delegate; + private final Trade trade; private final ArbitrationManager arbitrationManager; - - TransactionAwareTrade(Trade delegate, ArbitrationManager arbitrationManager) { - this.delegate = delegate; + private final RefundManager refundManager; + private final BtcWalletService btcWalletService; + private final PubKeyRing pubKeyRing; + + TransactionAwareTrade(Trade trade, + ArbitrationManager arbitrationManager, + RefundManager refundManager, + BtcWalletService btcWalletService, + PubKeyRing pubKeyRing) { + this.trade = trade; this.arbitrationManager = arbitrationManager; + this.refundManager = refundManager; + this.btcWalletService = btcWalletService; + this.pubKeyRing = pubKeyRing; } @Override public boolean isRelatedToTransaction(Transaction transaction) { String txId = transaction.getHashAsString(); - boolean isTakerOfferFeeTx = txId.equals(delegate.getTakerFeeTxId()); + boolean isTakerOfferFeeTx = txId.equals(trade.getTakerFeeTxId()); boolean isOfferFeeTx = isOfferFeeTx(txId); boolean isDepositTx = isDepositTx(txId); boolean isPayoutTx = isPayoutTx(txId); boolean isDisputedPayoutTx = isDisputedPayoutTx(txId); + boolean isDelayedPayoutTx = isDelayedPayoutTx(txId); + boolean isRefundPayoutTx = isRefundPayoutTx(txId); - return isTakerOfferFeeTx || isOfferFeeTx || isDepositTx || isPayoutTx || isDisputedPayoutTx; + return isTakerOfferFeeTx || isOfferFeeTx || isDepositTx || isPayoutTx || + isDisputedPayoutTx || isDelayedPayoutTx || isRefundPayoutTx; } private boolean isPayoutTx(String txId) { - return Optional.ofNullable(delegate.getPayoutTx()) + return Optional.ofNullable(trade.getPayoutTx()) .map(Transaction::getHashAsString) .map(hash -> hash.equals(txId)) .orElse(false); } private boolean isDepositTx(String txId) { - return Optional.ofNullable(delegate.getDepositTx()) + return Optional.ofNullable(trade.getDepositTx()) .map(Transaction::getHashAsString) .map(hash -> hash.equals(txId)) .orElse(false); } private boolean isOfferFeeTx(String txId) { - return Optional.ofNullable(delegate.getOffer()) + return Optional.ofNullable(trade.getOffer()) .map(Offer::getOfferFeePaymentTxId) .map(paymentTxId -> paymentTxId.equals(txId)) .orElse(false); } private boolean isDisputedPayoutTx(String txId) { - String delegateId = delegate.getId(); + String delegateId = trade.getId(); ObservableList disputes = arbitrationManager.getDisputesAsObservableList(); return disputes.stream() @@ -88,8 +115,67 @@ private boolean isDisputedPayoutTx(String txId) { }); } + boolean isDelayedPayoutTx(String txId) { + Transaction transaction = btcWalletService.getTransaction(txId); + if (transaction == null) + return false; + + if (transaction.getLockTime() == 0) + return false; + + if (transaction.getInputs() == null) + return false; + + return transaction.getInputs().stream() + .anyMatch(input -> { + TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput == null) { + return false; + } + Transaction parentTransaction = connectedOutput.getParentTransaction(); + if (parentTransaction == null) { + return false; + } + return isDepositTx(parentTransaction.getHashAsString()); + }); + } + + private boolean isRefundPayoutTx(String txId) { + String tradeId = trade.getId(); + ObservableList disputes = refundManager.getDisputesAsObservableList(); + AtomicBoolean isRefundTx = new AtomicBoolean(false); + AtomicBoolean isDisputeRelatedToThis = new AtomicBoolean(false); + disputes.forEach(dispute -> { + String disputeTradeId = dispute.getTradeId(); + isDisputeRelatedToThis.set(tradeId.equals(disputeTradeId)); + if (isDisputeRelatedToThis.get()) { + Transaction tx = btcWalletService.getTransaction(txId); + if (tx != null) { + tx.getOutputs().forEach(txo -> { + if (btcWalletService.isTransactionOutputMine(txo)) { + try { + Address receiverAddress = txo.getAddressFromP2PKHScript(btcWalletService.getParams()); + Contract contract = checkNotNull(trade.getContract()); + String myPayoutAddressString = contract.isMyRoleBuyer(pubKeyRing) ? + contract.getBuyerPayoutAddressString() : + contract.getSellerPayoutAddressString(); + if (receiverAddress != null && myPayoutAddressString.equals(receiverAddress.toString())) { + isRefundTx.set(true); + } + } catch (Throwable ignore) { + } + + } + }); + } + } + }); + + return isRefundTx.get() && isDisputeRelatedToThis.get(); + } + @Override public Tradable asTradable() { - return delegate; + return trade; } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java index e95974989d1..63ad288f1c1 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionListItemFactory.java @@ -20,42 +20,51 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.dao.DaoFacade; -import bisq.core.trade.Tradable; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; +import bisq.common.crypto.PubKeyRing; + import org.bitcoinj.core.Transaction; import javax.inject.Inject; import javax.inject.Singleton; -import java.util.Optional; - import javax.annotation.Nullable; + @Singleton public class TransactionListItemFactory { private final BtcWalletService btcWalletService; private final BsqWalletService bsqWalletService; private final DaoFacade daoFacade; + private final PubKeyRing pubKeyRing; private final BSFormatter formatter; private final Preferences preferences; @Inject - TransactionListItemFactory(BtcWalletService btcWalletService, BsqWalletService bsqWalletService, - DaoFacade daoFacade, BSFormatter formatter, Preferences preferences) { + TransactionListItemFactory(BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + DaoFacade daoFacade, + PubKeyRing pubKeyRing, + BSFormatter formatter, + Preferences preferences) { this.btcWalletService = btcWalletService; this.bsqWalletService = bsqWalletService; this.daoFacade = daoFacade; + this.pubKeyRing = pubKeyRing; this.formatter = formatter; this.preferences = preferences; } TransactionsListItem create(Transaction transaction, @Nullable TransactionAwareTradable tradable) { - Optional maybeTradable = Optional.ofNullable(tradable) - .map(TransactionAwareTradable::asTradable); - - return new TransactionsListItem(transaction, btcWalletService, bsqWalletService, maybeTradable, - daoFacade, formatter, preferences.getIgnoreDustThreshold()); + return new TransactionsListItem(transaction, + btcWalletService, + bsqWalletService, + tradable, + daoFacade, + pubKeyRing, + formatter, + preferences.getIgnoreDustThreshold()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java index 8809542976f..09e6b349be3 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java @@ -30,10 +30,13 @@ import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; +import bisq.core.trade.Contract; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; import bisq.core.util.BSFormatter; +import bisq.common.crypto.PubKeyRing; + import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; @@ -85,8 +88,9 @@ class TransactionsListItem { TransactionsListItem(Transaction transaction, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, - Optional tradableOptional, + TransactionAwareTradable transactionAwareTradable, DaoFacade daoFacade, + PubKeyRing pubKeyRing, BSFormatter formatter, long ignoreDustThreshold) { this.btcWalletService = btcWalletService; @@ -94,6 +98,9 @@ class TransactionsListItem { txId = transaction.getHashAsString(); + Optional optionalTradable = Optional.ofNullable(transactionAwareTradable) + .map(TransactionAwareTradable::asTradable); + Coin valueSentToMe = btcWalletService.getValueSentToMeForTransaction(transaction); Coin valueSentFromMe = btcWalletService.getValueSentFromMeForTransaction(transaction); @@ -195,48 +202,75 @@ public void onTransactionConfidenceChanged(TransactionConfidence confidence) { confirmations = confidence.getDepthInBlocks(); - if (tradableOptional.isPresent()) { - tradable = tradableOptional.get(); + if (optionalTradable.isPresent()) { + tradable = optionalTradable.get(); detailsAvailable = true; - String id = tradable.getShortId(); + String tradeId = tradable.getShortId(); if (tradable instanceof OpenOffer) { - details = Res.get("funds.tx.createOfferFee", id); + details = Res.get("funds.tx.createOfferFee", tradeId); } else if (tradable instanceof Trade) { Trade trade = (Trade) tradable; + TransactionAwareTrade transactionAwareTrade = (TransactionAwareTrade) transactionAwareTradable; if (trade.getTakerFeeTxId() != null && trade.getTakerFeeTxId().equals(txId)) { - details = Res.get("funds.tx.takeOfferFee", id); + details = Res.get("funds.tx.takeOfferFee", tradeId); } else { Offer offer = trade.getOffer(); String offerFeePaymentTxID = offer.getOfferFeePaymentTxId(); if (offerFeePaymentTxID != null && offerFeePaymentTxID.equals(txId)) { - details = Res.get("funds.tx.createOfferFee", id); + details = Res.get("funds.tx.createOfferFee", tradeId); } else if (trade.getDepositTx() != null && trade.getDepositTx().getHashAsString().equals(txId)) { - details = Res.get("funds.tx.multiSigDeposit", id); + details = Res.get("funds.tx.multiSigDeposit", tradeId); } else if (trade.getPayoutTx() != null && trade.getPayoutTx().getHashAsString().equals(txId)) { - details = Res.get("funds.tx.multiSigPayout", id); - } else if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_CLOSED) { - if (valueSentToMe.isPositive()) { - details = Res.get("funds.tx.disputePayout", id); + details = Res.get("funds.tx.multiSigPayout", tradeId); + } else { + Trade.DisputeState disputeState = trade.getDisputeState(); + if (disputeState == Trade.DisputeState.DISPUTE_CLOSED) { + if (valueSentToMe.isPositive()) { + details = Res.get("funds.tx.disputePayout", tradeId); + } else { + details = Res.get("funds.tx.disputeLost", tradeId); + txConfidenceIndicator.setVisible(false); + } + } else if (disputeState == Trade.DisputeState.REFUND_REQUEST_CLOSED || + disputeState == Trade.DisputeState.REFUND_REQUESTED || + disputeState == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) { + if (valueSentToMe.isPositive()) { + details = Res.get("funds.tx.refund", tradeId); + } else { + Contract contract = trade.getContract(); + Coin tradeAmount = trade.getTradeAmount(); + if (contract != null && tradeAmount != null) { + boolean isBuyer = contract.isMyRoleBuyer(pubKeyRing); + amountAsCoin = isBuyer ? trade.getOffer().getBuyerSecurityDeposit().multiply(-1) : + (trade.getOffer().getSellerSecurityDeposit().add(tradeAmount)).multiply(-1); + details = Res.get("funds.tx.collateralForRefund", tradeId); + txConfidenceIndicator.setVisible(false); + } + } } else { - details = Res.get("funds.tx.disputeLost", id); - txConfidenceIndicator.setVisible(false); + if (transactionAwareTrade.isDelayedPayoutTx(txId)) { + details = Res.get("funds.tx.timeLockedPayoutTx", tradeId); + txConfidenceIndicator.setVisible(false); + } else { + details = Res.get("funds.tx.unknown", tradeId); + } } - } else { - details = Res.get("funds.tx.unknown", id); } } } } else { - if (amountAsCoin.isZero()) + if (amountAsCoin.isZero()) { details = Res.get("funds.tx.noFundsFromDispute"); - else if (withdrawalFromBSQWallet) + txConfidenceIndicator.setVisible(false); + } else if (withdrawalFromBSQWallet) { details = Res.get("funds.tx.withdrawnFromBSQWallet"); - else if (!txFeeForBsqPayment) + } else if (!txFeeForBsqPayment) { details = received ? Res.get("funds.tx.receivedFunds") : Res.get("funds.tx.withdrawnFromWallet"); - else if (details.isEmpty()) + } else if (details.isEmpty()) { details = Res.get("funds.tx.txFeePaymentForBsqTx"); + } } // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime() date = transaction.getIncludedInBestChainAt() != null ? transaction.getIncludedInBestChainAt() : transaction.getUpdateTime(); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index 39bbf2ccdf6..97ff63c8f1c 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -21,7 +21,6 @@ import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; -import bisq.core.account.witness.AccountAgeRestrictions; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.TxFeeEstimationService; import bisq.core.btc.listeners.BalanceListener; @@ -358,7 +357,8 @@ Offer createAndGetOffer() { Map extraDataMap = OfferUtil.getExtraDataMap(accountAgeWitnessService, referralIdService, paymentAccount, - currencyCode); + currencyCode, + preferences); OfferUtil.validateOfferData(filterManager, p2PService, @@ -581,10 +581,11 @@ boolean isMakerFeeValid() { } long getMaxTradeLimit() { - if (paymentAccount != null) - return AccountAgeRestrictions.getMyTradeLimitAtCreateOffer(accountAgeWitnessService, paymentAccount, tradeCurrencyCode.get(), direction); - else + if (paymentAccount != null) { + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get(), direction); + } else { return 0; + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -594,7 +595,7 @@ long getMaxTradeLimit() { double calculateMarketPriceManual(double marketPrice, double volumeAsDouble, double amountAsDouble) { double manualPriceAsDouble = volumeAsDouble / amountAsDouble; double percentage = MathUtils.roundDouble(manualPriceAsDouble / marketPrice, 4); - + setMarketPriceMargin(percentage); return manualPriceAsDouble; diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index 26eb7e603b7..7cff53410e7 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -42,6 +42,7 @@ import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; +import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; import bisq.core.locale.CurrencyUtil; @@ -67,6 +68,7 @@ import javax.inject.Inject; +import de.jensd.fx.glyphs.GlyphIcons; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import javafx.scene.Scene; @@ -119,12 +121,12 @@ public class OfferBookView extends ActivatableViewAndModel currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; - private AutoTooltipTableColumn amountColumn, volumeColumn, marketColumn, - priceColumn, avatarColumn; + private AutoTooltipTableColumn amountColumn, volumeColumn, marketColumn, priceColumn, signingStateColumn, avatarColumn; private TableView tableView; private OfferView.OfferActionHandler offerActionHandler; @@ -145,7 +147,8 @@ public class OfferBookView extends ActivatableViewAndModel paymentMethodColumn = getPaymentMethodColumn(); tableView.getColumns().add(paymentMethodColumn); + signingStateColumn = getSigningStateColumn(); + tableView.getColumns().add(signingStateColumn); avatarColumn = getAvatarColumn(); tableView.getColumns().add(getActionColumn()); tableView.getColumns().add(avatarColumn); @@ -530,8 +536,7 @@ private void onCreateOffer() { private void onShowInfo(Offer offer, boolean isPaymentAccountValidForOffer, - boolean isRiskyBuyOfferWithImmatureAccountAge, - boolean isSellOfferAndAllTakerPaymentAccountsForOfferImmature, + boolean isInsufficientCounterpartyTradeLimit, boolean hasSameProtocolVersion, boolean isIgnored, boolean isOfferBanned, @@ -545,12 +550,8 @@ private void onShowInfo(Offer offer, Res.get("offerbook.warning.noMatchingAccount.msg"), FiatAccountsView.class, "navigation.account"); - } else if (isRiskyBuyOfferWithImmatureAccountAge) { - new Popup<>().warning(Res.get("offerbook.warning.riskyBuyOfferWithImmatureAccountAge", - Res.get("offerbook.warning.newVersionAnnouncement"))).show(); - } else if (isSellOfferAndAllTakerPaymentAccountsForOfferImmature) { - new Popup<>().warning(Res.get("offerbook.warning.sellOfferAndAnyTakerPaymentAccountForOfferMature", - Res.get("offerbook.warning.newVersionAnnouncement"))).show(); + } else if (isInsufficientCounterpartyTradeLimit) { + new Popup<>().warning(Res.get("offerbook.warning.counterpartyTradeRestrictions")).show(); } else if (!hasSameProtocolVersion) { new Popup<>().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show(); } else if (isIgnored) { @@ -568,7 +569,8 @@ private void onShowInfo(Offer offer, } else if (isInsufficientTradeLimit) { final Optional account = model.getMostMaturePaymentAccountForOffer(offer); if (account.isPresent()) { - final long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), offer.getCurrencyCode()); + final long tradeLimit = model.accountAgeWitnessService.getMyTradeLimit(account.get(), + offer.getCurrencyCode(), offer.getMirroredDirection()); new Popup<>() .warning(Res.get("offerbook.warning.tradeLimitNotMatching", DisplayUtils.formatAccountAge(model.accountAgeWitnessService.getMyAccountAge(account.get().getPaymentAccountPayload())), @@ -885,6 +887,7 @@ public void updateItem(final OfferBookListItem item, boolean empty) { field = new HyperlinkWithIcon(model.getPaymentMethod(item)); field.setOnAction(event -> offerDetailsWindow.show(item.getOffer())); field.setTooltip(new Tooltip(model.getPaymentMethodToolTip(item))); + setGraphic(field); } else { setGraphic(null); @@ -914,10 +917,10 @@ public TableCell call(TableColumn() { final ImageView iconView = new ImageView(); final AutoTooltipButton button = new AutoTooltipButton(); - boolean isTradable, isPaymentAccountValidForOffer, isRiskyBuyOfferWithImmatureAccountAge, - isSellOfferAndAllTakerPaymentAccountsForOfferImmature, + boolean isTradable, isPaymentAccountValidForOffer, + isInsufficientCounterpartyTradeLimit, hasSameProtocolVersion, isIgnored, isOfferBanned, isCurrencyBanned, - isPaymentMethodBanned, isNodeAddressBanned, isInsufficientTradeLimit, + isPaymentMethodBanned, isNodeAddressBanned, isMyInsufficientTradeLimit, requireUpdateToNewVersion; { @@ -937,8 +940,7 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { boolean myOffer = model.isMyOffer(offer); if (tableRow != null) { isPaymentAccountValidForOffer = model.isAnyPaymentAccountValidForOffer(offer); - isRiskyBuyOfferWithImmatureAccountAge = model.isRiskyBuyOfferWithImmatureAccountAge(offer); - isSellOfferAndAllTakerPaymentAccountsForOfferImmature = model.isSellOfferAndAllTakerPaymentAccountsForOfferImmature(offer); + isInsufficientCounterpartyTradeLimit = model.isInsufficientCounterpartyTradeLimit(offer); hasSameProtocolVersion = model.hasSameProtocolVersion(offer); isIgnored = model.isIgnored(offer); isOfferBanned = model.isOfferBanned(offer); @@ -946,10 +948,9 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { isPaymentMethodBanned = model.isPaymentMethodBanned(offer); isNodeAddressBanned = model.isNodeAddressBanned(offer); requireUpdateToNewVersion = model.requireUpdateToNewVersion(); - isInsufficientTradeLimit = model.isInsufficientTradeLimit(offer); + isMyInsufficientTradeLimit = model.isMyInsufficientTradeLimit(offer); isTradable = isPaymentAccountValidForOffer && - !isRiskyBuyOfferWithImmatureAccountAge && - !isSellOfferAndAllTakerPaymentAccountsForOfferImmature && + !isInsufficientCounterpartyTradeLimit && hasSameProtocolVersion && !isIgnored && !isOfferBanned && @@ -957,7 +958,7 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { !isPaymentMethodBanned && !isNodeAddressBanned && !requireUpdateToNewVersion && - !isInsufficientTradeLimit; + !isMyInsufficientTradeLimit; tableRow.setOpacity(isTradable || myOffer ? 1 : 0.4); @@ -972,8 +973,7 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) onShowInfo(offer, isPaymentAccountValidForOffer, - isRiskyBuyOfferWithImmatureAccountAge, - isSellOfferAndAllTakerPaymentAccountsForOfferImmature, + isInsufficientCounterpartyTradeLimit, hasSameProtocolVersion, isIgnored, isOfferBanned, @@ -981,7 +981,7 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { isPaymentMethodBanned, isNodeAddressBanned, requireUpdateToNewVersion, - isInsufficientTradeLimit); + isMyInsufficientTradeLimit); }); } } @@ -1014,8 +1014,7 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { if (!myOffer && !isTradable) button.setOnAction(e -> onShowInfo(offer, isPaymentAccountValidForOffer, - isRiskyBuyOfferWithImmatureAccountAge, - isSellOfferAndAllTakerPaymentAccountsForOfferImmature, + isInsufficientCounterpartyTradeLimit, hasSameProtocolVersion, isIgnored, isOfferBanned, @@ -1023,7 +1022,7 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { isPaymentMethodBanned, isNodeAddressBanned, requireUpdateToNewVersion, - isInsufficientTradeLimit)); + isMyInsufficientTradeLimit)); button.updateText(title); setPadding(new Insets(0, 15, 0, 0)); @@ -1043,6 +1042,66 @@ public void updateItem(final OfferBookListItem newItem, boolean empty) { return column; } + private AutoTooltipTableColumn getSigningStateColumn() { + AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("offerbook.timeSinceSigning"), Res.get("offerbook.timeSinceSigning.help")) { + { + setMinWidth(60); + setSortable(true); + } + }; + + column.getStyleClass().add("number-column"); + column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon field; + + @Override + public void updateItem(final OfferBookListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + + GlyphIcons icon; + String info; + String timeSinceSigning; + + if (accountAgeWitnessService.hasSignedWitness(item.getOffer())) { + //TODO sqrrm: We need four states in here: + // - signed by arbitrator + // - signed by peer + // - signed by peer and limit lifted + // - signed by peer and able to sign + // Additionally we need to have some enum or so how the account signing took place. + // e.g. if in the future we'll also offer the "pay with two different accounts"-signing + icon = MaterialDesignIcon.APPROVAL; + info = "This account was verified and signed by an arbitrator or peer."; + //TODO sqrrm: add time since signing + timeSinceSigning = "3 days"; + } else { + + //TODO sqrrm: Here we need two states: + // - not signing necessary for this payment account + // - signing required and not signed + icon = MaterialDesignIcon.ALERT_CIRCLE_OUTLINE; + info = Res.get("shared.notSigned"); + timeSinceSigning = Res.get("offerbook.timeSinceSigning.notSigned"); + } + + InfoAutoTooltipLabel label = new InfoAutoTooltipLabel(timeSinceSigning, icon, ContentDisplay.RIGHT, info); + setGraphic(label); + } else { + setGraphic(null); + } + } + }; + } + }); + return column; + } + private AutoTooltipTableColumn getAvatarColumn() { AutoTooltipTableColumn column = new AutoTooltipTableColumn<>(Res.get("offerbook.trader")) { { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index e095db7cdfa..bf2bc5b8719 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -509,15 +509,6 @@ boolean isAnyPaymentAccountValidForOffer(Offer offer) { PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); } - boolean isSellOfferAndAllTakerPaymentAccountsForOfferImmature(Offer offer) { - return user.getPaymentAccounts() != null && - PaymentAccountUtil.isSellOfferAndAllTakerPaymentAccountsForOfferImmature(offer, user.getPaymentAccounts(), accountAgeWitnessService); - } - - boolean isRiskyBuyOfferWithImmatureAccountAge(Offer offer) { - return PaymentAccountUtil.isRiskyBuyOfferWithImmatureAccountAge(offer, accountAgeWitnessService); - } - boolean hasPaymentAccountForCurrency() { return (showAllTradeCurrenciesProperty.get() && user.getPaymentAccounts() != null && @@ -525,12 +516,6 @@ boolean hasPaymentAccountForCurrency() { user.hasPaymentAccountForCurrency(selectedTradeCurrency); } - boolean hasMakerAnyMatureAccountForBuyOffer() { - return direction == OfferPayload.Direction.SELL || - (user.getPaymentAccounts() != null && - PaymentAccountUtil.hasMakerAnyMatureAccountForBuyOffer(user.getPaymentAccounts(), accountAgeWitnessService)); - } - boolean canCreateOrTakeOffer() { return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && GUIUtil.isBootstrappedOrShowPopup(p2PService); @@ -579,10 +564,17 @@ boolean requireUpdateToNewVersion() { return filterManager.requireUpdateToNewVersionForTrading(); } - boolean isInsufficientTradeLimit(Offer offer) { + boolean isInsufficientCounterpartyTradeLimit(Offer offer) { + return CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) && + !accountAgeWitnessService.verifyPeersTradeAmount(offer, offer.getAmount(), errorMessage -> { + }); + } + + boolean isMyInsufficientTradeLimit(Offer offer) { Optional accountOptional = getMostMaturePaymentAccountForOffer(offer); final long myTradeLimit = accountOptional - .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, offer.getCurrencyCode())) + .map(paymentAccount -> accountAgeWitnessService.getMyTradeLimit(paymentAccount, + offer.getCurrencyCode(), offer.getMirroredDirection())) .orElse(0L); final long offerMinAmount = offer.getMinAmount().value; log.debug("isInsufficientTradeLimit accountOptional={}, myTradeLimit={}, offerMinAmount={}, ", diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 49d4cde16c5..eb47b7d3e1a 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -22,7 +22,6 @@ import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; -import bisq.core.account.witness.AccountAgeRestrictions; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.TxFeeEstimationService; import bisq.core.btc.listeners.BalanceListener; @@ -434,10 +433,12 @@ public PaymentAccount getLastSelectedPaymentAccount() { } long getMaxTradeLimit() { - if (paymentAccount != null) - return AccountAgeRestrictions.getMyTradeLimitAtTakeOffer(accountAgeWitnessService, paymentAccount, offer, getCurrencyCode(), getDirection()); - else + if (paymentAccount != null) { + return accountAgeWitnessService.getMyTradeLimit(paymentAccount, getCurrencyCode(), + offer.getMirroredDirection()); + } else { return 0; + } } boolean canTakeOffer() { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/editor/PeerInfoWithTagEditor.java b/desktop/src/main/java/bisq/desktop/main/overlays/editor/PeerInfoWithTagEditor.java index 352bea454e3..ab2bb338817 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/editor/PeerInfoWithTagEditor.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/editor/PeerInfoWithTagEditor.java @@ -90,6 +90,8 @@ public class PeerInfoWithTagEditor extends Overlay { private EventHandler keyEventEventHandler; @Nullable private String accountAge; + @Nullable + private String accountSigningState; public PeerInfoWithTagEditor(PrivateNotificationManager privateNotificationManager, Offer offer, @@ -126,6 +128,11 @@ public PeerInfoWithTagEditor accountAge(@Nullable String accountAge) { return this; } + public PeerInfoWithTagEditor accountSigningState(@Nullable String accountSigningState) { + this.accountSigningState = accountSigningState; + return this; + } + public PeerInfoWithTagEditor numTrades(int numTrades) { this.numTrades = numTrades; if (numTrades == 0) @@ -194,6 +201,10 @@ private void addContent() { if (accountAge != null) GridPane.setColumnSpan(addCompactTopLabelTextField(gridPane, ++rowIndex, Res.get("peerInfo.age"), accountAge).third, 2); + if (accountSigningState != null) { + GridPane.setColumnSpan(addCompactTopLabelTextField(gridPane, ++rowIndex, Res.get("shared.accountSigningState"), accountSigningState).third, 2); + } + inputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("peerInfo.setTag")); GridPane.setColumnSpan(inputTextField, 2); Map peerTagMap = preferences.getPeerTagMap(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java index 15372e9820f..24f04b304e3 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java @@ -27,8 +27,8 @@ import bisq.desktop.main.support.dispute.client.mediation.MediationClientView; import bisq.core.locale.Res; -import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.BuyerTrade; import bisq.core.trade.MakerTrade; import bisq.core.trade.SellerTrade; @@ -38,7 +38,6 @@ import bisq.core.user.Preferences; import bisq.common.UserThread; -import bisq.common.app.DevEnv; import com.google.inject.Inject; @@ -82,8 +81,8 @@ static void add(Notification notification) { /////////////////////////////////////////////////////////////////////////////////////////// private final TradeManager tradeManager; - private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; + private final RefundManager refundManager; private final Navigation navigation; private final Map disputeStateSubscriptionsMap = new HashMap<>(); @@ -97,13 +96,13 @@ static void add(Notification notification) { @Inject public NotificationCenter(TradeManager tradeManager, - ArbitrationManager arbitrationManager, MediationManager mediationManager, + RefundManager refundManager, Preferences preferences, Navigation navigation) { this.tradeManager = tradeManager; - this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; + this.refundManager = refundManager; this.navigation = navigation; EasyBind.subscribe(preferences.getUseAnimationsProperty(), useAnimations -> NotificationCenter.useAnimations = useAnimations); @@ -230,26 +229,26 @@ else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.FIAT_SEN private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) { String message = null; - if (arbitrationManager.findOwnDispute(trade.getId()).isPresent()) { - String disputeOrTicket = arbitrationManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? + if (refundManager.findOwnDispute(trade.getId()).isPresent()) { + String disputeOrTicket = refundManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.dispute"); switch (disputeState) { case NO_DISPUTE: break; - case DISPUTE_REQUESTED: + case REFUND_REQUESTED: break; - case DISPUTE_STARTED_BY_PEER: + case REFUND_REQUEST_STARTED_BY_PEER: message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket); break; - case DISPUTE_CLOSED: + case REFUND_REQUEST_CLOSED: message = Res.get("notification.trade.disputeClosed", disputeOrTicket); break; default: - if (DevEnv.isDevMode()) { - log.error("arbitrationDisputeManager must not contain mediation disputes"); - throw new RuntimeException("arbitrationDisputeManager must not contain mediation disputes"); - } +// if (DevEnv.isDevMode()) { +// log.error("refundManager must not contain mediation or arbitration disputes. disputeState={}", disputeState); +// throw new RuntimeException("arbitrationDisputeManager must not contain mediation disputes"); +// } break; } if (message != null) { @@ -271,10 +270,10 @@ private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) message = Res.get("notification.trade.disputeClosed", disputeOrTicket); break; default: - if (DevEnv.isDevMode()) { - log.error("mediationDisputeManager must not contain arbitration disputes"); - throw new RuntimeException("mediationDisputeManager must not contain arbitration disputes"); - } +// if (DevEnv.isDevMode()) { +// log.error("mediationDisputeManager must not contain arbitration or refund disputes. disputeState={}", disputeState); +// throw new RuntimeException("mediationDisputeManager must not contain arbitration disputes"); +// } break; } if (message != null) { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java index e64c57c996c..046c68dfc5a 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java @@ -35,6 +35,7 @@ import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Contract; import bisq.core.util.BSFormatter; @@ -72,6 +73,7 @@ public class ContractWindow extends Overlay { private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; + private final RefundManager refundManager; private final AccountAgeWitnessService accountAgeWitnessService; private final BSFormatter formatter; private Dispute dispute; @@ -84,10 +86,12 @@ public class ContractWindow extends Overlay { @Inject public ContractWindow(ArbitrationManager arbitrationManager, MediationManager mediationManager, + RefundManager refundManager, AccountAgeWitnessService accountAgeWitnessService, BSFormatter formatter) { this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; + this.refundManager = refundManager; this.accountAgeWitnessService = accountAgeWitnessService; this.formatter = formatter; type = Type.Confirmation; @@ -169,8 +173,9 @@ private void addContent() { getAccountAge(contract.getBuyerPaymentAccountPayload(), contract.getBuyerPubKeyRing(), offer.getCurrencyCode()) + " / " + getAccountAge(contract.getSellerPaymentAccountPayload(), contract.getSellerPubKeyRing(), offer.getCurrencyCode())); - String nrOfDisputesAsBuyer = getDisputeManager(dispute).getNrOfDisputes(true, contract); - String nrOfDisputesAsSeller = getDisputeManager(dispute).getNrOfDisputes(false, contract); + DisputeManager> disputeManager = getDisputeManager(dispute); + String nrOfDisputesAsBuyer = disputeManager != null ? disputeManager.getNrOfDisputes(true, contract) : ""; + String nrOfDisputesAsSeller = disputeManager != null ? disputeManager.getNrOfDisputes(false, contract) : ""; addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.numDisputes"), nrOfDisputesAsBuyer + " / " + nrOfDisputesAsSeller); @@ -179,9 +184,25 @@ private void addContent() { addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), sellerPaymentAccountPayload.getPaymentDetails()).second.setMouseTransparent(false); - // TODO update in next release to shared.selectedArbitrator and delete shared.arbitrator entry - String title = dispute.isMediationDispute() ? Res.get("shared.selectedMediator") : Res.get("shared.arbitrator"); - String agentNodeAddress = getDisputeManager(dispute).getAgentNodeAddress(dispute).getFullAddress(); + String title = ""; + if (dispute.getSupportType() != null) { + switch (dispute.getSupportType()) { + case ARBITRATION: + // TODO update in next release to shared.selectedArbitrator and delete shared.arbitrator entry + title = Res.get("shared.arbitrator"); + break; + case MEDIATION: + title = Res.get("shared.selectedMediator"); + break; + case TRADE: + break; + case REFUND: + title = Res.get("shared.refundAgent"); + break; + } + } + + String agentNodeAddress = disputeManager != null ? disputeManager.getAgentNodeAddress(dispute).getFullAddress() : ""; addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, title, agentNodeAddress); if (showAcceptedCountryCodes) { @@ -272,7 +293,19 @@ private void addContent() { } private DisputeManager> getDisputeManager(Dispute dispute) { - return dispute.isMediationDispute() ? mediationManager : arbitrationManager; + if (dispute.getSupportType() != null) { + switch (dispute.getSupportType()) { + case ARBITRATION: + return arbitrationManager; + case MEDIATION: + return mediationManager; + case TRADE: + break; + case REFUND: + return refundManager; + } + } + return null; } private String getAccountAge(PaymentAccountPayload paymentAccountPayload, diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 7af9ed4d5eb..7880d605dc8 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -26,29 +26,38 @@ import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.Layout; +import bisq.core.btc.TxFeeEstimationService; import bisq.core.btc.exceptions.TransactionVerificationException; -import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.locale.Res; import bisq.core.offer.Offer; +import bisq.core.provider.fee.FeeService; +import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeResult; -import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Contract; import bisq.core.util.BSFormatter; +import bisq.core.util.CoinUtil; import bisq.core.util.ParsingUtils; import bisq.common.UserThread; import bisq.common.app.DevEnv; +import bisq.common.handlers.ResultHandler; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; -import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; import javax.inject.Inject; @@ -82,15 +91,18 @@ import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; +import static com.google.common.base.Preconditions.checkNotNull; public class DisputeSummaryWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(DisputeSummaryWindow.class); private final BSFormatter formatter; - private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; - private final BtcWalletService walletService; + private final RefundManager refundManager; private final TradeWalletService tradeWalletService; + private final BtcWalletService btcWalletService; + private final TxFeeEstimationService txFeeEstimationService; + private final FeeService feeService; private Dispute dispute; private Optional finalizeDisputeHandlerOptional = Optional.empty(); private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; @@ -119,16 +131,20 @@ public class DisputeSummaryWindow extends Overlay { @Inject public DisputeSummaryWindow(BSFormatter formatter, - ArbitrationManager arbitrationManager, MediationManager mediationManager, - BtcWalletService walletService, - TradeWalletService tradeWalletService) { + RefundManager refundManager, + TradeWalletService tradeWalletService, + BtcWalletService btcWalletService, + TxFeeEstimationService txFeeEstimationService, + FeeService feeService) { this.formatter = formatter; - this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; - this.walletService = walletService; + this.refundManager = refundManager; this.tradeWalletService = tradeWalletService; + this.btcWalletService = btcWalletService; + this.txFeeEstimationService = txFeeEstimationService; + this.feeService = feeService; type = Type.Confirmation; } @@ -355,7 +371,12 @@ private boolean isPayoutAmountValid() { .add(offer.getBuyerSecurityDeposit()) .add(offer.getSellerSecurityDeposit()); Coin totalAmount = buyerAmount.add(sellerAmount); - return (totalAmount.compareTo(available) == 0); + if (getDisputeManager(dispute) instanceof RefundManager) { + // We allow to spend less in case of RefundAgent + return totalAmount.compareTo(available) <= 0; + } else { + return totalAmount.compareTo(available) == 0; + } } private void applyCustomAmounts(InputTextField inputTextField) { @@ -365,10 +386,17 @@ private void applyCustomAmounts(InputTextField inputTextField) { .add(offer.getBuyerSecurityDeposit()) .add(offer.getSellerSecurityDeposit()); Coin enteredAmount = ParsingUtils.parseToCoin(inputTextField.getText(), formatter); + if (enteredAmount.isNegative()) { + enteredAmount = Coin.ZERO; + inputTextField.setText(formatter.formatCoin(enteredAmount)); + } + if (enteredAmount.isPositive() && !Restrictions.isAboveDust(enteredAmount)) { + enteredAmount = Restrictions.getMinNonDustOutput(); + inputTextField.setText(formatter.formatCoin(enteredAmount)); + } if (enteredAmount.compareTo(available) > 0) { enteredAmount = available; - Coin finalEnteredAmount = enteredAmount; - inputTextField.setText(formatter.formatCoin(finalEnteredAmount)); + inputTextField.setText(formatter.formatCoin(enteredAmount)); } Coin counterPartAsCoin = available.subtract(enteredAmount); String formattedCounterPartAmount = formatter.formatCoin(counterPartAsCoin); @@ -377,11 +405,27 @@ private void applyCustomAmounts(InputTextField inputTextField) { if (inputTextField == buyerPayoutAmountInputTextField) { buyerAmount = enteredAmount; sellerAmount = counterPartAsCoin; - sellerPayoutAmountInputTextField.setText(formattedCounterPartAmount); + Coin sellerAmountFromField = ParsingUtils.parseToCoin(sellerPayoutAmountInputTextField.getText(), formatter); + Coin totalAmountFromFields = enteredAmount.add(sellerAmountFromField); + // RefundAgent can enter less then available + if (getDisputeManager(dispute) instanceof MediationManager || + totalAmountFromFields.compareTo(available) > 0) { + sellerPayoutAmountInputTextField.setText(formattedCounterPartAmount); + } else { + sellerAmount = sellerAmountFromField; + } } else { sellerAmount = enteredAmount; buyerAmount = counterPartAsCoin; - buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount); + Coin buyerAmountFromField = ParsingUtils.parseToCoin(buyerPayoutAmountInputTextField.getText(), formatter); + Coin totalAmountFromFields = enteredAmount.add(buyerAmountFromField); + // RefundAgent can enter less then available + if (getDisputeManager(dispute) instanceof MediationManager || + totalAmountFromFields.compareTo(available) > 0) { + buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount); + } else { + buyerAmount = buyerAmountFromField; + } } disputeResult.setBuyerPayoutAmount(buyerAmount); @@ -506,7 +550,6 @@ private void addButtons(Contract contract) { Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.close.button"), Res.get("shared.cancel"), 15, true); - //GridPane.setColumnSpan(tuple.third, 2); Button closeTicketButton = tuple.first; closeTicketButton.disableProperty().bind(Bindings.createBooleanBinding( () -> tradeAmountToggleGroup.getSelectedToggle() == null @@ -525,67 +568,149 @@ private void addButtons(Contract contract) { log.warn("dispute.getDepositTxSerialized is null"); return; } - - if (!dispute.isMediationDispute()) { - try { - AddressEntry arbitratorAddressEntry = walletService.getArbitratorAddressEntry(); - disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); - byte[] arbitratorSignature = tradeWalletService.arbitratorSignsDisputedPayoutTx( - dispute.getDepositTxSerialized(), - disputeResult.getBuyerPayoutAmount(), - disputeResult.getSellerPayoutAmount(), - contract.getBuyerPayoutAddressString(), - contract.getSellerPayoutAddressString(), - arbitratorAddressEntry.getKeyPair(), - contract.getBuyerMultiSigPubKey(), - contract.getSellerMultiSigPubKey(), - arbitratorAddressEntry.getPubKey() - ); - disputeResult.setArbitratorSignature(arbitratorSignature); - } catch (AddressFormatException | TransactionVerificationException e2) { - log.error("Error at close dispute", e2); - return; - } + if (dispute.getSupportType() == SupportType.REFUND && + peersDisputeOptional.isPresent() && + !peersDisputeOptional.get().isClosed()) { + showPayoutTxConfirmation(contract, disputeResult, + () -> { + doClose(closeTicketButton); + }); + } else { + doClose(closeTicketButton); } + }); - disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); - disputeResult.setCloseDate(new Date()); + cancelButton.setOnAction(e -> { dispute.setDisputeResult(disputeResult); - dispute.setIsClosed(true); - String text = Res.get("disputeSummaryWindow.close.msg", - DisplayUtils.formatDateTime(disputeResult.getCloseDate()), - formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), - formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), - disputeResult.summaryNotesProperty().get()); - - if (dispute.isMediationDispute()) { - text += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); - } + hide(); + }); + } + + private void showPayoutTxConfirmation(Contract contract, DisputeResult disputeResult, ResultHandler resultHandler) { + Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); + String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); + Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); + String sellerPayoutAddressString = contract.getSellerPayoutAddressString(); + Coin outputAmount = buyerPayoutAmount.add(sellerPayoutAmount); + Tuple2 feeTuple = txFeeEstimationService.getEstimatedFeeAndTxSize(outputAmount, feeService, btcWalletService); + Coin fee = feeTuple.first; + Integer txSize = feeTuple.second; + double feePerByte = CoinUtil.getFeePerByte(fee, txSize); + double kb = txSize / 1000d; + Coin inputAmount = outputAmount.add(fee); + String buyerDetails = ""; + if (buyerPayoutAmount.isPositive()) { + buyerDetails = Res.get("disputeSummaryWindow.close.txDetails.buyer", + formatter.formatCoinWithCode(buyerPayoutAmount), + buyerPayoutAddressString); + } + String sellerDetails = ""; + if (sellerPayoutAmount.isPositive()) { + sellerDetails = Res.get("disputeSummaryWindow.close.txDetails.seller", + formatter.formatCoinWithCode(sellerPayoutAmount), + sellerPayoutAddressString); + } + new Popup<>().width(900) + .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) + .confirmation(Res.get("disputeSummaryWindow.close.txDetails", + formatter.formatCoinWithCode(inputAmount), + buyerDetails, + sellerDetails, + formatter.formatCoinWithCode(fee), + feePerByte, + kb)) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + doPayout(buyerPayoutAmount, + sellerPayoutAmount, + fee, + buyerPayoutAddressString, + sellerPayoutAddressString, + resultHandler); + }) + .closeButtonText(Res.get("shared.cancel")) + .onClose(() -> { + }) + .show(); + } - getDisputeManager(dispute).sendDisputeResultMessage(disputeResult, dispute, text); + private void doPayout(Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + Coin fee, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + ResultHandler resultHandler) { + try { + Transaction tx = btcWalletService.createRefundPayoutTx(buyerPayoutAmount, + sellerPayoutAmount, + fee, + buyerPayoutAddressString, + sellerPayoutAddressString); + log.error("transaction " + tx); + tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + resultHandler.handleResult(); + } - if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { - UserThread.runAfter(() -> new Popup<>() - .attention(Res.get("disputeSummaryWindow.close.closePeer")) - .show(), - 200, TimeUnit.MILLISECONDS); - } + @Override + public void onFailure(TxBroadcastException exception) { + log.error("TxBroadcastException at doPayout", exception); + new Popup<>().error(exception.toString()).show(); + ; + } + }); + } catch (InsufficientMoneyException | WalletException | TransactionVerificationException e) { + log.error("Exception at doPayout", e); + new Popup<>().error(e.toString()).show(); + } + } - finalizeDisputeHandlerOptional.ifPresent(Runnable::run); + private void doClose(Button closeTicketButton) { + disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); + disputeResult.setCloseDate(new Date()); + dispute.setDisputeResult(disputeResult); + dispute.setIsClosed(true); + String text = Res.get("disputeSummaryWindow.close.msg", + DisplayUtils.formatDateTime(disputeResult.getCloseDate()), + formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), + formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), + disputeResult.summaryNotesProperty().get()); + + if (dispute.getSupportType() == SupportType.MEDIATION) { + text += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); + } - closeTicketButton.disableProperty().unbind(); + checkNotNull(getDisputeManager(dispute)).sendDisputeResultMessage(disputeResult, dispute, text); - hide(); - }); + if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { + UserThread.runAfter(() -> new Popup<>() + .attention(Res.get("disputeSummaryWindow.close.closePeer")) + .show(), + 200, TimeUnit.MILLISECONDS); + } - cancelButton.setOnAction(e -> { - dispute.setDisputeResult(disputeResult); - hide(); - }); + finalizeDisputeHandlerOptional.ifPresent(Runnable::run); + + closeTicketButton.disableProperty().unbind(); + + hide(); } private DisputeManager> getDisputeManager(Dispute dispute) { - return dispute.isMediationDispute() ? mediationManager : arbitrationManager; + if (dispute.getSupportType() != null) { + switch (dispute.getSupportType()) { + case ARBITRATION: + return null; + case MEDIATION: + return mediationManager; + case TRADE: + break; + case REFUND: + return refundManager; + } + } + return null; } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index e3abcbc2c28..921d208872e 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -137,6 +137,7 @@ private void addContent() { bannedPaymentMethodsInputTextField.setPromptText("E.g. PERFECT_MONEY"); // Do not translate InputTextField arbitratorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); InputTextField mediatorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); + InputTextField refundAgentsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.refundAgents")); InputTextField seedNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.seedNode")); InputTextField priceRelayNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.priceRelayNode")); InputTextField btcNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcNode")); @@ -176,6 +177,9 @@ private void addContent() { if (filter.getMediators() != null) mediatorsInputTextField.setText(filter.getMediators().stream().collect(Collectors.joining(", "))); + if (filter.getRefundAgents() != null) + refundAgentsInputTextField.setText(filter.getRefundAgents().stream().collect(Collectors.joining(", "))); + if (filter.getSeedNodes() != null) seedNodesInputTextField.setText(filter.getSeedNodes().stream().collect(Collectors.joining(", "))); @@ -200,6 +204,7 @@ private void addContent() { List bannedPaymentMethods = new ArrayList<>(); List arbitrators = new ArrayList<>(); List mediators = new ArrayList<>(); + List refundAgents = new ArrayList<>(); List seedNodes = new ArrayList<>(); List priceRelayNodes = new ArrayList<>(); List btcNodes = new ArrayList<>(); @@ -244,6 +249,9 @@ private void addContent() { if (!mediatorsInputTextField.getText().isEmpty()) { mediators = new ArrayList<>(Arrays.asList(StringUtils.deleteWhitespace(mediatorsInputTextField.getText()).split(","))); } + if (!refundAgentsInputTextField.getText().isEmpty()) { + refundAgents = new ArrayList<>(Arrays.asList(StringUtils.deleteWhitespace(refundAgentsInputTextField.getText()).split(","))); + } if (!seedNodesInputTextField.getText().isEmpty()) { seedNodes = new ArrayList<>(Arrays.asList(StringUtils.deleteWhitespace(seedNodesInputTextField.getText()).split(","))); @@ -271,7 +279,8 @@ private void addContent() { disableDaoCheckBox.isSelected(), disableDaoBelowVersionInputTextField.getText(), disableTradeBelowVersionInputTextField.getText(), - mediators), + mediators, + refundAgents), keyInputTextField.getText())) hide(); else diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java index 64b755d5a03..83c839b5975 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java @@ -120,20 +120,15 @@ private void addContent() { InputTextField sellerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "sellerPubKeyAsHex"); InputTextField arbitratorPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "arbitratorPubKeyAsHex"); - InputTextField P2SHMultiSigOutputScript = addInputTextField(gridPane, ++rowIndex, "P2SHMultiSigOutputScript"); - - // Notes: // Open with alt+g and enable DEV mode // Priv key is only visible if pw protection is removed (wallet details data (alt+j)) - // Take P2SHMultiSigOutputScript from depositTx in blockexplorer // Take missing buyerPubKeyAsHex and sellerPubKeyAsHex from contract data! // Lookup sellerPrivateKeyAsHex associated with sellerPubKeyAsHex (or buyers) in wallet details data // sellerPubKeys/buyerPubKeys are auto generated if used the fields below // Never set the priv arbitr. key here! depositTxHex.setText(""); - P2SHMultiSigOutputScript.setText(""); buyerPayoutAmount.setText(""); sellerPayoutAmount.setText(""); @@ -158,9 +153,7 @@ public void onSuccess(@Nullable Transaction result) { log.error("onSuccess"); UserThread.execute(() -> { String txId = result != null ? result.getHashAsString() : "null"; - new Popup<>() - .information("Transaction successful published. Transaction ID: " + txId) - .show(); + new Popup<>().information("Transaction successful published. Transaction ID: " + txId).show(); }); } @@ -173,7 +166,7 @@ public void onFailure(TxBroadcastException exception) { onAction(() -> { if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { try { - tradeWalletService.emergencySignAndPublishPayoutTx(depositTxHex.getText(), + tradeWalletService.emergencySignAndPublishPayoutTxFrom2of3MultiSig(depositTxHex.getText(), Coin.parseCoin(buyerPayoutAmount.getText()), Coin.parseCoin(sellerPayoutAmount.getText()), Coin.parseCoin(arbitratorPayoutAmount.getText()), @@ -187,7 +180,6 @@ public void onFailure(TxBroadcastException exception) { buyerPubKeyAsHex.getText(), sellerPubKeyAsHex.getText(), arbitratorPubKeyAsHex.getText(), - P2SHMultiSigOutputScript.getText(), callback); } catch (AddressFormatException | WalletException | TransactionVerificationException e) { log.error(e.toString()); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java new file mode 100644 index 00000000000..e5a28a8df43 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java @@ -0,0 +1,249 @@ +/* + * 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.desktop.main.overlays.windows; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.overlays.popups.Popup; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.TraderDataItem; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; + +import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DatePicker; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import javafx.geometry.VPos; + +import javafx.collections.FXCollections; + +import javafx.util.Callback; +import javafx.util.StringConverter; + +import java.time.ZoneOffset; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.*; + +@Slf4j +public class SignPaymentAccountsWindow extends Overlay { + + private Label descriptionLabel; + private ComboBox paymentMethodComboBox; + private DatePicker datePicker; + private InputTextField privateKey; + private ListView selectedPaymentAccountsList = new ListView<>(); + private final AccountAgeWitnessService accountAgeWitnessService; + private final ArbitratorManager arbitratorManager; + private final ArbitrationManager arbitrationManager; + + + @Inject + public SignPaymentAccountsWindow(AccountAgeWitnessService accountAgeWitnessService, + ArbitratorManager arbitratorManager, + ArbitrationManager arbitrationManager) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.arbitratorManager = arbitratorManager; + this.arbitrationManager = arbitrationManager; + } + + @Override + public void show() { + rowIndex = -1; + createGridPane(); + gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER); + + + headLine(Res.get("popup.accountSigning.selectAccounts.headline")); + type = Type.Attention; + + addHeadLine(); + addSelectAccountsContent(); + addButtons(); + applyStyles(); + + display(); + } + + private void addSelectAccountsContent() { + + descriptionLabel = addMultilineLabel(gridPane, ++rowIndex, + Res.get("popup.accountSigning.selectAccounts.description")); + + paymentMethodComboBox = addComboBox(gridPane, ++rowIndex, Res.get("shared.selectPaymentMethod")); + paymentMethodComboBox.setVisibleRowCount(11); + paymentMethodComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(PaymentMethod paymentMethod) { + return paymentMethod != null ? Res.get(paymentMethod.getId()) : ""; + } + + @Override + public PaymentMethod fromString(String s) { + return null; + } + }); + + List list = PaymentMethod.getPaymentMethods().stream() + .filter(paymentMethod -> !paymentMethod.isAsset()) + .collect(Collectors.toList()); + + paymentMethodComboBox.setItems(FXCollections.observableArrayList(list)); + paymentMethodComboBox.setOnAction(e -> updateAccountSelectionState()); + + datePicker = addTopLabelDatePicker(gridPane, ++rowIndex, + Res.get("popup.accountSigning.selectAccounts.datePicker"), + 0).second; + datePicker.setOnAction(e -> updateAccountSelectionState()); + } + + private void addECKeyField() { + privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey")); + GridPane.setVgrow(privateKey, Priority.ALWAYS); + GridPane.setValignment(privateKey, VPos.TOP); + } + + private void updateAccountSelectionState() { + actionButton.setDisable(paymentMethodComboBox.getSelectionModel().isEmpty() || + datePicker.getValue() == null + ); + } + + private void removeContent() { + removeRowsFromGridPane(gridPane, 2, 3); + rowIndex = 1; + } + + private void addSelectedAccountsContent() { + removeContent(); + Tuple3, VBox> selectedPaymentAccountsTuple = + addTopLabelListView(gridPane, + ++rowIndex, Res.get("popup.accountSigning.confirmSelectedAccounts.headline")); + GridPane.setRowSpan(selectedPaymentAccountsTuple.third, 2); + selectedPaymentAccountsList = selectedPaymentAccountsTuple.second; + selectedPaymentAccountsList.setItems(FXCollections.observableArrayList( + accountAgeWitnessService.getTraderPaymentAccounts( + datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000, + paymentMethodComboBox.getSelectionModel().getSelectedItem(), + arbitrationManager.getDisputesAsObservableList()))); + + headLineLabel.setText(Res.get("popup.accountSigning.confirmSelectedAccounts.headline")); + descriptionLabel.setText(Res.get("popup.accountSigning.confirmSelectedAccounts.description", + selectedPaymentAccountsList.getItems().size())); + ((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.confirmSelectedAccounts.button")); + + actionButton.setOnAction(e -> addAccountsToSignContent()); + + selectedPaymentAccountsList.setCellFactory(new Callback<>() { + @Override + public ListCell call( + ListView param) { + return new ListCell<>() { + @Override + protected void updateItem(TraderDataItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + setText(item.getPaymentAccountPayload().toString()); + } else { + setText(null); + } + } + }; + } + }); + } + + private void addAccountsToSignContent() { + removeContent(); + addECKeyField(); + + headLineLabel.setText(Res.get("popup.accountSigning.signAccounts.headline")); + descriptionLabel.setText(Res.get("popup.accountSigning.signAccounts.description", selectedPaymentAccountsList.getItems().size())); + ((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.signAccounts.button")); + actionButton.setOnAction(a -> { + ECKey arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText()); + if (arbitratorKey != null) { + String arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey()); + boolean isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex); + if (isKeyValid) { + selectedPaymentAccountsList.getItems().forEach(item -> { + accountAgeWitnessService.arbitratorSignAccountAgeWitness(item.getTradeAmount(), item.getAccountAgeWitness(), arbitratorKey, item.getPeersPubKey()); + }); + addSuccessContent(); + } + } else { + new Popup<>().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(() -> hide()).show(); + } + + }); + } + + private void addSuccessContent() { + removeContent(); + GridPane.setVgrow(descriptionLabel, Priority.ALWAYS); + GridPane.setValignment(descriptionLabel, VPos.TOP); + + closeButton.setVisible(false); + closeButton.setManaged(false); + headLineLabel.setText(Res.get("popup.accountSigning.success.headline")); + descriptionLabel.setText(Res.get("popup.accountSigning.success.description", selectedPaymentAccountsList.getItems().size())); + ((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok")); + actionButton.setOnAction(a -> { + hide(); + }); + } + + @Override + protected void addButtons() { + + Tuple2 buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex, + Res.get("popup.accountSigning.selectAccounts.headline"), Res.get("shared.cancel")); + + actionButton = buttonTuple.first; + actionButton.setDisable(true); + actionButton.setOnAction(e -> addSelectedAccountsContent()); + + closeButton = (AutoTooltipButton) buttonTuple.second; + closeButton.setOnAction(e -> hide()); + + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java index 0f05ce75b22..bac108ae2c2 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TacWindow.java @@ -52,13 +52,12 @@ public TacWindow() { this.width = primaryScreenBoundsWidth * 0.8; log.warn("Very small screen: primaryScreenBounds=" + primaryScreenBounds.toString()); } else { - width = 968; + width = 1100; } } @Override public void show() { - //noinspection ConstantConditions headLine(Res.get("tacWindow.headline")); // We do not translate the tacs because of the legal nature. We would need translations checked by lawyers @@ -88,15 +87,20 @@ public void show() { " - You must complete trades within the maximum duration specified for each payment method.\n" + " - You must enter the trade ID in the \"reason for payment\" text field when doing the fiat payment transfer.\n" + " - If the bank of the fiat sender charges fees, the sender (" + Res.getBaseCurrencyCode() + " buyer) has to cover the fees.\n" + - " - You must cooperate with the arbitrator during the arbitration process.\n" + - " - You must reply within 48 hours to each arbitrator inquiry.\n" + + " - You must cooperate with the mediator during the mediation process.\n" + + " - You must reply within 48 hours to each mediator inquiry.\n" + + " - If mediation does not lead to a payout by consensus of both traders the traders can open arbitration after 2 weeks.\n" + + " - Opening a refund request from arbitrators will trigger publishing the delayed payout transaction where the funds from the deposit transaction are sent to the Bisq DAO receiver address as collateral. The arbitrator will refund the traders according to his judgement.\n" + + " - Opening a refund request from arbitrators should be used only if the trade peer is not reacting or the trader considers the mediators suggested payout as unfair. " + + "If the arbitrator comes to the same conclusion as the mediator he will take a part of the payout from the trader who opened the dispute for covering his efforts.\n" + + " - The arbitrator will make a reimbursement request to the Bisq DAO to get refunded for the funds he paid out to traders in refund requests.\n" + " - Failure to follow the above requirements may result in loss of your security deposit.\n\n" + "For more details and a general overview please read the full documentation about the " + "arbitration system and the dispute process."; message(text); actionButtonText(Res.get("tacWindow.agree")); closeButtonText(Res.get("tacWindow.disagree")); - onClose(BisqApp.getShutDownHandler()::run); + onClose(BisqApp.getShutDownHandler()); super.show(); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java index 334a8517708..82a0a1a1383 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java @@ -149,6 +149,8 @@ String getState(ClosedTradableListItem item) { return Res.get("portfolio.closed.ticketClosed"); } else if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED) { return Res.get("portfolio.closed.mediationTicketClosed"); + } else if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_CLOSED) { + return Res.get("portfolio.closed.ticketClosed"); } else { log.error("That must not happen. We got a pending state but we are in the closed trades list. state={}", trade.getState().toString()); return Res.get("shared.na"); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 8a9ce8f5d86..1aaf7319963 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -33,18 +33,22 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeAlreadyOpenException; import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; -import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.messages.ChatMessage; import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.BuyerTrade; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.user.Preferences; +import bisq.core.util.BSFormatter; import bisq.network.p2p.P2PService; @@ -84,8 +88,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { public final TradeManager tradeManager; public final BtcWalletService btcWalletService; - public final ArbitrationManager arbitrationManager; public final MediationManager mediationManager; + public final RefundManager refundManager; private final P2PService p2PService; private final WalletsSetup walletsSetup; @Getter @@ -100,6 +104,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { final ObjectProperty selectedItemProperty = new SimpleObjectProperty<>(); public final StringProperty txId = new SimpleStringProperty(); + @Getter private final TraderChatManager traderChatManager; public final Preferences preferences; @@ -117,8 +122,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { public PendingTradesDataModel(TradeManager tradeManager, BtcWalletService btcWalletService, PubKeyRing pubKeyRing, - ArbitrationManager arbitrationManager, MediationManager mediationManager, + RefundManager refundManager, TraderChatManager traderChatManager, Preferences preferences, P2PService p2PService, @@ -130,8 +135,8 @@ public PendingTradesDataModel(TradeManager tradeManager, this.tradeManager = tradeManager; this.btcWalletService = btcWalletService; this.pubKeyRing = pubKeyRing; - this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; + this.refundManager = refundManager; this.traderChatManager = traderChatManager; this.preferences = preferences; this.p2PService = p2PService; @@ -502,17 +507,13 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { Trade.DisputeState disputeState = trade.getDisputeState(); DisputeManager> disputeManager; boolean useMediation; - boolean useArbitration; - // If mediation is not activated we use arbitration - if (MediationManager.isMediationActivated()) { - // In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or - useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED; - // in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED - useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED; - } else { - useMediation = false; - useArbitration = true; - } + boolean useRefundAgent; + // In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED + useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED; + // In case we re-open a dispute we allow Trade.DisputeState.REFUND_REQUESTED + useRefundAgent = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.REFUND_REQUESTED; + + ResultHandler resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class); if (useMediation) { // If no dispute state set we start with mediation disputeManager = mediationManager; @@ -537,15 +538,49 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { trade.getMakerContractSignature(), trade.getTakerContractSignature(), mediatorPubKeyRing, - isSupportTicket); + isSupportTicket, + SupportType.MEDIATION); trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); - sendOpenNewDisputeMessage(dispute, false, disputeManager); - } else if (useArbitration) { - // Only if we have completed mediation we allow arbitration - disputeManager = arbitrationManager; - PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); - checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); + disputeManager.sendOpenNewDisputeMessage(dispute, + false, + resultHandler, + (errorMessage, throwable) -> { + if ((throwable instanceof DisputeAlreadyOpenException)) { + errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); + new Popup<>().warning(errorMessage) + .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) + .onAction(() -> disputeManager.sendOpenNewDisputeMessage(dispute, + true, + resultHandler, + (e, t) -> { + log.error(e); + })) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + new Popup<>().warning(errorMessage).show(); + } + }); + } else if (useRefundAgent) { + if (trade.getDelayedPayoutTx() == null) { + return; + } + + long lockTime = trade.getDelayedPayoutTx().getLockTime(); + int bestChainHeight = btcWalletService.getBestChainHeight(); + long remaining = lockTime - bestChainHeight; + if (remaining > 0) { + new Popup<>() + .instruction(Res.get("portfolio.pending.timeLockNotOver", + BSFormatter.getDateFromBlockHeight(remaining), remaining)) + .show(); + return; + } + + disputeManager = refundManager; + PubKeyRing refundAgentPubKeyRing = trade.getRefundAgentPubKeyRing(); + checkNotNull(refundAgentPubKeyRing, "refundAgentPubKeyRing must not be null"); byte[] depositTxSerialized = depositTx.bitcoinSerialize(); String depositTxHashAsString = depositTx.getHashAsString(); Dispute dispute = new Dispute(disputeManager.getStorage(), @@ -564,36 +599,59 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { trade.getContractAsJson(), trade.getMakerContractSignature(), trade.getTakerContractSignature(), - arbitratorPubKeyRing, - isSupportTicket); + refundAgentPubKeyRing, + isSupportTicket, + SupportType.REFUND); + + String tradeId = dispute.getTradeId(); + mediationManager.findDispute(tradeId) + .ifPresent(mediatorsDispute -> { + DisputeResult mediatorsDisputeResult = mediatorsDispute.getDisputeResultProperty().get(); + ChatMessage mediatorsResultMessage = mediatorsDisputeResult.getChatMessage(); + if (mediatorsResultMessage != null) { + String mediatorAddress = Res.get("support.mediatorsAddress", + mediatorsDispute.getContract().getRefundAgentNodeAddress().getFullAddress()); + String message = mediatorAddress + "\n\n" + mediatorsResultMessage.getMessage(); + dispute.setMediatorsDisputeResult(message); + } + }); + + trade.setDisputeState(Trade.DisputeState.REFUND_REQUESTED); + + //todo add UI spinner as it can take a bit if peer is offline + tradeManager.publishDelayedPayoutTx(tradeId, + () -> { + log.info("DelayedPayoutTx published and message sent to peer"); + disputeManager.sendOpenNewDisputeMessage(dispute, + false, + resultHandler, + (errorMessage, throwable) -> { + if ((throwable instanceof DisputeAlreadyOpenException)) { + errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); + new Popup<>().warning(errorMessage) + .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) + .onAction(() -> disputeManager.sendOpenNewDisputeMessage(dispute, + true, + resultHandler, + (e, t) -> { + log.error(e); + })) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + new Popup<>().warning(errorMessage).show(); + } + }); + }, + errorMessage -> { + new Popup<>().error(errorMessage).show(); + }); - trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - sendOpenNewDisputeMessage(dispute, false, disputeManager); } else { log.warn("Invalid dispute state {}", disputeState.name()); } } - private void sendOpenNewDisputeMessage(Dispute dispute, - boolean reOpen, - DisputeManager> disputeManager) { - disputeManager.sendOpenNewDisputeMessage(dispute, - reOpen, - () -> navigation.navigateTo(MainView.class, SupportView.class), - (errorMessage, throwable) -> { - if ((throwable instanceof DisputeAlreadyOpenException)) { - errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); - new Popup<>().warning(errorMessage) - .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) - .onAction(() -> sendOpenNewDisputeMessage(dispute, true, disputeManager)) - .closeButtonText(Res.get("shared.cancel")) - .show(); - } else { - new Popup<>().warning(errorMessage).show(); - } - }); - } - public boolean isReadyForTxBroadcast() { return GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index baba88b46a9..a58583accfc 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -368,7 +368,7 @@ private void openChat(Trade trade) { trade.stateProperty().addListener(tradeStateListener); disputeStateListener = (observable, oldValue, newValue) -> { - if (newValue == Trade.DisputeState.DISPUTE_CLOSED) { + if (newValue == Trade.DisputeState.DISPUTE_CLOSED || newValue == Trade.DisputeState.REFUND_REQUEST_CLOSED) { chatPopupStage.hide(); } }; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 34366ac4d66..e4001bae2a7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -22,6 +22,7 @@ import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; +import bisq.core.account.witness.AccountAgeWitness; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Res; import bisq.core.network.MessageState; @@ -58,6 +59,7 @@ import javax.annotation.Nullable; import static bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel.SellerState.UNDEFINED; +import static com.google.common.base.Preconditions.checkNotNull; public class PendingTradesViewModel extends ActivatableWithDataModel implements ViewModel { @@ -341,6 +343,28 @@ public int getNumPastTrades(Trade trade) { .size(); } + /////////////////////////////////////////////////////////////////////////////////////////// + // AccountAgeWitness signing + /////////////////////////////////////////////////////////////////////////////////////////// + + + public boolean isSignWitnessTrade(boolean asSeller) { + checkNotNull(trade, "trade must not be null"); + checkNotNull(trade.getOffer(), "offer must not be null"); + AccountAgeWitness myWitness = accountAgeWitnessService.getMyWitness(asSeller ? + dataModel.getSellersPaymentAccountPayload() : + dataModel.getBuyersPaymentAccountPayload()); + + return accountAgeWitnessService.accountIsSigner(myWitness) && + !accountAgeWitnessService.peerHasSignedWitness(trade); + } + + public void maybeSignWitness(boolean asSeller) { + if (isSignWitnessTrade(asSeller)) { + accountAgeWitnessService.traderSignPeersAccountAgeWitness(trade); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// @@ -378,20 +402,20 @@ private void onTradeStateChanged(Trade.State tradeState) { // #################### Phase DEPOSIT_PAID - case TAKER_PUBLISHED_DEPOSIT_TX: + case SELLER_PUBLISHED_DEPOSIT_TX: // DEPOSIT_TX_PUBLISHED_MSG - // taker perspective - case TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG: - case TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG: - case TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG: - case TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG: + // seller perspective + case SELLER_SENT_DEPOSIT_TX_PUBLISHED_MSG: + case SELLER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG: + case SELLER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG: + case SELLER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG: - // maker perspective - case MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG: + // buyer perspective + case BUYER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG: // Alternatively the maker could have seen the deposit tx earlier before he received the DEPOSIT_TX_PUBLISHED_MSG - case MAKER_SAW_DEPOSIT_TX_IN_NETWORK: + case BUYER_SAW_DEPOSIT_TX_IN_NETWORK: buyerState.set(BuyerState.STEP1); sellerState.set(SellerState.STEP1); break; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java index a04dc306ee1..1b3f1f1c349 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java @@ -21,7 +21,6 @@ import bisq.desktop.components.TitledGroupBg; import bisq.core.locale.Res; -import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.trade.Trade; import javafx.scene.control.Label; @@ -51,6 +50,8 @@ public enum State { MEDIATION_RESULT_PEER_ACCEPTED, IN_ARBITRATION_SELF_REQUESTED, IN_ARBITRATION_PEER_REQUESTED, + IN_REFUND_REQUEST_SELF_REQUESTED, + IN_REFUND_REQUEST_PEER_REQUESTED, WARN_HALF_PERIOD, WARN_PERIOD_OVER, TRADE_COMPLETED @@ -102,8 +103,7 @@ public void setState(State state) { case SHOW_GET_HELP_BUTTON: // grey button titledGroupBg.setText(Res.get("portfolio.pending.support.headline.getHelp")); - label.setText(MediationManager.isMediationActivated() ? - Res.get("portfolio.pending.support.text.getHelp") : Res.get("portfolio.pending.support.text.getHelp.arbitrator")); + label.setText(Res.get("portfolio.pending.support.text.getHelp")); button.setText(Res.get("portfolio.pending.support.button.getHelp").toUpperCase()); button.setId(null); button.getStyleClass().remove("action-button"); @@ -154,20 +154,20 @@ public void setState(State state) { button.getStyleClass().add("action-button"); button.setDisable(false); break; - case IN_ARBITRATION_SELF_REQUESTED: + case IN_REFUND_REQUEST_SELF_REQUESTED: // red button - titledGroupBg.setText(Res.get("portfolio.pending.arbitrationRequested")); + titledGroupBg.setText(Res.get("portfolio.pending.refundRequested")); label.setText(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithArbitrator"))); - button.setText(Res.get("portfolio.pending.arbitrationRequested").toUpperCase()); + button.setText(Res.get("portfolio.pending.refundRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(true); break; - case IN_ARBITRATION_PEER_REQUESTED: + case IN_REFUND_REQUEST_PEER_REQUESTED: // red button - titledGroupBg.setText(Res.get("portfolio.pending.arbitrationRequested")); + titledGroupBg.setText(Res.get("portfolio.pending.refundRequested")); label.setText(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator"))); - button.setText(Res.get("portfolio.pending.arbitrationRequested").toUpperCase()); + button.setText(Res.get("portfolio.pending.refundRequested").toUpperCase()); button.setId("open-dispute-button"); button.getStyleClass().remove("action-button"); button.setDisable(true); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 1f3ad13cb38..0d3f9c80ebd 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -29,10 +29,10 @@ import bisq.core.locale.Res; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeResult; -import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.user.Preferences; +import bisq.core.util.BSFormatter; import bisq.network.p2p.BootstrapListener; @@ -174,8 +174,7 @@ public void activate() { if (!isMediationClosedState()) { tradeStepInfo.setOnAction(e -> { - new Popup<>().attention(MediationManager.isMediationActivated() ? - Res.get("portfolio.pending.support.popup.info") : Res.get("portfolio.pending.support.popup.info.arbitrator")) + new Popup<>().attention(Res.get("portfolio.pending.support.popup.info")) .actionButtonText(Res.get("portfolio.pending.support.popup.button")) .onAction(this::openSupportTicket) .closeButtonText(Res.get("shared.cancel")) @@ -388,33 +387,6 @@ private void updateDisputeState(Trade.DisputeState disputeState) { switch (disputeState) { case NO_DISPUTE: break; - case DISPUTE_REQUESTED: - if (tradeStepInfo != null) { - tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); - } - applyOnDisputeOpened(); - - ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); - ownDispute.ifPresent(dispute -> { - if (tradeStepInfo != null) - tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); - }); - - break; - case DISPUTE_STARTED_BY_PEER: - if (tradeStepInfo != null) { - tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); - } - applyOnDisputeOpened(); - - ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); - ownDispute.ifPresent(dispute -> { - if (tradeStepInfo != null) - tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); - }); - break; - case DISPUTE_CLOSED: - break; case MEDIATION_REQUESTED: if (tradeStepInfo != null) { tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); @@ -456,15 +428,45 @@ private void updateDisputeState(Trade.DisputeState disputeState) { updateMediationResultState(); break; + case REFUND_REQUESTED: + deactivatePaymentButtons(true); + if (tradeStepInfo != null) { + tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); + } + applyOnDisputeOpened(); + + ownDispute = model.dataModel.refundManager.findOwnDispute(trade.getId()); + ownDispute.ifPresent(dispute -> { + if (tradeStepInfo != null) + tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED); + }); + + break; + case REFUND_REQUEST_STARTED_BY_PEER: + deactivatePaymentButtons(true); + if (tradeStepInfo != null) { + tradeStepInfo.setFirstHalfOverWarnTextSupplier(this::getFirstHalfOverWarnText); + } + applyOnDisputeOpened(); + + ownDispute = model.dataModel.refundManager.findOwnDispute(trade.getId()); + ownDispute.ifPresent(dispute -> { + if (tradeStepInfo != null) + tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); + }); + break; + case REFUND_REQUEST_CLOSED: + deactivatePaymentButtons(true); + break; } } private void updateMediationResultState() { if (isInArbitration()) { - if (isArbitrationStartedByPeer()) { - tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); - } else if (isArbitrationSelfStarted()) { - tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); + if (isRefundRequestStartedByPeer()) { + tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); + } else if (isRefundRequestSelfStarted()) { + tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED); } } else if (isMediationClosedState()) { // We do not use the state itself as it is not guaranteed the last state reflects relevant information @@ -485,15 +487,15 @@ private void updateMediationResultState() { } private boolean isInArbitration() { - return isArbitrationStartedByPeer() || isArbitrationSelfStarted(); + return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted(); } - private boolean isArbitrationStartedByPeer() { - return trade.getDisputeState() == Trade.DisputeState.DISPUTE_STARTED_BY_PEER; + private boolean isRefundRequestStartedByPeer() { + return trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; } - private boolean isArbitrationSelfStarted() { - return trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED; + private boolean isRefundRequestSelfStarted() { + return trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED; } private boolean isMediationClosedState() { @@ -530,10 +532,18 @@ private void openMediationResultPopup(String headLine) { String myPayoutAmount = isMyRoleBuyer ? buyerPayoutAmount : sellerPayoutAmount; String peersPayoutAmount = isMyRoleBuyer ? sellerPayoutAmount : buyerPayoutAmount; + checkNotNull(trade.getDelayedPayoutTx(), + "trade.getDelayedPayoutTx() must not be null at openMediationResultPopup"); + long lockTime = trade.getDelayedPayoutTx().getLockTime(); + int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight(); + long remaining = lockTime - bestChainHeight; acceptMediationResultPopup = new Popup<>().width(900) .headLine(headLine) .instruction(Res.get("portfolio.pending.mediationResult.popup.info", - myPayoutAmount, peersPayoutAmount)) + myPayoutAmount, + peersPayoutAmount, + BSFormatter.getDateFromBlockHeight(remaining), + lockTime)) .actionButtonText(Res.get("shared.accept")) .onAction(() -> { model.dataModel.mediationManager.acceptMediationResult(trade, diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index dcc1db013f9..30deb5fb4f5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -120,6 +120,12 @@ protected void addContent() { withdrawAddressTextField.setManaged(false); withdrawAddressTextField.setVisible(false); + if (model.isSignWitnessTrade(false)) { + Label signLabel = new Label(Res.get("portfolio.pending.step5_buyer.signer")); + GridPane.setRowIndex(signLabel, ++gridRow); + gridPane.getChildren().add(signLabel); + } + HBox hBox = new HBox(); hBox.setSpacing(10); useSavingsWalletButton = new AutoTooltipButton(Res.get("portfolio.pending.step5_buyer.moveToBisqWallet")); @@ -135,10 +141,14 @@ protected void addContent() { gridPane.getChildren().add(hBox); useSavingsWalletButton.setOnAction(e -> { + model.maybeSignWitness(false); handleTradeCompleted(); model.dataModel.tradeManager.addTradeToClosedTrades(trade); }); - withdrawToExternalWalletButton.setOnAction(e -> onWithdrawal()); + withdrawToExternalWalletButton.setOnAction(e -> { + model.maybeSignWitness(false); + onWithdrawal(); + }); String key = "tradeCompleted" + trade.getId(); if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index b29d884be0c..284bbad185e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -294,6 +294,9 @@ private void onPaymentReceived() { } } message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.note"); + if (model.isSignWitnessTrade(true)) { + message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer"); + } new Popup<>() .headLine(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.headline")) .confirmation(message) @@ -363,6 +366,8 @@ private void confirmPaymentReceived() { if (!trade.isPayoutPublished()) trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT); + model.maybeSignWitness(true); + model.dataModel.onFiatPaymentReceived(() -> { // In case the first send failed we got the support button displayed. // If it succeeds at a second try we remove the support button again. diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 8da003572d5..d8233b5589f 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -103,7 +103,6 @@ public class PreferencesView extends ActivatableViewAndModel userLanguageComboBox; private ComboBox userCountryComboBox; private ComboBox preferredTradeCurrencyComboBox; - //private ComboBox selectBaseCurrencyNetworkComboBox; private ToggleButton showOwnOffersInOfferBook, useAnimations, useDarkMode, sortMarketCurrenciesNumerically, avoidStandbyMode, useCustomFee; @@ -228,26 +227,6 @@ private void initializeGeneralOptions() { TitledGroupBg titledGroupBg = addTitledGroupBg(root, gridRow, 8, Res.get("setting.preferences.general")); GridPane.setColumnSpan(titledGroupBg, 1); - // selectBaseCurrencyNetwork - /* selectBaseCurrencyNetworkComboBox = FormBuilder.addComboBox(root, gridRow, - Res.get("settings.preferences.selectCurrencyNetwork"), Layout.FIRST_ROW_DISTANCE); - - selectBaseCurrencyNetworkComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("settings.preferences.selectCurrencyNetwork"), - selectBaseCurrencyNetworkComboBox, false)); - selectBaseCurrencyNetworkComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(BaseCurrencyNetwork baseCurrencyNetwork) { - return baseCurrencyNetwork != null ? - Res.get(baseCurrencyNetwork.name()) : - Res.get("na"); - } - - @Override - public BaseCurrencyNetwork fromString(String string) { - return null; - } - });*/ - userLanguageComboBox = addComboBox(root, gridRow, Res.get("shared.language"), Layout.FIRST_ROW_DISTANCE); userCountryComboBox = addComboBox(root, ++gridRow, diff --git a/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml b/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml index 7c5fc259ac0..b26ee6db59a 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml @@ -26,6 +26,7 @@ xmlns:fx="http://javafx.com/fxml"> + diff --git a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java index b7fc867bcd8..ac314e66c0c 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java @@ -28,8 +28,10 @@ import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.support.dispute.agent.arbitration.ArbitratorView; import bisq.desktop.main.support.dispute.agent.mediation.MediatorView; +import bisq.desktop.main.support.dispute.agent.refund.RefundAgentView; import bisq.desktop.main.support.dispute.client.arbitration.ArbitrationClientView; import bisq.desktop.main.support.dispute.client.mediation.MediationClientView; +import bisq.desktop.main.support.dispute.client.refund.RefundClientView; import bisq.core.locale.Res; import bisq.core.support.dispute.arbitration.ArbitrationManager; @@ -38,6 +40,9 @@ import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgent; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.network.p2p.NodeAddress; @@ -60,15 +65,17 @@ public class SupportView extends ActivatableViewAndModel { @FXML - Tab tradersArbitrationDisputesTab, tradersMediationDisputesTab; + Tab tradersMediationDisputesTab, tradersRefundDisputesTab, tradersArbitrationDisputesTab; - private Tab arbitratorTab, mediatorTab; + private Tab arbitratorTab, mediatorTab, refundAgentTab; private final Navigation navigation; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; + private final RefundManager refundManager; private final KeyRing keyRing; private Navigation.Listener navigationListener; @@ -77,21 +84,26 @@ public class SupportView extends ActivatableViewAndModel { private final ViewLoader viewLoader; private MapChangeListener arbitratorMapChangeListener; private MapChangeListener mediatorMapChangeListener; + private MapChangeListener refundAgentMapChangeListener; @Inject public SupportView(CachingViewLoader viewLoader, Navigation navigation, ArbitratorManager arbitratorManager, MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, ArbitrationManager arbitrationManager, MediationManager mediationManager, + RefundManager refundManager, KeyRing keyRing) { this.viewLoader = viewLoader; this.navigation = navigation; this.arbitratorManager = arbitratorManager; this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; + this.refundManager = refundManager; this.keyRing = keyRing; } @@ -100,8 +112,9 @@ public void initialize() { // has to be called before loadView updateAgentTabs(); - tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support").toUpperCase()); tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support").toUpperCase()); + tradersRefundDisputesTab.setText(Res.get("support.tab.refund.support").toUpperCase()); + tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support").toUpperCase()); navigationListener = viewPath -> { if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1) loadView(viewPath.tip()); @@ -112,24 +125,26 @@ public void initialize() { navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); else if (newValue == tradersMediationDisputesTab) navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); + else if (newValue == tradersRefundDisputesTab) + navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); else if (newValue == arbitratorTab) navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); else if (newValue == mediatorTab) navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); + else if (newValue == refundAgentTab) + navigation.navigateTo(MainView.class, SupportView.class, RefundAgentView.class); }; arbitratorMapChangeListener = change -> updateAgentTabs(); mediatorMapChangeListener = change -> updateAgentTabs(); - + refundAgentMapChangeListener = change -> updateAgentTabs(); } private void updateAgentTabs() { PubKeyRing myPubKeyRing = keyRing.getPubKeyRing(); + boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream() .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); - boolean isActiveMediator = mediatorManager.getObservableMap().values().stream() - .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); - if (arbitratorTab == null) { // In case a arbitrator has become inactive he still might get disputes from pending trades boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() @@ -140,6 +155,9 @@ private void updateAgentTabs() { root.getTabs().add(arbitratorTab); } } + + boolean isActiveMediator = mediatorManager.getObservableMap().values().stream() + .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); if (mediatorTab == null) { // In case a mediator has become inactive he still might get disputes from pending trades boolean hasDisputesAsMediator = mediationManager.getDisputesAsObservableList().stream() @@ -151,6 +169,19 @@ private void updateAgentTabs() { } } + boolean isActiveRefundAgent = refundAgentManager.getObservableMap().values().stream() + .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); + if (refundAgentTab == null) { + // In case a refundAgent has become inactive he still might get disputes from pending trades + boolean hasDisputesAsRefundAgent = refundManager.getDisputesAsObservableList().stream() + .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); + if (isActiveRefundAgent || hasDisputesAsRefundAgent) { + refundAgentTab = new Tab(); + refundAgentTab.setClosable(false); + root.getTabs().add(refundAgentTab); + } + } + // We might get that method called before we have the map is filled in the arbitratorManager if (arbitratorTab != null) { arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator2")).toUpperCase()); @@ -158,6 +189,9 @@ private void updateAgentTabs() { if (mediatorTab != null) { mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator")).toUpperCase()); } + if (refundAgentTab != null) { + refundAgentTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.refundAgent")).toUpperCase()); + } } @Override @@ -168,6 +202,9 @@ protected void activate() { mediatorManager.updateMap(); mediatorManager.getObservableMap().addListener(mediatorMapChangeListener); + refundAgentManager.updateMap(); + refundAgentManager.getObservableMap().addListener(refundAgentMapChangeListener); + updateAgentTabs(); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); @@ -177,10 +214,14 @@ protected void activate() { navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); } else if (root.getSelectionModel().getSelectedItem() == tradersArbitrationDisputesTab) { navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); + } else if (root.getSelectionModel().getSelectedItem() == tradersRefundDisputesTab) { + navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); } else if (arbitratorTab != null) { navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); } else if (mediatorTab != null) { navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); + } else if (refundAgentTab != null) { + navigation.navigateTo(MainView.class, SupportView.class, RefundAgentView.class); } String key = "supportInfo"; @@ -195,6 +236,7 @@ protected void activate() { protected void deactivate() { arbitratorManager.getObservableMap().removeListener(arbitratorMapChangeListener); mediatorManager.getObservableMap().removeListener(mediatorMapChangeListener); + refundAgentManager.getObservableMap().removeListener(refundAgentMapChangeListener); root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); navigation.removeListener(navigationListener); currentTab = null; @@ -211,10 +253,14 @@ private void loadView(Class viewClass) { currentTab = tradersMediationDisputesTab; } else if (view instanceof ArbitrationClientView) { currentTab = tradersArbitrationDisputesTab; + } else if (view instanceof RefundClientView) { + currentTab = tradersRefundDisputesTab; } else if (view instanceof ArbitratorView) { currentTab = arbitratorTab; } else if (view instanceof MediatorView) { currentTab = mediatorTab; + } else if (view instanceof RefundAgentView) { + currentTab = refundAgentTab; } else { currentTab = null; } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 6922b560560..eacf362d27c 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -124,7 +124,7 @@ public abstract class DisputeView extends ActivatableView { private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; - private EventHandler keyEventEventHandler; + protected EventHandler keyEventEventHandler; private Scene scene; protected FilteredList filteredList; private InputTextField filterTextField; @@ -306,6 +306,8 @@ public void initialize() { .onAddAlertMessage(privateNotificationManager::sendPrivateNotificationMessageIfKeyIsValid) .show(); } + } else { + handleKeyPressed(event); } }; @@ -469,6 +471,9 @@ protected void onCloseDispute(Dispute dispute) { } } + protected void handleKeyPressed(KeyEvent event) { + } + /////////////////////////////////////////////////////////////////////////////////////////// // Table /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java index c84baa62704..a173b90fdee 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java @@ -17,9 +17,11 @@ package bisq.desktop.main.support.dispute.agent.arbitration; +import bisq.common.util.Utilities; import bisq.desktop.common.view.FxmlView; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.SignPaymentAccountsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.support.dispute.agent.DisputeAgentView; @@ -37,12 +39,16 @@ import bisq.common.crypto.KeyRing; import com.google.inject.name.Named; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javax.inject.Inject; @FxmlView public class ArbitratorView extends DisputeAgentView { + private final SignPaymentAccountsWindow signPaymentAccountsWindow; + @Inject public ArbitratorView(ArbitrationManager arbitrationManager, KeyRing keyRing, @@ -53,7 +59,8 @@ public ArbitratorView(ArbitrationManager arbitrationManager, ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, - @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys, + SignPaymentAccountsWindow signPaymentAccountsWindow) { super(arbitrationManager, keyRing, tradeManager, @@ -64,6 +71,7 @@ public ArbitratorView(ArbitrationManager arbitrationManager, tradeDetailsWindow, accountAgeWitnessService, useDevPrivilegeKeys); + this.signPaymentAccountsWindow = signPaymentAccountsWindow; } @Override @@ -75,4 +83,11 @@ protected SupportType getType() { protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { return new ArbitrationSession(dispute, disputeManager.isTrader(dispute)); } + + @Override + protected void handleKeyPressed(KeyEvent event) { + if (Utilities.isAltOrCtrlPressed(KeyCode.S, event)) { + signPaymentAccountsWindow.show(); + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.fxml new file mode 100644 index 00000000000..71015c434b8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java new file mode 100644 index 00000000000..3f34b2b9c23 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java @@ -0,0 +1,78 @@ +/* + * 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.desktop.main.support.dispute.agent.refund; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.agent.DisputeAgentView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.AppOptionKeys; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.dispute.refund.RefundSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class RefundAgentView extends DisputeAgentView { + + @Inject + public RefundAgentView(RefundManager refundManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(refundManager, + keyRing, + tradeManager, + formatter, + disputeSummaryWindow, + privateNotificationManager, + contractWindow, + tradeDetailsWindow, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected SupportType getType() { + return SupportType.REFUND; + } + + @Override + protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { + return new RefundSession(dispute, disputeManager.isTrader(dispute)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.fxml new file mode 100644 index 00000000000..4d7b40d279a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java new file mode 100644 index 00000000000..91d5c75cd8c --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java @@ -0,0 +1,70 @@ +/* + * 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.desktop.main.support.dispute.client.refund; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.client.DisputeClientView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.AppOptionKeys; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.refund.RefundManager; +import bisq.core.support.dispute.refund.RefundSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class RefundClientView extends DisputeClientView { + @Inject + public RefundClientView(RefundManager refundManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow, + privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected SupportType getType() { + return SupportType.REFUND; + } + + @Override + protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { + return new RefundSession(dispute, disputeManager.isTrader(dispute)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index 28b90b771bf..80e76d7e9a9 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -2018,7 +2018,7 @@ public static void removeRowFromGridPane(GridPane gridPane, int gridRow) { public static void removeRowsFromGridPane(GridPane gridPane, int fromGridRow, int toGridRow) { Set nodes = new CopyOnWriteArraySet<>(gridPane.getChildren()); nodes.stream() - .filter(e -> GridPane.getRowIndex(e) >= fromGridRow && GridPane.getRowIndex(e) <= toGridRow) + .filter(e -> GridPane.getRowIndex(e) != null && GridPane.getRowIndex(e) >= fromGridRow && GridPane.getRowIndex(e) <= toGridRow) .forEach(e -> gridPane.getChildren().remove(e)); } diff --git a/desktop/src/test/java/bisq/desktop/MaterialDesignIconDemo.java b/desktop/src/test/java/bisq/desktop/MaterialDesignIconDemo.java new file mode 100644 index 00000000000..8ad84cce91f --- /dev/null +++ b/desktop/src/test/java/bisq/desktop/MaterialDesignIconDemo.java @@ -0,0 +1,63 @@ +/* + * 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.desktop; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; + +import javafx.application.Application; + +import javafx.stage.Stage; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.FlowPane; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class MaterialDesignIconDemo extends Application { + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + + FlowPane flowPane = new FlowPane(); + flowPane.setStyle("-fx-background-color: #ddd;"); + flowPane.setHgap(2); + flowPane.setVgap(2); + List values = new ArrayList<>(Arrays.asList(MaterialDesignIcon.values())); + values.sort(Comparator.comparing(Enum::name)); + for (MaterialDesignIcon icon : values) { + Button button = MaterialDesignIconFactory.get().createIconButton(icon, icon.name()); + flowPane.getChildren().add(button); + } + + scrollPane.setContent(flowPane); + + primaryStage.setScene(new Scene(scrollPane, 1200, 950)); + primaryStage.show(); + } +} diff --git a/desktop/src/test/java/bisq/desktop/MaterialDesignIconDemoLauncher.java b/desktop/src/test/java/bisq/desktop/MaterialDesignIconDemoLauncher.java new file mode 100644 index 00000000000..6b6cbafcbdf --- /dev/null +++ b/desktop/src/test/java/bisq/desktop/MaterialDesignIconDemoLauncher.java @@ -0,0 +1,26 @@ +/* + * 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.desktop; + +import javafx.application.Application; + +public class MaterialDesignIconDemoLauncher { + public static void main(String[] args) { + Application.launch(MaterialDesignIconDemo.class); + } +} diff --git a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java index 8b93668bc3f..86a62b28d24 100644 --- a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java +++ b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java @@ -17,8 +17,8 @@ package bisq.desktop.main.funds.transactions; -import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.offer.OpenOffer; +import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; @@ -34,7 +34,8 @@ public class TransactionAwareTradableFactoryTest { public void testCreateWhenNotOpenOfferOrTrade() { ArbitrationManager arbitrationManager = mock(ArbitrationManager.class); - TransactionAwareTradableFactory factory = new TransactionAwareTradableFactory(arbitrationManager); + TransactionAwareTradableFactory factory = new TransactionAwareTradableFactory(arbitrationManager, + null, null, null); Tradable delegate = mock(Tradable.class); assertFalse(delegate instanceof OpenOffer); diff --git a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java index 66d26038ecd..fb520f46d7f 100644 --- a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java +++ b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java @@ -19,6 +19,7 @@ import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Trade; import org.bitcoinj.core.Transaction; @@ -42,15 +43,17 @@ public class TransactionAwareTradeTest { private ArbitrationManager arbitrationManager; private Trade delegate; private TransactionAwareTradable trade; + private RefundManager refundManager; @Before public void setUp() { this.transaction = mock(Transaction.class); when(transaction.getHashAsString()).thenReturn(XID); - this.delegate = mock(Trade.class, RETURNS_DEEP_STUBS); - this.arbitrationManager = mock(ArbitrationManager.class, RETURNS_DEEP_STUBS); - this.trade = new TransactionAwareTrade(this.delegate, this.arbitrationManager); + delegate = mock(Trade.class, RETURNS_DEEP_STUBS); + arbitrationManager = mock(ArbitrationManager.class, RETURNS_DEEP_STUBS); + refundManager = mock(RefundManager.class, RETURNS_DEEP_STUBS); + trade = new TransactionAwareTrade(delegate, arbitrationManager, refundManager, null, null); } @Test diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 6fcb5950bac..6ffaf5cfbf7 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -99,7 +99,7 @@ public void setUp() { when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); - when(accountAgeWitnessService.getMyTradeLimit(any(), any())).thenReturn(100000000L); + when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L); when(preferences.getUserCountry()).thenReturn(new Country("ES", "Spain", null)); when(bsqFormatter.formatCoin(any())).thenReturn("0"); when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO); diff --git a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java index 776aa627a18..d95ba36bc70 100644 --- a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java @@ -84,7 +84,7 @@ public void setUp() { when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount); when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet()); when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); - when(accountAgeWitnessService.getMyTradeLimit(any(), any())).thenReturn(100000000L); + when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L); when(preferences.getUserCountry()).thenReturn(new Country("US", "United States", null)); when(bsqFormatter.formatCoin(any())).thenReturn("0"); when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO); diff --git a/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java b/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java index 679c1384591..4724bc7e56f 100644 --- a/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java +++ b/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java @@ -23,5 +23,6 @@ public enum AckMessageSourceType { TRADE_MESSAGE, ARBITRATION_MESSAGE, MEDIATION_MESSAGE, - TRADE_CHAT_MESSAGE + TRADE_CHAT_MESSAGE, + REFUND_MESSAGE }