UNPKG

xud

Version:
866 lines 78.7 kB
"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