Release/v1.2.0 (#3532)

* New trade protocol (#3333)

* Remove arbitration key, cleanup

* Add BuyerAsMakerProcessDepositTxAndDelayedPayoutTxMessage

* Adopt trade protocol

- Add handler for DepositTxAndDelayedPayoutTxMessage
- Change handler for DepositTxPublishedMessage
- Add MakerSetsLockTime
- Rename MakerProcessPayDepositRequest to MakerProcessPayDepositRequest
- Rename MakerSendPublishDepositTxRequest to MakerSendsProvideInputsForDepositTxMessage
- Rename DepositTxPublishedMessage to DelayedPayoutTxSignatureRequest
- Rename MakerProcessDepositTxPublishedMessage to MakerAsBuyerProcessSignDelayedPayoutTxMessage

* Remove arbitratorKey

* Add new classes

* Add new message classes

* Add new task classes

* Renamed classed (no functional change yet)

* Add lockTime

* Add delayedPayoutTxSignature field

* Add useReimbursementModel field

* Add new classes

* Add setting.preferences.useReimbursementModel

* Apply renamed classes (new classes not added yet)

* Add useReimbursementModel

* Add preferences param

* Add new methods, cleanup

* Add daoFacade param, apply renaming

* Add delayedPayoutTx, lockTime and delayedPayoutTxId

- Support daoFacade param

* Remove DirectMessage interface

* Rename emergencySignAndPublishPayoutTx method, add new one for 2of2 MS

* Apply new protocol

* Apply new protocol

* Add renaming (no functional change yet)

* Add new messages, apply renaming

* Remove unneeded P2SHMultiSigOutputScript

* Remove PREFERRED_PROJECT_CODE_STYLE

* Refactor: Rename class

* Use InputsForDepositTxRequest instead of TradeMessage in handleTakeOfferRequest

* Do not sign deposit tx if maker is seller

We change behaviour that the maker as seller does not send the pre
signed deposit tx to the taker as the seller has more to lose and he
wants to control the creation process of the delayed payout tx.

* Apply new trade protocol to seller as maker version

* Apply new trade protocol

Delayed payout tx are now working for all scenarios but we use a small
hack to get around an issue with not receiving confirmations and the
peers tx.
We add a tiny output to both peers, so we see the tx and confirmation.
Without that only the publisher sees the tx and confirmations are not
displayed. Need further work to get that working without that extra
outputs.

* Set TRADE_PROTOCOL_VERSION to 2

* Add PeerPublishedDelayedPayoutTxMessage

We need add the delayed payout tx to the wallet once the peer publishes
it. We will not see the confidence as we do not receive or sent funds
from our address. Same is with dispute payouts where one peer does not
receive anything. Then the confidence is not set. It seems that is a
restriction in BitcoinJ or it requires some extra handling. We set the
confidence indicator invisible in the dispute case and that might be an
acceptable option here as well.

* Add refund agent domain

* Add refundAgentNodeAddress

* Apply refund domain

* Add refund views

* Apply refundAgent domain

* Support refundAgent

* Remove useReimbursementModel field

We dont need in the offer anymore the decision if reimbursement or
arbitration is chosen.

* Apply refundAgent payout

* Handle tx info and balances

* Remove mediation activation

* Add new tac accepted flag for v1.2.0 and adjust text

* Fix params for test classes

* Signed witness trading (#3334)

* Added basic UI for account signing for arbitrators

* Add domain layer for signed account age witnesses (credits ManfredKarrer and oscarguindzberg)

* Remove testing gridlines

* Arbitrator sign accountAgeWitnesses

Automatically filter to only sign accounts that
- have chargeback risk
- bought BTC
- was winner in dispute

* Handle chargebackrisk by currency

* Check winners only for closed disputes

* Show sign status of paymentaccounts in AccountsView

* Rename service to accountAgeWitnessService

* Refactor: Move account signing helpers to AccountAgeWitnessService

* Refactor: rename hasSignedWitness to myHasSignedWitness

* Show if witness is signed in offerbook view

* Use witness sign age for age comparison

* Refactor: rename to isTaker... to isMyTaker...

* Allow trading with signed witnesses

* Use witness age for showing account age icon

* Move AccountAgeRestrictions into AccountAgeWitnessService

* Handle trade limit of unverified accounts as normal case

* Avoid optional as argument

* Set trade limit depending on trade direction

* Avoid optional arguments

* Add text for seller as signer

* Seller with signer privilege signs buyer witness

* Fix merge issues

* Remove explicit check for risky offers

* Remove sellers explicit account age check

* Add limit check based on common accountAgeWitness function

* Fix arbitrator key event handling

* Filter accounts on tradelimit instead of maturity

* Fix test

* Buyer sign seller account

Add SIGNED_ACCOUNT_AGE_WITNESS capability

* Fix checks for signing at end of trade

Get correct valid accounts for offer

* Rename BuyerDataItem -> TraderDataItem

* Arbitrator sign both parties in a buyer payout dispute

* Only sign unsigned accountAgeWitnesses

* Remove unused code

* Add demo for material design icons

* Use different account age limits for sell/buy

* Fix signing interface for arbitrator

* Add signing state column to offer book

* Add signing state to fiat accounts overview

* Add signing state to selected fiat account

* Fix popover padding

* Add account signing state to peer info popup

* Retrieve only unsigned witnesses for arbitrator to sign

* Accounts signed by arbitrators are signers

* Disable test due to travis issues

* Improve witness handling (#3342)

* Fix comparison

* Add display strings for witness sign state

* Fix immaturity check

* Use accountAgeWitness age for non risky payment methods

* Show information about non risky account types

* Fix peer info icon account age text

* Complete new trade protocol (#3340)

* Improve handling of adding tx to wallet

* Add delayedPayoutTx to dispute

* Fix test

* Use RECIPIENT_BTC_ADDRESS from DAO for trade fee

* Set lockTime to 10 days for altcoins, 20 days others.

- Devmode uses 1 block

* Fix params

* Update text

* Update docs

* Update logging

if (log.isDebugEnabled()) only matches if logLevel is debug not
if it is INFO

* Remove log

* Remove arbitrator checks

* Remove arbitrator address

- It works not if not legacy arbitrator is registered.
We cannot remove too much from arbitration as we would risk to break
account signing and display of old arbitration cases.
Though if testing time permits we should try to clean out more of
arbitration domain what is not needed anymore.

* Use account signing state in accounts view (#3365)

* Add account signing icons to signing state in account display

* Remove unnecessary "." that caused layout issues in the past

* Add additional warning in the received payment popup for account signer

* Fix Revolut padding issues for currencies

* Hide signing icon for non-high-risk payment methods

* Add correct icon state and info text for account signing state

* Remove not implemented notification part

* Test self signing witnesses

* Change verified account limit factor to 0.5

* Account Signing: Add information popups for signing state (#3374)

* Add account signing icons to signing state in account display

* Remove not implemented notification part

* Hide time since signing column when not needed

* Remove fiat rounding popup as feature was introduced a long time ago already

* Add information popups for new signed states (only shown once for user) and minor clean-ups

* Update core/src/main/resources/i18n/displayStrings.properties

Co-Authored-By: sqrrm <sqrrm@users.noreply.github.com>

* Account Signing: Improve signed state notificaton (#3388)

* Remove new badge from Altcoin instant feature

* Remove new badge from percentage user deposit feature

* Fix line break issues in received payment confirmation popup

* Check if received payload fulfills signing state condition and not any personal witness

* Show additional badge for account sections to guide user to check out new signing states

* Fix account signing state in offer book (#3390)

* Account Signing: Fix verified usage (#3392)

* Rename witnessHash -> accountAgeWitnessHash

* Add enum for SignedWitness verification method

* Fix usage of isValidAccountAgeWitness

* Revert icon for signstate change

* Account signing: add signing state to payment account selection (#3403)

* Clean up dead code parts

* Add account signing state to payment account selection

* Account signing: revert dev date setting for trusted accounts (#3404)

* Revert temporary value for dev testing

* Only enable button if there are accounts to be signed

* Add trade limit exceptions (#3406)

* Remove dead code

* Add trade limit exception for accounts signed by arbitrator

* Update translations to adapt to new unified delay (#3409)

* NTP: Fix a couple of UI issues in the New Trade Protocol (#3410)

* Add badge support for refund agent (new arbitrator) tickets

* Fix translation typo

* Clean up arbitrator issues in translation

* Only show refund agent label to support staff

Every user should still see this role as arbitration

* NTP: Improve differentiation between mediation and new arbitration (#3414)

* Clean up property exposure

* Improve differentiation between mediation and arbitration cases

* Go to new refund view if it is no mediation and not open mediation notification if refund is already in progress

* Don't sign filtered accounts

* NTP: merge with master (#3420)

* Temporarily disable onion host for @KanoczTomas's BTC node

* Add Ergo (ERG) without Bouncy Castle dependency.

See #3195.

* List CTSCoin (CTSC)

* Tweak the English name of Japan Bank Transfer payment method

* Add mediator prefix to trade statistics

* List Faircoin (FAIR)

* List uPlexa (UPX)

* Remove not used private methods from BisqEnvironment

* Add onInitP2pNetwork and onInitWallet to BisqSetupListener

- Rename BisqSetupCompleteListener to BisqSetupListener
- Add onInitP2pNetwork and onInitWallet to BisqSetupListener
- make onInitP2pNetwork and onInitWallet default so no impl. required

* Start server at onInitWallet and add wallet password handler

- Add onInitWallet to HttpApiMain and start http server there
- Add onRequestWalletPassword to BisqSetupListener
- Override setupHandlers in HttpApiHeadlessApp and adjust
setRequestWalletPasswordHandler (impl. missing)
- Add onRequestWalletPassword to HttpApiMain

* Add combination (Blockstream.info + Mempool.space) block explorer

* Revert "Temporarily disable onion host for @KanoczTomas's BTC node"

This reverts commit d3335208bb.

* Temporarily disable KanoczTomas btcnode on both onion and clearnet

* Refactor BisqApp - update scene size calculation

* Refactor BisqApp - update error popup message build

* Refactor BisqApp - move icon load into ImageUtil

* Remove unused Utilities

* Increase minimum TX fee to 2 sats/vByte to fix #3106 (#3387)

* Fix mistakes in English source (#3386)

* Fix broken placeholders

* Replace non existing pending trades screen with open trades screen

* Update core/src/main/resources/i18n/displayStrings.properties

Co-Authored-By: Steve Jain <mfiver@gmail.com>

* Update message in failed trade popup

* Refactor BisqEnvironment

* Account Signing: Improve arbitrator signing flow (#3421)

* Pre-select a point of time 2 months in the past

So all arbitrator signed payment accounts will have their limits lifted completely

* Only show payment methods with high chargeback risk to be signed

* Show connected Bitcoin network peer info

* List Ndau (XND)

- Official project URL: https://ndau.io/
- Official block explorer URL: https://explorer.service.ndau.tech

* List Animecoin (ANI)

* Apply rule to not allow BSQ outputs after BTC output for regular txs (#3413)

* Apply rule to not allow BSQ outputs after BTC output for regular txs

* Enforce exactly 1 BSQ output for vote reveal tx

* Fix missing balance and button state update

* Refactor isBtcOutputOfBurnFeeTx method and add comments and TODOs

No functional change.

* Handle asset listing fee in custom method

We need to enforce a BSQ change output

As this is just tx creation code it has no consequences for the hard
fork.

* Use getPreparedBurnFeeTxForAssetListing

* Update comments to not use dust output values

* Fix missing balance and button state update

* Use same method for asset listing fee and proof of burn

Use same method for asset listing fee and proof of burn as tx structure
is same.
Update comments to be more general.

* Use getPreparedProofOfBurnTx

* Require mandatory BSQ change output for proposal fee tx.

We had in the doc stated that we require a mandatory BSQ change output
but it was not enforced in the implementation, causing similar issues
as in Asset listing and proof of  burn txs.

* Add fix for not correctly handled issuance tx

* Use new method for issuance tx

// For issuance txs we also require a BSQ change output before the issuance output gets added. There was a
    // minor bug with the old version that multiple inputs would have caused an exception in case there was no
    // change output (e.g. inputs of 21 and 6 BSQ for BSQ fee of 21 BSQ would have caused that only 1 input was used
    // and then caused an error as we enforced a change output. This new version handles such cases correctly.

* Handle all possible blind vote fee transactions

* Move check for invalid opReturn output up

* Add dust check at final sign method

* Fix incorrect comments

* Refactor

- Remove requireChangeOutput param which is always false
- Remove method which is used only by one caller
- Cleanup

* Add comment

* Fix comments, rename methods

* Move code of isBlindVoteBurnedFeeOutput to isBtcOutputOfBurnFeeTx

* Update account signing strings for v1.2 release (#3435)

* Update account signing strings for v1.2 release

* Add minor corrections from ripcurlx review

* Adjust tradeLimitDueAccountAgeRestriction string

So that it describes why an account isn't signed (in general) instead of
why it wasn't signed by an arbitrator.

* Account Signing/NTP: More improvements and fixes (#3436)

* Select the the correct sub view when a dispute is created

* Require capability REFUND_AGENT to receive RefundAgent Messages

* Remove unused return type for account signing

* Add new feature popup for account signing and new trade protocol

* Return void from account signing

* Fix bug with not updating vote result table at vote result block

* NTP: improve backwards compatibility for mediation (#3439)

* Improve readability of offer update

* Add type safeguard for dispute lists

* Set not existing dispute support type for clients < 1.2.0 from message support type

* Enable handling of mediation cases for old trade protocol disputes in 1.2.0 clients

* Remove unnecessary forEach

* Use correct formatter and add missing value for placeholder

* Bump version number

* Add sign all checkbox. Fix list entry display (#3450)

* Add sign all checkbox. Fix list entry display

* Add summary to log and clipboard

* Use safe version for seednodes (#3452)

* Apply shutdown and memory check again

To not risk issues with the release and seed nodes we merge back the
old code base for handling memory check and shutdowns.
The newly added changes for cross connecting between seed nodes cause
out of memory issues and require more work and testing before it can be
used.

* Revert code change for periodic updates between seed nodes.

The periodic updates code caused out of memory issues and require more
work and testing before it can be used.

* Arbitrator republish signedWitnesses on startup (#3448)

* Arbitrator republish signedWitnesses on startup

* Keep republish internal to SignedWitnessService

* Improve new feature popup for ntp and account signing (#3453)

* Do not commit delayedPayoutTx to avoid publishing at restart

Fixes https://github.com/bisq-network/bisq/issues/3463

BitcoinJ publishes automatically committed transactions.
We committed it to the wallet to be able to access it later after a
restart. We stored the txId in Trade and used that to request the tx
from the wallet (as it was committed). Now we store the
bitcoin serialized bytes of the tx and do not commit the tx before
broadcasting it (if a trader opens refund agent ticket).

* [1.2.0] Update client resources (#3456)

* Update bitcoinj checkpoint file

* Update data stores

* Update translations

* [1.2.0] Improve new feature popup (#3465)

* Improve layout of new feature popup

* Extract external hyperlinks into component to make it easier to update

* Comment in necessary showAgain check

* Add Raspberry Pi to build process (#3466)

* Add Raspberry Pi to build process

* Rename deploy variable to improve readability

* Update informational prompt upon creating fiat account with account signing details (#3467)

* Update informational prompt upon creating fiat account with account signing details

* Fix wrong buyer limit for first 30 days

* Set delayedPayoutTxBytes when setting delayedPayoutTx

Fixes https://github.com/bisq-network/bisq/issues/3473

The delayedPayoutTx is not committed to the wallet as long it is not
published. The seller who creates the delayedPayoutTx has not stored the
delayedPayoutTxBytes which caused a nullpointer after restart.

* Minor updates (#3474)

* Remove unnecessary log statement

This seems to be a left over log statement from debugging.

* Use a small delay for MakerSetsLockTime on regtest

When testing on regtest, not in devmode, we want a relatively short
delay to be able to test the delay period.

* Clarify payment limits up to 30 days after signing

* Update RECIPIENT_BTC_ADDRESS for regtest (#3478)

Use an address that is owned by the regtest wallet in the dao-setup.zip
file. This allows for easily verifying BTC trading fees are sent to
this address correctly. Also, it helps verify spending of the time lock
payout.

* Remove btc nodes from Manfred Karrer (#3480)

* Avoid null objects (#3481)

* Avoid null objects

* Remove check for type

Historical data can be arbitration instead of mediation (arbitration
was fallback at last update), so we need to tolerate the incorrect type
here. Is only for tickets from pre 1.2.

* Display appropriate account age info header

Depending on charge back risk type, accounts should show
accountAgeWitness age or time since signing

* Set amount for delayed payout tx to 0 (#3471)

We have shown the spent funds from the deposit tx to the bisq donation
address before. But that was incorrect from the wallet perspective and
would have lead to incorrect summary of all transaction amounts. We set
it now to 0 as we are not spending funds nor receiving any in our wallet.

* Check for result phase at activate method

Fixes https://github.com/bisq-network/bisq/issues/3487

* Only show warning for risky payment menthods (#3497)

* Fix style issues with dark mode (#3495)

* Addresses issues mentioned in https://github.com/bisq-network/bisq/issues/3482#issuecomment-546812730 (#3496)

* Clean up trade statistics from duplicate entries (#3476)

* Clean up trade statistics from duplicate entries

At software updates we added new entries to the extraMap which caused
duplicate entries (if one if the traders was on the new and the other on
the old version or at republishing). We set it now json exclude so avoid
that in future and clean up the map.

* Avoid repeated calls to addPersistableNetworkPayloadFromInitialRequest

For trade stat cleanup we don't want to apply it multiple times as it
is a bit expensive. We get from each seed node the initial data response
and would pollute with the second response our map again and if our node
is a seed node, the seed node itself could not get into a clean state and
would continue pollution other nodes.

* Refactor

Remove not used param
Rename method
Inline method
Cleanups

* Change unsigned to N/A

* [1.2.0] Update data stores and adding SignedWitnessStore (#3494)

* Update data stores and adding SignedWitnessStore

* Update translations

* Update cleaned TradeStatistics2Store and changes in other stores

* VoteResultView update results on any block in result phase

Avoid updating the result more than once per result phase but make
sure it's done if activated during the result phase

* [1.2.0] Format maker fee for BTC and BSQ correctly (#3498)

* Format maker fee for BTC and BSQ correctly

* Update tests

* Only automatically open popup if result wasn't accepted and disable action button when being accepted (#3503)

* Fix tradestatistics (#3469)

* Remove delayed re-publishing of tradeStatistics

This was done earlier when only maker was publishing trade statistics.
Now both traders do it so we get already higher resilience.

* Remove unused method

Forgot in prev. commit to remove also the method.

* Remove support for TradeStatistics2.ARBITRATOR_ADDRESS

* Add comment and set ARBITRATOR_ADDRESS deprecated

* Remove setting of arbitrator data from makers side

The 2 arbitrator related fields in Trade are only set by the maker and
not used anymore for reading, so it can be removed. The whole arbitrator
domain should be cleaned out some day, but because of backward
compatibility issues it id difficult to do it entirely at release date.
With release after v 1.2. when no old offers are out anymore we are
able to clean up that domain.

* Remove dev log

* Update translations

* [1.2.0] Improve dispute section (#3504)

* Improve wording for mediation summary and add specific next steps for refund agent case

* Select the first dispute case when entering the support section

* Revert to SNAPSHOT version

* Fix but with initialRequestApplied (#3512)

* Fix resource name (#3514)

* Remove minor version number in news popup

* Fix copy SignedWitnessStore db script

* Not show payment account details for blocked offers

* Use age of accountAgeWitness as basis for sell limits

* Bump version number

* Revert to SNAPSHOT version

* Merge v1.2.0/v1.2.1 with master (#3521)

* List Krypton (ZOD)

* Temporarily disable onion host for @KanoczTomas's BTC node

* Add Ergo (ERG) without Bouncy Castle dependency.

See #3195.

* List CTSCoin (CTSC)

* Tweak the English name of Japan Bank Transfer payment method

* List Animecoin (ANI)

* Add mediator prefix to trade statistics

* List Faircoin (FAIR)

* List uPlexa (UPX)

* Remove not used private methods from BisqEnvironment

* Add onInitP2pNetwork and onInitWallet to BisqSetupListener

- Rename BisqSetupCompleteListener to BisqSetupListener
- Add onInitP2pNetwork and onInitWallet to BisqSetupListener
- make onInitP2pNetwork and onInitWallet default so no impl. required

* Start server at onInitWallet and add wallet password handler

- Add onInitWallet to HttpApiMain and start http server there
- Add onRequestWalletPassword to BisqSetupListener
- Override setupHandlers in HttpApiHeadlessApp and adjust
setRequestWalletPasswordHandler (impl. missing)
- Add onRequestWalletPassword to HttpApiMain

* Add combination (Blockstream.info + Mempool.space) block explorer

* Revert "Temporarily disable onion host for @KanoczTomas's BTC node"

This reverts commit d3335208bb.

* Temporarily disable KanoczTomas btcnode on both onion and clearnet

* Refactor BisqApp - update scene size calculation

* Refactor BisqApp - update error popup message build

* Refactor BisqApp - move icon load into ImageUtil

* Remove unused Utilities

* Increase minimum TX fee to 2 sats/vByte to fix #3106 (#3387)

* Fix mistakes in English source (#3386)

* Fix broken placeholders

* Replace non existing pending trades screen with open trades screen

* Update core/src/main/resources/i18n/displayStrings.properties

Co-Authored-By: Steve Jain <mfiver@gmail.com>

* Update message in failed trade popup

* Refactor BisqEnvironment

* List Ndau (XND)

- Official project URL: https://ndau.io/
- Official block explorer URL: https://explorer.service.ndau.tech

* Show connected Bitcoin network peer info

* Not show payment account details for blocked offers (#3425)

* Add GitHub issue template for user reported bugs (#3454)

* Add issue template with steps to reproduce and actual/expected behavior

* Fix typo in .github/ISSUE_TEMPLATE.md

* Fix wrong auto merge

* Add CapabilityRequiringPayload to TradeStatistics2

With v1.2.0 we changed the way how the hash is created.
To not create too heavy load for seed nodes from
requests from old nodes we use the SIGNED_ACCOUNT_AGE_WITNESS
capability to send trade statistics only to new nodes.
As trade statistics are only used for informational purpose it will
not have any critical issue for the old nodes beside that they don't see the latest trades.

* Fix tradestat hash issue (#3529)

* Recreate hash from protobuf data

To ensure all data are using the new hash method (excluding extraMap) we
do not use the hash field from the protobug data but pass null which
causes to create the hash new based on the new hash method.

* Add filter.toString method and log filter in case of wrong signature

We have atm a invalid filter (prob. some dev polluted a test filter to mainnet)

* Change log level, add log

* Refactor: Move code to dump method

* Add TRADE_STATISTICS_HASH_UPDATE capability

We changed the hash method in 1.2.0 and that requires update to 1.2.2
for handling it correctly, otherwise the seed nodes have to process too
much data.

* Add logs for size of data exchange messages

* Add more data in log

* Improve logs

* Fix wrong msg in log, cahnge log level

* Add check for depositTxId not empty

* Remove check for duplicates

As we recreate the hash for all trade stat objects we don't need that
check anymore.

* Add logs

* Temporarily remove this part of the statistics

It prevents merging with master because through auto merge a duplication of this part of the code is happening and prevents Travis from succeeding
This commit is contained in:
Christoph Atteneder 2019-10-31 12:49:26 +01:00 committed by GitHub
parent 2967702db1
commit b976570426
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
302 changed files with 34604 additions and 5185 deletions

View File

@ -1,6 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -274,7 +274,7 @@ configure(project(':desktop')) {
apply plugin: 'witness'
apply from: '../gradle/witness/gradle-witness.gradle'
version = '1.1.7-SNAPSHOT'
version = '1.2.1-SNAPSHOT'
mainClassName = 'bisq.desktop.app.BisqAppMain'

View File

@ -35,9 +35,10 @@ public enum Capability {
RECEIVE_BSQ_BLOCK, // Signaling that node which wants to receive BSQ blocks (DAO lite node)
@Deprecated DAO_STATE, // Not required anymore as no old clients out there not having that support
//TODO can be set deprecated after v1.1.6 as we enforce update there
BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval
@Deprecated BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval
SIGNED_ACCOUNT_AGE_WITNESS, // Supports the signed account age witness feature
MEDIATION // Supports mediation feature
MEDIATION, // Supports mediation feature
REFUND_AGENT, // Supports refund agents
TRADE_STATISTICS_HASH_UPDATE // We changed the hash method in 1.2.0 and that requires update to 1.2.2 for handling it correctly, otherwise the seed nodes have to process too much data.
}

View File

@ -27,7 +27,7 @@ public class Version {
// VERSION = 0.5.0 introduces proto buffer for the P2P network and local DB and is a not backward compatible update
// Therefore all sub versions start again with 1
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.1.7";
public static final String VERSION = "1.2.1";
public static int getMajorVersion(String version) {
return getSubVersion(version, 0);
@ -73,7 +73,7 @@ public class Version {
// 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.1 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 @@ public class Version {
// 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.1 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2;
private static int p2pMessageVersion;
public static final String BSQ_TX_VERSION = "1";

View File

@ -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<string, string> 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<string, string> 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
@ -670,8 +722,14 @@ message AccountAgeWitness {
}
message SignedWitness {
bool signed_by_arbitrator = 1;
bytes witness_hash = 2;
enum VerificationMethod {
PB_ERROR = 0;
ARBITRATOR = 1;
TRADE = 2;
}
VerificationMethod verification_method = 1;
bytes account_age_witness_hash = 2;
bytes signature = 3;
bytes signer_pub_key = 4;
bytes witness_owner_pub_key = 5;
@ -708,6 +766,9 @@ 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;
string delayed_payout_tx_id = 26;
}
message Attachment {
@ -759,7 +820,7 @@ message Contract {
int64 trade_amount = 2;
int64 trade_price = 3;
string taker_fee_tx_id = 4;
NodeAddress arbitrator_node_address = 5;
reserved 5; // WAS: arbitrator_node_address
bool is_buyer_maker_and_seller_taker = 6;
string maker_account_id = 7;
string taker_account_id = 8;
@ -774,6 +835,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 +856,7 @@ enum AvailabilityResult {
NO_MEDIATORS = 7;
USER_IGNORED = 8;
MISSING_MANDATORY_CAPABILITY = 9;
NO_REFUND_AGENTS = 10;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -1074,6 +1138,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 +1263,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 +1286,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 +1332,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 +1374,11 @@ message Trade {
string counter_currency_tx_id = 28;
repeated ChatMessage chat_message = 29;
MediationResultState mediation_result_state = 30;
int64 lock_time = 31;
bytes delayed_payout_tx_bytes = 32;
NodeAddress refund_agent_node_address = 33;
PubKeyRing refund_agent_pub_key_ring = 34;
RefundResultState refund_result_state = 35;
}
message BuyerAsMakerTrade {
@ -1330,8 +1404,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 +1450,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 +1473,12 @@ enum MediationResultState {
PAYOUT_TX_SEEN_IN_NETWORK = 15;
}
//todo
enum RefundResultState {
PB_ERROR_REFUND_RESULT = 0;
UNDEFINED_REFUND_RESULT = 1;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Preferences
///////////////////////////////////////////////////////////////////////////////////////////
@ -1454,6 +1538,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 +1559,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;
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -26,6 +26,7 @@ import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.crypto.Hash;
import bisq.common.proto.ProtoUtil;
import bisq.common.proto.persistable.PersistableEnvelope;
import bisq.common.util.Utilities;
@ -46,10 +47,24 @@ import lombok.extern.slf4j.Slf4j;
@Value
public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPayload, PersistableEnvelope,
DateTolerantPayload, CapabilityRequiringPayload {
public enum VerificationMethod {
ARBITRATOR,
TRADE;
public static SignedWitness.VerificationMethod fromProto(protobuf.SignedWitness.VerificationMethod method) {
return ProtoUtil.enumFromProto(SignedWitness.VerificationMethod.class, method.name());
}
public static protobuf.SignedWitness.VerificationMethod toProtoMessage(SignedWitness.VerificationMethod method) {
return protobuf.SignedWitness.VerificationMethod.valueOf(method.name());
}
}
private static final long TOLERANCE = TimeUnit.DAYS.toMillis(1);
private final boolean signedByArbitrator;
private final byte[] witnessHash;
private final VerificationMethod verificationMethod;
private final byte[] accountAgeWitnessHash;
private final byte[] signature;
private final byte[] signerPubKey;
private final byte[] witnessOwnerPubKey;
@ -58,15 +73,15 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
transient private final byte[] hash;
public SignedWitness(boolean signedByArbitrator,
byte[] witnessHash,
public SignedWitness(VerificationMethod verificationMethod,
byte[] accountAgeWitnessHash,
byte[] signature,
byte[] signerPubKey,
byte[] witnessOwnerPubKey,
long date,
long tradeAmount) {
this.signedByArbitrator = signedByArbitrator;
this.witnessHash = witnessHash.clone();
this.verificationMethod = verificationMethod;
this.accountAgeWitnessHash = accountAgeWitnessHash.clone();
this.signature = signature.clone();
this.signerPubKey = signerPubKey.clone();
this.witnessOwnerPubKey = witnessOwnerPubKey.clone();
@ -80,7 +95,7 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
// same peer to add more security as if that one would be colluding it would be not detected anyway. The total
// number of signed trades with different peers is still available and can be considered more valuable data for
// security.
byte[] data = Utilities.concatenateByteArrays(witnessHash, signature);
byte[] data = Utilities.concatenateByteArrays(accountAgeWitnessHash, signature);
data = Utilities.concatenateByteArrays(data, signerPubKey);
hash = Hash.getSha256Ripemd160hash(data);
}
@ -93,8 +108,8 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
@Override
public protobuf.PersistableNetworkPayload toProtoMessage() {
final protobuf.SignedWitness.Builder builder = protobuf.SignedWitness.newBuilder()
.setSignedByArbitrator(signedByArbitrator)
.setWitnessHash(ByteString.copyFrom(witnessHash))
.setVerificationMethod(VerificationMethod.toProtoMessage(verificationMethod))
.setAccountAgeWitnessHash(ByteString.copyFrom(accountAgeWitnessHash))
.setSignature(ByteString.copyFrom(signature))
.setSignerPubKey(ByteString.copyFrom(signerPubKey))
.setWitnessOwnerPubKey(ByteString.copyFrom(witnessOwnerPubKey))
@ -108,8 +123,9 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
}
public static SignedWitness fromProto(protobuf.SignedWitness proto) {
return new SignedWitness(proto.getSignedByArbitrator(),
proto.getWitnessHash().toByteArray(),
return new SignedWitness(
SignedWitness.VerificationMethod.fromProto(proto.getVerificationMethod()),
proto.getAccountAgeWitnessHash().toByteArray(),
proto.getSignature().toByteArray(),
proto.getSignerPubKey().toByteArray(),
proto.getWitnessOwnerPubKey().toByteArray(),
@ -124,7 +140,7 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
@Override
public boolean isDateInTolerance(Clock clock) {
// We don't allow older or newer then 1 day.
// We don't allow older or newer than 1 day.
// Preventing forward dating is also important to protect against a sophisticated attack
return Math.abs(new Date().getTime() - date) <= TOLERANCE;
}
@ -145,6 +161,9 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
return hash;
}
public boolean isSignedByArbitrator() {
return verificationMethod == VerificationMethod.ARBITRATOR;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
@ -157,8 +176,8 @@ public class SignedWitness implements LazyProcessedPayload, PersistableNetworkPa
@Override
public String toString() {
return "SignedWitness{" +
",\n signedByArbitrator=" + signedByArbitrator +
",\n witnessHash=" + Utilities.bytesAsHexString(witnessHash) +
",\n verificationMethod=" + verificationMethod +
",\n witnessHash=" + Utilities.bytesAsHexString(accountAgeWitnessHash) +
",\n signature=" + Utilities.bytesAsHexString(signature) +
",\n signerPubKey=" + Utilities.bytesAsHexString(signerPubKey) +
",\n witnessOwnerPubKey=" + Utilities.bytesAsHexString(witnessOwnerPubKey) +

View File

@ -18,23 +18,17 @@
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.core.user.User;
import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService;
import bisq.common.UserThread;
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,30 +51,24 @@ import java.util.Date;
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 = 30;
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 User user;
private final Map<P2PDataStorage.ByteArray, SignedWitness> signedWitnessMap = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@ -88,18 +76,14 @@ public class SignedWitnessService {
@Inject
public SignedWitnessService(KeyRing keyRing,
P2PService p2PService,
AccountAgeWitnessService accountAgeWitnessService,
ArbitratorManager arbitratorManager,
SignedWitnessStorageService signedWitnessStorageService,
AppendOnlyDataStoreService appendOnlyDataStoreService,
ArbitrationManager arbitrationManager,
ChargeBackRisk chargeBackRisk) {
User user) {
this.keyRing = keyRing;
this.p2PService = p2PService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.arbitratorManager = arbitratorManager;
this.arbitrationManager = arbitrationManager;
this.chargeBackRisk = chargeBackRisk;
this.user = user;
// We need to add that early (before onAllServicesInitialized) as it will be used at startup.
appendOnlyDataStoreService.addService(signedWitnessStorageService);
@ -121,15 +105,45 @@ public class SignedWitnessService {
if (e instanceof SignedWitness)
addToMap((SignedWitness) e);
});
if (p2PService.isBootstrapped()) {
onBootstrapComplete();
} else {
p2PService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onUpdatedDataReceived() {
onBootstrapComplete();
}
});
}
}
private void onBootstrapComplete() {
if (user.getRegisteredArbitrator() != null) {
UserThread.runAfter(this::doRepublishAllSignedWitnesses, 60);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public List<Long> getMyWitnessAgeList(PaymentAccountPayload myPaymentAccountPayload) {
AccountAgeWitness accountAgeWitness = accountAgeWitnessService.getMyWitness(myPaymentAccountPayload);
/**
* List of dates as long when accountAgeWitness was signed
*/
public List<Long> getVerifiedWitnessDateList(AccountAgeWitness accountAgeWitness) {
return getSignedWitnessSet(accountAgeWitness).stream()
.filter(this::verifySignature)
.map(SignedWitness::getDate)
.sorted()
.collect(Collectors.toList());
}
/**
* List of dates as long when accountAgeWitness was signed
* Not verifying that signatures are correct
*/
public List<Long> getWitnessDateList(AccountAgeWitness accountAgeWitness) {
// We do not validate as it would not make sense to cheat one self...
return getSignedWitnessSet(accountAgeWitness).stream()
.map(SignedWitness::getDate)
@ -137,24 +151,26 @@ public class SignedWitnessService {
.collect(Collectors.toList());
}
public List<Long> getVerifiedWitnessAgeList(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
.filter(e -> Arrays.equals(e.getWitnessHash(), accountAgeWitness.getHash()))
.filter(this::verifySignature)
.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) {
public void signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
ECKey key,
PublicKey peersPubKey) {
if (isSignedAccountAgeWitness(accountAgeWitness)) {
log.warn("Arbitrator trying to sign already signed accountagewitness {}", accountAgeWitness.toString());
return;
}
String accountAgeWitnessHashAsHex = Utilities.encodeToHex(accountAgeWitness.getHash());
String signatureBase64 = key.signMessage(accountAgeWitnessHashAsHex);
SignedWitness signedWitness = new SignedWitness(true,
SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.ARBITRATOR,
accountAgeWitness.getHash(),
signatureBase64.getBytes(Charsets.UTF_8),
key.getPubKey(),
@ -162,15 +178,20 @@ public class SignedWitnessService {
new Date().getTime(),
tradeAmount.value);
publishSignedWitness(signedWitness);
return signedWitness;
log.info("Arbitrator signed witness {}", signedWitness.toString());
}
// Any peer can sign with DSA key
public SignedWitness signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
PublicKey peersPubKey) throws CryptoException {
public void signAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
PublicKey peersPubKey) throws CryptoException {
if (isSignedAccountAgeWitness(accountAgeWitness)) {
log.warn("Trader trying to sign already signed accountagewitness {}", accountAgeWitness.toString());
return;
}
byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash());
SignedWitness signedWitness = new SignedWitness(false,
SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE,
accountAgeWitness.getHash(),
signature,
keyRing.getSignatureKeyPair().getPublic().getEncoded(),
@ -178,7 +199,7 @@ public class SignedWitnessService {
new Date().getTime(),
tradeAmount.value);
publishSignedWitness(signedWitness);
return signedWitness;
log.info("Trader signed witness {}", signedWitness.toString());
}
public boolean verifySignature(SignedWitness signedWitness) {
@ -191,7 +212,7 @@ public class SignedWitnessService {
private boolean verifySignatureWithECKey(SignedWitness signedWitness) {
try {
String message = Utilities.encodeToHex(signedWitness.getWitnessHash());
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) {
@ -211,7 +232,7 @@ public class SignedWitnessService {
private boolean verifySignatureWithDSAKey(SignedWitness signedWitness) {
try {
PublicKey signaturePubKey = Sig.getPublicKeyFromBytes(signedWitness.getSignerPubKey());
Sig.verify(signaturePubKey, signedWitness.getWitnessHash(), signedWitness.getSignature());
Sig.verify(signaturePubKey, signedWitness.getAccountAgeWitnessHash(), signedWitness.getSignature());
return true;
} catch (CryptoException e) {
log.warn("verifySignature signedWitness failed. signedWitness={}", signedWitness);
@ -220,9 +241,9 @@ public class SignedWitnessService {
}
}
public Set<SignedWitness> getSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
private Set<SignedWitness> getSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
.filter(e -> Arrays.equals(e.getWitnessHash(), accountAgeWitness.getHash()))
.filter(e -> Arrays.equals(e.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.collect(Collectors.toSet());
}
@ -230,7 +251,7 @@ public class SignedWitnessService {
public Set<SignedWitness> getArbitratorsSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
.filter(SignedWitness::isSignedByArbitrator)
.filter(e -> Arrays.equals(e.getWitnessHash(), accountAgeWitness.getHash()))
.filter(e -> Arrays.equals(e.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.collect(Collectors.toSet());
}
@ -238,31 +259,41 @@ public class SignedWitnessService {
public Set<SignedWitness> getTrustedPeerSignedWitnessSet(AccountAgeWitness accountAgeWitness) {
return signedWitnessMap.values().stream()
.filter(e -> !e.isSignedByArbitrator())
.filter(e -> Arrays.equals(e.getWitnessHash(), accountAgeWitness.getHash()))
.filter(e -> Arrays.equals(e.getAccountAgeWitnessHash(), accountAgeWitness.getHash()))
.collect(Collectors.toSet());
}
// We go one level up by using the signer Key to lookup for SignedWitness objects which contain the signerKey as
// witnessOwnerPubKey
public Set<SignedWitness> getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey,
Stack<P2PDataStorage.ByteArray> excluded) {
private Set<SignedWitness> getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey,
Stack<P2PDataStorage.ByteArray> excluded) {
return signedWitnessMap.values().stream()
.filter(e -> Arrays.equals(e.getWitnessOwnerPubKey(), ownerPubKey))
.filter(e -> !excluded.contains(new P2PDataStorage.ByteArray(e.getSignerPubKey())))
.collect(Collectors.toSet());
}
public boolean isSignedAccountAgeWitness(AccountAgeWitness accountAgeWitness) {
return isSignerAccountAgeWitness(accountAgeWitness, new Date().getTime() + SIGNER_AGE);
}
public boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness) {
return isSignerAccountAgeWitness(accountAgeWitness, new Date().getTime());
}
/**
* Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator.
* @param accountAgeWitness
* @return true if accountAgeWitness is valid, false otherwise.
* Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator and is allowed to sign
* other accounts.
*
* @param accountAgeWitness accountAgeWitness
* @param time time of signing
* @return true if accountAgeWitness is allowed to sign at time, false otherwise.
*/
public boolean isValidAccountAgeWitness(AccountAgeWitness accountAgeWitness) {
private boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness, long time) {
Stack<P2PDataStorage.ByteArray> excludedPubKeys = new Stack<>();
long now = new Date().getTime();
Set<SignedWitness> signedWitnessSet = getSignedWitnessSet(accountAgeWitness);
for (SignedWitness signedWitness : signedWitnessSet) {
if (isValidSignedWitnessInternal(signedWitness, now, excludedPubKeys)) {
if (isValidSignerWitnessInternal(signedWitness, time, excludedPubKeys)) {
return true;
}
}
@ -272,12 +303,13 @@ public class SignedWitnessService {
/**
* 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,
private boolean isValidSignerWitnessInternal(SignedWitness signedWitness,
long childSignedWitnessDateMillis,
Stack<P2PDataStorage.ByteArray> excludedPubKeys) {
if (!verifySignature(signedWitness)) {
@ -299,7 +331,7 @@ public class SignedWitnessService {
// Iterate over signedWitness signers
Set<SignedWitness> signerSignedWitnessSet = getSignedWitnessSetByOwnerPubKey(signedWitness.getSignerPubKey(), excludedPubKeys);
for (SignedWitness signerSignedWitness : signerSignedWitnessSet) {
if (isValidSignedWitnessInternal(signerSignedWitness, signedWitness.getDate(), excludedPubKeys)) {
if (isValidSignerWitnessInternal(signerSignedWitness, signedWitness.getDate(), excludedPubKeys)) {
return true;
}
}
@ -311,7 +343,8 @@ public class SignedWitnessService {
}
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 +355,18 @@ public class SignedWitnessService {
@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<BuyerDataItem> 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 void doRepublishAllSignedWitnesses() {
signedWitnessMap.forEach((e, signedWitness) -> p2PService.addPersistableNetworkPayload(signedWitness, true));
}
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<AccountAgeWitness> optionalWitness = accountAgeWitnessService
.findWitness(buyerPaymentAccountPaload, buyerPubKeyRing);
return optionalWitness.map(witness -> new BuyerDataItem(
buyerPaymentAccountPaload,
witness,
dispute.getContract().getTradeAmount(),
dispute.getContract().getSellerPubKeyRing().getSignaturePubKey()))
.orElse(null);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -17,12 +17,22 @@
package bisq.core.account.witness;
import bisq.core.account.sign.SignedWitnessService;
import bisq.core.filter.FilterManager;
import bisq.core.filter.PaymentAccountFilter;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
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 +53,7 @@ import bisq.common.util.MathUtils;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import javax.inject.Inject;
@ -52,10 +63,14 @@ import java.util.Arrays;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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,16 +80,40 @@ import static com.google.common.base.Preconditions.checkNotNull;
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.MARCH, 1).getTime();
public enum AccountAge {
UNVERIFIED,
LESS_ONE_MONTH,
ONE_TO_TWO_MONTHS,
TWO_MONTHS_OR_MORE
}
public enum SignState {
UNSIGNED(Res.get("offerbook.timeSinceSigning.notSigned")),
ARBITRATOR(Res.get("offerbook.timeSinceSigning.info.arbitrator")),
PEER_INITIAL(Res.get("offerbook.timeSinceSigning.info.peer")),
PEER_LIMIT_LIFTED(Res.get("offerbook.timeSinceSigning.info.peerLimitLifted")),
PEER_SIGNER(Res.get("offerbook.timeSinceSigning.info.signer"));
private String presentation;
SignState(String presentation) {
this.presentation = presentation;
}
public String getPresentation() {
return presentation;
}
}
private final KeyRing keyRing;
private final P2PService p2PService;
private final User user;
private final SignedWitnessService signedWitnessService;
private final ChargeBackRisk chargeBackRisk;
private final FilterManager filterManager;
private final Map<P2PDataStorage.ByteArray, AccountAgeWitness> accountAgeWitnessMap = new HashMap<>();
@ -85,12 +124,20 @@ public class AccountAgeWitnessService {
@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) {
AppendOnlyDataStoreService appendOnlyDataStoreService,
FilterManager filterManager) {
this.keyRing = keyRing;
this.p2PService = p2PService;
this.user = user;
this.signedWitnessService = signedWitnessService;
this.chargeBackRisk = chargeBackRisk;
this.filterManager = filterManager;
// We need to add that early (before onAllServicesInitialized) as it will be used at startup.
appendOnlyDataStoreService.addService(accountAgeWitnessStorageService);
@ -164,7 +211,8 @@ public class AccountAgeWitnessService {
return new AccountAgeWitness(hash, new Date().getTime());
}
public Optional<AccountAgeWitness> findWitness(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing) {
private Optional<AccountAgeWitness> findWitness(PaymentAccountPayload paymentAccountPayload,
PubKeyRing pubKeyRing) {
byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload);
byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt,
pubKeyRing.getSignaturePubKeyBytes()));
@ -172,6 +220,19 @@ public class AccountAgeWitnessService {
return getWitnessByHash(hash);
}
private Optional<AccountAgeWitness> findWitness(Offer offer) {
final Optional<String> accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex();
return accountAgeWitnessHash.isPresent() ?
getWitnessByHashAsHex(accountAgeWitnessHash.get()) :
Optional.empty();
}
private Optional<AccountAgeWitness> findTradePeerWitness(Trade trade) {
TradingPeer tradingPeer = trade.getProcessModel().getTradingPeer();
return (tradingPeer.getPaymentAccountPayload() == null || tradingPeer.getPubKeyRing() == null) ?
Optional.empty() : findWitness(tradingPeer.getPaymentAccountPayload(), tradingPeer.getPubKeyRing());
}
private Optional<AccountAgeWitness> getWitnessByHash(byte[] hash) {
P2PDataStorage.ByteArray hashAsByteArray = new P2PDataStorage.ByteArray(hash);
@ -186,19 +247,57 @@ public class AccountAgeWitnessService {
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)) {
public long getAccountAge(Offer offer) {
return findWitness(offer)
.map(accountAgeWitness -> getAccountAge(accountAgeWitness, new Date()))
.orElse(-1L);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Signed age
///////////////////////////////////////////////////////////////////////////////////////////
// Return -1 if not signed
public long getWitnessSignAge(AccountAgeWitness accountAgeWitness, Date now) {
List<Long> 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);
}
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 +306,85 @@ public class AccountAgeWitnessService {
}
}
private long getTradeLimit(Coin maxTradeLimit, String currencyCode, Optional<AccountAgeWitness> 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,
PaymentMethod paymentMethod) {
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;
boolean isRisky = PaymentMethod.hasChargebackRisk(paymentMethod, currencyCode);
if (!isRisky || direction == OfferPayload.Direction.SELL) {
// Get age of witness rather than time since signing for non risky payment methods and for selling
accountAgeCategory = getAccountAgeCategory(getAccountAge(accountAgeWitness, new Date()));
}
long limit = OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
if (direction == OfferPayload.Direction.BUY && isRisky) {
// Used only for bying of BTC with risky payment methods
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:
default:
factor = 0;
}
} else {
// Used by non risky payment methods and for selling BTC with risky methods
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;
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Trade limit exceptions
///////////////////////////////////////////////////////////////////////////////////////////
private boolean isImmature(AccountAgeWitness accountAgeWitness) {
return accountAgeWitness.getDate() > SAFE_ACCOUNT_AGE_DATE;
}
public boolean myHasTradeLimitException(PaymentAccount myPaymentAccount) {
return hasTradeLimitException(getMyWitness(myPaymentAccount.getPaymentAccountPayload()));
}
// There are no trade limits on accounts that
// - are mature
// - were signed by an arbitrator
private boolean hasTradeLimitException(AccountAgeWitness accountAgeWitness) {
return !isImmature(accountAgeWitness) || signedWitnessService.isSignedByArbitrator(accountAgeWitness);
}
///////////////////////////////////////////////////////////////////////////////////////////
// My witness
@ -264,44 +407,27 @@ public class AccountAgeWitnessService {
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<AccountAgeWitness> 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<String> accountAgeWitnessHash = offer.getAccountAgeWitnessHashAsHex();
final Optional<AccountAgeWitness> 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 (hasTradeLimitException(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,
paymentAccount.getPaymentMethod());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Verification
///////////////////////////////////////////////////////////////////////////////////////////
@ -343,7 +469,8 @@ public class AccountAgeWitnessService {
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 +478,22 @@ public class AccountAgeWitnessService {
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 +531,23 @@ public class AccountAgeWitnessService {
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 (!hasTradeLimitException(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, offer.getPaymentMethod());
}
// Makers current trade limit cannot be smaller than that in the offer
boolean result = tradeAmount.value <= peersCurrentTradeLimit;
if (!result) {
@ -436,4 +580,139 @@ public class AccountAgeWitnessService {
}
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Witness signing
///////////////////////////////////////////////////////////////////////////////////////////
public void arbitratorSignAccountAgeWitness(Coin tradeAmount,
AccountAgeWitness accountAgeWitness,
ECKey key,
PublicKey peersPubKey) {
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<TraderDataItem> getTraderPaymentAccounts(long safeDate, PaymentMethod paymentMethod,
List<Dispute> disputes) {
return disputes.stream()
.filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId()))
.filter(this::isNotFiltered)
.filter(this::hasChargebackRisk)
.filter(this::isBuyerWinner)
.flatMap(this::getTraderData)
.filter(Objects::nonNull)
.filter(traderDataItem ->
!signedWitnessService.isSignedAccountAgeWitness(traderDataItem.getAccountAgeWitness()))
.filter(traderDataItem -> traderDataItem.getAccountAgeWitness().getDate() < safeDate)
.distinct()
.collect(Collectors.toList());
}
private boolean isNotFiltered(Dispute dispute) {
boolean isFiltered = filterManager.isNodeAddressBanned(dispute.getContract().getBuyerNodeAddress()) ||
filterManager.isNodeAddressBanned(dispute.getContract().getSellerNodeAddress()) ||
filterManager.isCurrencyBanned(dispute.getContract().getOfferPayload().getCurrencyCode()) ||
filterManager.isPaymentMethodBanned(
PaymentMethod.getPaymentMethodById(dispute.getContract().getPaymentMethodId())) ||
filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getBuyerPaymentAccountPayload(),
new PaymentAccountFilter[1]) ||
filterManager.isPeersPaymentAccountDataAreBanned(dispute.getContract().getSellerPaymentAccountPayload(),
new PaymentAccountFilter[1]);
return !isFiltered;
}
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<TraderDataItem> 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);
}
public boolean hasSignedWitness(Offer offer) {
return findWitness(offer)
.map(signedWitnessService::isSignedAccountAgeWitness)
.orElse(false);
}
public boolean peerHasSignedWitness(Trade trade) {
return findTradePeerWitness(trade)
.map(signedWitnessService::isSignedAccountAgeWitness)
.orElse(false);
}
public boolean accountIsSigner(AccountAgeWitness accountAgeWitness) {
return signedWitnessService.isSignerAccountAgeWitness(accountAgeWitness);
}
public SignState getSignState(Offer offer) {
return findWitness(offer)
.map(this::getSignState)
.orElse(SignState.UNSIGNED);
}
public SignState getSignState(AccountAgeWitness accountAgeWitness) {
if (signedWitnessService.isSignedByArbitrator(accountAgeWitness)) {
return SignState.ARBITRATOR;
} else {
final long accountSignAge = getWitnessSignAge(accountAgeWitness, new Date());
switch (getAccountAgeCategory(accountSignAge)) {
case TWO_MONTHS_OR_MORE:
return SignState.PEER_SIGNER;
case ONE_TO_TWO_MONTHS:
return SignState.PEER_LIMIT_LIFTED;
case LESS_ONE_MONTH:
return SignState.PEER_INITIAL;
case UNVERIFIED:
default:
return SignState.UNSIGNED;
}
}
}
}

View File

@ -17,6 +17,7 @@
package bisq.core.app;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.sign.SignedWitnessService;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.Alert;
@ -43,12 +44,15 @@ import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.TradeLimits;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
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;
@ -61,6 +65,7 @@ import bisq.network.crypto.DecryptedDataTuple;
import bisq.network.crypto.EncryptionService;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.peers.keepalive.messages.Ping;
import bisq.network.p2p.storage.payload.PersistableNetworkPayload;
import bisq.common.ClockWatcher;
import bisq.common.Timer;
@ -100,6 +105,7 @@ import java.net.Socket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
@ -143,11 +149,13 @@ public class BisqSetup {
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;
@ -183,7 +191,8 @@ public class BisqSetup {
private Consumer<String> cryptoSetupFailedHandler, chainFileLockedExceptionHandler,
spvFileCorruptedHandler, lockedUpFundsHandler, daoErrorMessageHandler, daoWarnMessageHandler,
filterWarningHandler, displaySecurityRecommendationHandler, displayLocalhostHandler,
wrongOSArchitectureHandler;
wrongOSArchitectureHandler, displaySignedByArbitratorHandler,
displaySignedByPeerHandler, displayPeerLimitLiftedHandler, displayPeerSignerHandler;
@Setter
@Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler;
@ -225,11 +234,13 @@ public class BisqSetup {
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,
@ -269,11 +280,13 @@ public class BisqSetup {
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;
@ -313,11 +326,8 @@ public class BisqSetup {
}
public void start() {
if (log.isDebugEnabled()) {
UserThread.runPeriodically(() -> {
log.debug("1 second heartbeat");
}, 1);
}
UserThread.runPeriodically(() -> {
}, 1);
maybeReSyncSPVChain();
maybeShowTac();
}
@ -346,6 +356,7 @@ public class BisqSetup {
// in MainViewModel
maybeShowSecurityRecommendation();
maybeShowLocalhostRunningInfo();
maybeShowAccountSigningStateInfo();
}
@ -440,10 +451,10 @@ public class BisqSetup {
}
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 {
@ -593,9 +604,7 @@ public class BisqSetup {
if (allBasicServicesInitialized)
checkForLockedUpFunds();
},
() -> {
walletInitialized.set(true);
});
() -> walletInitialized.set(true));
}
@ -637,6 +646,7 @@ public class BisqSetup {
arbitrationManager.onAllServicesInitialized();
mediationManager.onAllServicesInitialized();
refundManager.onAllServicesInitialized();
traderChatManager.onAllServicesInitialized();
tradeManager.onAllServicesInitialized();
@ -650,6 +660,7 @@ public class BisqSetup {
arbitratorManager.onAllServicesInitialized();
mediatorManager.onAllServicesInitialized();
refundAgentManager.onAllServicesInitialized();
alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) ->
displayAlertIfPresent(newValue, false));
@ -715,9 +726,7 @@ public class BisqSetup {
voteResultService.getVoteResultExceptions().addListener((ListChangeListener<VoteResultException>) c -> {
c.next();
if (c.wasAdded() && voteResultExceptionHandler != null) {
c.getAddedSubList().forEach(e -> {
voteResultExceptionHandler.accept(e);
});
c.getAddedSubList().forEach(e -> voteResultExceptionHandler.accept(e));
}
});
@ -741,9 +750,62 @@ public class BisqSetup {
}
private void maybeShowLocalhostRunningInfo() {
String key = "bitcoinLocalhostNode";
if (bisqEnvironment.isBitcoinLocalhostNodeRunning() && preferences.showAgain(key) &&
displayLocalhostHandler != null)
displayLocalhostHandler.accept(key);
maybeTriggerDisplayHandler("bitcoinLocalhostNode", displayLocalhostHandler, bisqEnvironment.isBitcoinLocalhostNodeRunning());
}
private void maybeShowAccountSigningStateInfo() {
String keySignedByArbitrator = "accountSignedByArbitrator";
String keySignedByPeer = "accountSignedByPeer";
String keyPeerLimitedLifted = "accountLimitLifted";
String keyPeerSigner = "accountPeerSigner";
// check signed witness on startup
checkSigningState(AccountAgeWitnessService.SignState.ARBITRATOR, keySignedByArbitrator, displaySignedByArbitratorHandler);
checkSigningState(AccountAgeWitnessService.SignState.PEER_INITIAL, keySignedByPeer, displaySignedByPeerHandler);
checkSigningState(AccountAgeWitnessService.SignState.PEER_LIMIT_LIFTED, keyPeerLimitedLifted, displayPeerLimitLiftedHandler);
checkSigningState(AccountAgeWitnessService.SignState.PEER_SIGNER, keyPeerSigner, displayPeerSignerHandler);
// check signed witness during runtime
p2PService.getP2PDataStorage().addAppendOnlyDataStoreListener(
payload -> {
maybeTriggerDisplayHandler(keySignedByArbitrator, displaySignedByArbitratorHandler,
isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.ARBITRATOR));
maybeTriggerDisplayHandler(keySignedByPeer, displaySignedByPeerHandler,
isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_INITIAL));
maybeTriggerDisplayHandler(keyPeerLimitedLifted, displayPeerLimitLiftedHandler,
isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_LIMIT_LIFTED));
maybeTriggerDisplayHandler(keyPeerSigner, displayPeerSignerHandler,
isSignedWitnessOfMineWithState(payload, AccountAgeWitnessService.SignState.PEER_SIGNER));
});
}
private void checkSigningState(AccountAgeWitnessService.SignState state,
String key, Consumer<String> displayHandler) {
boolean signingStateFound = p2PService.getP2PDataStorage().getAppendOnlyDataStoreMap().values().stream()
.anyMatch(payload -> isSignedWitnessOfMineWithState(payload, state));
maybeTriggerDisplayHandler(key, displayHandler, signingStateFound);
}
private boolean isSignedWitnessOfMineWithState(PersistableNetworkPayload payload,
AccountAgeWitnessService.SignState state) {
if (payload instanceof SignedWitness && user.getPaymentAccounts() != null) {
// We know at this point that it is already added to the signed witness list
// Check if new signed witness is for one of my own accounts
return user.getPaymentAccounts().stream()
.filter(a -> PaymentMethod.hasChargebackRisk(a.getPaymentMethod(), a.getTradeCurrencies()))
.filter(a -> Arrays.equals(((SignedWitness) payload).getAccountAgeWitnessHash(),
accountAgeWitnessService.getMyWitness(a.getPaymentAccountPayload()).getHash()))
.anyMatch(a -> accountAgeWitnessService.getSignState(accountAgeWitnessService.getMyWitness(
a.getPaymentAccountPayload())).equals(state));
}
return false;
}
private void maybeTriggerDisplayHandler(String key, Consumer<String> displayHandler, boolean signingStateFound) {
if (signingStateFound && preferences.showAgain(key) &&
displayHandler != null) {
displayHandler.accept(key);
}
}
}

View File

@ -41,14 +41,18 @@ import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class ExecutableForAppWithP2p extends BisqExecutable implements UncaughtExceptionHandler {
private static final long MAX_MEMORY_MB_DEFAULT = 500;
private static final long MAX_MEMORY_MB_DEFAULT = 1200;
private static final long CHECK_MEMORY_PERIOD_SEC = 300;
private static final long CHECK_SHUTDOWN_SEC = TimeUnit.HOURS.toSeconds(1);
private static final long SHUTDOWN_INTERVAL = TimeUnit.HOURS.toMillis(24);
private volatile boolean stopped;
private final long startTime = System.currentTimeMillis();
private static long maxMemory = MAX_MEMORY_MB_DEFAULT;
public ExecutableForAppWithP2p(String fullName, String scriptName, String version) {
@ -120,6 +124,20 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable implements
}
}
protected void startShutDownInterval(GracefulShutDownHandler gracefulShutDownHandler) {
UserThread.runPeriodically(() -> {
if (System.currentTimeMillis() - startTime > SHUTDOWN_INTERVAL) {
log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" +
"Shut down as node was running longer as {} hours" +
"\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n",
SHUTDOWN_INTERVAL / 3600000);
shutDown(gracefulShutDownHandler);
}
}, CHECK_SHUTDOWN_SEC);
}
protected void checkMemory(BisqEnvironment environment, GracefulShutDownHandler gracefulShutDownHandler) {
String maxMemoryOption = environment.getProperty(AppOptionKeys.MAX_MEMORY);
if (maxMemoryOption != null && !maxMemoryOption.isEmpty()) {
@ -153,16 +171,24 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable implements
long usedMemory = Profiler.getUsedMemoryInMB();
if (usedMemory > maxMemory) {
log.warn("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" +
"We are over our memory limit ({}) and trigger a restart. usedMemory: {} MB. freeMemory: {} MB" +
"We are over our memory limit ({}) and trigger a shutdown. usedMemory: {} MB. freeMemory: {} MB" +
"\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n\n",
(int) maxMemory, usedMemory, Profiler.getFreeMemoryInMB());
restart(environment, gracefulShutDownHandler);
shutDown(gracefulShutDownHandler);
}
}, 5);
}
}, CHECK_MEMORY_PERIOD_SEC);
}
protected void shutDown(GracefulShutDownHandler gracefulShutDownHandler) {
stopped = true;
gracefulShutDownHandler.gracefulShutDown(() -> {
log.info("Shutdown complete");
System.exit(1);
});
}
protected void restart(BisqEnvironment bisqEnvironment, GracefulShutDownHandler gracefulShutDownHandler) {
stopped = true;
gracefulShutDownHandler.gracefulShutDown(() -> {
@ -180,5 +206,4 @@ public abstract class ExecutableForAppWithP2p extends BisqExecutable implements
}
});
}
}

View File

@ -22,6 +22,8 @@ import bisq.core.btc.model.AddressEntry;
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<Coin> availableBalance = new SimpleObjectProperty<>();
@ -65,17 +68,20 @@ public class Balances {
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<OpenOffer>) c -> updateBalance());
tradeManager.getTradableList().addListener((ListChangeListener<Trade>) change -> updateBalance());
refundManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> updateBalance());
btcWalletService.addBalanceListener(new BalanceListener() {
@Override
public void onBalanceChanged(Coin balance, Transaction tx) {

View File

@ -126,6 +126,25 @@ public class TxFeeEstimationService {
return new Tuple2<>(txFee, size);
}
public Tuple2<Coin, Integer> 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

View File

@ -47,10 +47,6 @@ public class BtcNodes {
public List<BtcNode> getProvidedBtcNodes() {
return useProvidedBtcNodes() ?
Arrays.asList(
// ManfredKarrer
new BtcNode("btc1.0-2-1.net", "r3dsojfhwcm7x7p6.onion", "159.89.16.222", BtcNode.DEFAULT_PORT, "@manfredkarrer"),
new BtcNode("btc2.0-2-1.net", "vlf5i3grro3wux24.onion", "165.227.34.56", BtcNode.DEFAULT_PORT, "@manfredkarrer"),
// emzy
new BtcNode("kirsche.emzy.de", "fz6nsij6jiyuwlsc.onion", "78.47.61.83", BtcNode.DEFAULT_PORT, "@emzy"),
new BtcNode("node2.emzy.de", "c6ac4jdfyeiakex2.onion", "62.75.210.81", BtcNode.DEFAULT_PORT, "@emzy"),

View File

@ -493,6 +493,15 @@ public class BsqWalletService extends WalletService implements DaoStateListener
}
}
for (TransactionOutput txo : tx.getOutputs()) {
Coin value = txo.getValue();
// OpReturn outputs have value 0
if (value.isPositive()) {
checkArgument(Restrictions.isAboveDust(txo.getValue()),
"An output value is below dust limit. Transaction=" + tx);
}
}
checkWalletConsistency(wallet);
verifyTransaction(tx);
printTx("BSQ wallet: Signed Tx", tx);
@ -556,11 +565,11 @@ public class BsqWalletService extends WalletService implements DaoStateListener
// Tx has as first output BSQ and an optional second BSQ change output.
// At that stage we do not have added the BTC inputs so there is no BTC change output here.
if (tx.getOutputs().size() == 2) {
TransactionOutput bsqChangeOutput = tx.getOutputs().get(1);
if (!Restrictions.isAboveDust(bsqChangeOutput.getValue())) {
String msg = "BSQ change output is below dust limit. outputValue=" + bsqChangeOutput.getValue().toFriendlyString();
Coin bsqChangeOutputValue = tx.getOutputs().get(1).getValue();
if (!Restrictions.isAboveDust(bsqChangeOutputValue)) {
String msg = "BSQ change output is below dust limit. outputValue=" + bsqChangeOutputValue.value / 100 + " BSQ";
log.warn(msg);
throw new BsqChangeBelowDustException(msg, bsqChangeOutput.getValue());
throw new BsqChangeBelowDustException(msg, bsqChangeOutputValue);
}
}
@ -573,60 +582,128 @@ public class BsqWalletService extends WalletService implements DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// Burn fee tx
// Burn fee txs
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction getPreparedTradeFeeTx(Coin fee) throws InsufficientBsqException {
daoKillSwitch.assertDaoIsNotDisabled();
Transaction tx = new Transaction(params);
addInputsAndChangeOutputForTx(tx, fee, bsqCoinSelector);
return tx;
}
// We create a tx with Bsq inputs for the fee and optional BSQ change output.
// As the fee amount will be missing in the output those BSQ fees are burned.
public Transaction getPreparedProposalTx(Coin fee, boolean requireChangeOutput) throws InsufficientBsqException {
return getPreparedBurnFeeTx(fee, requireChangeOutput);
public Transaction getPreparedProposalTx(Coin fee) throws InsufficientBsqException {
return getPreparedTxWithMandatoryBsqChangeOutput(fee);
}
public Transaction getPreparedBurnFeeTx(Coin fee) throws InsufficientBsqException {
return getPreparedBurnFeeTx(fee, false);
public Transaction getPreparedIssuanceTx(Coin fee) throws InsufficientBsqException {
return getPreparedTxWithMandatoryBsqChangeOutput(fee);
}
private Transaction getPreparedBurnFeeTx(Coin fee, boolean requireChangeOutput) throws InsufficientBsqException {
public Transaction getPreparedProofOfBurnTx(Coin fee) throws InsufficientBsqException {
return getPreparedTxWithMandatoryBsqChangeOutput(fee);
}
public Transaction getPreparedBurnFeeTxForAssetListing(Coin fee) throws InsufficientBsqException {
return getPreparedTxWithMandatoryBsqChangeOutput(fee);
}
// We need to require one BSQ change output as we could otherwise not be able to distinguish between 2
// structurally same transactions where only the BSQ fee is different. In case of asset listing fee and proof of
// burn it is a user input, so it is not know to the parser, instead we derive the burned fee from the parser.
// In case of proposal fee we could derive it from the params.
// For issuance txs we also require a BSQ change output before the issuance output gets added. There was a
// minor bug with the old version that multiple inputs would have caused an exception in case there was no
// change output (e.g. inputs of 21 and 6 BSQ for BSQ fee of 21 BSQ would have caused that only 1 input was used
// and then caused an error as we enforced a change output. This new version handles such cases correctly.
// Examples for the structurally indistinguishable transactions:
// Case 1: 10 BSQ fee to burn
// In: 17 BSQ
// Out: BSQ change 7 BSQ -> valid BSQ
// Out: OpReturn
// Miner fee: 1000 sat (10 BSQ burned)
// Case 2: 17 BSQ fee to burn
// In: 17 BSQ
// Out: burned BSQ change 7 BSQ -> BTC (7 BSQ burned)
// Out: OpReturn
// Miner fee: 1000 sat (10 BSQ burned)
private Transaction getPreparedTxWithMandatoryBsqChangeOutput(Coin fee) throws InsufficientBsqException {
daoKillSwitch.assertDaoIsNotDisabled();
final Transaction tx = new Transaction(params);
addInputsAndChangeOutputForTx(tx, fee, bsqCoinSelector, requireChangeOutput);
// printTx("getPreparedFeeTx", tx);
return tx;
Transaction tx = new Transaction(params);
// We look for inputs covering out BSQ fee we want to pay.
CoinSelection coinSelection = bsqCoinSelector.select(fee, wallet.calculateAllSpendCandidates());
try {
Coin change = bsqCoinSelector.getChange(fee, coinSelection);
if (change.isZero() || Restrictions.isDust(change)) {
// If change is zero or below dust we increase required input amount to enforce a BSQ change output.
// All outputs after that are considered BTC and therefore would be burned BSQ if BSQ is left from what
// we use for miner fee.
Coin minDustThreshold = Coin.valueOf(preferences.getIgnoreDustThreshold());
Coin increasedRequiredInput = fee.add(minDustThreshold);
coinSelection = bsqCoinSelector.select(increasedRequiredInput, wallet.calculateAllSpendCandidates());
change = bsqCoinSelector.getChange(fee, coinSelection);
log.warn("We increased required input as change output was zero or dust: New change value={}", change);
String info = "Available BSQ balance=" + coinSelection.valueGathered.value / 100 + " BSQ. " +
"Intended fee to burn=" + fee.value / 100 + " BSQ. " +
"Please increase your balance to at least " + (coinSelection.valueGathered.value + minDustThreshold.value) / 100 + " BSQ.";
checkArgument(coinSelection.valueGathered.compareTo(fee) > 0,
"This transaction require a change output of at least " + minDustThreshold.value / 100 + " BSQ (dust limit). " +
info);
checkArgument(!Restrictions.isDust(change),
"This transaction would create a dust output of " + change.value / 100 + " BSQ. " +
"It requires a change output of at least " + minDustThreshold.value / 100 + " BSQ (dust limit). " +
info);
}
coinSelection.gathered.forEach(tx::addInput);
tx.addOutput(change, getChangeAddress());
return tx;
} catch (InsufficientMoneyException e) {
log.error("coinSelection.gathered={}", coinSelection.gathered);
throw new InsufficientBsqException(e.missing);
}
}
private void addInputsAndChangeOutputForTx(Transaction tx,
Coin fee,
BsqCoinSelector bsqCoinSelector,
boolean requireChangeOutput)
BsqCoinSelector bsqCoinSelector)
throws InsufficientBsqException {
Coin requiredInput;
// If our fee is less then dust limit we increase it so we are sure to not get any dust output.
if (Restrictions.isDust(fee))
requiredInput = Restrictions.getMinNonDustOutput().add(fee);
else
if (Restrictions.isDust(fee)) {
requiredInput = fee.add(Restrictions.getMinNonDustOutput());
} else {
requiredInput = fee;
}
CoinSelection coinSelection = bsqCoinSelector.select(requiredInput, wallet.calculateAllSpendCandidates());
coinSelection.gathered.forEach(tx::addInput);
try {
// TODO why is fee passed to getChange ???
Coin change = bsqCoinSelector.getChange(fee, coinSelection);
if (requireChangeOutput) {
checkArgument(change.isPositive(),
"This transaction requires a mandatory BSQ change output. " +
"At least " + Restrictions.getMinNonDustOutput().add(fee).value / 100d + " " +
"BSQ is needed for this transaction");
}
if (change.isPositive()) {
checkArgument(Restrictions.isAboveDust(change),
"The change output of " + change.value / 100d + " BSQ is below the min. dust value of "
+ Restrictions.getMinNonDustOutput().value / 100d +
". At least " + Restrictions.getMinNonDustOutput().add(fee).value / 100d + " " +
"BSQ is needed for this transaction");
". At least " + Restrictions.getMinNonDustOutput().add(fee).value / 100d +
" BSQ is needed for this transaction");
tx.addOutput(change, getChangeAddress());
}
} catch (InsufficientMoneyException e) {
log.error(tx.toString());
throw new InsufficientBsqException(e.missing);
}
}
@ -642,7 +719,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
daoKillSwitch.assertDaoIsNotDisabled();
Transaction tx = new Transaction(params);
tx.addOutput(new TransactionOutput(params, tx, stake, getUnusedAddress()));
addInputsAndChangeOutputForTx(tx, fee.add(stake), bsqCoinSelector, false);
addInputsAndChangeOutputForTx(tx, fee.add(stake), bsqCoinSelector);
//printTx("getPreparedBlindVoteTx", tx);
return tx;
}
@ -676,7 +753,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
Transaction tx = new Transaction(params);
checkArgument(Restrictions.isAboveDust(lockupAmount), "The amount is too low (dust limit).");
tx.addOutput(new TransactionOutput(params, tx, lockupAmount, getUnusedAddress()));
addInputsAndChangeOutputForTx(tx, lockupAmount, bsqCoinSelector, false);
addInputsAndChangeOutputForTx(tx, lockupAmount, bsqCoinSelector);
printTx("prepareLockupTx", tx);
return tx;
}

View File

@ -408,13 +408,13 @@ public class BtcWalletService extends WalletService {
TransactionVerificationException, WalletException, InsufficientMoneyException {
// preparedBsqTx has following structure:
// inputs [1-n] BSQ inputs
// outputs [0-1] BSQ receivers output
// outputs [1] BSQ receivers output
// outputs [0-1] BSQ change output
// We add BTC mining fee. Result tx looks like:
// inputs [1-n] BSQ inputs
// inputs [1-n] BTC inputs
// outputs [0-1] BSQ receivers output
// outputs [1] BSQ receivers output
// outputs [0-1] BSQ change output
// outputs [0-1] BTC change output
// mining fee: BTC mining fee
@ -426,7 +426,7 @@ public class BtcWalletService extends WalletService {
// preparedBsqTx has following structure:
// inputs [1-n] BSQ inputs
// outputs [0-1] BSQ receivers output
// outputs [1] BSQ receivers output
// outputs [0-1] BSQ change output
// mining fee: optional burned BSQ fee (only if opReturnData != null)
@ -1114,4 +1114,55 @@ public class BtcWalletService extends WalletService {
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;
}
}

View File

@ -104,12 +104,12 @@ public class TxBroadcaster {
}
// 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<Transaction>() {
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 class TxBroadcaster {
// 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);
}
}

View File

@ -34,6 +34,7 @@ import org.bitcoinj.core.Address;
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.Transaction;
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<AddressConfidenceListener> addressConfidenceListeners = new CopyOnWriteArraySet<>();
protected final CopyOnWriteArraySet<TxConfidenceListener> txConfidenceListeners = new CopyOnWriteArraySet<>();
protected final CopyOnWriteArraySet<BalanceListener> balanceListeners = new CopyOnWriteArraySet<>();
@Getter
protected Wallet wallet;
@Getter
protected KeyParameter aesKey;
@ -223,7 +226,9 @@ public abstract class WalletService {
}
}
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 abstract class WalletService {
// 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 abstract class WalletService {
// 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,45 @@ public abstract class WalletService {
}
/**
* @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());
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
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -322,11 +322,11 @@ public class AssetService implements DaoSetupService, DaoStateListener {
checkArgument(listingFee % 100 == 0, "Fee must be a multiple of 1 BSQ (100 satoshi).");
try {
// We create a prepared Bsq Tx for the listing fee.
final Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTx(Coin.valueOf(listingFee));
Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTxForAssetListing(Coin.valueOf(listingFee));
byte[] hash = AssetConsensus.getHash(statefulAsset);
byte[] opReturnData = AssetConsensus.getOpReturnData(hash);
// We add the BTC inputs for the miner fee.
final Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData);
Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData);
// We sign the BSQ inputs of the final tx.
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
log.info("Asset listing fee tx: " + transaction);

View File

@ -107,9 +107,9 @@ public enum Param {
// but can be also a burner address if we prefer to burn the BTC
@SuppressWarnings("SpellCheckingInspection")
RECIPIENT_BTC_ADDRESS(BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? "1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7" : // mainnet
BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7" : // mainnet
BisqEnvironment.getBaseCurrencyNetwork().isDaoBetaNet() ? "1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7" : // daoBetaNet
BisqEnvironment.getBaseCurrencyNetwork().isTestnet() ? "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV" : // testnet
"mquz1zFmhs7iy8qJTkhY7C9bhJ5S3g8Xim", // regtest or DAO testnet (regtest)
"2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w", // regtest or DAO testnet (regtest)
ParamType.ADDRESS),
// Fee for activating an asset or re-listing after deactivation due lack of trade activity. Fee per day of trial period without activity checks.

View File

@ -134,11 +134,11 @@ public class ProofOfBurnService implements DaoSetupService, DaoStateListener {
public Transaction burn(String preImageAsString, long amount) throws InsufficientMoneyException, TxException {
try {
// We create a prepared Bsq Tx for the burn amount
final Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTx(Coin.valueOf(amount));
Transaction preparedBurnFeeTx = bsqWalletService.getPreparedProofOfBurnTx(Coin.valueOf(amount));
byte[] hash = getHashFromPreImage(preImageAsString);
byte[] opReturnData = ProofOfBurnConsensus.getOpReturnData(hash);
// We add the BTC inputs for the miner fee.
final Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData);
Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData);
// We sign the BSQ inputs of the final tx.
Transaction transaction = bsqWalletService.signTx(txWithBtcFee);
log.info("Proof of burn tx: " + transaction);

View File

@ -87,8 +87,9 @@ public abstract class BaseProposalFactory<R extends Proposal> {
try {
Coin fee = ProposalConsensus.getFee(daoStateService, daoStateService.getChainHeight());
// We create a prepared Bsq Tx for the proposal fee.
boolean requireChangeOutput = proposal instanceof IssuanceProposal;
Transaction preparedBurnFeeTx = bsqWalletService.getPreparedProposalTx(fee, requireChangeOutput);
Transaction preparedBurnFeeTx = proposal instanceof IssuanceProposal ?
bsqWalletService.getPreparedIssuanceTx(fee) :
bsqWalletService.getPreparedProposalTx(fee);
// payload does not have txId at that moment
byte[] hashOfPayload = ProposalConsensus.getHashOfPayload(proposal);

View File

@ -17,7 +17,9 @@
package bisq.core.dao.node.parser;
import bisq.core.app.BisqEnvironment;
import bisq.core.dao.governance.bond.BondConsensus;
import bisq.core.dao.governance.param.Param;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.OpReturnType;
import bisq.core.dao.state.model.blockchain.TxOutput;
@ -38,11 +40,57 @@ import static com.google.common.base.Preconditions.checkNotNull;
/**
* Checks if an output is a BSQ output and apply state change.
*
* With block 602500 (about 4 weeks after v1.2.0 release) we enforce a new rule which represents a
* hard fork. Not updated nodes would see an out of sync dao state hash if a relevant transaction would
* happen again.
* Further (highly unlikely) consequences could be:
* If the BSQ output would be sent to a BSQ address the old client would accept that even it is
* invalid according to the new rules. But sending such a output would require a manually crafted tx
* (not possible in the UI). Worst case a not updated user would buy invalid BSQ but that is not possible as we
* enforce update to 1.2.0 for trading a few days after release as that release introduced the new trade protocol
* and protection tool. Only of both both traders would have deactivated filter messages they could trade.
*
* Problem description:
* We did not apply the check to not allow BSQ outputs after we had detected a BTC output.
* The supported BSQ transactions did not support such cases anyway but we missed an edge case:
* A trade fee tx in case when the BTC input matches exactly the BTC output
* (or BTC change was <= the miner fee) and the BSQ fee was > the miner fee. Then we
* create a change output after the BTC output (using an address from the BTC wallet) and as
* available BSQ was >= as spent BSQ it was considered a valid BSQ output.
* There have been observed 5 such transactions where 4 got spent later to a BTC address and by that burned
* the pending BSQ (spending amount was higher than sending amount). One was still unspent.
* The BSQ was sitting in the BTC wallet so not even visible as BSQ to the user.
* If the user would have crafted a custom BSQ tx he could have avoided that the full trade fee was burned.
*
* Not an universal rule:
* We cannot enforce the rule that no BSQ output is permitted to all possible transactions because there can be cases
* where we need to permit this case.
* For instance in case we confiscate a lockupTx we have usually 2 BSQ outputs: The first one is the bond which
* should be confiscated and the second one is the BSQ change output.
* At confiscating we set the first to TxOutputType.BTC_OUTPUT but we do not want to confiscate
* the second BSQ change output as well. So we do not apply the rule that no BSQ is allowed once a BTC output is
* found. Theoretically other transactions could be confiscated as well and all BSQ tx which allow > 1 BSQ outputs
* would have the same issue as well if the first output gets confiscated.
* We also don't enforce the rule for irregular or invalid txs which are usually set and detected at the end of
* the tx parsing which is done in the TxParser. Blind vote and LockupTx with invalid OpReturn would be such cases
* where we don't want to invalidate the change output (See comments in TxParser).
*
* Most transactions created in Bisq (Proposal, blind vote and lockup,...) have only 1 or 2 BSQ
* outputs but we do not enforce a limit of max. 2 transactions in the parser.
* We leave for now that flexibility but it should not be considered as a rule. We might strengthen
* it any time if we find a reason for that (e.g. attack risk) and add checks that no more
* BSQ outputs are permitted for those txs.
* Some transactions like issuance, vote reveal and unlock have exactly 1 BSQ output and that rule
* is enforced.
*/
@Slf4j
public class TxOutputParser {
private final DaoStateService daoStateService;
class TxOutputParser {
private static int ACTIVATE_HARD_FORK_1_HEIGHT_MAINNET = 605000;
private static int ACTIVATE_HARD_FORK_1_HEIGHT_TESTNET = 1583054;
private static int ACTIVATE_HARD_FORK_1_HEIGHT_REGTEST = 1;
private final DaoStateService daoStateService;
// Setters
@Getter
@Setter
@ -66,10 +114,12 @@ public class TxOutputParser {
private Optional<TempTxOutput> optionalVoteRevealUnlockStakeOutput = Optional.empty();
@Getter
private Optional<TempTxOutput> optionalLockupOutput = Optional.empty();
private Optional<Integer> optionalOpReturnIndex = Optional.empty();
// Private
private int lockTime;
private final List<TempTxOutput> utxoCandidates = new ArrayList<>();
private boolean prohibitMoreBsqOutputs = false;
///////////////////////////////////////////////////////////////////////////////////////////
@ -93,6 +143,8 @@ public class TxOutputParser {
optionalOpReturnType = getMappedOpReturnType(txOutputType);
optionalOpReturnType.ifPresent(e -> optionalOpReturnIndex = Optional.of(tempTxOutput.getIndex()));
// If we have a LOCKUP opReturn output we save the lockTime to apply it later to the LOCKUP output.
// We keep that data in that other output as it makes parsing of the UNLOCK tx easier.
optionalOpReturnType.filter(opReturnType -> opReturnType == OpReturnType.LOCKUP)
@ -100,14 +152,14 @@ public class TxOutputParser {
}
void processTxOutput(TempTxOutput tempTxOutput) {
if (!daoStateService.isConfiscatedOutput(tempTxOutput.getKey())) {
// We don not expect here an opReturn output as we do not get called on the last output. Any opReturn at
// another output index is invalid.
if (tempTxOutput.isOpReturnOutput()) {
tempTxOutput.setTxOutputType(TxOutputType.INVALID_OUTPUT);
return;
}
// We don not expect here an opReturn output as we do not get called on the last output. Any opReturn at
// another output index is invalid.
if (tempTxOutput.isOpReturnOutput()) {
tempTxOutput.setTxOutputType(TxOutputType.INVALID_OUTPUT);
return;
}
if (!daoStateService.isConfiscatedOutput(tempTxOutput.getKey())) {
long txOutputValue = tempTxOutput.getValue();
int index = tempTxOutput.getIndex();
if (isUnlockBondTx(tempTxOutput.getValue(), index)) {
@ -118,8 +170,18 @@ public class TxOutputParser {
} else if (isBtcOutputOfBurnFeeTx(tempTxOutput)) {
// In case we have the opReturn for a burn fee tx all outputs after 1st output are considered BTC
handleBtcOutput(tempTxOutput, index);
} else if (isHardForkActivated(tempTxOutput) && isIssuanceCandidateTxOutput(tempTxOutput)) {
// After the hard fork activation we fix a bug with a transaction which would have interpreted the
// issuance output as BSQ if the availableInputValue was >= issuance amount.
// Such a tx was never created but as we don't know if it will happen before activation date we cannot
// enforce the bug fix which represents a rule change before the activation date.
handleIssuanceCandidateOutput(tempTxOutput);
} else if (availableInputValue > 0 && availableInputValue >= txOutputValue) {
handleBsqOutput(tempTxOutput, index, txOutputValue);
if (isHardForkActivated(tempTxOutput) && prohibitMoreBsqOutputs) {
handleBtcOutput(tempTxOutput, index);
} else {
handleBsqOutput(tempTxOutput, index, txOutputValue);
}
} else {
handleBtcOutput(tempTxOutput, index);
}
@ -127,6 +189,9 @@ public class TxOutputParser {
log.warn("TxOutput {} is confiscated ", tempTxOutput.getKey());
// We only burn that output
availableInputValue -= tempTxOutput.getValue();
// We must not set prohibitMoreBsqOutputs at confiscation transactions as optional
// BSQ change output (output 2) must not be confiscated.
tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
}
}
@ -139,6 +204,7 @@ public class TxOutputParser {
* This sets all outputs to BTC_OUTPUT and doesn't add any txOutputs to the unspentTxOutput map in daoStateService
*/
void invalidateUTXOCandidates() {
// We do not need to apply prohibitMoreBsqOutputs as all spendable outputs are set to BTC_OUTPUT anyway.
utxoCandidates.forEach(output -> output.setTxOutputType(TxOutputType.BTC_OUTPUT));
}
@ -171,17 +237,108 @@ public class TxOutputParser {
utxoCandidates.add(txOutput);
bsqOutputFound = true;
// We do not permit more BSQ outputs after the unlock txo as we don't expect additional BSQ outputs.
prohibitMoreBsqOutputs = true;
}
private boolean isBtcOutputOfBurnFeeTx(TempTxOutput tempTxOutput) {
// If we get a asset listing or proof of burn tx we have only 1 BSQ output and if the
// burned amount is larger than the miner fee we might have a BTC output for receiving the burned funds.
// If the burned funds are less than the miner fee a BTC input is used for miner fee and a BTC change output for
// the remaining funds. In any case only the first output is BSQ all the others are BTC.
return optionalOpReturnType.isPresent() &&
(optionalOpReturnType.get() == OpReturnType.ASSET_LISTING_FEE ||
optionalOpReturnType.get() == OpReturnType.PROOF_OF_BURN) &&
tempTxOutput.getIndex() >= 1;
if (optionalOpReturnType.isPresent()) {
int index = tempTxOutput.getIndex();
switch (optionalOpReturnType.get()) {
case UNDEFINED:
break;
case PROPOSAL:
if (isHardForkActivated(tempTxOutput)) {
// We enforce a mandatory BSQ change output.
// We need that as similar to ASSET_LISTING_FEE and PROOF_OF_BURN
// we could not distinguish between 2 structurally same transactions otherwise (only way here
// would be to check the proposal fee as that is known from the params).
return index >= 1;
}
break;
case COMPENSATION_REQUEST:
break;
case REIMBURSEMENT_REQUEST:
break;
case BLIND_VOTE:
if (isHardForkActivated(tempTxOutput)) {
// After the hard fork activation we fix a bug with a transaction which would have interpreted the
// burned vote fee output as BSQ if the vote fee was >= miner fee.
// Such a tx was never created but as we don't know if it will happen before activation date we cannot
// enforce the bug fix which represents a rule change before the activation date.
// If it is the vote stake output we return false.
if (index == 0) {
return false;
}
// There must be a vote fee left
if (availableInputValue <= 0) {
return false;
}
// Burned BSQ output is last output before opReturn.
// We could have also a BSQ change output as last output before opReturn but that will
// be detected at blindVoteFee check.
// We always have the BSQ change before the burned BSQ output if both are present.
checkArgument(optionalOpReturnIndex.isPresent());
if (index != optionalOpReturnIndex.get() - 1) {
return false;
}
// Without checking the fee we would not be able to distinguish between 2 structurally same transactions, one
// where the output is burned BSQ and one where it is a BSQ change output.
long blindVoteFee = daoStateService.getParamValueAsCoin(Param.BLIND_VOTE_FEE, tempTxOutput.getBlockHeight()).value;
return availableInputValue == blindVoteFee;
}
case VOTE_REVEAL:
break;
case LOCKUP:
break;
case ASSET_LISTING_FEE:
case PROOF_OF_BURN:
// Asset listing fee and proof of burn tx are structurally the same.
// We need to require one BSQ change output as we could otherwise not be able to distinguish between 2
// structurally same transactions where only the BSQ fee is different. In case of asset listing fee and proof of
// burn it is a user input, so it is not know to the parser, instead we derive the burned fee from the parser.
// In case of proposal fee we could derive it from the params.
// Case 1: 10 BSQ fee to burn
// In: 17 BSQ
// Out: BSQ change 7 BSQ -> valid BSQ
// Out: OpReturn
// Miner fee: 1000 sat (10 BSQ burned)
// Case 2: 17 BSQ fee to burn
// In: 17 BSQ
// Out: burned BSQ change 7 BSQ -> BTC (7 BSQ burned)
// Out: OpReturn
// Miner fee: 1000 sat (10 BSQ burned)
return index >= 1;
}
}
return false;
}
private boolean isIssuanceCandidateTxOutput(TempTxOutput tempTxOutput) {
// If we have BSQ left as fee and we are at the second output we interpret it as a compensation request output.
return availableInputValue > 0 &&
tempTxOutput.getIndex() == 1 &&
optionalOpReturnType.isPresent() &&
(optionalOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST ||
optionalOpReturnType.get() == OpReturnType.REIMBURSEMENT_REQUEST);
}
private void handleIssuanceCandidateOutput(TempTxOutput tempTxOutput) {
// We do not permit more BSQ outputs after the issuance candidate.
prohibitMoreBsqOutputs = true;
// We store the candidate but we don't apply the TxOutputType yet as we need to verify the fee after all
// outputs are parsed and check the phase. The TxParser will do that....
optionalIssuanceCandidate = Optional.of(tempTxOutput);
}
private void handleBsqOutput(TempTxOutput txOutput, int index, long txOutputValue) {
@ -201,6 +358,9 @@ public class TxOutputParser {
} else if (isFirstOutput && opReturnTypeCandidate == OpReturnType.VOTE_REVEAL) {
txOutputType = TxOutputType.VOTE_REVEAL_UNLOCK_STAKE_OUTPUT;
optionalVoteRevealUnlockStakeOutput = Optional.of(txOutput);
// We do not permit more BSQ outputs after the VOTE_REVEAL_UNLOCK_STAKE_OUTPUT.
prohibitMoreBsqOutputs = true;
} else if (isFirstOutput && opReturnTypeCandidate == OpReturnType.LOCKUP) {
txOutputType = TxOutputType.LOCKUP_OUTPUT;
@ -219,20 +379,43 @@ public class TxOutputParser {
}
private void handleBtcOutput(TempTxOutput txOutput, int index) {
// If we have BSQ left as fee and we are at the second output it might be a compensation request output.
// We store the candidate but we don't apply the TxOutputType yet as we need to verify the fee after all
// outputs are parsed and check the phase. The TxParser will do that....
if (availableInputValue > 0 &&
index == 1 &&
optionalOpReturnType.isPresent() &&
(optionalOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST ||
optionalOpReturnType.get() == OpReturnType.REIMBURSEMENT_REQUEST)) {
optionalIssuanceCandidate = Optional.of(txOutput);
} else {
if (isHardForkActivated(txOutput)) {
txOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
// For regular transactions we don't permit BSQ outputs after a BTC output was detected.
prohibitMoreBsqOutputs = true;
} else {
// If we have BSQ left as fee and we are at the second output it might be a compensation request output.
// We store the candidate but we don't apply the TxOutputType yet as we need to verify the fee after all
// outputs are parsed and check the phase. The TxParser will do that....
if (availableInputValue > 0 &&
index == 1 &&
optionalOpReturnType.isPresent() &&
(optionalOpReturnType.get() == OpReturnType.COMPENSATION_REQUEST ||
optionalOpReturnType.get() == OpReturnType.REIMBURSEMENT_REQUEST)) {
optionalIssuanceCandidate = Optional.of(txOutput);
// We do not permit more BSQ outputs after the issuance candidate.
prohibitMoreBsqOutputs = true;
} else {
txOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
// For regular transactions we don't permit BSQ outputs after a BTC output was detected.
prohibitMoreBsqOutputs = true;
}
}
}
private boolean isHardForkActivated(TempTxOutput tempTxOutput) {
return tempTxOutput.getBlockHeight() >= getActivateHardFork1Height();
}
private int getActivateHardFork1Height() {
return BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ? ACTIVATE_HARD_FORK_1_HEIGHT_MAINNET :
BisqEnvironment.getBaseCurrencyNetwork().isTestnet() ? ACTIVATE_HARD_FORK_1_HEIGHT_TESTNET :
ACTIVATE_HARD_FORK_1_HEIGHT_REGTEST;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Static

View File

@ -217,19 +217,33 @@ public class TxParser {
// We need to check if any tempTxOutput is available and if so and the OpReturn data is invalid we
// set the output to a BTC output. We must not use `if else` cases here!
if (opReturnType != OpReturnType.COMPENSATION_REQUEST && opReturnType != OpReturnType.REIMBURSEMENT_REQUEST) {
// We applied already the check to not permit further BSQ outputs after the issuanceCandidate in the
// txOutputParser so we don't need to do any additional check here when we change to BTC_OUTPUT.
txOutputParser.getOptionalIssuanceCandidate().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
}
if (opReturnType != OpReturnType.BLIND_VOTE) {
txOutputParser.getOptionalBlindVoteLockStakeOutput().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
txOutputParser.getOptionalBlindVoteLockStakeOutput().ifPresent(tempTxOutput -> {
// We cannot apply the rule to not allow BSQ outputs after a BTC output as the 2nd output is an
// optional BSQ change output and we don't want to burn that in case the opReturn is invalid.
tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
});
}
if (opReturnType != OpReturnType.VOTE_REVEAL) {
txOutputParser.getOptionalVoteRevealUnlockStakeOutput().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
txOutputParser.getOptionalVoteRevealUnlockStakeOutput().ifPresent(tempTxOutput -> {
// We do not apply the rule to not allow BSQ outputs after a BTC output here because we expect only
// one BSQ output anyway.
tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
});
}
if (opReturnType != OpReturnType.LOCKUP) {
txOutputParser.getOptionalLockupOutput().ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
txOutputParser.getOptionalLockupOutput().ifPresent(tempTxOutput -> {
// We cannot apply the rule to not allow BSQ outputs after a BTC output as the 2nd output is an
// optional BSQ change output and we don't want to burn that in case the opReturn is invalid.
tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT);
});
}
}
@ -259,12 +273,14 @@ public class TxParser {
}
} else {
// This could be a valid compensation request that failed to be included in a block during the
// correct phase due to no fault of the user. Better not burn the change as long as the BSQ inputs
// correct phase due to no fault of the user. We must not burn the change as long as the BSQ inputs
// cover the value of the outputs.
// We tolerate such an incorrect tx and do not burn the BSQ
tempTx.setTxType(TxType.IRREGULAR);
// Make sure the optionalIssuanceCandidate is set to BTC
// We applied already the check to not permit further BSQ outputs after the issuanceCandidate in the
// txOutputParser so we don't need to do any additional check here when we change to BTC_OUTPUT.
optionalIssuanceCandidate.ifPresent(tempTxOutput -> tempTxOutput.setTxOutputType(TxOutputType.BTC_OUTPUT));
// Empty Optional case is a possible valid case where a random tx matches our opReturn rules but it is not a
// valid BSQ tx.

View File

@ -98,6 +98,10 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
@Nullable
private final List<String> mediators;
// added in v1.2.0
@Nullable
private final List<String> refundAgents;
public Filter(List<String> bannedOfferIds,
List<String> bannedNodeAddress,
List<PaymentAccountFilter> bannedPaymentAccounts,
@ -111,7 +115,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
boolean disableDao,
@Nullable String disableDaoBelowVersion,
@Nullable String disableTradeBelowVersion,
@Nullable List<String> mediators) {
@Nullable List<String> mediators,
@Nullable List<String> refundAgents) {
this.bannedOfferIds = bannedOfferIds;
this.bannedNodeAddress = bannedNodeAddress;
this.bannedPaymentAccounts = bannedPaymentAccounts;
@ -126,6 +131,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
this.disableDaoBelowVersion = disableDaoBelowVersion;
this.disableTradeBelowVersion = disableTradeBelowVersion;
this.mediators = mediators;
this.refundAgents = refundAgents;
}
@ -150,7 +156,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
String signatureAsBase64,
byte[] ownerPubKeyBytes,
@Nullable Map<String, String> extraDataMap,
@Nullable List<String> mediators) {
@Nullable List<String> mediators,
@Nullable List<String> refundAgents) {
this(bannedOfferIds,
bannedNodeAddress,
bannedPaymentAccounts,
@ -164,7 +171,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
disableDao,
disableDaoBelowVersion,
disableTradeBelowVersion,
mediators);
mediators,
refundAgents);
this.signatureAsBase64 = signatureAsBase64;
this.ownerPubKeyBytes = ownerPubKeyBytes;
this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap);
@ -198,6 +206,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
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 final class Filter implements ProtectedStoragePayload, ExpirablePayload {
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()));
}
@ -240,4 +250,26 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
ownerPubKeyBytes = Sig.getPublicKeyBytes(this.ownerPubKey);
}
@Override
public String toString() {
return "Filter{" +
"\n bannedOfferIds=" + bannedOfferIds +
",\n bannedNodeAddress=" + bannedNodeAddress +
",\n bannedPaymentAccounts=" + bannedPaymentAccounts +
",\n bannedCurrencies=" + bannedCurrencies +
",\n bannedPaymentMethods=" + bannedPaymentMethods +
",\n arbitrators=" + arbitrators +
",\n seedNodes=" + seedNodes +
",\n priceRelayNodes=" + priceRelayNodes +
",\n preventPublicBtcNetwork=" + preventPublicBtcNetwork +
",\n btcNodes=" + btcNodes +
",\n extraDataMap=" + extraDataMap +
",\n disableDao=" + disableDao +
",\n disableDaoBelowVersion='" + disableDaoBelowVersion + '\'' +
",\n disableTradeBelowVersion='" + disableTradeBelowVersion + '\'' +
",\n mediators=" + mediators +
",\n refundAgents=" + refundAgents +
"\n}";
}
}

View File

@ -297,7 +297,7 @@ public class FilterManager {
ECKey.fromPublicOnly(HEX.decode(pubKeyAsHex)).verifyMessage(getHexFromData(filter), filter.getSignatureAsBase64());
return true;
} catch (SignatureException e) {
log.warn("verifySignature failed");
log.warn("verifySignature failed. filter={}", filter);
return false;
}
}

View File

@ -26,5 +26,6 @@ public enum AvailabilityResult {
NO_ARBITRATORS,
NO_MEDIATORS,
USER_IGNORED,
MISSING_MANDATORY_CAPABILITY
MISSING_MANDATORY_CAPABILITY,
NO_REFUND_AGENTS
}

View File

@ -28,7 +28,6 @@ import bisq.network.p2p.storage.HashMapChangedListener;
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 class OfferBookService {
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 class OfferBookService {
}
}
private boolean showOffer(Offer offer) {
return !OfferRestrictions.requiresUpdate() ||
OfferRestrictions.hasOfferMandatoryCapability(offer, Capability.MEDIATION);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
@ -208,7 +200,6 @@ public class OfferBookService {
offer.setPriceFeedService(priceFeedService);
return offer;
})
.filter(this::showOffer)
.collect(Collectors.toList());
}

View File

@ -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 @@ public class OfferRestrictions {
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<String, String> extraDataMap = offer.getOfferPayload().getExtraDataMap();
if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) {

View File

@ -32,7 +32,6 @@ import bisq.core.provider.price.MarketPrice;
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 class OfferUtil {
public static Map<String, String> getExtraDataMap(AccountAgeWitnessService accountAgeWitnessService,
ReferralIdService referralIdService,
PaymentAccount paymentAccount,
String currencyCode) {
String currencyCode,
Preferences preferences) {
Map<String, String> extraDataMap = new HashMap<>();
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload());

View File

@ -65,6 +65,12 @@ public final class OpenOffer implements Tradable {
@Nullable
private NodeAddress mediatorNodeAddress;
// Added v1.2.0
@Getter
@Setter
@Nullable
private NodeAddress refundAgentNodeAddress;
transient private Storage<TradableList<OpenOffer>> storage;
public OpenOffer(Offer offer, Storage<TradableList<OpenOffer>> storage) {
@ -80,11 +86,13 @@ public final class OpenOffer implements Tradable {
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 final class OpenOffer implements Tradable {
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 final class OpenOffer implements Tradable {
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 final class OpenOffer implements Tradable {
",\n state=" + state +
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n mediatorNodeAddress=" + mediatorNodeAddress +
",\n refundAgentNodeAddress=" + refundAgentNodeAddress +
"\n}";
}
}

View File

@ -20,6 +20,7 @@ package bisq.core.offer;
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.exceptions.TradePriceOutOfToleranceException;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.offer.messages.OfferAvailabilityRequest;
@ -29,6 +30,7 @@ import bisq.core.offer.placeoffer.PlaceOfferProtocol;
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 +53,7 @@ import bisq.common.Timer;
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 +107,8 @@ 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 DaoFacade daoFacade;
private final Storage<TradableList<OpenOffer>> openOfferTradableListStorage;
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
private boolean stopped;
@ -129,6 +134,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
TradeStatisticsManager tradeStatisticsManager,
ArbitratorManager arbitratorManager,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
Storage<TradableList<OpenOffer>> storage) {
this.keyRing = keyRing;
this.user = user;
@ -143,6 +150,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.tradeStatisticsManager = tradeStatisticsManager;
this.arbitratorManager = arbitratorManager;
this.mediatorManager = mediatorManager;
this.refundAgentManager = refundAgentManager;
this.daoFacade = daoFacade;
openOfferTradableListStorage = storage;
@ -339,6 +348,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
offerBookService,
arbitratorManager,
tradeStatisticsManager,
daoFacade,
user);
PlaceOfferProtocol placeOfferProtocol = new PlaceOfferProtocol(
model,
@ -577,6 +587,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 +595,26 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) {
availabilityResult = AvailabilityResult.AVAILABLE;
List<NodeAddress> 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);
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;
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 +630,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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);
@ -715,14 +712,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private void maybeUpdatePersistedOffers() {
// We need to clone to avoid ConcurrentModificationException
ArrayList<OpenOffer> openOffersClone = new ArrayList<>(openOffers.getList());
openOffersClone.forEach(openOffer -> {
Offer originalOffer = openOffer.getOffer();
openOffersClone.forEach(originalOpenOffer -> {
Offer originalOffer = originalOpenOffer.getOffer();
OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
// 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.
// We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and
// Capability.REFUND_AGENT in v1.2.0 and want to rewrite a
// persisted offer after the user has updated to 1.2.0 so their offer will be accepted by the network.
if (!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION)) {
if (originalOfferPayload.getProtocolVersion() < Version.TRADE_PROTOCOL_VERSION ||
!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION) ||
!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.REFUND_AGENT)) {
// We rewrite our offer with the additional capabilities entry
Map<String, String> originalExtraDataMap = originalOfferPayload.getExtraDataMap();
@ -735,6 +735,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// 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,17 +775,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.isPrivateOffer(),
originalOfferPayload.getHashOfChallenge(),
updatedExtraDataMap,
originalOfferPayload.getProtocolVersion());
protocolVersion);
// Save states from original data to use the for updated
// Save states from original data to use for the updated
Offer.State originalOfferState = originalOffer.getState();
OpenOffer.State originalOpenOfferState = openOffer.getState();
OpenOffer.State originalOpenOfferState = originalOpenOffer.getState();
// remove old offer
originalOffer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
openOffer.setStorage(openOfferTradableListStorage);
openOffers.remove(openOffer);
originalOpenOffer.setState(OpenOffer.State.CANCELED);
originalOpenOffer.setStorage(openOfferTradableListStorage);
openOffers.remove(originalOpenOffer);
// Create new Offer
Offer updatedOffer = new Offer(updatedPayload);
@ -794,7 +797,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
updatedOpenOffer.setStorage(openOfferTradableListStorage);
openOffers.add(updatedOpenOffer);
log.info("Converted offer to support new Capability.MEDIATION capability. id={}", originalOffer.getId());
log.info("Converted offer to support new Capability.MEDIATION and Capability.REFUND_AGENT capability. id={}", originalOffer.getId());
}
});
}

View File

@ -57,6 +57,13 @@ public class DisputeAgentSelection {
TradeStatistics2.MEDIATOR_ADDRESS);
}
public static <T extends DisputeAgent> T getLeastUsedRefundAgent(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager) {
return getLeastUsedDisputeAgent(tradeStatisticsManager,
disputeAgentManager,
TradeStatistics2.REFUND_AGENT_ADDRESS);
}
private static <T extends DisputeAgent> T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager,
DisputeAgentManager<T> disputeAgentManager,
String extraMapKey) {

View File

@ -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,

View File

@ -57,12 +57,16 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
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" +

View File

@ -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,24 +79,27 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
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
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
final protobuf.OfferAvailabilityResponse.Builder builder = protobuf.OfferAvailabilityResponse.newBuilder()
.setOfferId(offerId)
.setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name()))
.setArbitrator(arbitrator.toProtoMessage());
.setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name()));
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()));
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator(arbitrator.toProtoMessage()));
return getNetworkEnvelopeBuilder()
.setOfferAvailabilityResponse(builder)
@ -103,7 +112,8 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
messageVersion,
proto.getUid().isEmpty() ? null : proto.getUid(),
NodeAddress.fromProto(proto.getArbitrator()),
proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null);
proto.hasArbitrator() ? NodeAddress.fromProto(proto.getArbitrator()) : null,
proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null,
proto.hasRefundAgent() ? NodeAddress.fromProto(proto.getRefundAgent()) : null);
}
}

View File

@ -20,6 +20,7 @@ package bisq.core.offer.placeoffer;
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.offer.Offer;
import bisq.core.offer.OfferBookService;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
@ -48,6 +49,7 @@ public class PlaceOfferModel implements Model {
private final OfferBookService offerBookService;
private final ArbitratorManager arbitratorManager;
private final TradeStatisticsManager tradeStatisticsManager;
private final DaoFacade daoFacade;
private final User user;
// Mutable
@ -65,6 +67,7 @@ public class PlaceOfferModel implements Model {
OfferBookService offerBookService,
ArbitratorManager arbitratorManager,
TradeStatisticsManager tradeStatisticsManager,
DaoFacade daoFacade,
User user) {
this.offer = offer;
this.reservedFundsForOffer = reservedFundsForOffer;
@ -75,6 +78,7 @@ public class PlaceOfferModel implements Model {
this.offerBookService = offerBookService;
this.arbitratorManager = arbitratorManager;
this.tradeStatisticsManager = tradeStatisticsManager;
this.daoFacade = daoFacade;
this.user = user;
}

View File

@ -25,11 +25,10 @@ import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.btc.wallet.WalletService;
import bisq.core.dao.exceptions.DaoDisabledException;
import bisq.core.dao.governance.param.Param;
import bisq.core.dao.state.model.blockchain.TxType;
import bisq.core.offer.Offer;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.offer.placeoffer.PlaceOfferModel;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.common.UserThread;
import bisq.common.taskrunner.Task;
@ -45,7 +44,6 @@ import javax.annotation.Nullable;
public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
private static final Logger log = LoggerFactory.getLogger(CreateMakerFeeTx.class);
private Transaction tradeFeeTx = null;
@SuppressWarnings({"unused"})
public CreateMakerFeeTx(TaskRunner taskHandler, PlaceOfferModel model) {
@ -62,17 +60,15 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
String id = offer.getId();
BtcWalletService walletService = model.getWalletService();
Arbitrator arbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(),
model.getArbitratorManager());
Address fundingAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.OFFER_FUNDING).getAddress();
Address reservedForTradeAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress();
Address changeAddress = walletService.getFreshAddressEntry().getAddress();
final TradeWalletService tradeWalletService = model.getTradeWalletService();
TradeWalletService tradeWalletService = model.getTradeWalletService();
String feeReceiver = model.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS);
if (offer.isCurrencyForMakerFeeBtc()) {
tradeFeeTx = tradeWalletService.createBtcTradingFeeTx(
tradeWalletService.createBtcTradingFeeTx(
fundingAddress,
reservedForTradeAddress,
changeAddress,
@ -80,7 +76,7 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
model.isUseSavingsWallet(),
offer.getMakerFee(),
offer.getTxFee(),
arbitrator.getBtcAddress(),
feeReceiver,
true,
new TxBroadcaster.Callback() {
@Override
@ -113,7 +109,7 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
});
} else {
final BsqWalletService bsqWalletService = model.getBsqWalletService();
Transaction preparedBurnFeeTx = model.getBsqWalletService().getPreparedBurnFeeTx(offer.getMakerFee());
Transaction preparedBurnFeeTx = model.getBsqWalletService().getPreparedTradeFeeTx(offer.getMakerFee());
Transaction txWithBsqFee = tradeWalletService.completeBsqTradingFeeTx(preparedBurnFeeTx,
fundingAddress,
reservedForTradeAddress,

View File

@ -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 @@ import javax.annotation.Nullable;
@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<PaymentAccount> 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<PaymentAccount> 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<PaymentAccount> takerPaymentAccounts) {
public static boolean isAnyTakerPaymentAccountValidForOffer(Offer offer,
Collection<PaymentAccount> takerPaymentAccounts) {
for (PaymentAccount takerPaymentAccount : takerPaymentAccounts) {
if (isTakerPaymentAccountValidForOffer(offer, takerPaymentAccount))
return true;
@ -105,11 +54,21 @@ public class PaymentAccountUtil {
ObservableList<PaymentAccount> 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

View File

@ -37,17 +37,17 @@ class PaymentAccounts {
private static final Logger log = LoggerFactory.getLogger(PaymentAccounts.class);
private final Set<PaymentAccount> accounts;
private final AccountAgeWitnessService service;
private final AccountAgeWitnessService accountAgeWitnessService;
private final BiFunction<Offer, PaymentAccount, Boolean> validator;
PaymentAccounts(Set<PaymentAccount> accounts, AccountAgeWitnessService service) {
this(accounts, service, PaymentAccountUtil::isTakerPaymentAccountValidForOffer);
PaymentAccounts(Set<PaymentAccount> accounts, AccountAgeWitnessService accountAgeWitnessService) {
this(accounts, accountAgeWitnessService, PaymentAccountUtil::isTakerPaymentAccountValidForOffer);
}
PaymentAccounts(Set<PaymentAccount> accounts, AccountAgeWitnessService service,
PaymentAccounts(Set<PaymentAccount> accounts, AccountAgeWitnessService accountAgeWitnessService,
BiFunction<Offer, PaymentAccount, Boolean> validator) {
this.accounts = accounts;
this.service = service;
this.accountAgeWitnessService = accountAgeWitnessService;
this.validator = validator;
}
@ -61,7 +61,7 @@ class PaymentAccounts {
}
private List<PaymentAccount> sortValidAccounts(Offer offer) {
Comparator<PaymentAccount> comparator = this::compareByAge;
Comparator<PaymentAccount> comparator = this::compareByTradeLimit;
return accounts.stream()
.filter(account -> validator.apply(offer, account))
.sorted(comparator.reversed())
@ -78,7 +78,7 @@ class PaymentAccounts {
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 @@ class PaymentAccounts {
}
}
private int compareByAge(PaymentAccount left, PaymentAccount right) {
AccountAgeWitness leftWitness = service.getMyWitness(left.getPaymentAccountPayload());
AccountAgeWitness rightWitness = service.getMyWitness(right.getPaymentAccountPayload());
// Accounts ranked by trade limit
private int compareByTradeLimit(PaymentAccount left, PaymentAccount right) {
// Mature accounts count as infinite sign age
if (accountAgeWitnessService.myHasTradeLimitException(left)) {
return !accountAgeWitnessService.myHasTradeLimitException(right) ? 1 : 0;
}
if (accountAgeWitnessService.myHasTradeLimitException(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);
}
}

View File

@ -19,6 +19,7 @@ package bisq.core.payment.payload;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.payment.TradeLimits;
import bisq.common.proto.persistable.PersistablePayload;
@ -322,6 +323,15 @@ public final class PaymentMethod implements PersistablePayload, Comparable {
return this.equals(BLOCK_CHAINS_INSTANT) || this.equals(BLOCK_CHAINS);
}
public static boolean hasChargebackRisk(PaymentMethod paymentMethod, List<TradeCurrency> tradeCurrencies) {
return tradeCurrencies.stream()
.anyMatch(tradeCurrency -> hasChargebackRisk(paymentMethod, tradeCurrency.getCode()));
}
public static boolean hasChargebackRisk(PaymentMethod paymentMethod) {
return hasChargebackRisk(paymentMethod, CurrencyUtil.getMatureMarketCurrencies());
}
public static boolean hasChargebackRisk(PaymentMethod paymentMethod, String currencyCode) {
if (paymentMethod == null)
return false;

View File

@ -19,6 +19,7 @@ package bisq.core.presentation;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.refund.RefundManager;
import javax.inject.Inject;
@ -27,51 +28,39 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.Getter;
public class SupportTicketsPresentation {
@Getter
private final StringProperty numOpenArbitrationTickets = new SimpleStringProperty();
@Getter
private final BooleanProperty showOpenArbitrationTicketsNotification = new SimpleBooleanProperty();
@Getter
private final StringProperty numOpenMediationTickets = new SimpleStringProperty();
@Getter
private final BooleanProperty showOpenMediationTicketsNotification = new SimpleBooleanProperty();
@Getter
private final StringProperty numOpenSupportTickets = new SimpleStringProperty();
@Getter
private final BooleanProperty showOpenSupportTicketsNotification = new SimpleBooleanProperty();
@org.jetbrains.annotations.NotNull
private final ArbitrationManager arbitrationManager;
@org.jetbrains.annotations.NotNull
private final MediationManager mediationManager;
@org.jetbrains.annotations.NotNull
private final RefundManager refundManager;
@Inject
public SupportTicketsPresentation(ArbitrationManager arbitrationManager, MediationManager mediationManager) {
public SupportTicketsPresentation(ArbitrationManager arbitrationManager,
MediationManager mediationManager,
RefundManager refundManager) {
this.arbitrationManager = arbitrationManager;
this.mediationManager = mediationManager;
this.refundManager = refundManager;
arbitrationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange());
mediationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange());
refundManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange());
}
private void onChange() {
AtomicInteger openArbitrationDisputes = new AtomicInteger(arbitrationManager.getNumOpenDisputes().get());
int arbitrationTickets = openArbitrationDisputes.get();
numOpenArbitrationTickets.set(String.valueOf(arbitrationTickets));
showOpenArbitrationTicketsNotification.set(arbitrationTickets > 0);
int supportTickets = arbitrationManager.getNumOpenDisputes().get() +
mediationManager.getNumOpenDisputes().get() +
refundManager.getNumOpenDisputes().get();
AtomicInteger openMediationDisputes = new AtomicInteger(mediationManager.getNumOpenDisputes().get());
int mediationTickets = openMediationDisputes.get();
numOpenMediationTickets.set(String.valueOf(mediationTickets));
showOpenMediationTicketsNotification.set(mediationTickets > 0);
int supportTickets = arbitrationTickets + mediationTickets;
numOpenSupportTickets.set(String.valueOf(supportTickets));
showOpenSupportTicketsNotification.set(supportTickets > 0);
}

View File

@ -44,14 +44,19 @@ import bisq.core.support.dispute.mediation.mediator.Mediator;
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 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class CoreNetworkProtoResolver extends CoreProtoResolver implements NetworkProtoResolver {
@Inject
public CoreNetworkProtoResolver() {
}
@ -134,16 +138,28 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
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 class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
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:

View File

@ -37,6 +37,7 @@ import bisq.core.payment.PaymentAccountList;
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 class CorePersistenceProtoResolver extends CoreProtoResolver implements P
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:

View File

@ -37,7 +37,9 @@ public class CoreNetworkCapabilities {
Capability.BLIND_VOTE,
Capability.DAO_STATE,
Capability.BUNDLE_OF_ENVELOPES,
Capability.MEDIATION
Capability.MEDIATION,
Capability.SIGNED_ACCOUNT_AGE_WITNESS,
Capability.REFUND_AGENT
);
if (BisqEnvironment.isDaoActivated(bisqEnvironment)) {

View File

@ -29,6 +29,7 @@ import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService;
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 class CorePersistedDataHost {
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)))) {

View File

@ -22,7 +22,8 @@ import bisq.common.proto.ProtoUtil;
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) {

View File

@ -48,6 +48,7 @@ import java.util.stream.Collectors;
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<ChatMessage> chatMessages = FXCollections.observableArrayList();
private BooleanProperty isClosedProperty = new SimpleBooleanProperty();
@ -92,6 +93,16 @@ public final class Dispute implements NetworkPayload {
transient private Storage<? extends DisputeList> 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;
@Setter
@Nullable
private String delayedPayoutTxId;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -114,7 +125,8 @@ public final class Dispute implements NetworkPayload {
@Nullable String makerContractSignature,
@Nullable String takerContractSignature,
PubKeyRing agentPubKeyRing,
boolean isSupportTicket) {
boolean isSupportTicket,
SupportType supportType) {
this(tradeId,
traderId,
disputeOpenerIsBuyer,
@ -131,7 +143,8 @@ public final class Dispute implements NetworkPayload {
makerContractSignature,
takerContractSignature,
agentPubKeyRing,
isSupportTicket);
isSupportTicket,
supportType);
this.storage = storage;
openingDate = new Date().getTime();
}
@ -157,7 +170,8 @@ public final class Dispute implements NetworkPayload {
@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 +189,7 @@ public final class Dispute implements NetworkPayload {
this.takerContractSignature = takerContractSignature;
this.agentPubKeyRing = agentPubKeyRing;
this.isSupportTicket = isSupportTicket;
this.supportType = supportType;
id = tradeId + "_" + traderId;
}
@ -210,11 +225,14 @@ public final class Dispute implements NetworkPayload {
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));
Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId));
return builder.build();
}
public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver coreProtoResolver) {
final Dispute dispute = new Dispute(proto.getTradeId(),
Dispute dispute = new Dispute(proto.getTradeId(),
proto.getTraderId(),
proto.getDisputeOpenerIsBuyer(),
proto.getDisputeOpenerIsMaker(),
@ -230,7 +248,8 @@ public final class Dispute implements NetworkPayload {
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 +260,17 @@ public final class Dispute implements NetworkPayload {
if (proto.hasDisputeResult())
dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult()));
dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId());
String mediatorsDisputeResult = proto.getMediatorsDisputeResult();
if (!mediatorsDisputeResult.isEmpty()) {
dispute.setMediatorsDisputeResult(mediatorsDisputeResult);
}
String delayedPayoutTxId = proto.getDelayedPayoutTxId();
if (!delayedPayoutTxId.isEmpty()) {
dispute.setDelayedPayoutTxId(delayedPayoutTxId);
}
return dispute;
}
@ -258,10 +288,6 @@ public final class Dispute implements NetworkPayload {
}
}
public boolean isMediationDispute() {
return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
@ -293,6 +319,9 @@ public final class Dispute implements NetworkPayload {
storage.queueUpForSave();
}
public void setSupportType(SupportType supportType) {
this.supportType = supportType;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
@ -322,34 +351,37 @@ public final class Dispute implements NetworkPayload {
return isClosedProperty.get();
}
@Override
public String toString() {
return "Dispute{" +
"tradeId='" + tradeId + '\'' +
", id='" + id + '\'' +
", traderId=" + traderId +
", disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
", disputeOpenerIsMaker=" + disputeOpenerIsMaker +
", openingDate=" + openingDate +
", traderPubKeyRing=" + traderPubKeyRing +
", tradeDate=" + tradeDate +
", contract=" + contract +
", contractHash=" + Utilities.bytesAsHexString(contractHash) +
", depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) +
", payoutTxSerialized not displayed for privacy reasons..." +
", depositTxId='" + depositTxId + '\'' +
", payoutTxId='" + payoutTxId + '\'' +
", contractAsJson='" + contractAsJson + '\'' +
", makerContractSignature='" + makerContractSignature + '\'' +
", takerContractSignature='" + takerContractSignature + '\'' +
", agentPubKeyRing=" + agentPubKeyRing +
", isSupportTicket=" + isSupportTicket +
", chatMessages=" + chatMessages +
", isClosed=" + isClosedProperty.get() +
", disputeResult=" + disputeResultProperty.get() +
", disputePayoutTxId='" + disputePayoutTxId + '\'' +
", isClosedProperty=" + isClosedProperty +
", disputeResultProperty=" + disputeResultProperty +
'}';
"\n tradeId='" + tradeId + '\'' +
",\n id='" + id + '\'' +
",\n traderId=" + traderId +
",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker +
",\n traderPubKeyRing=" + traderPubKeyRing +
",\n tradeDate=" + tradeDate +
",\n contract=" + contract +
",\n contractHash=" + Utilities.bytesAsHexString(contractHash) +
",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) +
",\n payoutTxSerialized=" + Utilities.bytesAsHexString(payoutTxSerialized) +
",\n depositTxId='" + depositTxId + '\'' +
",\n payoutTxId='" + payoutTxId + '\'' +
",\n contractAsJson='" + contractAsJson + '\'' +
",\n makerContractSignature='" + makerContractSignature + '\'' +
",\n takerContractSignature='" + takerContractSignature + '\'' +
",\n agentPubKeyRing=" + agentPubKeyRing +
",\n isSupportTicket=" + isSupportTicket +
",\n chatMessages=" + chatMessages +
",\n isClosedProperty=" + isClosedProperty +
",\n disputeResultProperty=" + disputeResultProperty +
",\n disputePayoutTxId='" + disputePayoutTxId + '\'' +
",\n openingDate=" + openingDate +
",\n storage=" + storage +
",\n supportType=" + supportType +
",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' +
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
"\n}";
}
}

View File

@ -55,12 +55,14 @@ import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public abstract class DisputeManager<T extends DisputeList<? extends DisputeList>> 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 +76,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
public DisputeManager(P2PService p2PService,
TradeWalletService tradeWalletService,
BtcWalletService walletService,
BtcWalletService btcWalletService,
WalletsSetup walletsSetup,
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
@ -84,7 +86,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
super(p2PService, walletsSetup);
this.tradeWalletService = tradeWalletService;
this.walletService = walletService;
this.btcWalletService = btcWalletService;
this.tradeManager = tradeManager;
this.closedTradableManager = closedTradableManager;
this.openOfferManager = openOfferManager;
@ -157,12 +159,19 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
// We get that message at both peers. The dispute object is in context of the trader
public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage);
@Nullable
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
protected abstract Trade.DisputeState getDisputeState_StartedByPeer();
public abstract void cleanupDisputes();
protected abstract String getDisputeInfo(Dispute dispute);
protected abstract String getDisputeIntroForPeer(String disputeInfo);
protected abstract String getDisputeIntroForDisputeCreator(String disputeInfo);
///////////////////////////////////////////////////////////////////////////////////////////
// Delegates for disputeListService
@ -236,7 +245,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
// 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) {
@ -246,13 +255,17 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
String errorMessage = null;
Dispute dispute = openNewDisputeMessage.getDispute();
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
dispute.setSupportType(openNewDisputeMessage.getSupportType());
dispute.setStorage(disputeListService.getStorage());
Contract contractFromOpener = dispute.getContract();
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing();
if (isAgent(dispute)) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
dispute.setStorage(disputeListService.getStorage());
disputeList.add(dispute);
errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing);
} else {
@ -270,15 +283,29 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
}
// We use the ChatMessage not the openNewDisputeMessage for the ACK
ObservableList<ChatMessage> messages = openNewDisputeMessage.getDispute().getChatMessages();
ObservableList<ChatMessage> 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 +372,19 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent() || reOpen) {
String disputeInfo = getDisputeInfo(dispute.isMediationDispute());
String disputeInfo = getDisputeInfo(dispute);
String disputeMessage = getDisputeIntroForDisputeCreator(disputeInfo);
String sysMsg = dispute.isSupportTicket() ?
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
: disputeMessage;
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);
@ -364,15 +393,22 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
}
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
if (agentNodeAddress == null) {
return;
}
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(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 +468,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
}
}
// 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,13 +495,17 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
disputeFromOpener.getMakerContractSignature(),
disputeFromOpener.getTakerContractSignature(),
disputeFromOpener.getAgentPubKeyRing(),
disputeFromOpener.isSupportTicket());
disputeFromOpener.isSupportTicket(),
disputeFromOpener.getSupportType());
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
String disputeInfo = getDisputeInfo(dispute.isMediationDispute());
String disputeInfo = getDisputeInfo(dispute);
String disputeMessage = getDisputeIntroForPeer(disputeInfo);
String sysMsg = dispute.isSupportTicket() ?
Res.get("support.peerOpenedTicket", disputeInfo)
: Res.get("support.peerOpenedDispute", disputeInfo);
Res.get("support.peerOpenedTicket", disputeInfo, Version.VERSION)
: disputeMessage;
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
dispute.getTradeId(),
@ -485,11 +525,12 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
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 +587,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
}
}
// 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 +731,4 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
.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);
}
}

View File

@ -18,6 +18,7 @@
package bisq.core.support.dispute.arbitration;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList;
@ -33,6 +34,8 @@ import java.util.stream.Collectors;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
@ToString
/*
@ -67,6 +70,9 @@ public final class ArbitrationDisputeList extends DisputeList<ArbitrationDispute
@Override
public Message toProtoMessage() {
list.forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.ARBITRATION), "Support type has to be ARBITRATION"));
return protobuf.PersistableEnvelope.newBuilder().setArbitrationDisputeList(protobuf.ArbitrationDisputeList.newBuilder()
.addAllDispute(ProtoUtil.collectionToProto(new ArrayList<>(list)))).build();
}
@ -77,7 +83,11 @@ public final class ArbitrationDisputeList extends DisputeList<ArbitrationDispute
List<Dispute> list = proto.getDisputeList().stream()
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
.collect(Collectors.toList());
list.forEach(e -> e.setStorage(storage));
list.forEach(e -> {
checkArgument(e.getSupportType().equals(SupportType.ARBITRATION), "Support type has to be ARBITRATION");
e.setStorage(storage);
});
return new ArbitrationDisputeList(storage, list);
}
}

View File

@ -24,6 +24,8 @@ import bisq.core.btc.setup.WalletsSetup;
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;
@ -49,6 +51,7 @@ import bisq.network.p2p.SendMailboxMessageListener;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing;
import org.bitcoinj.core.AddressFormatException;
@ -64,6 +67,8 @@ import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -120,9 +125,10 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
}
@Nullable
@Override
public NodeAddress getAgentNodeAddress(Dispute dispute) {
return dispute.getContract().getArbitratorNodeAddress();
return null;
}
@Override
@ -140,6 +146,22 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED));
}
@Override
protected String getDisputeInfo(Dispute dispute) {
String role = Res.get("shared.arbitrator").toLowerCase();
String link = "https://docs.bisq.network/trading-rules.html#legacy-arbitration"; //TODO needs to be created
return Res.get("support.initialInfo", role, role, link);
}
@Override
protected String getDisputeIntroForPeer(String disputeInfo) {
return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION);
}
@Override
protected String getDisputeIntroForDisputeCreator(String disputeInfo) {
return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
@ -152,7 +174,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
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 +252,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
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 +265,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
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 +350,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
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);

View File

@ -30,20 +30,20 @@ import lombok.Getter;
// 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;
}
}

View File

@ -20,6 +20,7 @@ package bisq.core.support.dispute.mediation;
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;
@ -43,22 +44,22 @@ import bisq.network.p2p.P2PService;
import bisq.common.Timer;
import bisq.common.UserThread;
import bisq.common.app.Version;
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;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@ -66,14 +67,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
@Singleton
public final class MediationManager extends DisputeManager<MediationDisputeList> {
// 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 +134,22 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
});
}
@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);
}
@Override
protected String getDisputeIntroForPeer(String disputeInfo) {
return Res.get("support.peerOpenedDisputeForMediation", disputeInfo, Version.VERSION);
}
@Override
protected String getDisputeIntroForDisputeCreator(String disputeInfo) {
return Res.get("support.youOpenedDisputeForMediation", disputeInfo, Version.VERSION);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
@ -205,6 +214,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Nullable
@Override
public NodeAddress getAgentNodeAddress(Dispute dispute) {
return dispute.getContract().getMediatorNodeAddress();

View File

@ -0,0 +1,93 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.refund;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
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;
import static com.google.common.base.Preconditions.checkArgument;
@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> {
RefundDisputeList(Storage<RefundDisputeList> 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<RefundDisputeList> storage, List<Dispute> list) {
super(storage, list);
}
@Override
public Message toProtoMessage() {
list.forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND"));
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<RefundDisputeList> storage) {
List<Dispute> list = proto.getDisputeList().stream()
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
.collect(Collectors.toList());
list.forEach(e -> {
checkArgument(e.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND");
e.setStorage(storage);
});
return new RefundDisputeList(storage, list);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<RefundDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public RefundDisputeListService(Storage<RefundDisputeList> storage) {
super(storage);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected RefundDisputeList getConcreteDisputeList() {
return new RefundDisputeList(storage);
}
}

View File

@ -0,0 +1,219 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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.app.Version;
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 javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@Singleton
public final class RefundManager extends DisputeManager<RefundDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// 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://docs.bisq.network/trading-rules.html#arbitration";
return Res.get("support.initialInfo", role, role, link);
}
@Override
protected String getDisputeIntroForPeer(String disputeInfo) {
return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION);
}
@Override
protected String getDisputeIntroForDisputeCreator(String disputeInfo) {
return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
}
///////////////////////////////////////////////////////////////////////////////////////////
// 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<Dispute> 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<Trade> 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<OpenOffer> 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<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Nullable
@Override
public NodeAddress getAgentNodeAddress(Dispute dispute) {
return dispute.getContract().getRefundAgentNodeAddress();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -0,0 +1,117 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.support.dispute.refund.refundagent;
import bisq.core.support.dispute.agent.DisputeAgent;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
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 implements CapabilityRequiringPayload {
public RefundAgent(NodeAddress nodeAddress,
PubKeyRing pubKeyRing,
List<String> languageCodes,
long registrationDate,
byte[] registrationPubKey,
String registrationSignature,
@Nullable String emailAddress,
@Nullable String info,
@Nullable Map<String, String> 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();
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.REFUND_AGENT);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<RefundAgent> {
@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<String> 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<RefundAgent> 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<RefundAgent> {
@Inject
public RefundAgentService(P2PService p2PService, FilterManager filterManager) {
super(p2PService, filterManager);
}
@Override
protected Set<RefundAgent> getDisputeAgentSet(List<String> 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<String> getDisputeAgentsFromFilter() {
return filterManager.getFilter() != null ? filterManager.getFilter().getRefundAgents() : new ArrayList<>();
}
public Map<NodeAddress, RefundAgent> getRefundAgents() {
return super.getDisputeAgents();
}
}

View File

@ -20,7 +20,7 @@ package bisq.core.trade;
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 final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
boolean isCurrencyForTakerFeeBtc,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> storage,
BtcWalletService btcWalletService) {
super(offer,
@ -56,6 +57,7 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
isCurrencyForTakerFeeBtc,
arbitratorNodeAddress,
mediatorNodeAddress,
refundAgentNodeAddress,
storage,
btcWalletService);
}
@ -84,6 +86,7 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
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 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
}
@Override
public void handleTakeOfferRequest(TradeMessage message,
public void handleTakeOfferRequest(InputsForDepositTxRequest message,
NodeAddress taker,
ErrorMessageHandler errorMessageHandler) {
((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler);

View File

@ -51,6 +51,7 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
NodeAddress tradingPeerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> storage,
BtcWalletService btcWalletService) {
super(offer,
@ -62,6 +63,7 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
tradingPeerNodeAddress,
arbitratorNodeAddress,
mediatorNodeAddress,
refundAgentNodeAddress,
storage,
btcWalletService);
}
@ -94,6 +96,7 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
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,

View File

@ -47,6 +47,7 @@ public abstract class BuyerTrade extends Trade {
NodeAddress tradingPeerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> 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<? extends TradableList> storage,
BtcWalletService btcWalletService) {
super(offer,
@ -76,6 +79,7 @@ public abstract class BuyerTrade extends Trade {
isCurrencyForTakerFeeBtc,
arbitratorNodeAddress,
mediatorNodeAddress,
refundAgentNodeAddress,
storage,
btcWalletService);
}

View File

@ -55,7 +55,6 @@ public final class Contract implements NetworkPayload {
private final String takerFeeTxID;
private final NodeAddress buyerNodeAddress;
private final NodeAddress sellerNodeAddress;
private final NodeAddress arbitratorNodeAddress;
private final NodeAddress mediatorNodeAddress;
private final boolean isBuyerMakerAndSellerTaker;
private final String makerAccountId;
@ -73,13 +72,16 @@ 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,
String takerFeeTxID,
NodeAddress buyerNodeAddress,
NodeAddress sellerNodeAddress,
NodeAddress arbitratorNodeAddress,
NodeAddress mediatorNodeAddress,
boolean isBuyerMakerAndSellerTaker,
String makerAccountId,
@ -91,14 +93,15 @@ public final class Contract implements NetworkPayload {
String makerPayoutAddressString,
String takerPayoutAddressString,
byte[] makerMultiSigPubKey,
byte[] takerMultiSigPubKey) {
byte[] takerMultiSigPubKey,
long lockTime,
NodeAddress refundAgentNodeAddress) {
this.offerPayload = offerPayload;
this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice;
this.takerFeeTxID = takerFeeTxID;
this.buyerNodeAddress = buyerNodeAddress;
this.sellerNodeAddress = sellerNodeAddress;
this.arbitratorNodeAddress = arbitratorNodeAddress;
this.mediatorNodeAddress = mediatorNodeAddress;
this.isBuyerMakerAndSellerTaker = isBuyerMakerAndSellerTaker;
this.makerAccountId = makerAccountId;
@ -111,6 +114,8 @@ public final class Contract implements NetworkPayload {
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 +133,6 @@ public final class Contract implements NetworkPayload {
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Nullable
public static Contract fromProto(protobuf.Contract proto, CoreProtoResolver coreProtoResolver) {
return new Contract(OfferPayload.fromProto(proto.getOfferPayload()),
proto.getTradeAmount(),
@ -136,7 +140,6 @@ public final class Contract implements NetworkPayload {
proto.getTakerFeeTxId(),
NodeAddress.fromProto(proto.getBuyerNodeAddress()),
NodeAddress.fromProto(proto.getSellerNodeAddress()),
NodeAddress.fromProto(proto.getArbitratorNodeAddress()),
NodeAddress.fromProto(proto.getMediatorNodeAddress()),
proto.getIsBuyerMakerAndSellerTaker(),
proto.getMakerAccountId(),
@ -148,7 +151,9 @@ public final class Contract implements NetworkPayload {
proto.getMakerPayoutAddressString(),
proto.getTakerPayoutAddressString(),
proto.getMakerMultiSigPubKey().toByteArray(),
proto.getTakerMultiSigPubKey().toByteArray());
proto.getTakerMultiSigPubKey().toByteArray(),
proto.getLockTime(),
NodeAddress.fromProto(proto.getRefundAgentNodeAddress()));
}
@Override
@ -160,7 +165,6 @@ public final class Contract implements NetworkPayload {
.setTakerFeeTxId(takerFeeTxID)
.setBuyerNodeAddress(buyerNodeAddress.toProtoMessage())
.setSellerNodeAddress(sellerNodeAddress.toProtoMessage())
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())
.setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage())
.setIsBuyerMakerAndSellerTaker(isBuyerMakerAndSellerTaker)
.setMakerAccountId(makerAccountId)
@ -173,6 +177,8 @@ public final class Contract implements NetworkPayload {
.setTakerPayoutAddressString(takerPayoutAddressString)
.setMakerMultiSigPubKey(ByteString.copyFrom(makerMultiSigPubKey))
.setTakerMultiSigPubKey(ByteString.copyFrom(takerMultiSigPubKey))
.setLockTime(lockTime)
.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())
.build();
}
@ -289,8 +295,8 @@ public final class Contract implements NetworkPayload {
",\n takerFeeTxID='" + takerFeeTxID + '\'' +
",\n buyerNodeAddress=" + buyerNodeAddress +
",\n sellerNodeAddress=" + sellerNodeAddress +
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
",\n mediatorNodeAddress=" + mediatorNodeAddress +
",\n refundAgentNodeAddress=" + refundAgentNodeAddress +
",\n isBuyerMakerAndSellerTaker=" + isBuyerMakerAndSellerTaker +
",\n makerAccountId='" + makerAccountId + '\'' +
",\n takerAccountId='" + takerAccountId + '\'' +
@ -304,6 +310,7 @@ public final class Contract implements NetworkPayload {
",\n takerMultiSigPubKey=" + Utilities.bytesAsHexString(takerMultiSigPubKey) +
",\n buyerMultiSigPubKey=" + Utilities.bytesAsHexString(getBuyerMultiSigPubKey()) +
",\n sellerMultiSigPubKey=" + Utilities.bytesAsHexString(getSellerMultiSigPubKey()) +
",\n lockTime=" + lockTime +
"\n}";
}
}

View File

@ -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);
}

View File

@ -20,7 +20,7 @@ package bisq.core.trade;
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 final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
boolean isCurrencyForTakerFeeBtc,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> 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 final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
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 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
}
@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);
}
}

View File

@ -51,6 +51,7 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
NodeAddress tradingPeerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> storage,
BtcWalletService btcWalletService) {
super(offer,
@ -62,6 +63,7 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
tradingPeerNodeAddress,
arbitratorNodeAddress,
mediatorNodeAddress,
refundAgentNodeAddress,
storage,
btcWalletService);
}
@ -94,6 +96,7 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
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,

View File

@ -46,6 +46,7 @@ public abstract class SellerTrade extends Trade {
NodeAddress tradingPeerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> 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<? extends TradableList> storage,
BtcWalletService btcWalletService) {
super(offer,
@ -75,6 +78,7 @@ public abstract class SellerTrade extends Trade {
isCurrencyForTakerFeeBtc,
arbitratorNodeAddress,
mediatorNodeAddress,
refundAgentNodeAddress,
storage,
btcWalletService);
}

View File

@ -21,6 +21,7 @@ import bisq.core.account.witness.AccountAgeWitnessService;
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.Arbitrator;
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 abstract class Trade implements Tradable, Model {
// 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 abstract class Trade implements Tradable, Model {
// #################### 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 abstract class Trade implements Tradable, Model {
// 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 abstract class Trade implements Tradable, Model {
@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 abstract class Trade implements Tradable, Model {
transient private ObjectProperty<Volume> tradeVolumeProperty;
final transient private Set<DecryptedMessageWithPubKey> 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<MediationResultState> mediationResultStateProperty = new SimpleObjectProperty<>(mediationResultState);
// Added in v1.2.0
@Getter
@Setter
private long lockTime;
@Nullable
@Getter
@Setter
private byte[] delayedPayoutTxBytes;
@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<RefundResultState> refundResultStateProperty = new SimpleObjectProperty<>(refundResultState);
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization
@ -396,6 +430,7 @@ public abstract class Trade implements Tradable, Model {
boolean isCurrencyForTakerFeeBtc,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> storage,
BtcWalletService btcWalletService) {
this.offer = offer;
@ -406,6 +441,7 @@ public abstract class Trade implements Tradable, Model {
this.btcWalletService = btcWalletService;
this.arbitratorNodeAddress = arbitratorNodeAddress;
this.mediatorNodeAddress = mediatorNodeAddress;
this.refundAgentNodeAddress = refundAgentNodeAddress;
txFeeAsLong = txFee.value;
takerFeeAsLong = takerFee.value;
@ -425,6 +461,7 @@ public abstract class Trade implements Tradable, Model {
NodeAddress tradingPeerNodeAddress,
@Nullable NodeAddress arbitratorNodeAddress,
@Nullable NodeAddress mediatorNodeAddress,
@Nullable NodeAddress refundAgentNodeAddress,
Storage<? extends TradableList> storage,
BtcWalletService btcWalletService) {
@ -434,6 +471,7 @@ public abstract class Trade implements Tradable, Model {
isCurrencyForTakerFeeBtc,
arbitratorNodeAddress,
mediatorNodeAddress,
refundAgentNodeAddress,
storage,
btcWalletService);
this.tradePrice = tradePrice;
@ -463,7 +501,8 @@ public abstract class Trade implements Tradable, Model {
.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 abstract class Trade implements Tradable, Model {
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(delayedPayoutTxBytes).ifPresent(e -> builder.setDelayedPayoutTxBytes(ByteString.copyFrom(delayedPayoutTxBytes)));
return builder.build();
}
@ -502,13 +545,18 @@ public abstract class Trade implements Tradable, Model {
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()));
trade.setDelayedPayoutTxBytes(ProtoUtil.byteArrayOrNullFromProto(proto.getDelayedPayoutTxBytes()));
trade.setLockTime(proto.getLockTime());
trade.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)
@ -531,6 +579,7 @@ public abstract class Trade implements Tradable, Model {
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
TradeWalletService tradeWalletService,
DaoFacade daoFacade,
TradeManager tradeManager,
OpenOfferManager openOfferManager,
ReferralIdService referralIdService,
@ -540,6 +589,7 @@ public abstract class Trade implements Tradable, Model {
TradeStatisticsManager tradeStatisticsManager,
ArbitratorManager arbitratorManager,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
KeyRing keyRing,
boolean useSavingsWallet,
Coin fundsNeededForTrade) {
@ -550,6 +600,7 @@ public abstract class Trade implements Tradable, Model {
btcWalletService,
bsqWalletService,
tradeWalletService,
daoFacade,
referralIdService,
user,
filterManager,
@ -557,6 +608,7 @@ public abstract class Trade implements Tradable, Model {
tradeStatisticsManager,
arbitratorManager,
mediatorManager,
refundAgentManager,
keyRing,
useSavingsWallet,
fundsNeededForTrade);
@ -572,6 +624,11 @@ public abstract class Trade implements Tradable, Model {
persist();
});
refundAgentManager.getDisputeAgentByNodeAddress(refundAgentNodeAddress).ifPresent(refundAgent -> {
refundAgentPubKeyRing = refundAgent.getPubKeyRing();
persist();
});
createTradeProtocol();
// If we have already received a msg we apply it.
@ -590,11 +647,10 @@ public abstract class Trade implements Tradable, Model {
// 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) {
log.debug("setDepositTx " + tx);
public void applyDepositTx(Transaction tx) {
this.depositTx = tx;
depositTxId = depositTx.getHashAsString();
setupConfidenceListener();
@ -608,6 +664,25 @@ public abstract class Trade implements Tradable, Model {
return depositTx;
}
public void applyDelayedPayoutTx(Transaction delayedPayoutTx) {
this.delayedPayoutTx = delayedPayoutTx;
this.delayedPayoutTxBytes = delayedPayoutTx.bitcoinSerialize();
persist();
}
public void applyDelayedPayoutTxBytes(byte[] delayedPayoutTxBytes) {
this.delayedPayoutTxBytes = delayedPayoutTxBytes;
persist();
}
@Nullable
public Transaction getDelayedPayoutTx() {
if (delayedPayoutTx == null) {
delayedPayoutTx = delayedPayoutTxBytes != null ? processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxBytes) : 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 +778,14 @@ public abstract class Trade implements Tradable, Model {
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 +904,12 @@ public abstract class Trade implements Tradable, Model {
}
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 +920,6 @@ public abstract class Trade implements Tradable, Model {
return getState().getPhase().ordinal() >= Phase.FIAT_SENT.ordinal();
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isFiatReceived() {
return getState().getPhase().ordinal() >= Phase.FIAT_RECEIVED.ordinal();
}
@ -861,6 +948,10 @@ public abstract class Trade implements Tradable, Model {
return mediationResultStateProperty;
}
public ReadOnlyObjectProperty<RefundResultState> refundResultStateProperty() {
return refundResultStateProperty;
}
public ReadOnlyObjectProperty<TradePeriodState> tradePeriodStateProperty() {
return tradePeriodStateProperty;
}
@ -987,7 +1078,7 @@ public abstract class Trade implements Tradable, Model {
",\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 +1095,14 @@ public abstract class Trade implements Tradable, Model {
",\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 +1113,21 @@ public abstract class Trade implements Tradable, Model {
",\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 delayedPayoutTxBytes=" + Utilities.bytesAsHexString(delayedPayoutTxBytes) +
",\n refundAgentNodeAddress=" + refundAgentNodeAddress +
",\n refundAgentPubKeyRing=" + refundAgentPubKeyRing +
",\n refundResultState=" + refundResultState +
",\n refundResultStateProperty=" + refundResultStateProperty +
"\n}";
}
}

View File

@ -19,10 +19,14 @@ package bisq.core.trade;
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.offer.availability.OfferAvailabilityModel;
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,9 +53,9 @@ import bisq.network.p2p.AckMessageSourceType;
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;
import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.FaultHandler;
@ -82,7 +88,7 @@ import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -116,6 +122,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<TradableList<Trade>> tradableListStorage;
@ -150,6 +158,8 @@ public class TradeManager implements PersistedDataHost {
AccountAgeWitnessService accountAgeWitnessService,
ArbitratorManager arbitratorManager,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
ClockWatcher clockWatcher,
Storage<TradableList<Trade>> storage) {
this.user = user;
@ -168,6 +178,8 @@ public class TradeManager implements PersistedDataHost {
this.accountAgeWitnessService = accountAgeWitnessService;
this.arbitratorManager = arbitratorManager;
this.mediatorManager = mediatorManager;
this.refundAgentManager = refundAgentManager;
this.daoFacade = daoFacade;
this.clockWatcher = clockWatcher;
tradableListStorage = storage;
@ -176,8 +188,8 @@ public class TradeManager implements PersistedDataHost {
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);
}
});
@ -274,11 +286,6 @@ public class TradeManager implements PersistedDataHost {
cleanUpAddressEntries();
// TODO remove once we support Taker side publishing at take offer process
// We start later to have better connectivity to the network
UserThread.runAfter(() -> tradeStatisticsManager.publishTradeStatistics(tradesForStatistics),
30, TimeUnit.SECONDS);
pendingTradesInitialized.set(true);
}
@ -307,18 +314,18 @@ public class TradeManager implements PersistedDataHost {
});
}
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<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(payDepositRequest.getTradeId());
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(inputsForDepositTxRequest.getTradeId());
if (openOfferOptional.isPresent() && openOfferOptional.get().getState() == OpenOffer.State.AVAILABLE) {
OpenOffer openOffer = openOfferOptional.get();
Offer offer = openOffer.getOffer();
@ -326,26 +333,28 @@ public class TradeManager implements PersistedDataHost {
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 +371,7 @@ public class TradeManager implements PersistedDataHost {
btcWalletService,
bsqWalletService,
tradeWalletService,
daoFacade,
this,
openOfferManager,
referralIdService,
@ -371,6 +381,7 @@ public class TradeManager implements PersistedDataHost {
tradeStatisticsManager,
arbitratorManager,
mediatorManager,
refundAgentManager,
keyRing,
useSavingsWallet,
fundsNeededForTrade);
@ -453,6 +464,7 @@ public class TradeManager implements PersistedDataHost {
model.getPeerNodeAddress(),
model.getSelectedArbitrator(),
model.getSelectedMediator(),
model.getSelectedRefundAgent(),
tradableListStorage,
btcWalletService);
else
@ -465,6 +477,7 @@ public class TradeManager implements PersistedDataHost {
model.getPeerNodeAddress(),
model.getSelectedArbitrator(),
model.getSelectedMediator(),
model.getSelectedRefundAgent(),
tradableListStorage,
btcWalletService);
@ -567,6 +580,70 @@ public class TradeManager implements PersistedDataHost {
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// 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
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 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 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();
}
}

View File

@ -21,6 +21,7 @@ import bisq.core.btc.model.RawTransactionInput;
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 bisq.common.util.Utilities;
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 @@ import javax.annotation.Nullable;
@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,43 @@ public final class PayDepositRequest extends TradeMessage {
private final String takerFeeTxId;
private final List<NodeAddress> acceptedArbitratorNodeAddresses;
private final List<NodeAddress> acceptedMediatorNodeAddresses;
private final List<NodeAddress> acceptedRefundAgentNodeAddresses;
@Nullable
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<RawTransactionInput> rawTransactionInputs,
long changeOutputValue,
@Nullable String changeOutputAddress,
byte[] takerMultiSigPubKey,
String takerPayoutAddressString,
PubKeyRing takerPubKeyRing,
PaymentAccountPayload takerPaymentAccountPayload,
String takerAccountId,
String takerFeeTxId,
List<NodeAddress> acceptedArbitratorNodeAddresses,
List<NodeAddress> 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<RawTransactionInput> rawTransactionInputs,
long changeOutputValue,
@Nullable String changeOutputAddress,
byte[] takerMultiSigPubKey,
String takerPayoutAddressString,
PubKeyRing takerPubKeyRing,
PaymentAccountPayload takerPaymentAccountPayload,
String takerAccountId,
String takerFeeTxId,
List<NodeAddress> acceptedArbitratorNodeAddresses,
List<NodeAddress> acceptedMediatorNodeAddresses,
List<NodeAddress> 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 +115,10 @@ public final class PayDepositRequest extends TradeMessage {
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 +130,7 @@ public final class PayDepositRequest extends TradeMessage {
@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 +151,23 @@ public final class PayDepositRequest extends TradeMessage {
.map(NodeAddress::toProtoMessage).collect(Collectors.toList()))
.addAllAcceptedMediatorNodeAddresses(acceptedMediatorNodeAddresses.stream()
.map(NodeAddress::toProtoMessage).collect(Collectors.toList()))
.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage())
.addAllAcceptedRefundAgentNodeAddresses(acceptedRefundAgentNodeAddresses.stream()
.map(NodeAddress::toProtoMessage).collect(Collectors.toList()))
.setMediatorNodeAddress(mediatorNodeAddress.toProtoMessage())
.setRefundAgentNodeAddress(refundAgentNodeAddress.toProtoMessage())
.setUid(uid);
Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress);
Optional.ofNullable(accountAgeWitnessSignatureOfOfferId).ifPresent(e -> builder.setAccountAgeWitnessSignatureOfOfferId(ByteString.copyFrom(e)));
Optional.ofNullable(arbitratorNodeAddress).ifPresent(e -> builder.setArbitratorNodeAddress(arbitratorNodeAddress.toProtoMessage()));
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<RawTransactionInput> rawTransactionInputs = proto.getRawTransactionInputsList().stream()
.map(rawTransactionInput -> new RawTransactionInput(rawTransactionInput.getIndex(),
rawTransactionInput.getParentTransaction().toByteArray(), rawTransactionInput.getValue()))
@ -166,8 +176,10 @@ public final class PayDepositRequest extends TradeMessage {
.map(NodeAddress::fromProto).collect(Collectors.toList());
List<NodeAddress> acceptedMediatorNodeAddresses = proto.getAcceptedMediatorNodeAddressesList().stream()
.map(NodeAddress::fromProto).collect(Collectors.toList());
List<NodeAddress> 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 +197,10 @@ public final class PayDepositRequest extends TradeMessage {
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 +209,7 @@ public final class PayDepositRequest extends TradeMessage {
@Override
public String toString() {
return "PayDepositRequest{" +
return "InputsForDepositTxRequest{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n tradeAmount=" + tradeAmount +
",\n tradePrice=" + tradePrice +
@ -213,11 +227,12 @@ public final class PayDepositRequest extends TradeMessage {
",\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();
}
}

View File

@ -21,7 +21,7 @@ import bisq.core.btc.model.RawTransactionInput;
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 lombok.Value;
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;
private final long lockTime;
public PublishDepositTxRequest(String tradeId,
PaymentAccountPayload makerPaymentAccountPayload,
String makerAccountId,
byte[] makerMultiSigPubKey,
String makerContractAsJson,
String makerContractSignature,
String makerPayoutAddressString,
byte[] preparedDepositTx,
List<RawTransactionInput> makerInputs,
NodeAddress senderNodeAddress,
String uid,
@Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx,
long currentDate) {
public InputsForDepositTxResponse(String tradeId,
PaymentAccountPayload makerPaymentAccountPayload,
String makerAccountId,
byte[] makerMultiSigPubKey,
String makerContractAsJson,
String makerContractSignature,
String makerPayoutAddressString,
byte[] preparedDepositTx,
List<RawTransactionInput> makerInputs,
NodeAddress senderNodeAddress,
String uid,
@Nullable byte[] accountAgeWitnessSignatureOfPreparedDepositTx,
long currentDate,
long lockTime) {
this(tradeId,
makerPaymentAccountPayload,
makerAccountId,
@ -87,7 +86,8 @@ public final class PublishDepositTxRequest extends TradeMessage implements Mailb
uid,
Version.getP2PMessageVersion(),
accountAgeWitnessSignatureOfPreparedDepositTx,
currentDate);
currentDate,
lockTime);
}
@ -95,20 +95,21 @@ public final class PublishDepositTxRequest extends TradeMessage implements Mailb
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private PublishDepositTxRequest(String tradeId,
PaymentAccountPayload makerPaymentAccountPayload,
String makerAccountId,
byte[] makerMultiSigPubKey,
String makerContractAsJson,
String makerContractSignature,
String makerPayoutAddressString,
byte[] preparedDepositTx,
List<RawTransactionInput> 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<RawTransactionInput> 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 @@ public final class PublishDepositTxRequest extends TradeMessage implements Mailb
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 final class PublishDepositTxRequest extends TradeMessage implements Mailb
.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<RawTransactionInput> 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 final class PublishDepositTxRequest extends TradeMessage implements Mailb
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 final class PublishDepositTxRequest extends TradeMessage implements Mailb
",\n uid='" + uid + '\'' +
",\n accountAgeWitnessSignatureOfPreparedDepositTx=" + Utilities.bytesAsHexString(accountAgeWitnessSignatureOfPreparedDepositTx) +
",\n currentDate=" + new Date(currentDate) +
",\n lockTime=" + lockTime +
"\n} " + super.toString();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 @@ import lombok.ToString;
@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;

View File

@ -19,38 +19,40 @@ package bisq.core.trade.protocol;
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 class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
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 class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) {
this.trade = trade;
public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) {
super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress);
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());
}
if (tradeMessage instanceof DepositTxAndDelayedPayoutTxMessage) {
handle((DepositTxAndDelayedPayoutTxMessage) tradeMessage, peerNodeAddress);
} else if (tradeMessage instanceof PayoutTxPublishedMessage) {
handle((PayoutTxPublishedMessage) tradeMessage, peerNodeAddress);
}
}
@ -115,11 +107,10 @@ public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
@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 class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
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 class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
// 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 class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
ApplyFilter.class,
MakerVerifyTakerAccount.class,
MakerVerifyTakerFeePayment.class,
BuyerAsMakerSignPayoutTx.class,
BuyerSignPayoutTx.class,
BuyerSendCounterCurrencyTransferStartedMessage.class,
BuyerSetupPayoutTxListener.class
);
@ -232,8 +240,10 @@ public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol
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);
}

View File

@ -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 class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
this.buyerAsTakerTrade = trade;
processModel.getTradingPeer().setPubKeyRing(trade.getOffer().getPubKeyRing());
Offer offer = checkNotNull(trade.getOffer());
processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing());
if (trade.isFiatSent() && !trade.isPayoutPublished()) {
Trade.Phase phase = trade.getState().getPhase();
if (phase == Trade.Phase.TAKER_FEE_PUBLISHED) {
TradeTaskRunner taskRunner = new TradeTaskRunner(trade,
() -> handleTaskRunnerSuccess("BuyerSetupDepositTxListener"),
this::handleTaskRunnerFault);
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 class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) {
this.trade = trade;
public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) {
super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress);
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());
}
if (tradeMessage instanceof DepositTxAndDelayedPayoutTxMessage) {
handle((DepositTxAndDelayedPayoutTxMessage) tradeMessage, peerNodeAddress);
} else if (tradeMessage instanceof PayoutTxPublishedMessage) {
handle((PayoutTxPublishedMessage) tradeMessage, peerNodeAddress);
}
}
@ -117,7 +126,7 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
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 class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
// 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 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
},
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 class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
ApplyFilter.class,
TakerVerifyMakerAccount.class,
TakerVerifyMakerFeePayment.class,
BuyerAsMakerSignPayoutTx.class,
BuyerSignPayoutTx.class,
BuyerSendCounterCurrencyTransferStartedMessage.class,
BuyerSetupPayoutTxListener.class
);
@ -221,8 +265,12 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
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);
}

View File

@ -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);
}

View File

@ -22,6 +22,7 @@ import bisq.core.btc.model.RawTransactionInput;
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.payment.payload.PaymentAccountPayload;
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 lombok.extern.slf4j.Slf4j;
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;
// Added in v1.2.0
@Setter
@Nullable
transient private byte[] delayedPayoutTxSignature;
@Setter
@Nullable
transient private Transaction preparedDelayedPayoutTx;
// Persistable Immutable (only set by PB)
@Setter
// Persistable Immutable (private setter only used by PB method)
private TradingPeer tradingPeer = new TradingPeer();
@Setter
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<NodeAddress> takerAcceptedArbitratorNodeAddresses;
@Nullable
@Setter
private List<NodeAddress> takerAcceptedMediatorNodeAddresses;
@Nullable
@Setter
private byte[] preparedDepositTx;
@Nullable
@Setter
@ -182,14 +187,13 @@ public class ProcessModel implements Model, PersistablePayload {
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 class ProcessModel implements Model, PersistablePayload {
// nullable
processModel.setTakeOfferFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakeOfferFeeTxId()));
processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature()));
List<NodeAddress> takerAcceptedArbitratorNodeAddresses = proto.getTakerAcceptedArbitratorNodeAddressesList().isEmpty() ?
null : proto.getTakerAcceptedArbitratorNodeAddressesList().stream()
.map(NodeAddress::fromProto).collect(Collectors.toList());
List<NodeAddress> 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<RawTransactionInput> rawTransactionInputs = proto.getRawTransactionInputsList().isEmpty() ?
null : proto.getRawTransactionInputsList().stream()
@ -243,6 +239,7 @@ public class ProcessModel implements Model, PersistablePayload {
BtcWalletService walletService,
BsqWalletService bsqWalletService,
TradeWalletService tradeWalletService,
DaoFacade daoFacade,
ReferralIdService referralIdService,
User user,
FilterManager filterManager,
@ -250,6 +247,7 @@ public class ProcessModel implements Model, PersistablePayload {
TradeStatisticsManager tradeStatisticsManager,
ArbitratorManager arbitratorManager,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
KeyRing keyRing,
boolean useSavingsWallet,
Coin fundsNeededForTrade) {
@ -259,6 +257,7 @@ public class ProcessModel implements Model, PersistablePayload {
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 class ProcessModel implements Model, PersistablePayload {
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 class ProcessModel implements Model, PersistablePayload {
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;
}
}

View File

@ -21,38 +21,42 @@ package bisq.core.trade.protocol;
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 class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
() -> handleTaskRunnerSuccess("MakerSetupDepositTxListener"),
this::handleTaskRunnerFault);
taskRunner.addTasks(MakerSetupDepositTxListener.class);
taskRunner.run();
}
}
@ -84,23 +87,11 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) {
this.trade = trade;
public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) {
super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress);
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());
}
if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) {
handle((CounterCurrencyTransferStartedMessage) tradeMessage, peerNodeAddress);
}
}
@ -110,11 +101,10 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
///////////////////////////////////////////////////////////////////////////////////////////
@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 class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
});
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 class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
// 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 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
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 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc
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);
}

View File

@ -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 class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
this.sellerAsTakerTrade = trade;
processModel.getTradingPeer().setPubKeyRing(trade.getOffer().getPubKeyRing());
Offer offer = checkNotNull(trade.getOffer());
processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing());
}
@ -74,22 +82,11 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade) {
this.trade = trade;
public void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress) {
super.doApplyMailboxTradeMessage(tradeMessage, peerNodeAddress);
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());
}
if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) {
handle((CounterCurrencyTransferStartedMessage) tradeMessage, peerNodeAddress);
}
}
@ -109,11 +106,9 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
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 class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
// 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 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
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 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
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);
}

View File

@ -21,11 +21,13 @@ import bisq.core.trade.MakerTrade;
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 lombok.extern.slf4j.Slf4j;
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 abstract class TradeProtocol {
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 abstract class TradeProtocol {
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 @@ public abstract class TradeProtocol {
}
///////////////////////////////////////////////////////////////////////////////////////////
// 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 @@ public abstract class TradeProtocol {
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 @@ public abstract class TradeProtocol {
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 @@ public abstract class TradeProtocol {
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 @@ public abstract class TradeProtocol {
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();
}
}

View File

@ -38,10 +38,23 @@ import lombok.extern.slf4j.Slf4j;
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() {
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -66,9 +66,9 @@ public class PublishTradeStatistics extends TradeTask {
NodeAddress mediatorNodeAddress = trade.getMediatorNodeAddress();
if (mediatorNodeAddress != null) {
// The first 4 chars are sufficient to identify an arbitrator.
// The first 4 chars are sufficient to identify an mediator.
// For testing with regtest/localhost we use the full address as its localhost and would result in
// same values for multiple arbitrators.
// same values for multiple mediators.
NetworkNode networkNode = model.getProcessModel().getP2PService().getNetworkNode();
String address = networkNode instanceof TorNetworkNode ?
mediatorNodeAddress.getFullAddress().substring(0, 4) :

View File

@ -54,10 +54,10 @@ public abstract class SetupPayoutTxListener extends TradeTask {
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 {

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -15,12 +15,13 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
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.checkArgument;
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,22 @@ public class MakerProcessDepositTxPublishedMessage extends TradeTask {
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.
byte[] delayedPayoutTxBytes = message.getDelayedPayoutTx();
trade.applyDelayedPayoutTxBytes(delayedPayoutTxBytes);
BtcWalletService.printTx("delayedPayoutTx received from peer", trade.getDelayedPayoutTx());
// update to the latest peer address of our peer if the message is correct
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
@ -63,8 +68,8 @@ public class MakerProcessDepositTxPublishedMessage extends TradeTask {
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);

View File

@ -19,6 +19,7 @@ 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.PayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.TradeTask;
@ -54,9 +55,9 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask {
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);

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -15,7 +15,7 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
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 lombok.extern.slf4j.Slf4j;
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 class MakerSetupDepositTxListener extends TradeTask {
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());
}

View File

@ -15,7 +15,7 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
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.checkArgument;
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 @@ public class BuyerAsMakerSignPayoutTx extends TradeTask {
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 @@ public class BuyerAsMakerSignPayoutTx extends TradeTask {
sellerPayoutAddressString,
buyerMultiSigKeyPair,
buyerMultiSigPubKey,
sellerMultiSigPubKey,
trade.getArbitratorBtcPubKey());
sellerMultiSigPubKey);
processModel.setPayoutTxSignature(payoutTxSignature);
complete();

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -15,22 +15,23 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
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 @@ public class SellerVerifiesPeersAccountAge extends TradeTask {
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);

View File

@ -57,52 +57,40 @@ public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask {
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<AddressEntry> 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<RawTransactionInput> 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<RawTransactionInput> 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 @@ public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask {
makerAddress,
makerChangeAddress,
buyerPubKey,
sellerPubKey,
arbitratorBtcPubKey);
sellerPubKey);
processModel.setPreparedDepositTx(result.depositTransaction);
processModel.setRawTransactionInputs(result.rawMakerInputs);

Some files were not shown because too many files have changed in this diff Show More