Skip to content

Commit

Permalink
feat(swaps): check payment immediately on recovery
Browse files Browse the repository at this point in the history
This commit makes it so that any swap deals that fail as maker while
sending payment will have the payment checked immediately upon being
added to set of swaps tracked by the `SwapRecovery` module. This is
instead of waiting for the 30 second timer to periodically retry tracked
swaps

It also refactors the code around checking and rechecking swaps that
may have a pending payment involved, and enhances logging output.

Closes #1598.
  • Loading branch information
sangaman committed Jun 3, 2020
1 parent 9e7e110 commit 428ac5d
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 66 deletions.
138 changes: 84 additions & 54 deletions lib/swaps/SwapRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import SwapClientManager from './SwapClientManager';
* ensuring that we do not lose funds on a partially completed swap.
*/
class SwapRecovery {
/** A map of payment hashes to swaps where we have a pending outgoing payment but don't know the preimage. */
public pendingSwaps: Map<string, SwapDealInstance> = new Map();
/** A map of payment hashes to swaps where we have recovered the preimage but not used it to claim payment yet. */
public recoveredPreimageSwaps: Map<string, SwapDealInstance> = new Map();
/** A map of payment hashes to swaps where we have a pending outgoing payment but don't know the preimage. */
private pendingSwaps: Map<string, SwapDealInstance> = new Map();
private pendingSwapsTimer?: NodeJS.Timeout;
/** The time in milliseconds between checks on the status of pending swaps. */
private static readonly PENDING_SWAP_RECHECK_INTERVAL = 300000;
Expand All @@ -25,94 +25,124 @@ class SwapRecovery {
}
}

private checkPendingSwaps = () => {
this.pendingSwaps.forEach(pendingSwap => this.recoverDeal(pendingSwap).catch(this.logger.error));
}

public stopTimer = () => {
if (this.pendingSwapsTimer) {
clearInterval(this.pendingSwapsTimer);
this.pendingSwapsTimer = undefined;
}
}

public getPendingSwapHashes = () => {
return Array.from(this.pendingSwaps.keys());
}

private checkPendingSwaps = () => {
this.pendingSwaps.forEach(pendingSwap => this.checkPaymentStatus(pendingSwap).catch(this.logger.error));
}

private failDeal = async (deal: SwapDealInstance, receivingSwapClient?: SwapClient) => {
if (receivingSwapClient) {
try {
await receivingSwapClient.removeInvoice(deal.rHash);
} catch (err) {
this.logger.error(`could not remove invoice for ${deal.rHash}`, err);
this.logger.warn(`could not remove invoice for ${deal.rHash}: ${err}`);
}
}
deal.state = SwapState.Error;
deal.failureReason = SwapFailureReason.Crash;

if (deal.state !== SwapState.Error) {
deal.state = SwapState.Error;
deal.failureReason = SwapFailureReason.Crash;
}

this.logger.info(`failed swap ${deal.rHash}`);
this.pendingSwaps.delete(deal.rHash);
await deal.save();
}

public recoverDeal = async (deal: SwapDealInstance) => {
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
/**
* Checks the status of the outgoing payment for a swap where we have begun
* sending a payment and handles the resolution of the swap once a final
* status for the payment is determined.
*/
private checkPaymentStatus = async (deal: SwapDealInstance) => {
this.logger.debug(`checking outgoing payment status for swap ${deal.rHash}`);
// ensure that we are tracking this pending swap
this.pendingSwaps.set(deal.rHash, deal);

const takerSwapClient = this.swapClientManager.get(deal.takerCurrency);
if (!makerSwapClient || !makerSwapClient.isConnected()) {
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
this.pendingSwaps.set(deal.rHash, deal);
return;
}
if (!takerSwapClient || !takerSwapClient.isConnected()) {
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline`);
this.pendingSwaps.set(deal.rHash, deal);
return;
}

if (deal.role === SwapRole.Maker) {
// we should check to see if our payment went through
// if it did, we can claim payment with the preimage for our side of the swap
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
if (!makerSwapClient || !makerSwapClient.isConnected()) {
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
this.pendingSwaps.set(deal.rHash, deal);
return;
}

const paymentStatus = await takerSwapClient.lookupPayment(deal.rHash, deal.takerCurrency);
if (paymentStatus.state === PaymentState.Succeeded) {
try {
deal.rPreimage = paymentStatus.preimage!;
if (makerSwapClient.type === SwapClientType.Raiden) {
this.logger.info(`recovered preimage ${deal.rPreimage} for swap ${deal.rHash}, ` +
'waiting for raiden to request secret and claim payment.');
this.recoveredPreimageSwaps.set(deal.rHash, deal);
} else {
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
deal.state = SwapState.Recovered;
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
}

this.pendingSwaps.delete(deal.rHash);
await deal.save();
// TODO: update order and trade in database to indicate they were executed
} catch (err) {
// tslint:disable-next-line: max-line-length
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash} and preimage ${deal.rPreimage}, **this must be resolved manually**`, err);
await this.failDeal(deal);
}
} else if (paymentStatus.state === PaymentState.Failed) {
// the payment failed, so cancel the open invoice if we have one
await this.failDeal(deal, makerSwapClient);
} else {
// the payment is pending, we will need to follow up on this
this.logger.debug(`swap for ${deal.rHash} still has pending payments and will be monitored`);
}
} else if (deal.role === SwapRole.Taker) {
// we are not at risk of losing funds, but we should cancel any open invoices
await this.failDeal(deal, takerSwapClient);
}
}

