xud
Version:
Exchange Union Daemon
866 lines • 78.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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 distributions_poisson_quantile_1 = __importDefault(require("distributions-poisson-quantile"));
const events_1 = require("events");
const enums_1 = require("../constants/enums");
const packets_1 = require("../p2p/packets");
const packets = __importStar(require("../p2p/packets/types"));
const cryptoUtils_1 = require("../utils/cryptoUtils");
const UnitConverter_1 = require("../utils/UnitConverter");
const utils_1 = require("../utils/utils");
const consts_1 = require("./consts");
const errors_1 = __importStar(require("./errors"));
const SwapClient_1 = require("./SwapClient");
const SwapRecovery_1 = __importDefault(require("./SwapRecovery"));
const SwapRepository_1 = __importDefault(require("./SwapRepository"));
let Swaps = /** @class */ (() => {
class Swaps extends events_1.EventEmitter {
constructor({ logger, models, pool, swapClientManager, strict = true }) {
super();
/** A map between payment hashes and pending sanity swaps. */
this.sanitySwaps = new Map();
/** A map between payment hashes and swap deals. */
this.deals = new Map();
/** A map between payment hashes and timeouts for swaps. */
this.timeouts = new Map();
this.usedHashes = new Set();
this.init = () => __awaiter(this, void 0, void 0, function* () {
// update pool with current lnd & connext pubkeys
this.swapClientManager.getLndClientsMap().forEach(({ pubKey, chain, currency, uris }) => {
if (pubKey && chain) {
this.pool.updateLndState({
currency,
pubKey,
chain,
uris,
});
}
});
if (this.swapClientManager.connextClient) {
this.pool.updateConnextState(this.swapClientManager.connextClient.tokenAddresses, this.swapClientManager.connextClient.userIdentifier);
}
this.swapRecovery.beginTimer();
const swapDealInstances = yield this.repository.getSwapDeals();
swapDealInstances.forEach((deal) => {
this.usedHashes.add(deal.rHash);
if (deal.state === enums_1.SwapState.Active) {
this.swapRecovery.recoverDeal(deal).catch(this.logger.error);
}
});
});
/**
* Checks if there are connected swap clients for both currencies in a given trading pair.
* @returns `undefined` if both currencies are active, otherwise the ticker symbol for an inactive currency.
*/
this.checkInactiveCurrencyClients = (pairId) => {
// TODO: these checks are happening on a per-swap basis, it would be more efficient to
// check up front and disable currencies for inactive swap clients when we detect they
// become disconnected, then re-enable once they reconnect.
const [baseCurrency, quoteCurrency] = pairId.split('/');
const baseCurrencyClient = this.swapClientManager.get(baseCurrency);
if (baseCurrencyClient === undefined || !baseCurrencyClient.isConnected()) {
return baseCurrency;
}
const quoteCurrencyClient = this.swapClientManager.get(quoteCurrency);
if (quoteCurrencyClient === undefined || !quoteCurrencyClient.isConnected()) {
return quoteCurrency;
}
return;
};
/**
* Sends a swap failed packet to the counterparty peer in a swap with details about the error
* that caused the failure. Sets reqId if packet is a response to a request.
*/
this.sendErrorToPeer = ({ peer, rHash, failureReason = enums_1.SwapFailureReason.UnknownError, errorMessage, reqId }) => __awaiter(this, void 0, void 0, function* () {
const errorBody = {
rHash,
failureReason,
errorMessage,
};
this.logger.debug(`Sending ${enums_1.SwapFailureReason[errorBody.failureReason]} error to peer: ${JSON.stringify(errorBody)}`);
yield peer.sendPacket(new packets.SwapFailedPacket(errorBody, reqId));
});
/**
* Saves deal to database and deletes it from memory if it is no longer active.
* @param deal The deal to persist.
*/
this.persistDeal = (deal) => __awaiter(this, void 0, void 0, function* () {
yield this.repository.saveSwapDeal(deal);
if (deal.state !== enums_1.SwapState.Active) {
this.deals.delete(deal.rHash);
}
});
this.getPendingSwapHashes = () => {
return this.swapRecovery.getPendingSwapHashes();
};
/**
* Gets a deal by its rHash value.
* @param rHash The rHash value of the deal to get.
* @returns A deal if one is found, otherwise undefined.
*/
this.getDeal = (rHash) => {
return this.deals.get(rHash);
};
this.addDeal = (deal) => {
this.deals.set(deal.rHash, deal);
this.usedHashes.add(deal.rHash);
this.logger.debug(`New deal: ${JSON.stringify(deal)}`);
};
/**
* Checks if a swap for two given orders can be executed by ensuring both swap clients are active
* and if there exists a route to the maker.
* @param maker maker order
* @param taker taker order
* @returns `void` if the swap can be executed, throws a [[SwapFailureReason]] otherwise
*/
this.verifyExecution = (maker, taker) => __awaiter(this, void 0, void 0, function* () {
if (maker.pairId !== taker.pairId) {
throw enums_1.SwapFailureReason.InvalidOrders;
}
if (this.checkInactiveCurrencyClients(maker.pairId)) {
throw enums_1.SwapFailureReason.SwapClientNotSetup;
}
const { makerCurrency, makerUnits } = Swaps.calculateMakerTakerAmounts(taker.quantity, maker.price, maker.isBuy, maker.pairId);
const swapClient = this.swapClientManager.get(makerCurrency);
const peer = this.pool.getPeer(maker.peerPubKey);
const destination = peer.getIdentifier(swapClient.type, makerCurrency);
if (!destination) {
throw enums_1.SwapFailureReason.SwapClientNotSetup;
}
let route;
try {
route = yield swapClient.getRoute(makerUnits, destination, makerCurrency);
}
catch (err) {
if (err === errors_1.default.INSUFFICIENT_BALANCE) {
throw enums_1.SwapFailureReason.InsufficientBalance;
}
throw enums_1.SwapFailureReason.UnexpectedClientError;
}
if (!route) {
throw enums_1.SwapFailureReason.NoRouteFound;
}
});
/**
* A promise wrapper for a swap procedure
* @param maker the remote maker order we are filling
* @param taker our local taker order
* @returns A promise that resolves to a [[SwapSuccess]] once the swap is completed, throws a [[SwapFailureReason]] if it fails
*/
this.executeSwap = (maker, taker) => __awaiter(this, void 0, void 0, function* () {
yield this.verifyExecution(maker, taker);
const rHash = yield this.beginSwap(maker, taker);
return new Promise((resolve, reject) => {
const cleanup = () => {
this.removeListener('swap.paid', onPaid);
this.removeListener('swap.failed', onFailed);
};
const onPaid = (swapSuccess) => {
if (swapSuccess.rHash === rHash) {
cleanup();
resolve(swapSuccess);
}
};
const onFailed = (deal) => {
if (deal.rHash === rHash) {
cleanup();
reject(deal.failureReason);
}
};
this.on('swap.paid', onPaid);
this.on('swap.failed', onFailed);
});
});
/**
* Executes a sanity swap with a peer for a specified currency.
* @returns `true` if the swap succeeds, otherwise `false`
*/
this.executeSanitySwap = (currency, peer) => __awaiter(this, void 0, void 0, function* () {
const { rPreimage, rHash } = yield cryptoUtils_1.generatePreimageAndHash();
const peerPubKey = peer.nodePubKey;
const swapClient = this.swapClientManager.get(currency);
if (!swapClient) {
return false;
}
const destination = peer.getIdentifier(swapClient.type, currency);
if (!destination) {
return false;
}
const sanitySwap = {
rHash,
rPreimage,
currency,
peerPubKey,
};
this.sanitySwaps.set(rHash, sanitySwap);
const sanitySwapInitPacket = new packets.SanitySwapInitPacket({
currency,
rHash,
});
try {
yield Promise.all([
swapClient.addInvoice({ rHash, units: 1 }),
peer.sendPacket(sanitySwapInitPacket),
peer.wait(sanitySwapInitPacket.header.id, packets_1.PacketType.SanitySwapAck, Swaps.SANITY_SWAP_INIT_TIMEOUT),
]);
}
catch (err) {
this.logger.warn(`sanity swap could not be initiated for ${currency} using rHash ${rHash}: ${err.message}`);
swapClient.removeInvoice(rHash).catch(this.logger.error);
return false;
}
try {
yield swapClient.sendSmallestAmount(rHash, destination, currency);
this.logger.debug(`performed successful sanity swap with peer ${peerPubKey} for ${currency} using rHash ${rHash}`);
return true;
}
catch (err) {
this.logger.warn(`got payment error during sanity swap with ${peerPubKey} for ${currency} using rHash ${rHash}: ${err.message}`);
swapClient.removeInvoice(rHash).catch(this.logger.error);
return false;
}
});
/**
* Begins a swap to fill an order by sending a [[SwapRequestPacket]] to the maker.
* @param maker The remote maker order we are filling
* @param taker Our local taker order
* @returns The rHash for the swap, or a [[SwapFailureReason]] if the swap could not be initiated
*/
this.beginSwap = (maker, taker) => __awaiter(this, void 0, void 0, function* () {
const peer = this.pool.getPeer(maker.peerPubKey);
const quantity = Math.min(maker.quantity, taker.quantity);
const { makerCurrency, makerAmount, makerUnits, takerCurrency, takerAmount, takerUnits } = Swaps.calculateMakerTakerAmounts(quantity, maker.price, maker.isBuy, maker.pairId);
const clientType = this.swapClientManager.get(makerCurrency).type;
const destination = peer.getIdentifier(clientType, makerCurrency);
const takerCltvDelta = this.swapClientManager.get(takerCurrency).finalLock;
const { rPreimage, rHash } = yield cryptoUtils_1.generatePreimageAndHash();
const swapRequestBody = {
takerCltvDelta,
rHash,
orderId: maker.id,
pairId: maker.pairId,
proposedQuantity: taker.quantity,
};
const deal = Object.assign(Object.assign({}, swapRequestBody), { rPreimage,
takerCurrency,
makerCurrency,
takerAmount,
makerAmount,
takerUnits,
makerUnits,
destination, peerPubKey: peer.nodePubKey, localId: taker.localId, price: maker.price, isBuy: maker.isBuy, phase: enums_1.SwapPhase.SwapCreated, state: enums_1.SwapState.Active, role: enums_1.SwapRole.Taker, createTime: Date.now() });
this.addDeal(deal);
// Make sure we are connected to both swap clients
const inactiveCurrency = this.checkInactiveCurrencyClients(deal.pairId);
if (inactiveCurrency) {
yield this.failDeal({ deal, failureReason: enums_1.SwapFailureReason.SwapClientNotSetup, failedCurrency: inactiveCurrency });
throw enums_1.SwapFailureReason.SwapClientNotSetup;
}
yield peer.sendPacket(new packets.SwapRequestPacket(swapRequestBody));
yield this.setDealPhase(deal, enums_1.SwapPhase.SwapRequested);
return deal.rHash;
});
/**
* Accepts a proposed deal for a specified amount if a route and CLTV delta could be determined
* for the swap. Stores the deal in the local collection of deals.
* @returns A promise resolving to `true` if the deal was accepted, `false` otherwise.
*/
this.acceptDeal = (orderToAccept, requestPacket, peer) => __awaiter(this, void 0, void 0, function* () {
// TODO: max cltv to limit routes
// TODO: consider the time gap between taking the routes and using them.
this.logger.debug(`trying to accept deal: ${JSON.stringify(orderToAccept)} from xudPubKey: ${peer.nodePubKey}`);
const { rHash, proposedQuantity, pairId, takerCltvDelta, orderId } = requestPacket.body;
const reqId = requestPacket.header.id;
if (this.usedHashes.has(rHash)) {
yield this.sendErrorToPeer({
peer,
rHash,
reqId,
failureReason: enums_1.SwapFailureReason.PaymentHashReuse,
});
return false;
}
const { quantity, price, isBuy } = orderToAccept;
const { makerCurrency, makerAmount, makerUnits, takerCurrency, takerAmount, takerUnits } = Swaps.calculateMakerTakerAmounts(quantity, price, isBuy, pairId);
const makerSwapClient = this.swapClientManager.get(makerCurrency);
if (!makerSwapClient) {
yield this.sendErrorToPeer({
peer,
rHash,
reqId,
failureReason: enums_1.SwapFailureReason.SwapClientNotSetup,
errorMessage: 'Unsupported maker currency',
});
return false;
}
const takerSwapClient = this.swapClientManager.get(takerCurrency);
if (!takerSwapClient) {
yield this.sendErrorToPeer({
peer,
rHash,
reqId,
failureReason: enums_1.SwapFailureReason.SwapClientNotSetup,
errorMessage: 'Unsupported taker currency',
});
return false;
}
// Make sure we are connected to swap clients for both currencies
const inactiveCurrency = this.checkInactiveCurrencyClients(pairId);
if (inactiveCurrency) {
yield this.sendErrorToPeer({
peer,
rHash,
reqId,
failureReason: enums_1.SwapFailureReason.SwapClientNotSetup,
errorMessage: `${inactiveCurrency} is inactive`,
});
return false;
}
const takerIdentifier = peer.getIdentifier(takerSwapClient.type, takerCurrency);
const deal = {
rHash,
pairId,
proposedQuantity,
orderId,
price,
isBuy,
quantity,
makerAmount,
takerAmount,
makerCurrency,
takerCurrency,
makerUnits,
takerUnits,
takerCltvDelta,
takerPubKey: takerIdentifier,
destination: takerIdentifier,
peerPubKey: peer.nodePubKey,
localId: orderToAccept.localId,
phase: enums_1.SwapPhase.SwapCreated,
state: enums_1.SwapState.Active,
role: enums_1.SwapRole.Maker,
createTime: Date.now(),
};
// add the deal. Going forward we can "record" errors related to this deal.
this.addDeal(deal);
let makerToTakerRoute;
try {
makerToTakerRoute = yield takerSwapClient.getRoute(takerUnits, takerIdentifier, deal.takerCurrency, deal.takerCltvDelta);
}
catch (err) {
yield this.failDeal({
deal,
peer,
reqId,
failureReason: (err === errors_1.default.INSUFFICIENT_BALANCE) ? enums_1.SwapFailureReason.InsufficientBalance : enums_1.SwapFailureReason.UnexpectedClientError,
errorMessage: err.message,
failedCurrency: deal.takerCurrency,
});
return false;
}
if (!makerToTakerRoute) {
yield this.failDeal({
deal,
peer,
reqId,
failureReason: enums_1.SwapFailureReason.NoRouteFound,
errorMessage: 'Unable to find route to destination',
failedCurrency: deal.takerCurrency,
});
return false;
}
let height;
try {
height = yield takerSwapClient.getHeight();
}
catch (err) {
yield this.failDeal({
deal,
peer,
reqId,
failureReason: enums_1.SwapFailureReason.UnexpectedClientError,
errorMessage: `Unable to fetch block height: ${err.message}`,
failedCurrency: takerCurrency,
});
return false;
}
if (height) {
this.logger.debug(`got ${takerCurrency} block height of ${height}`);
const routeTotalTimeLock = makerToTakerRoute.getTotalTimeLock();
const routeLockDuration = routeTotalTimeLock - height;
const routeLockHours = Math.round(routeLockDuration * takerSwapClient.minutesPerBlock / 60);
this.logger.debug(`found route to taker with total lock duration of ${routeLockDuration} ${takerCurrency} blocks (~${routeLockHours}h)`);
// Add an additional buffer equal to our final lock to allow for more possible routes.
deal.takerMaxTimeLock = routeLockDuration + takerSwapClient.finalLock;
// Here we calculate the minimum lock delta we will expect as maker on the final hop to us on
// the first leg of the swap. This should ensure a very high probability that the final hop
// of the payment to us won't expire before our payment to the taker with time leftover to
// satisfy our finalLock/cltvDelta requirement for the incoming payment swap client.
const lockBuffer = Swaps.calculateLockBuffer(deal.takerMaxTimeLock, takerSwapClient.minutesPerBlock, makerSwapClient.minutesPerBlock);
const lockBufferHours = Math.round(lockBuffer * makerSwapClient.minutesPerBlock / 60);
this.logger.debug(`calculated lock buffer for first leg: ${lockBuffer} ${makerCurrency} blocks (~${lockBufferHours}h)`);
deal.makerCltvDelta = lockBuffer + makerSwapClient.finalLock;
const makerCltvDeltaHours = Math.round(deal.makerCltvDelta * makerSwapClient.minutesPerBlock / 60);
this.logger.debug(`lock delta for final hop to maker: ${deal.makerCltvDelta} ${makerCurrency} blocks (~${makerCltvDeltaHours}h)`);
}
if (!deal.makerCltvDelta) {
yield this.failDeal({
deal,
peer,
reqId,
failedCurrency: makerCurrency,
failureReason: enums_1.SwapFailureReason.UnexpectedClientError,
errorMessage: 'Could not calculate makerCltvDelta.',
});
return false;
}
try {
yield makerSwapClient.addInvoice({
rHash: deal.rHash,
units: deal.makerUnits,
expiry: deal.makerCltvDelta,
currency: deal.makerCurrency,
});
}
catch (err) {
yield this.failDeal({
deal,
peer,
reqId,
failedCurrency: makerCurrency,
failureReason: enums_1.SwapFailureReason.UnexpectedClientError,
errorMessage: `could not add invoice for while accepting deal: ${err.message}`,
});
return false;
}
// persist the swap deal to the database after we've added an invoice for it
const newPhasePromise = this.setDealPhase(deal, enums_1.SwapPhase.SwapAccepted);
const responseBody = {
rHash,
makerCltvDelta: deal.makerCltvDelta || 1,
quantity: proposedQuantity,
};
this.emit('swap.accepted', Object.assign(Object.assign({}, deal), { currencySending: deal.takerCurrency, currencyReceiving: deal.makerCurrency, amountSending: deal.takerAmount, amountReceiving: deal.makerAmount, quantity: deal.quantity }));
this.logger.debug(`sending swap accepted packet: ${JSON.stringify(responseBody)} to peer: ${peer.nodePubKey}`);
const sendSwapAcceptedPromise = peer.sendPacket(new packets.SwapAcceptedPacket(responseBody, requestPacket.header.id));
yield Promise.all([newPhasePromise, sendSwapAcceptedPromise]);
return true;
});
this.handleHtlcAccepted = (swapClient, rHash, amount, currency) => __awaiter(this, void 0, void 0, function* () {
let rPreimage;
const deal = this.getDeal(rHash);
if ((deal === null || deal === void 0 ? void 0 : deal.state) === enums_1.SwapState.Error) {
// we double check here to ensure that we don't attempt to resolve a hash
// and/or send payment for a stale deal that has already failed but
// eventually gets an incoming htlc accepted
this.logger.warn(`htlc accepted for failed deal ${rHash}`);
return;
}
try {
rPreimage = yield this.resolveHash(rHash, amount, currency);
}
catch (err) {
this.logger.error(`could not resolve hash for deal ${rHash}`, err);
return;
}
if (!deal) {
// if there's no deal associated with this hash, we treat it as a sanity swap
// and attempt to settle our incoming payment
yield swapClient.settleInvoice(rHash, rPreimage, currency).catch(this.logger.error);
}
else if (deal.state === enums_1.SwapState.Active) {
// we check that the deal is still active before we try to settle the invoice
try {
yield swapClient.settleInvoice(rHash, rPreimage, currency);
}
catch (err) {
this.logger.error(`could not settle invoice for deal ${rHash}`, err);
if (deal.role === enums_1.SwapRole.Maker) {
// if we are the maker, we must be able to settle the invoice otherwise we lose funds
// we will continuously retry settling the invoice until it succeeds
// 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
this.logger.alert(`incoming ${currency} payment with hash ${rHash} could not be settled with preimage ${rPreimage}, this is not expected and funds may be at risk`);
const settleRetryPromise = new Promise((resolve) => {
const settleRetryTimer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
try {
yield swapClient.settleInvoice(rHash, rPreimage, currency);
this.logger.info(`successfully settled invoice for deal ${rHash} on retry`);
resolve();
clearInterval(settleRetryTimer);
}
catch (err) {
this.logger.error(`could not settle invoice for deal ${rHash}`, err);
}
}), SwapRecovery_1.default.PENDING_SWAP_RECHECK_INTERVAL);
});
yield settleRetryPromise;
}
else {
// if we are the taker, funds are not at risk and we may simply fail the deal
yield this.failDeal({
deal,
failureReason: enums_1.SwapFailureReason.UnexpectedClientError,
errorMessage: err.message,
});
return;
}
}
// if we succeeded in settling our incoming payment we update the deal phase & state
yield this.setDealPhase(deal, enums_1.SwapPhase.PaymentReceived);
}
});
/**
* Handles a response from a peer to confirm a swap deal and updates the deal. If the deal is
* accepted, initiates the swap.
*/
this.handleSwapAccepted = (responsePacket, peer) => __awaiter(this, void 0, void 0, function* () {
assert_1.default(responsePacket.body, 'SwapAcceptedPacket does not contain a body');
const { quantity, rHash, makerCltvDelta } = responsePacket.body;
const deal = this.getDeal(rHash);
if (!deal) {
this.logger.warn(`received swap accepted for unrecognized deal: ${rHash}`);
// TODO: penalize peer
return;
}
if (deal.phase !== enums_1.SwapPhase.SwapRequested) {
this.logger.warn(`received swap accepted for deal that is not in SwapRequested phase: ${rHash}`);
// TODO: penalize peer
return;
}
if (deal.state === enums_1.SwapState.Error) {
// this swap deal may have already failed, either due to a DealTimedOut
// error while we were waiting for the swap to be accepted, or some
// other unexpected error or issue
this.logger.warn(`received swap accepted for deal that has already failed: ${rHash}`);
return;
}
// clear the timer waiting for acceptance of our swap offer, and set a new timer waiting for
// the swap to be completed
clearTimeout(this.timeouts.get(rHash));
this.timeouts.delete(rHash);
// update deal with maker's cltv delta
deal.makerCltvDelta = makerCltvDelta;
if (quantity) {
deal.quantity = quantity; // set the accepted quantity for the deal
if (quantity <= 0) {
yield this.failDeal({
deal,
peer,
failureReason: enums_1.SwapFailureReason.InvalidSwapPacketReceived,
errorMessage: 'accepted quantity must be a positive number',
});
// TODO: penalize peer
return;
}
else if (quantity > deal.proposedQuantity) {
yield this.failDeal({
deal,
peer,
failureReason: enums_1.SwapFailureReason.InvalidSwapPacketReceived,
errorMessage: 'accepted quantity should not be greater than proposed quantity',
});
// TODO: penalize peer
return;
}
else if (quantity < deal.proposedQuantity) {
const { makerAmount, takerAmount } = Swaps.calculateMakerTakerAmounts(quantity, deal.price, deal.isBuy, deal.pairId);
deal.takerAmount = takerAmount;
deal.makerAmount = makerAmount;
}
}
const makerSwapClient = this.swapClientManager.get(deal.makerCurrency);
const takerSwapClient = this.swapClientManager.get(deal.takerCurrency);
if (!makerSwapClient || !takerSwapClient) {
// We checked that we had a swap client for both currencies involved during the peer handshake. Still...
return;
}
try {
yield takerSwapClient.addInvoice({
rHash: deal.rHash,
units: deal.takerUnits,
expiry: deal.takerCltvDelta,
currency: deal.takerCurrency,
});
}
catch (err) {
yield this.failDeal({
deal,
peer,
failedCurrency: deal.takerCurrency,
failureReason: enums_1.SwapFailureReason.UnexpectedClientError,
errorMessage: err.message,
});
return;
}
// persist the deal to the database before we attempt to send
yield this.setDealPhase(deal, enums_1.SwapPhase.SendingPayment);
try {
yield makerSwapClient.sendPayment(deal);
}
catch (err) {
// first we must handle the edge case where the maker has paid us but failed to claim our payment
// in this case, we've already marked the swap as having been paid and completed
if (deal.state === enums_1.SwapState.Completed) {
this.logger.warn(`maker was unable to claim payment for ${deal.rHash} but has already paid us`);
return;
}
if (err.code === errors_1.errorCodes.PAYMENT_REJECTED) {
// if the maker rejected our payment, the swap failed due to an error on their side
// and we don't need to send them a SwapFailedPacket
yield this.failDeal({
deal,
failureReason: enums_1.SwapFailureReason.PaymentRejected,
errorMessage: err.message,
});
}
else {
yield this.failDeal({
deal,
peer,
failedCurrency: deal.makerCurrency,
failureReason: enums_1.SwapFailureReason.SendPaymentFailure,
errorMessage: err.message,
});
}
}
});
/**
* Verifies that the resolve request is valid. Checks the received amount vs
* the expected amount.
* @returns `true` if the resolve request is valid, `false` otherwise
*/
this.validateResolveRequest = (deal, resolveRequest) => {
var _a, _b;
const { amount, tokenAddress, expiration, chain_height } = resolveRequest;
const peer = this.pool.getPeer(deal.peerPubKey);
let expectedAmount;
let expectedTokenAddress;
let expectedCurrency;
let source;
let destination;
switch (deal.role) {
case enums_1.SwapRole.Maker:
expectedAmount = deal.makerUnits;
expectedCurrency = deal.makerCurrency;
expectedTokenAddress = (_a = this.swapClientManager.connextClient) === null || _a === void 0 ? void 0 : _a.tokenAddresses.get(deal.makerCurrency);
source = 'Taker';
destination = 'Maker';
const lockExpirationDelta = expiration - chain_height;
// We relax the validation by LOCK_EXPIRATION_SLIPPAGE blocks because
// new blocks could be mined during the time it takes from taker's
// payment to reach the maker for validation.
// This usually happens in simulated environments with fast mining enabled.
const LOCK_EXPIRATION_SLIPPAGE = 3;
if (deal.makerCltvDelta - LOCK_EXPIRATION_SLIPPAGE > lockExpirationDelta) {
this.logger.error(`
lockExpirationDelta of ${lockExpirationDelta} does not meet
makerCltvDelta ${deal.makerCltvDelta} - LOCK_EXPIRATION_SLIPPAGE ${LOCK_EXPIRATION_SLIPPAGE}
= ${deal.makerCltvDelta - LOCK_EXPIRATION_SLIPPAGE} minimum
`);
this.failDeal({
deal,
peer,
failureReason: enums_1.SwapFailureReason.InvalidResolveRequest,
failedCurrency: deal.makerCurrency,
errorMessage: 'Insufficient CLTV received on first leg',
}).catch(this.logger.error);
return false;
}
break;
case enums_1.SwapRole.Taker:
expectedAmount = deal.takerUnits;
expectedCurrency = deal.takerCurrency;
expectedTokenAddress = (_b = this.swapClientManager.connextClient) === null || _b === void 0 ? void 0 : _b.tokenAddresses.get(deal.takerCurrency);
source = 'Maker';
destination = 'Taker';
break;
default:
// this case should never happen, something is very wrong if so.
this.failDeal({
deal,
peer,
failureReason: enums_1.SwapFailureReason.UnknownError,
errorMessage: 'Unknown role detected for swap deal',
}).catch(this.logger.error);
return false;
}
if (!expectedTokenAddress || tokenAddress.toLowerCase() !== expectedTokenAddress.toLowerCase()) {
this.logger.error(`received token address ${tokenAddress}, expected ${expectedTokenAddress}`);
this.failDeal({
deal,
peer,
failedCurrency: expectedCurrency,
failureReason: enums_1.SwapFailureReason.InvalidResolveRequest,
errorMessage: `Token address ${tokenAddress} did not match ${expectedTokenAddress}`,
}).catch(this.logger.error);
return false;
}
if (amount < expectedAmount) {
this.logger.error(`received ${amount}, expected ${expectedAmount}`);
this.failDeal({
deal,
peer,
failedCurrency: expectedCurrency,
failureReason: enums_1.SwapFailureReason.InvalidResolveRequest,
errorMessage: `Amount sent from ${source} to ${destination} is too small`,
}).catch(this.logger.error);
return false;
}
return true;
};
/** Attempts to resolve the preimage for the payment hash of a pending sanity swap. */
this.resolveSanitySwap = (rHash, amount, htlcCurrency) => __awaiter(this, void 0, void 0, function* () {
assert_1.default(amount === 1, 'sanity swaps must have an amount of exactly 1 of the smallest unit supported by the currency');
const sanitySwap = this.sanitySwaps.get(rHash);
if (sanitySwap) {
assert_1.default(htlcCurrency === undefined || htlcCurrency === sanitySwap.currency, 'incoming htlc does not match sanity swap currency');
const { currency, peerPubKey, rPreimage } = sanitySwap;
this.sanitySwaps.delete(rHash); // we don't need to track sanity swaps that we've already attempted to resolve, delete to prevent a memory leak
if (rPreimage) {
// we initiated this sanity swap and can release the preimage immediately
return rPreimage;
}
else {
// we need to get the preimage by making a payment
const swapClient = this.swapClientManager.get(currency);
if (!swapClient) {
throw new Error('unsupported currency');
}
const peer = this.pool.getPeer(peerPubKey);
const destination = peer.getIdentifier(swapClient.type, currency);
try {
const preimage = yield swapClient.sendSmallestAmount(rHash, destination, currency);
this.logger.debug(`performed successful sanity swap with peer ${peerPubKey} for ${currency} using rHash ${rHash}`);
return preimage;
}
catch (err) {
this.logger.warn(`got payment error during sanity swap with ${peerPubKey} for ${currency} using rHash ${rHash}: ${err.message}`);
swapClient.removeInvoice(rHash).catch(this.logger.error);
throw err;
}
}
}
else {
throw errors_1.default.PAYMENT_HASH_NOT_FOUND(rHash);
}
});
/**
* Resolves the hash for an incoming HTLC to its preimage.
* @param rHash the payment hash to resolve
* @param amount the amount in satoshis
* @param htlcCurrency the currency of the HTLC
* @returns the preimage for the provided payment hash
*/
this.resolveHash = (rHash, amount, htlcCurrency) => __awaiter(this, void 0, void 0, function* () {
const deal = this.getDeal(rHash);
if (!deal) {
if (amount === 1) {
// if we don't have a deal for this hash, but its amount is exactly 1 satoshi, try to resolve it as a sanity swap
return this.resolveSanitySwap(rHash, amount, htlcCurrency);
}
else {
throw errors_1.default.PAYMENT_HASH_NOT_FOUND(rHash);
}
}
const peer = this.pool.getPeer(deal.peerPubKey);
if (deal.role === enums_1.SwapRole.Maker) {
// As the maker, we need to forward the payment to the other chain
assert_1.default(htlcCurrency === undefined || htlcCurrency === deal.makerCurrency, 'incoming htlc does not match expected deal currency');
this.logger.debug('Executing maker code to resolve hash');
const swapClient = this.swapClientManager.get(deal.takerCurrency);
// we update the phase persist the deal to the database before we attempt to send payment
yield this.setDealPhase(deal, enums_1.SwapPhase.SendingPayment);
// check to make sure we did not fail the deal for any reason. if we failed
// the deal then we may not be able to claim our payment even if we resolve the hash
assert_1.default(deal.state !== enums_1.SwapState.Error, `cannot send payment for failed swap ${deal.rHash}`);
try {
deal.rPreimage = yield swapClient.sendPayment(deal);
}
catch (err) {
this.logger.debug(`sendPayment in resolveHash for swap ${deal.rHash} failed due to ${err.message}`);
// the send payment call failed but we first double check its final status, so
// we only fail the deal when we know our payment won't go through. otherwise
// we extract the preimage if the payment went through in spite of the error
// or we fail the deal and go to SwapRecovery if it's still pending
const paymentStatus = yield swapClient.lookupPayment(rHash, deal.takerCurrency, deal.destination);
if (paymentStatus.state === SwapClient_1.PaymentState.Succeeded && paymentStatus.preimage) {
// just kidding, turns out the payment actually went through and we have the preimage!
// so we can continue with the swap
this.logger.debug(`payment for swap ${deal.rHash} succeeded despite sendPayment error, preimage is ${paymentStatus.preimage}`);
deal.rPreimage = paymentStatus.preimage;
}
else if (paymentStatus.state === SwapClient_1.PaymentState.Failed) {
// we've confirmed the payment has failed for good, so we can fail the deal
switch (err.code) {
case errors_1.errorCodes.FINAL_PAYMENT_ERROR:
yield this.failDeal({
deal,
peer,
failedCurrency: deal.takerCurrency,
failureReason: enums_1.SwapFailureReason.SendPaymentFailure,
errorMessage: err.message,
});
break;
case errors_1.errorCodes.PAYMENT_REJECTED:
yield this.failDeal({
deal,
failureReason: enums_1.SwapFailureReason.PaymentRejected,
errorMessage: err.message,
});
break;
default:
yield this.failDeal({
deal,
peer,
failedCurrency: deal.takerCurrency,
failureReason: enums_1.SwapFailureReason.UnknownError,
errorMessage: err.message,
});
break;
}
throw err;
}
else {
// the payment is in limbo, and could eventually go through. we need to make
// sure that the taker doesn't claim our payment without us having a chance
// to claim ours. we will monitor the outcome here.
this.logger.info(`started monitoring pending payment for swap ${deal.rHash}, will check every ${SwapRecovery_1.default.PENDING_SWAP_RECHECK_INTERVAL / 1000} seconds`);
const pendingPaymentPromise = new Promise((resolve, reject) => {
const recheckTimer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
this.logger.trace(`checking pending payment status for swap ${deal.rHash}`);
const paymentStatus = yield swapClient.lookupPayment(rHash, deal.takerCurrency, deal.destination);
this.logger.trace(`payment for swap ${deal.rHash} is in ${SwapClient_1.PaymentState[paymentStatus.state]} status}`);
if (paymentStatus.state === Sw