Skip to content

Commit 94cd695

Browse files
committed
do not delete mutisig wallet until payout has 60 confirmations
1 parent 3c34736 commit 94cd695

File tree

7 files changed

+58
-24
lines changed

7 files changed

+58
-24
lines changed

core/src/main/java/haveno/core/api/model/TradeInfo.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public class TradeInfo implements Payload {
9696
private final boolean isPayoutPublished;
9797
private final boolean isPayoutConfirmed;
9898
private final boolean isPayoutUnlocked;
99+
private final boolean isPayoutFinalized;
99100
private final boolean isCompleted;
100101
private final String contractAsJson;
101102
private final ContractInfo contract;
@@ -140,6 +141,7 @@ public TradeInfo(TradeInfoV1Builder builder) {
140141
this.isPayoutPublished = builder.isPayoutPublished();
141142
this.isPayoutConfirmed = builder.isPayoutConfirmed();
142143
this.isPayoutUnlocked = builder.isPayoutUnlocked();
144+
this.isPayoutFinalized = builder.isPayoutFinalized();
143145
this.isCompleted = builder.isCompleted();
144146
this.contractAsJson = builder.getContractAsJson();
145147
this.contract = builder.getContract();
@@ -204,6 +206,7 @@ public static TradeInfo toTradeInfo(Trade trade) {
204206
.withIsPayoutPublished(trade.isPayoutPublished())
205207
.withIsPayoutConfirmed(trade.isPayoutConfirmed())
206208
.withIsPayoutUnlocked(trade.isPayoutUnlocked())
209+
.withIsPayoutFinalized(trade.isPayoutFinalized())
207210
.withIsCompleted(trade.isCompleted())
208211
.withContractAsJson(trade.getContractAsJson())
209212
.withContract(contractInfo)
@@ -258,6 +261,7 @@ public haveno.proto.grpc.TradeInfo toProtoMessage() {
258261
.setIsPayoutPublished(isPayoutPublished)
259262
.setIsPayoutConfirmed(isPayoutConfirmed)
260263
.setIsPayoutUnlocked(isPayoutUnlocked)
264+
.setIsPayoutFinalized(isPayoutFinalized)
261265
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
262266
.setContract(contract.toProtoMessage())
263267
.setStartTime(startTime)
@@ -305,6 +309,7 @@ public static TradeInfo fromProto(haveno.proto.grpc.TradeInfo proto) {
305309
.withIsPayoutPublished(proto.getIsPayoutPublished())
306310
.withIsPayoutConfirmed(proto.getIsPayoutConfirmed())
307311
.withIsPayoutUnlocked(proto.getIsPayoutUnlocked())
312+
.withIsPayoutFinalized(proto.getIsPayoutFinalized())
308313
.withContractAsJson(proto.getContractAsJson())
309314
.withContract((ContractInfo.fromProto(proto.getContract())))
310315
.withStartTime(proto.getStartTime())
@@ -350,6 +355,7 @@ public String toString() {
350355
", isPayoutPublished=" + isPayoutPublished + "\n" +
351356
", isPayoutConfirmed=" + isPayoutConfirmed + "\n" +
352357
", isPayoutUnlocked=" + isPayoutUnlocked + "\n" +
358+
", isPayoutFinalized=" + isPayoutFinalized + "\n" +
353359
", isCompleted=" + isCompleted + "\n" +
354360
", offer=" + offer + "\n" +
355361
", contractAsJson=" + contractAsJson + "\n" +

core/src/main/java/haveno/core/api/model/builder/TradeInfoV1Builder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public final class TradeInfoV1Builder {
6969
private boolean isPayoutPublished;
7070
private boolean isPayoutConfirmed;
7171
private boolean isPayoutUnlocked;
72+
private boolean isPayoutFinalized;
7273
private boolean isCompleted;
7374
private String contractAsJson;
7475
private ContractInfo contract;
@@ -267,6 +268,11 @@ public TradeInfoV1Builder withIsPayoutUnlocked(boolean isPayoutUnlocked) {
267268
return this;
268269
}
269270

271+
public TradeInfoV1Builder withIsPayoutFinalized(boolean isPayoutFinalized) {
272+
this.isPayoutFinalized = isPayoutFinalized;
273+
return this;
274+
}
275+
270276
public TradeInfoV1Builder withIsCompleted(boolean isCompleted) {
271277
this.isCompleted = isCompleted;
272278
return this;

core/src/main/java/haveno/core/trade/Trade.java

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
148148
private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
149149
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
150150
private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5;
151+
private static final int NUM_BLOCKS_FINALIZED = 60; // ~2 hours before payout is considered finalized and multisig deleted
151152
protected final Object pollLock = new Object();
152153
private final Object removeTradeOnErrorLock = new Object();
153154
protected static final Object importMultisigLock = new Object();
@@ -264,7 +265,8 @@ public enum PayoutState {
264265
PAYOUT_UNPUBLISHED,
265266
PAYOUT_PUBLISHED,
266267
PAYOUT_CONFIRMED,
267-
PAYOUT_UNLOCKED;
268+
PAYOUT_UNLOCKED,
269+
PAYOUT_FINALIZED;
268270

269271
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
270272
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
@@ -658,7 +660,7 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
658660
if (newValue == Trade.Phase.DEPOSITS_CONFIRMED) onDepositsConfirmed();
659661
if (newValue == Trade.Phase.DEPOSITS_UNLOCKED) onDepositsUnlocked();
660662
if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent();
661-
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
663+
if (isDepositsPublished() && !isPayoutFinalized()) updatePollPeriod();
662664
if (isPaymentReceived()) {
663665
UserThread.execute(() -> {
664666
if (tradePhaseSubscription != null) {
@@ -704,10 +706,10 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
704706
processModel.getXmrWalletService().swapPayoutAddressEntryToAvailable(getId());
705707
}
706708

707-
// handle when payout unlocks
708-
if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) {
709+
// handle when payout finalized
710+
if (newValue == Trade.PayoutState.PAYOUT_FINALIZED) {
709711
if (!isInitialized) return;
710-
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId());
712+
log.info("Payout finalized for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId());
711713
if (isInitialized && isFinished()) clearAndShutDown();
712714
else deleteWallet();
713715
}
@@ -744,8 +746,8 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
744746
importMultisigHexIfScheduled();
745747
});
746748

747-
// done if deposit not requested or payout unlocked
748-
if (!isDepositRequested() || isPayoutUnlocked()) {
749+
// done if deposit not requested or payout finalized
750+
if (!isDepositRequested() || isPayoutFinalized()) {
749751
isInitialized = true;
750752
isFullyInitialized = true;
751753
return;
@@ -755,9 +757,17 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
755757
if (walletExists()) getWallet();
756758
else {
757759
MoneroTx payoutTx = getPayoutTx();
758-
if (payoutTx != null && payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) {
759-
log.warn("Payout state for {} {} is {} but payout is unlocked, updating state", getClass().getSimpleName(), getId(), getPayoutState());
760-
setPayoutStateUnlocked();
760+
if (payoutTx != null) {
761+
762+
// update payout state if necessary
763+
if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) {
764+
log.warn("Payout state for {} {} is {} but payout is unlocked, updating state", getClass().getSimpleName(), getId(), getPayoutState());
765+
setPayoutStateUnlocked();
766+
}
767+
if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_FINALIZED) {
768+
log.warn("Payout state for {} {} is {} but payout is finalized, updating state", getClass().getSimpleName(), getId(), getPayoutState());
769+
setPayoutStateFinalized();
770+
}
761771
isInitialized = true;
762772
isFullyInitialized = true;
763773
return;
@@ -777,7 +787,7 @@ public void initialize(ProcessModelServiceProvider serviceProvider) {
777787
}
778788

779789
public boolean isFinished() {
780-
return isPayoutUnlocked() && isCompleted();
790+
return isPayoutFinalized() && isCompleted();
781791
}
782792

783793
public void resetToPaymentSentState() {
@@ -790,7 +800,7 @@ public void resetToPaymentSentState() {
790800
}
791801

792802
public void initializeAfterMailboxMessages() {
793-
if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return;
803+
if (!isDepositRequested() || isPayoutFinalized() || isCompleted()) return;
794804
getProtocol().maybeReprocessPaymentSentMessage(false);
795805
getProtocol().maybeReprocessPaymentReceivedMessage(false);
796806
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
@@ -1030,15 +1040,15 @@ public void deleteWallet() {
10301040
syncedWallet = true;
10311041
}
10321042

1033-
// sync wallet if deposit requested and payout not unlocked
1034-
if (!isPayoutUnlocked() && !syncedWallet) {
1043+
// sync wallet if deposit requested and payout not finalized
1044+
if (!isPayoutFinalized() && !syncedWallet) {
10351045
log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId());
10361046
syncWallet(true);
10371047
}
10381048

1039-
// check if deposits published and payout not unlocked
1040-
if (isDepositsPublished() && !isPayoutUnlocked()) {
1041-
throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked");
1049+
// check if deposits published and payout not finalized
1050+
if (isDepositsPublished() && !isPayoutFinalized()) {
1051+
throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not finalized");
10421052
}
10431053

10441054
// check for balance
@@ -1645,7 +1655,7 @@ public void onShutDownStarted() {
16451655
public void shutDown() {
16461656
if (isShutDown) return; // ignore if already shut down
16471657
isShutDownStarted = true;
1648-
if (!isPayoutUnlocked()) log.info("Shutting down {} {}", getClass().getSimpleName(), getId());
1658+
if (!isPayoutFinalized()) log.info("Shutting down {} {}", getClass().getSimpleName(), getId());
16491659

16501660
// unregister p2p message listener
16511661
removeDecryptedDirectMessageListener();
@@ -2338,6 +2348,10 @@ public boolean isPayoutUnlocked() {
23382348
return getPayoutState().ordinal() >= PayoutState.PAYOUT_UNLOCKED.ordinal();
23392349
}
23402350

2351+
public boolean isPayoutFinalized() {
2352+
return getPayoutState().ordinal() >= PayoutState.PAYOUT_FINALIZED.ordinal();
2353+
}
2354+
23412355
public ReadOnlyDoubleProperty initProgressProperty() {
23422356
return initProgressProperty;
23432357
}
@@ -2718,8 +2732,8 @@ private void doPollWallet() {
27182732
// skip if shut down started
27192733
if (isShutDownStarted) return;
27202734

2721-
// skip if payout unlocked
2722-
if (isPayoutUnlocked()) return;
2735+
// skip if payout finalized
2736+
if (isPayoutFinalized()) return;
27232737

27242738
// skip if deposit txs unknown or not requested
27252739
if (!isDepositRequested() || processModel.getMaker().getDepositTxHash() == null || (processModel.getTaker().getDepositTxHash() == null && !hasBuyerAsTakerWithoutDeposit())) return;
@@ -2847,6 +2861,7 @@ else if (hasFailedTx && isPayoutPublished()) {
28472861
setPayoutStatePublished();
28482862
if (tx.isConfirmed()) setPayoutStateConfirmed();
28492863
if (!tx.isLocked()) setPayoutStateUnlocked();
2864+
if (tx.getNumConfirmations() != null && tx.getNumConfirmations() >= NUM_BLOCKS_FINALIZED) setPayoutStateFinalized();
28502865
}
28512866
}
28522867
}
@@ -3108,6 +3123,10 @@ private void setPayoutStateUnlocked() {
31083123
if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED);
31093124
}
31103125

3126+
private void setPayoutStateFinalized() {
3127+
if (!isPayoutFinalized()) setPayoutState(PayoutState.PAYOUT_FINALIZED);
3128+
}
3129+
31113130
private Trade getTrade() {
31123131
return this;
31133132
}
@@ -3128,8 +3147,8 @@ public void onNewBlock(long height) {
31283147
if (processing) return;
31293148
processing = true;
31303149

3131-
// skip if not idling and not waiting for payout to unlock
3132-
if (!isIdling() || !isPayoutPublished() || isPayoutUnlocked()) {
3150+
// skip if not idling and not waiting for payout to finalize
3151+
if (!isIdling() || !isPayoutPublished() || isPayoutFinalized()) {
31333152
processing = false;
31343153
return;
31353154
}

core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,7 @@ public synchronized void resetAddressEntriesForOpenOffer(String offerId) {
10771077
// swap trade payout to available if applicable
10781078
if (tradeManager == null) return;
10791079
Trade trade = tradeManager.getTrade(offerId);
1080-
if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
1080+
if (trade == null || trade.isPayoutFinalized()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
10811081
}
10821082

10831083
public synchronized void swapPayoutAddressEntryToAvailable(String offerId) {
@@ -1221,7 +1221,7 @@ public Stream<XmrAddressEntry> getAddressEntriesForAvailableBalanceStream() {
12211221
Stream<XmrAddressEntry> available = getFundedAvailableAddressEntries().stream();
12221222
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream());
12231223
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent()));
1224-
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked()));
1224+
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutFinalized()));
12251225
return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0);
12261226
}
12271227

desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ private void onPayoutStateChanged(Trade.PayoutState payoutState) {
453453
case PAYOUT_PUBLISHED:
454454
case PAYOUT_CONFIRMED:
455455
case PAYOUT_UNLOCKED:
456+
case PAYOUT_FINALIZED:
456457
sellerState.set(SellerState.STEP4);
457458
buyerState.set(BuyerState.STEP4);
458459
break;

proto/src/main/proto/grpc.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,7 @@ message TradeInfo {
911911
uint64 start_time = 40;
912912
uint64 max_duration_ms = 41;
913913
uint64 deadline_time = 42;
914+
bool is_payout_finalized = 43;
914915
}
915916

916917
message ContractInfo {

proto/src/main/proto/pb.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,7 @@ message Trade {
14861486
PAYOUT_PUBLISHED = 1;
14871487
PAYOUT_CONFIRMED = 2;
14881488
PAYOUT_UNLOCKED = 3;
1489+
PAYOUT_FINALIZED = 4;
14891490
}
14901491

14911492
enum DisputeState {

0 commit comments

Comments
 (0)