diff --git a/lib/constants/enums.ts b/lib/constants/enums.ts index 79b632b7c..07a1e5302 100644 --- a/lib/constants/enums.ts +++ b/lib/constants/enums.ts @@ -81,6 +81,12 @@ export enum SwapState { Active = 0, Error = 1, Completed = 2, + /** + * A swap that was executed but wasn't formally completed. This may occur as a result of xud + * crashing late in the swap process, after htlcs for both legs of the swap are set up but + * before the swap is formally complete. + */ + Recovered = 3, } export enum ReputationEvent { @@ -125,6 +131,8 @@ export enum SwapFailureReason { DealTimedOut = 11, /** The swap failed due to an unrecognized error. */ UnknownError = 12, + /** The swap failed because of a system or xud crash while the swap was being executed. */ + Crash = 12, } export enum DisconnectionReason { diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index ba582eccc..a9db3aa9c 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -505,6 +505,23 @@ class LndClient extends SwapClient { } } + public lookupPayment = async (rHash: string) => { + const payments = await this.listPayments(); + for (const payment of payments.getPaymentsList()) { + if (payment.getPaymentHash() === rHash) { + const preimage = payment.getPaymentPreimage(); + if (preimage) { + return preimage; + } + } + } + return undefined; + } + + private listPayments = (): Promise => { + return this.unaryInvoiceCall('listPayments', new lndrpc.ListPaymentsRequest()); + } + private addHoldInvoice = (request: lndinvoices.AddHoldInvoiceRequest): Promise => { return this.unaryInvoiceCall('addHoldInvoice', request); } diff --git a/lib/raidenclient/RaidenClient.ts b/lib/raidenclient/RaidenClient.ts index 54450d18c..5e6e50387 100644 --- a/lib/raidenclient/RaidenClient.ts +++ b/lib/raidenclient/RaidenClient.ts @@ -150,6 +150,12 @@ class RaidenClient extends SwapClient { // not implemented, raiden does not use invoices } + public lookupPayment = async (rHash: string) => { + // raiden does not currently expose an API to retrieve a preimage for a completed payment + this.logger.warn(`raiden API can not provide the preimage for payment ${rHash}, manual intervention may be required`); + return undefined; + } + public getRoutes = async (_amount: number, _destination: string) => { // stub placeholder, query routes not currently implemented in raiden return [{ diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 503fc0221..69528774d 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -121,6 +121,12 @@ abstract class SwapClient extends EventEmitter { public abstract async removeInvoice(rHash: string): Promise; + /** + * Checks to see whether we've made a payment using a given rHash. + * @returns the preimage for the payment, or `undefined` if no payment was made + */ + public abstract async lookupPayment(rHash: string): Promise; + /** * Gets the block height of the chain backing this swap client. */ diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index f4e993a10..a6ad08249 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -127,9 +127,86 @@ class Swaps extends EventEmitter { // Load Swaps from database const result = await this.repository.getSwapDeals(); + const recoverPromises: Promise[] = []; result.forEach((deal: SwapDealInstance) => { this.usedHashes.add(deal.rHash); + + if (deal.state === SwapState.Active) { + recoverPromises.push(this.recoverDeal(deal)); + } }); + await Promise.all(recoverPromises); + } + + private recoverDeal = async (deal: SwapDealInstance) => { + const makerSwapClient = this.swapClientManager.get(deal.makerCurrency); + 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 `); + return; + } + if (!takerSwapClient || !takerSwapClient.isConnected()) { + this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline `); + return; + } + + 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 + try { + await makerSwapClient.removeInvoice(deal.rHash); + } catch (err) {} + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + break; + case SwapPhase.SendingAmount: + // 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 preimage = await takerSwapClient.lookupPayment(deal.rHash); + if (preimage) { + try { + deal.rPreimage = preimage; + await makerSwapClient.settleInvoice(deal.rHash, preimage); + deal.state = SwapState.Recovered; + this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`); + // 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 ${preimage}, this should be investigated manually`, err); + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + } + } else { + // we didn't complete the payment, so cancel the open invoice if we have one + try { + await makerSwapClient.removeInvoice(deal.rHash); + } catch (err) {} + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + } + } else if (deal.role === SwapRole.Taker) { + // we are not at risk of losing funds, but we should cancel any open invoices + try { + await takerSwapClient.removeInvoice(deal.rHash); + } catch (err) {} + deal.state = SwapState.Error; + deal.failureReason = SwapFailureReason.Crash; + } + break; + case SwapPhase.AmountReceived: + // we've claimed our payment + // TODO: send a swap completed packet? it may be too late to do so + deal.state = SwapState.Recovered; + break; + default: + break; + } + + await deal.save(); } private bind() {