xud
Version:
Exchange Union Daemon
176 lines • 10.1 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const assert_1 = __importDefault(require("assert"));
const events_1 = require("events");
const enums_1 = require("../constants/enums");
const SwapClient_1 = require("./SwapClient");
/**
* A class that's responsible for recovering swap deals that were interrupted due to a system or xud crash,
* ensuring that we do not lose funds on a partially completed swap.
*/
let SwapRecovery = /** @class */ (() => {
class SwapRecovery extends events_1.EventEmitter {
/** The time in milliseconds between checks on the status of pending swaps. */
constructor(swapClientManager, logger) {
super();
this.swapClientManager = swapClientManager;
this.logger = logger;
/** A map of payment hashes to swaps where we have a pending outgoing payment but don't know the preimage. */
this.pendingSwaps = new Map();
this.beginTimer = () => {
if (!this.pendingSwapsTimer) {
this.pendingSwapsTimer = setInterval(this.checkPendingSwaps, SwapRecovery.PENDING_SWAP_RECHECK_INTERVAL);
}
};
this.stopTimer = () => {
if (this.pendingSwapsTimer) {
clearInterval(this.pendingSwapsTimer);
this.pendingSwapsTimer = undefined;
}
};
this.getPendingSwapHashes = () => {
return Array.from(this.pendingSwaps.keys());
};
this.checkPendingSwaps = () => {
this.pendingSwaps.forEach(pendingSwap => this.checkPaymentStatus(pendingSwap).catch(this.logger.error));
};
this.failDeal = (deal, receivingSwapClient) => __awaiter(this, void 0, void 0, function* () {
if (receivingSwapClient) {
try {
yield receivingSwapClient.removeInvoice(deal.rHash);
}
catch (err) {
this.logger.warn(`could not remove invoice for ${deal.rHash}: ${err}`);
}
}
if (deal.state !== enums_1.SwapState.Error) {
deal.state = enums_1.SwapState.Error;
deal.failureReason = enums_1.SwapFailureReason.Crash;
}
this.logger.info(`failed swap ${deal.rHash}`);
this.pendingSwaps.delete(deal.rHash);
yield deal.save();
});
/**
* Claims the incoming payment for a deal where the outgoing payment has
* already gone through and where we already know the preimage.
*/
this.claimPayment = (deal) => __awaiter(this, void 0, void 0, function* () {
assert_1.default(deal.rPreimage);
// the maker payment is always the one that is claimed second, after the payment to taker
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
if (!makerSwapClient || !makerSwapClient.isConnected()) {
this.logger.warn(`could not claim payment for ${deal.rHash} because ${deal.makerCurrency} swap client is offline`);
return;
}
try {
yield makerSwapClient.settleInvoice(deal.rHash, deal.rPreimage, deal.makerCurrency);
deal.state = enums_1.SwapState.Recovered;
this.logger.info(`recovered ${deal.makerCurrency} swap payment of ${deal.makerAmount} using preimage ${deal.rPreimage}`);
this.pendingSwaps.delete(deal.rHash);
yield deal.save();
this.emit('recovered', deal);
}
catch (err) {
this.logger.error(`could not settle ${deal.makerCurrency} invoice for payment ${deal.rHash}`, err);
this.logger.alert(`incoming ${deal.makerCurrency} payment with hash ${deal.rHash} could not be settled with preimage ${deal.rPreimage}, **funds may be lost and this must be investigated manually**`);
// TODO: determine when we are permanently unable (due to htlc expiration or unknown invoice hash) to
// settle an invoice and fail the deal, rather than endlessly retrying settle invoice calls
}
});
/**
* 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.
*/
this.checkPaymentStatus = (deal) => __awaiter(this, void 0, void 0, function* () {
// ensure that we are tracking this pending swap
this.pendingSwaps.set(deal.rHash, deal);
if (deal.rPreimage) {
// if we already have the preimage for this deal, we can attempt to claim our payment right away
yield this.claimPayment(deal);
return;
}
this.logger.debug(`checking outgoing payment status for swap ${deal.rHash}`);
const takerSwapClient = this.swapClientManager.get(deal.takerCurrency);
if (!takerSwapClient || !takerSwapClient.isConnected()) {
this.logger.warn(`could not recover deal ${deal.rHash} because ${deal.takerCurrency} swap client is offline`);
return;
}
if (deal.role === enums_1.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`);
return;
}
const paymentStatus = yield takerSwapClient.lookupPayment(deal.rHash, deal.takerCurrency);
if (paymentStatus.state === SwapClient_1.PaymentState.Succeeded) {
deal.rPreimage = paymentStatus.preimage;
yield deal.save(); // persist the preimage to the database once we retrieve it
yield this.claimPayment(deal);
}
else if (paymentStatus.state === SwapClient_1.PaymentState.Failed) {
// the payment failed, so cancel the open invoice if we have one
yield 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 === enums_1.SwapRole.Taker) {
// we are not at risk of losing funds, but we should cancel any open invoices
yield 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.
*/
this.recoverDeal = (deal) => __awaiter(this, void 0, void 0, function* () {
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 enums_1.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);
yield this.failDeal(deal, makerSwapClient);
break;
case enums_1.SwapPhase.SendingPayment:
case enums_1.SwapPhase.PreimageResolved:
// we started sending payment but didn't claim our payment
yield this.checkPaymentStatus(deal);
break;
case enums_1.SwapPhase.PaymentReceived:
// we've claimed our payment
deal.state = enums_1.SwapState.Recovered;
yield deal.save();
break;
default:
break;
}
});
}
}
SwapRecovery.PENDING_SWAP_RECHECK_INTERVAL = 300000;
return SwapRecovery;
})();
exports.default = SwapRecovery;
//# sourceMappingURL=SwapRecovery.js.map