Skip to content

Commit

Permalink
recover from failed payout tx
Browse files Browse the repository at this point in the history
  • Loading branch information
woodser committed May 15, 2024
1 parent 7847460 commit 7e3d897
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 59 deletions.
2 changes: 1 addition & 1 deletion core/src/main/java/haveno/core/app/HavenoHeadlessApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ protected void setupHandlers() {
});
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
havenoSetup.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
havenoSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler"));
havenoSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler"));
havenoSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert));
Expand Down
40 changes: 1 addition & 39 deletions core/src/main/java/haveno/core/app/HavenoSetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,11 @@
import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.TradeManager;
import haveno.core.trade.TradeTxException;
import haveno.core.user.Preferences;
import haveno.core.user.Preferences.UseTorForXmr;
import haveno.core.user.User;
import haveno.core.util.FormattingUtils;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.xmr.model.AddressEntry;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.BtcWalletService;
import haveno.core.xmr.wallet.WalletsManager;
Expand All @@ -92,7 +90,6 @@
import java.util.Objects;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
Expand All @@ -107,7 +104,6 @@
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicBinding;

Expand Down Expand Up @@ -449,11 +445,7 @@ private void initWallet() {
walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler,
showPopupIfInvalidBtcConfigHandler,
() -> {
if (allBasicServicesInitialized) {
checkForLockedUpFunds();
}
},
() -> {},
() -> {});
}

Expand All @@ -466,10 +458,6 @@ private void initDomainServices() {
revolutAccountsUpdateHandler,
amazonGiftCardAccountsUpdateHandler);

if (xmrWalletService.downloadPercentageProperty().get() == 1) {
checkForLockedUpFunds();
}

alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) ->
displayAlertIfPresent(newValue, false));
displayAlertIfPresent(alertManager.alertMessageProperty().get(), false);
Expand All @@ -484,32 +472,6 @@ private void initDomainServices() {
// Utils
///////////////////////////////////////////////////////////////////////////////////////////

private void checkForLockedUpFunds() {
// We check if there are locked up funds in failed or closed trades
try {
Set<String> setOfAllTradeIds = tradeManager.getSetOfFailedOrClosedTradeIdsFromLockedInFunds();
btcWalletService.getAddressEntriesForTrade().stream()
.filter(e -> setOfAllTradeIds.contains(e.getOfferId()) &&
e.getContext() == AddressEntry.Context.MULTI_SIG)
.forEach(e -> {
Coin balance = e.getCoinLockedInMultiSigAsCoin();
if (balance.isPositive()) {
String message = Res.get("popup.warning.lockedUpFunds",
formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId());
log.warn(message);
if (lockedUpFundsHandler != null) {
lockedUpFundsHandler.accept(message);
}
}
});
} catch (TradeTxException e) {
log.warn(e.getMessage());
if (lockedUpFundsHandler != null) {
lockedUpFundsHandler.accept(e.getMessage());
}
}
}

@Nullable
public static String getLastHavenoVersion() {
File versionFile = getVersionFile();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,12 @@ public boolean isMaker(Tradable tradable) {
private void requestPersistence() {
persistenceManager.requestPersistence();
}

public void removeTrade(Trade trade) {
synchronized (closedTradables) {
if (closedTradables.remove(trade)) {
requestPersistence();
}
}
}
}
27 changes: 20 additions & 7 deletions core/src/main/java/haveno/core/trade/Trade.java
Original file line number Diff line number Diff line change
Expand Up @@ -1408,7 +1408,7 @@ public void onProtocolError() {

// check if deposit published
if (isDepositsPublished()) {
restorePublishedTrade();
restoreDepositsPublishedTrade();
return;
}

Expand Down Expand Up @@ -1446,7 +1446,7 @@ public void onProtocolError() {
// listen for deposits published to restore trade
protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> {
if (isDepositsPublished()) {
restorePublishedTrade();
restoreDepositsPublishedTrade();
if (protocolErrorStateSubscription != null) { // unsubscribe
protocolErrorStateSubscription.unsubscribe();
protocolErrorStateSubscription = null;
Expand Down Expand Up @@ -1496,7 +1496,7 @@ public void onProtocolError() {
});
}

private void restorePublishedTrade() {
private void restoreDepositsPublishedTrade() {

// close open offer
if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) {
Expand Down Expand Up @@ -2370,15 +2370,23 @@ private void pollWallet() {
setDepositTxs(txs);

// check if any outputs spent (observed on payout published)
boolean hasSpentOutput = false;
boolean hasFailedTx = false;
for (MoneroTxWallet tx : txs) {
if (tx.isFailed()) hasFailedTx = true;
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (Boolean.TRUE.equals(output.isSpent())) setPayoutStatePublished();
if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true;
}
}
if (hasSpentOutput) setPayoutStatePublished();
else if (hasFailedTx && isPayoutPublished()) {
log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId());
setPayoutState(PayoutState.PAYOUT_UNPUBLISHED);
}

// check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
for (MoneroTxWallet tx : txs) {
if (tx.isOutgoing()) {
if (tx.isOutgoing() && !tx.isFailed()) {
setPayoutTx(tx);
setPayoutStatePublished();
if (tx.isConfirmed()) setPayoutStateConfirmed();
Expand Down Expand Up @@ -2460,6 +2468,10 @@ private void setPayoutStateUnlocked() {
if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED);
}

private Trade getTrade() {
return this;
}

/**
* Listen to block notifications from the main wallet in order to sync
* idling trade wallets awaiting the payout to confirm or unlock.
Expand All @@ -2485,9 +2497,10 @@ public void onNewBlock(long height) {
try {

// get payout height if unknown
if (payoutHeight == null && getPayoutTxId() != null) {
if (payoutHeight == null && getPayoutTxId() != null && isPayoutPublished()) {
MoneroTx tx = xmrWalletService.getDaemon().getTx(getPayoutTxId());
if (tx.isConfirmed()) payoutHeight = tx.getHeight();
if (tx == null) log.warn("Payout tx not found for {} {}, txId={}", getTrade().getClass().getSimpleName(), getId(), getPayoutTxId());
else if (tx.isConfirmed()) payoutHeight = tx.getHeight();
}

// sync wallet if confirm or unlock expected
Expand Down
28 changes: 25 additions & 3 deletions core/src/main/java/haveno/core/trade/TradeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.beans.property.BooleanProperty;
Expand All @@ -129,6 +130,7 @@
import javafx.collections.ObservableList;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.Setter;
import monero.daemon.model.MoneroTx;
import org.bitcoinj.core.Coin;
import org.bouncycastle.crypto.params.KeyParameter;
Expand Down Expand Up @@ -174,6 +176,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private final LongProperty numPendingTrades = new SimpleLongProperty();
private final ReferralIdService referralIdService;

@Setter
@Nullable
private Consumer<String> lockedUpFundsHandler; // TODO: this is unused

// set comparator for processing mailbox messages
static {
MailboxMessageService.setMailboxMessageComparator(new MailboxMessageComparator());
Expand Down Expand Up @@ -492,6 +498,8 @@ private void initPersistedTrades() {
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
xmrWalletService.swapAddressEntryToAvailable(addressEntry.getOfferId(), addressEntry.getContext());
});

checkForLockedUpFunds();
}

// notify that persisted trades initialized
Expand Down Expand Up @@ -1040,15 +1048,21 @@ public void onMoveInvalidTradeToFailedTrades(Trade trade) {
}

public void onMoveFailedTradeToPendingTrades(Trade trade) {
addFailedTradeToPendingTrades(trade);
addTradeToPendingTrades(trade);
failedTradesManager.removeTrade(trade);
}

public void removeFailedTrade(Trade trade) {
public void onMoveClosedTradeToPendingTrades(Trade trade) {
trade.setCompleted(false);
addTradeToPendingTrades(trade);
closedTradableManager.removeTrade(trade);
}

private void removeFailedTrade(Trade trade) {
failedTradesManager.removeTrade(trade);
}

public void addFailedTradeToPendingTrades(Trade trade) {
private void addTradeToPendingTrades(Trade trade) {
if (!trade.isInitialized()) {
initPersistedTrade(trade);
}
Expand All @@ -1061,6 +1075,14 @@ public Stream<Trade> getTradesStreamWithFundsLockedIn() {
}
}

private void checkForLockedUpFunds() {
try {
getSetOfFailedOrClosedTradeIdsFromLockedInFunds();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

public Set<String> getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException {
AtomicReference<TradeTxException> tradeTxException = new AtomicReference<>();
synchronized (tradableList) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
synchronized (trade) {

// skip if no need to reprocess
if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) {
if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) {
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected void run() {
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses

// ack and complete if already processed
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) {
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) {
log.warn("Received another PaymentReceivedMessage which was already processed, ACKing");
complete();
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ private void setupHandlers() {
havenoSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg)
.useShutDownButton()
.show());
havenoSetup.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show());
tradeManager.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show());

havenoSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config)
.actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.ClosedTradableUtil;
import haveno.core.trade.Tradable;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.user.Preferences;
import haveno.core.util.PriceUtil;
import haveno.core.util.VolumeUtil;
Expand All @@ -49,18 +51,21 @@ class ClosedTradesDataModel extends ActivatableDataModel {
final AccountAgeWitnessService accountAgeWitnessService;
private final ObservableList<ClosedTradesListItem> list = FXCollections.observableArrayList();
private final ListChangeListener<Tradable> tradesListChangeListener;
private final TradeManager tradeManager;

@Inject
public ClosedTradesDataModel(ClosedTradableManager closedTradableManager,
ClosedTradableFormatter closedTradableFormatter,
Preferences preferences,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService) {
AccountAgeWitnessService accountAgeWitnessService,
TradeManager tradeManager) {
this.closedTradableManager = closedTradableManager;
this.closedTradableFormatter = closedTradableFormatter;
this.preferences = preferences;
this.priceFeedService = priceFeedService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.tradeManager = tradeManager;

tradesListChangeListener = change -> applyList();
}
Expand Down Expand Up @@ -124,4 +129,8 @@ private void applyList() {
// We sort by date, the earliest first
list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate()));
}

public void onMoveTradeToPendingTrades(Trade trade) {
tradeManager.onMoveClosedTradeToPendingTrades(trade);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
<TableColumn fx:id="stateColumn" minWidth="80"/>
<TableColumn fx:id="duplicateColumn" minWidth="30" maxWidth="30" sortable="false"/>
<TableColumn fx:id="avatarColumn" minWidth="40" maxWidth="40"/>
<TableColumn fx:id="removeTradeColumn" minWidth="40" maxWidth="40"/>
</columns>
</TableView>

Expand Down
Loading

0 comments on commit 7e3d897

Please sign in to comment.