/**
* Attempts to recover a swap deal from whichever state it was left in
* including canceling or settling any related invoices & payments.
*/
public recoverDeal = async (deal: SwapDealInstance) => {
if (this.pendingSwaps.has(deal.rHash)) {
return; // we are already monitoring & attempting to recover this deal
}

this.logger.info(`recovering swap deal ${deal.rHash}`);
switch (deal.phase) {
case SwapPhase.SwapAccepted:
// we accepted the deal but stopped before sending payment
// cancel the open invoice if we have one
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
await this.failDeal(deal, makerSwapClient);
break;
case SwapPhase.SendingPayment:
// we started sending payment but didn't claim our payment
if (deal.role === SwapRole.Maker) {
// we should check to see if our payment went through
// if it did, we can claim payment with the preimage for our side of the swap
const paymentStatus = await takerSwapClient.lookupPayment(deal.rHash, deal.takerCurrency);
if (paymentStatus.state === PaymentState.Succeeded) {
try {
deal.rPreimage = paymentStatus.preimage!;
if (makerSwapClient.type === SwapClientType.Raiden) {
this.logger.info(`recovered preimage ${deal.rPreimage} for swap ${deal.rHash}, ` +
'waiting for raiden to request secret and claim payment.');
this.recoveredPreimageSwaps.set(deal.rHash, deal);
} else {
await makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
deal.state = SwapState.Recovered;
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
}
this.pendingSwaps.delete(deal.rHash);
await deal.save();
// TODO: update order and trade in database to indicate they were executed
} catch (err) {
// tslint:disable-next-line: max-line-length
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash} and preimage ${deal.rPreimage}, this should be investigated manually`, err);
await this.failDeal(deal);
}
} else if (paymentStatus.state === PaymentState.Failed) {
// the payment failed, so cancel the open invoice if we have one
await this.failDeal(deal, makerSwapClient);
} else {
// the payment is pending, we will need to follow up on this
this.logger.info(`recovered swap for ${deal.rHash} still has pending payments and will be monitored`);
this.pendingSwaps.set(deal.rHash, deal);
}
} else if (deal.role === SwapRole.Taker) {
// we are not at risk of losing funds, but we should cancel any open invoices
await this.failDeal(deal, takerSwapClient);
}
await this.checkPaymentStatus(deal);
break;
case SwapPhase.PaymentReceived:
// we've claimed our payment
// TODO: send a swap completed packet? it may be too late to do so
deal.state = SwapState.Recovered;
await deal.save();
break;
Expand Down
16 changes: 4 additions & 12 deletions lib/swaps/Swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class Swaps extends EventEmitter {
}

public getPendingSwapHashes = () => {
return Array.from(this.swapRecovery.pendingSwaps.keys());
return this.swapRecovery.getPendingSwapHashes();
}

/**
Expand Down Expand Up @@ -953,7 +953,7 @@ class Swaps extends EventEmitter {
return deal.rPreimage;
} catch (err) {
// the payment failed but we are unsure of its final status, so we fail
// the deal and assign the payment to be checked in swap recovery
// the deal and assign the payment to be checked in swap recovery.
// we don't remove our incoming invoice because we are not yet certain
// whether our outgoing payment can be claimed by the taker or not
switch (err.code) {
Expand Down Expand Up @@ -984,14 +984,6 @@ class Swaps extends EventEmitter {
break;
}

// we may already be in swap recovery for this deal due to a timeout
// prior to the payment failing . if not, we put this deal into swap
// recovery right now to monitor for a conclusive resolution
if (!this.swapRecovery.pendingSwaps.has(rHash)) {
const swapDealInstance = await this.repository.getSwapDeal(rHash);
this.swapRecovery.pendingSwaps.set(rHash, swapDealInstance!);
}

throw err;
}
} else {
Expand Down Expand Up @@ -1061,11 +1053,11 @@ class Swaps extends EventEmitter {
});

if (deal.phase === SwapPhase.SendingPayment && deal.role === SwapRole.Maker) {
// if the swap times out while we are in the middle of sending payment as the maker
// if the swap fails while we are in the middle of sending payment as the maker
// we need to make sure that the taker doesn't claim our payment without us having a chance
// to claim ours. we will send this swap to recovery to monitor its outcome
const swapDealInstance = await this.repository.getSwapDeal(rHash);
this.swapRecovery.pendingSwaps.set(rHash, swapDealInstance!);
await this.swapRecovery.recoverDeal(swapDealInstance!);
}
}

Expand Down

0 comments on commit 428ac5d

Please sign in to comment.