diff --git a/lib/connextclient/ConnextClient.ts b/lib/connextclient/ConnextClient.ts index 0cb803194..6827352bf 100644 --- a/lib/connextclient/ConnextClient.ts +++ b/lib/connextclient/ConnextClient.ts @@ -586,6 +586,8 @@ class ConnextClient extends SwapClient { } }, ); + + return undefined; }; private getTransferByRoutingId = async (routingId: string): Promise => { diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index f196a2bcb..001f937c6 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -45,10 +45,26 @@ interface LndClient { emit(event: 'initialized'): boolean; } +type ChannelCapacities = { + totalOutboundAmount: number; + totalInboundAmount: number; + balance: number; + inactiveBalance: number; + pendingOpenBalance: number; +}; + const GRPC_CLIENT_OPTIONS = { 'grpc.ssl_target_name_override': 'localhost', 'grpc.default_authority': 'localhost', }; +const LND_FAILURE_REASON_REVERSE_LOOKUP = { + 0: 'NONE', + 1: 'TIMEOUT', + 2: 'NO_ROUTE', + 3: 'ERROR', + 4: 'INCORRECT_PAYMENT_DETAILS', + 5: 'INSUFFICIENT_BALANCE', +}; /** A class representing a client to interact with lnd. */ class LndClient extends SwapClient { @@ -79,8 +95,7 @@ class LndClient extends SwapClient { private initRetryTimeout?: NodeJS.Timeout; private totalOutboundAmount = 0; private totalInboundAmount = 0; - private maxChannelOutboundAmount = 0; - private maxChannelInboundAmount = 0; + private refreshChannelCapacitiesPromise?: Promise; private initWalletResolve?: (value: boolean) => void; private watchMacaroonResolve?: (value: boolean) => void; @@ -89,6 +104,7 @@ class LndClient extends SwapClient { BTC: 10, LTC: 2.5, }; + private static MAX_PARTS = 5; /** * Creates an lnd client. @@ -244,8 +260,8 @@ class LndClient extends SwapClient { }; protected updateCapacity = async () => { - await this.channelBalance().catch(async (err) => { - this.logger.error('failed to update total outbound capacity', err); + await this.swapCapacities().catch(async (err) => { + this.logger.error('failed to update swap amount capacities', err); }); }; @@ -575,6 +591,7 @@ class LndClient extends SwapClient { }; public sendSmallestAmount = async (rHash: string, destination: string): Promise => { + // TODO: as of lnd 0.12.0, this won't work as PaymentAddr will be required and the only way to specify it will be with the pay req const request = this.buildSendRequest({ rHash, destination, @@ -590,34 +607,38 @@ class LndClient extends SwapClient { public sendPayment = async (deal: SwapDeal): Promise => { assert(deal.state === SwapState.Active); - let request: lndrouter.SendPaymentRequest; - assert(deal.makerCltvDelta, 'swap deal must have a makerCltvDelta'); + assert(deal.destination, 'swap deal as taker must have a destination'); + let amount: number; + let finalCltvDelta: number; + let cltvLimit: number | undefined; + if (deal.role === SwapRole.Taker) { // we are the taker paying the maker - assert(deal.destination, 'swap deal as taker must have a destination'); - request = this.buildSendRequest({ - rHash: deal.rHash, - destination: deal.destination, - amount: deal.makerAmount, - // Using the agreed upon makerCltvDelta. Maker won't accept - // our payment if we provide a smaller value. - finalCltvDelta: deal.makerCltvDelta, - }); + assert(deal.makerCltvDelta, 'swap deal must have a makerCltvDelta'); + amount = deal.makerAmount; + + // Using the agreed upon makerCltvDelta. Maker won't accept + // our payment if we provide a smaller value. + finalCltvDelta = deal.makerCltvDelta; } else { // we are the maker paying the taker - assert(deal.takerPubKey, 'swap deal as maker must have a takerPubKey'); assert(deal.takerCltvDelta, 'swap deal as maker must have a takerCltvDelta'); - request = this.buildSendRequest({ - rHash: deal.rHash, - destination: deal.takerPubKey, - amount: deal.takerAmount, - finalCltvDelta: deal.takerCltvDelta, - // Enforcing the maximum duration/length of the payment by specifying - // the cltvLimit. We add 3 blocks to offset the block padding set by lnd. - cltvLimit: deal.takerMaxTimeLock! + 3, - }); + amount = deal.takerAmount; + finalCltvDelta = deal.takerCltvDelta; + + // Enforcing the maximum duration/length of the payment by specifying + // the cltvLimit. We add 3 blocks to offset the block padding set by lnd. + cltvLimit = deal.takerMaxTimeLock! + 3; } - this.logger.debug(`sending payment of ${request.getAmt()} with hash ${deal.rHash}`); + const request = this.buildSendRequest({ + amount, + finalCltvDelta, + cltvLimit, + destination: deal.destination, + rHash: deal.rHash, + payReq: deal.payReq, + }); + this.logger.debug(`sending payment of ${amount} with hash ${deal.rHash}`); const preimage = await this.sendPaymentV2(request); return preimage; }; @@ -649,7 +670,7 @@ class LndClient extends SwapClient { case lndrpc.PaymentFailureReason.FAILURE_REASON_NO_ROUTE: case lndrpc.PaymentFailureReason.FAILURE_REASON_ERROR: case lndrpc.PaymentFailureReason.FAILURE_REASON_INSUFFICIENT_BALANCE: - reject(swapErrors.FINAL_PAYMENT_ERROR(lndrpc.PaymentFailureReason[response.getFailureReason()])); + reject(swapErrors.FINAL_PAYMENT_ERROR(LND_FAILURE_REASON_REVERSE_LOOKUP[response.getFailureReason()])); break; case lndrpc.PaymentFailureReason.FAILURE_REASON_INCORRECT_PAYMENT_DETAILS: reject(swapErrors.PAYMENT_REJECTED); @@ -693,26 +714,39 @@ class LndClient extends SwapClient { amount, finalCltvDelta, cltvLimit, + payReq, }: { rHash: string; destination: string; amount: number; finalCltvDelta: number; cltvLimit?: number; + payReq?: string; }): lndrouter.SendPaymentRequest => { const request = new lndrouter.SendPaymentRequest(); - request.setPaymentHash(Buffer.from(rHash, 'hex')); - request.setDest(Buffer.from(destination, 'hex')); - request.setAmt(amount); - request.setFinalCltvDelta(finalCltvDelta); - request.setTimeoutSeconds(MAX_PAYMENT_TIME / 1000); - const fee = Math.floor(MAX_FEE_RATIO * request.getAmt()); - request.setFeeLimitSat(fee); + if (payReq) { + request.setPaymentRequest(payReq); + request.setMaxParts(LndClient.MAX_PARTS); + } else { + // TODO: as of lnd 0.12.0, this won't work as PaymentAddr will be required and the only way to specify it will be with the pay req + request.setPaymentHash(Buffer.from(rHash, 'hex')); + request.setDest(Buffer.from(destination, 'hex')); + request.setAmt(amount); + request.setFinalCltvDelta(finalCltvDelta); + request.setMaxParts(1); + } + if (cltvLimit) { // cltvLimit is used to enforce the maximum // duration/length of the payment. request.setCltvLimit(cltvLimit); } + + request.setTimeoutSeconds(MAX_PAYMENT_TIME / 1000); + + const fee = Math.floor(MAX_FEE_RATIO * amount); + request.setFeeLimitSat(fee); + return request; }; @@ -741,82 +775,79 @@ class LndClient extends SwapClient { }; /** - * Updates all balances related to channels including active, inactive, and pending balances. - * Sets trading limits for this client accordingly. + * Updates and returns all capacities & balances related to channels including + * active, inactive, and pending balances. Sets trading limits for this client accordingly. */ - private updateChannelBalances = async () => { - const [channels, pendingChannels] = await Promise.all([this.listChannels(), this.pendingChannels()]); - - let maxOutbound = 0; - let maxInbound = 0; - let balance = 0; - let inactiveBalance = 0; - let totalOutboundAmount = 0; - let totalInboundAmount = 0; - channels.toObject().channelsList.forEach((channel) => { - if (channel.active) { - balance += channel.localBalance; - const outbound = Math.max(0, channel.localBalance - channel.localChanReserveSat); - totalOutboundAmount += outbound; - if (maxOutbound < outbound) { - maxOutbound = outbound; - } - - const inbound = Math.max(0, channel.remoteBalance - channel.remoteChanReserveSat); - totalInboundAmount += inbound; - if (maxInbound < inbound) { - maxInbound = inbound; - } - } else { - inactiveBalance += channel.localBalance; - } - }); - - if (this.maxChannelOutboundAmount !== maxOutbound) { - this.maxChannelOutboundAmount = maxOutbound; - this.logger.debug(`new channel maximum outbound capacity: ${maxOutbound}`); - } - - if (this.maxChannelInboundAmount !== maxInbound) { - this.maxChannelInboundAmount = maxInbound; - this.logger.debug(`new channel inbound capacity: ${maxInbound}`); + private refreshChannelCapacities = async (): Promise => { + // if we already have a pending refresh capacities call, then we reuse that one + // rather than have multiple or duplicate requests to lnd for channel balance + // info in parallel + if (this.refreshChannelCapacitiesPromise) { + return this.refreshChannelCapacitiesPromise; } + this.updateCapacityTimer?.refresh(); + + this.refreshChannelCapacitiesPromise = Promise.all([this.listChannels(), this.pendingChannels()]) + .then(([channels, pendingChannels]) => { + let balance = 0; + let inactiveBalance = 0; + let totalOutboundAmount = 0; + let totalInboundAmount = 0; + channels.toObject().channelsList.forEach((channel) => { + if (channel.active) { + balance += channel.localBalance; + const outbound = Math.max(0, channel.localBalance - channel.localChanReserveSat); + totalOutboundAmount += outbound; + + const inbound = Math.max(0, channel.remoteBalance - channel.remoteChanReserveSat); + totalInboundAmount += inbound; + } else { + inactiveBalance += channel.localBalance; + } + }); - if (this.totalOutboundAmount !== totalOutboundAmount) { - this.totalOutboundAmount = totalOutboundAmount; - this.logger.debug(`new channel total outbound capacity: ${totalOutboundAmount}`); - } + if (this.totalOutboundAmount !== totalOutboundAmount) { + this.totalOutboundAmount = totalOutboundAmount; + this.logger.debug(`new channel total outbound capacity: ${totalOutboundAmount}`); + } - if (this.totalInboundAmount !== totalInboundAmount) { - this.totalInboundAmount = totalInboundAmount; - this.logger.debug(`new channel total inbound capacity: ${totalInboundAmount}`); - } + if (this.totalInboundAmount !== totalInboundAmount) { + this.totalInboundAmount = totalInboundAmount; + this.logger.debug(`new channel total inbound capacity: ${totalInboundAmount}`); + } - const pendingOpenBalance = pendingChannels - .toObject() - .pendingOpenChannelsList.reduce((sum, pendingChannel) => sum + (pendingChannel.channel?.localBalance ?? 0), 0); + const pendingOpenBalance = pendingChannels + .toObject() + .pendingOpenChannelsList.reduce( + (sum, pendingChannel) => sum + (pendingChannel.channel?.localBalance ?? 0), + 0, + ); + + return { + totalOutboundAmount, + totalInboundAmount, + balance, + inactiveBalance, + pendingOpenBalance, + }; + }) + .finally(() => { + this.refreshChannelCapacitiesPromise = undefined; + }); - return { - maxOutbound, - maxInbound, - totalOutboundAmount, - totalInboundAmount, - balance, - inactiveBalance, - pendingOpenBalance, - }; + return this.refreshChannelCapacitiesPromise; }; public channelBalance = async (): Promise => { - const { balance, inactiveBalance, pendingOpenBalance } = await this.updateChannelBalances(); + const { balance, inactiveBalance, pendingOpenBalance } = await this.refreshChannelCapacities(); return { balance, inactiveBalance, pendingOpenBalance }; }; public swapCapacities = async (): Promise => { - const { maxOutbound, maxInbound, totalInboundAmount, totalOutboundAmount } = await this.updateChannelBalances(); // get fresh balances + const { totalInboundAmount, totalOutboundAmount } = await this.refreshChannelCapacities(); // get fresh balances return { - maxOutboundChannelCapacity: maxOutbound, - maxInboundChannelCapacity: maxInbound, + maxOutboundChannelCapacity: totalOutboundAmount, + maxInboundChannelCapacity: totalInboundAmount, totalOutboundCapacity: totalOutboundAmount, totalInboundCapacity: totalInboundAmount, }; @@ -928,8 +959,22 @@ class LndClient extends SwapClient { }; public getRoute = async (units: bigint, destination: string, _currency: string, finalLock = this.finalLock) => { + await this.refreshChannelCapacities(); + if (this.totalOutboundAmount < units) { + // if we don't have enough balance for this payment, we don't even try to find a route + throw swapErrors.INSUFFICIENT_BALANCE; + } + const request = new lndrpc.QueryRoutesRequest(); - request.setAmt(Number(units)); + // lnd does not currently support a way to find route(s) using multi path payments + // since we attempt to use up to multi path payments, we only check if there's any route + // that can support at least one part of the payment, as well as a check above to ensure + // we have sufficient balance across all channels to make the payment. This is opposed + // to attempting to replicate lnd's multi path payment route finding logic here, which + // would be complex and potentially slow/expensive or even impossible given that we don't + // have access to the lnd internal database and graph. + const minPartSize = Number(units) / LndClient.MAX_PARTS; + request.setAmt(minPartSize); request.setFinalCltvDelta(finalLock); request.setPubKey(destination); const fee = new lndrpc.FeeLimit(); @@ -951,7 +996,7 @@ class LndClient extends SwapClient { (!err.message.includes('unable to find a path to destination') && !err.message.includes('target not found')) ) { this.logger.error( - `error calling queryRoutes to ${destination}, amount ${units}, finalCltvDelta ${finalLock}`, + `error calling queryRoutes to ${destination}, amount ${minPartSize}, finalCltvDelta ${finalLock}`, err, ); throw err; @@ -960,10 +1005,12 @@ class LndClient extends SwapClient { if (route) { this.logger.debug( - `found a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}: ${route}`, + `found a route to ${destination} for ${minPartSize} units with finalCltvDelta ${finalLock}: ${route}`, ); } else { - this.logger.debug(`could not find a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}`); + this.logger.debug( + `could not find a route to ${destination} for ${minPartSize} units with finalCltvDelta ${finalLock}`, + ); } return route; }; @@ -1097,9 +1144,11 @@ class LndClient extends SwapClient { addHoldInvoiceRequest.setHash(hexToUint8Array(rHash)); addHoldInvoiceRequest.setValue(Number(units)); addHoldInvoiceRequest.setCltvExpiry(expiry); - await this.addHoldInvoice(addHoldInvoiceRequest); + const response = await this.addHoldInvoice(addHoldInvoiceRequest); this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${expiry}`); this.subscribeSingleInvoice(rHash); + + return response.getPaymentRequest(); }; public settleInvoice = async (rHash: string, rPreimage: string) => { diff --git a/lib/p2p/packets/types/SwapAcceptedPacket.ts b/lib/p2p/packets/types/SwapAcceptedPacket.ts index 09a13c932..9fdd7ea87 100644 --- a/lib/p2p/packets/types/SwapAcceptedPacket.ts +++ b/lib/p2p/packets/types/SwapAcceptedPacket.ts @@ -1,6 +1,7 @@ +import * as pb from '../../../proto/xudp2p_pb'; +import { removeUndefinedProps } from '../../../utils/utils'; import Packet, { PacketDirection, ResponseType } from '../Packet'; import PacketType from '../PacketType'; -import * as pb from '../../../proto/xudp2p_pb'; // TODO: proper error handling export type SwapAcceptedPacketBody = { @@ -9,6 +10,7 @@ export type SwapAcceptedPacketBody = { quantity: number; /** The CLTV delta from the current height that should be used to set the timelock for the final hop when sending to maker. */ makerCltvDelta: number; + payReq?: string; }; class SwapAcceptedPacket extends Packet { @@ -39,11 +41,12 @@ class SwapAcceptedPacket extends Packet { id: obj.id, reqId: obj.reqId, }, - body: { + body: removeUndefinedProps({ rHash: obj.rHash, quantity: obj.quantity, makerCltvDelta: obj.makerCltvDelta, - }, + payReq: obj.payReq || undefined, + }), }); }; @@ -54,6 +57,9 @@ class SwapAcceptedPacket extends Packet { msg.setRHash(this.body!.rHash); msg.setQuantity(this.body!.quantity); msg.setMakerCltvDelta(this.body!.makerCltvDelta); + if (this.body!.payReq) { + msg.setPayReq(this.body!.payReq); + } return msg.serializeBinary(); }; diff --git a/lib/p2p/packets/types/SwapRequestPacket.ts b/lib/p2p/packets/types/SwapRequestPacket.ts index 1c00702ef..c1740c817 100644 --- a/lib/p2p/packets/types/SwapRequestPacket.ts +++ b/lib/p2p/packets/types/SwapRequestPacket.ts @@ -1,6 +1,7 @@ +import { removeUndefinedProps } from '../../../utils/utils'; +import * as pb from '../../../proto/xudp2p_pb'; import Packet, { PacketDirection, ResponseType } from '../Packet'; import PacketType from '../PacketType'; -import * as pb from '../../../proto/xudp2p_pb'; export type SwapRequestPacketBody = { proposedQuantity: number; @@ -8,6 +9,7 @@ export type SwapRequestPacketBody = { orderId: string; rHash: string; takerCltvDelta: number; + payReq?: string; }; class SwapRequestPacket extends Packet { @@ -35,13 +37,14 @@ class SwapRequestPacket extends Packet { private static convert = (obj: pb.SwapRequestPacket.AsObject): SwapRequestPacket => { return new SwapRequestPacket({ header: { id: obj.id }, - body: { + body: removeUndefinedProps({ proposedQuantity: obj.proposedQuantity, pairId: obj.pairId, orderId: obj.orderId, rHash: obj.rHash, takerCltvDelta: obj.takerCltvDelta, - }, + payReq: obj.payReq || undefined, + }), }); }; @@ -53,6 +56,9 @@ class SwapRequestPacket extends Packet { msg.setOrderId(this.body!.orderId); msg.setRHash(this.body!.rHash); msg.setTakerCltvDelta(this.body!.takerCltvDelta); + if (this.body!.payReq) { + msg.setPayReq(this.body!.payReq); + } return msg.serializeBinary(); }; diff --git a/lib/proto/xudp2p_pb.d.ts b/lib/proto/xudp2p_pb.d.ts index 00e475be9..6df589fec 100644 --- a/lib/proto/xudp2p_pb.d.ts +++ b/lib/proto/xudp2p_pb.d.ts @@ -596,6 +596,9 @@ export class SwapRequestPacket extends jspb.Message { getTakerCltvDelta(): number; setTakerCltvDelta(value: number): void; + getPayReq(): string; + setPayReq(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): SwapRequestPacket.AsObject; @@ -615,6 +618,7 @@ export namespace SwapRequestPacket { orderId: string, rHash: string, takerCltvDelta: number, + payReq: string, } } @@ -634,6 +638,9 @@ export class SwapAcceptedPacket extends jspb.Message { getMakerCltvDelta(): number; setMakerCltvDelta(value: number): void; + getPayReq(): string; + setPayReq(value: string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): SwapAcceptedPacket.AsObject; @@ -652,6 +659,7 @@ export namespace SwapAcceptedPacket { rHash: string, quantity: number, makerCltvDelta: number, + payReq: string, } } diff --git a/lib/proto/xudp2p_pb.js b/lib/proto/xudp2p_pb.js index 805b75d9f..d04aadece 100644 --- a/lib/proto/xudp2p_pb.js +++ b/lib/proto/xudp2p_pb.js @@ -3974,7 +3974,8 @@ proto.xudp2p.SwapRequestPacket.toObject = function(includeInstance, msg) { pairId: jspb.Message.getFieldWithDefault(msg, 3, ""), orderId: jspb.Message.getFieldWithDefault(msg, 4, ""), rHash: jspb.Message.getFieldWithDefault(msg, 5, ""), - takerCltvDelta: jspb.Message.getFieldWithDefault(msg, 6, 0) + takerCltvDelta: jspb.Message.getFieldWithDefault(msg, 6, 0), + payReq: jspb.Message.getFieldWithDefault(msg, 7, "") }; if (includeInstance) { @@ -4035,6 +4036,10 @@ proto.xudp2p.SwapRequestPacket.deserializeBinaryFromReader = function(msg, reade var value = /** @type {number} */ (reader.readUint32()); msg.setTakerCltvDelta(value); break; + case 7: + var value = /** @type {string} */ (reader.readString()); + msg.setPayReq(value); + break; default: reader.skipField(); break; @@ -4106,6 +4111,13 @@ proto.xudp2p.SwapRequestPacket.serializeBinaryToWriter = function(message, write f ); } + f = message.getPayReq(); + if (f.length > 0) { + writer.writeString( + 7, + f + ); + } }; @@ -4199,6 +4211,21 @@ proto.xudp2p.SwapRequestPacket.prototype.setTakerCltvDelta = function(value) { }; +/** + * optional string pay_req = 7; + * @return {string} + */ +proto.xudp2p.SwapRequestPacket.prototype.getPayReq = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 7, "")); +}; + + +/** @param {string} value */ +proto.xudp2p.SwapRequestPacket.prototype.setPayReq = function(value) { + jspb.Message.setProto3StringField(this, 7, value); +}; + + /** * Generated by JsPbCodeGenerator. @@ -4250,7 +4277,8 @@ proto.xudp2p.SwapAcceptedPacket.toObject = function(includeInstance, msg) { reqId: jspb.Message.getFieldWithDefault(msg, 2, ""), rHash: jspb.Message.getFieldWithDefault(msg, 3, ""), quantity: +jspb.Message.getFieldWithDefault(msg, 4, 0.0), - makerCltvDelta: jspb.Message.getFieldWithDefault(msg, 5, 0) + makerCltvDelta: jspb.Message.getFieldWithDefault(msg, 5, 0), + payReq: jspb.Message.getFieldWithDefault(msg, 6, "") }; if (includeInstance) { @@ -4307,6 +4335,10 @@ proto.xudp2p.SwapAcceptedPacket.deserializeBinaryFromReader = function(msg, read var value = /** @type {number} */ (reader.readUint32()); msg.setMakerCltvDelta(value); break; + case 6: + var value = /** @type {string} */ (reader.readString()); + msg.setPayReq(value); + break; default: reader.skipField(); break; @@ -4371,6 +4403,13 @@ proto.xudp2p.SwapAcceptedPacket.serializeBinaryToWriter = function(message, writ f ); } + f = message.getPayReq(); + if (f.length > 0) { + writer.writeString( + 6, + f + ); + } }; @@ -4449,6 +4488,21 @@ proto.xudp2p.SwapAcceptedPacket.prototype.setMakerCltvDelta = function(value) { }; +/** + * optional string pay_req = 6; + * @return {string} + */ +proto.xudp2p.SwapAcceptedPacket.prototype.getPayReq = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 6, "")); +}; + + +/** @param {string} value */ +proto.xudp2p.SwapAcceptedPacket.prototype.setPayReq = function(value) { + jspb.Message.setProto3StringField(this, 6, value); +}; + + /** * Generated by JsPbCodeGenerator. diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 8cecff130..05f4f2bc4 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -91,8 +91,8 @@ abstract class SwapClient extends EventEmitter { public static readonly RECONNECT_INTERVAL = 5000; protected status: ClientStatus = ClientStatus.NotInitialized; protected reconnectionTimer?: NodeJS.Timer; + protected updateCapacityTimer?: NodeJS.Timer; - private updateCapacityTimer?: NodeJS.Timer; /** The maximum amount of time we will wait for the connection to be verified during initialization. */ private static INITIALIZATION_TIME_LIMIT = 5000; /** Time in milliseconds between updating the maximum outbound capacity. */ @@ -319,7 +319,7 @@ abstract class SwapClient extends EventEmitter { units: bigint; expiry?: number; currency?: string; - }): Promise; + }): Promise; public abstract async settleInvoice(rHash: string, rPreimage: string, currency?: string): Promise; diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index 4457c441a..f6fbb217c 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -471,19 +471,34 @@ class Swaps extends EventEmitter { const clientType = this.swapClientManager.get(makerCurrency)!.type; const destination = peer.getIdentifier(clientType, makerCurrency)!; - const takerCltvDelta = this.swapClientManager.get(takerCurrency)!.finalLock; + const takerSwapClient = this.swapClientManager.get(takerCurrency)!; + const takerCltvDelta = takerSwapClient.finalLock; const { rPreimage, rHash } = await generatePreimageAndHash(); + // TODO: once we can specify PaymentAddr on lnd invoices, we can move the add + // invoice step back to after the swap has been accepted + const payReq = await takerSwapClient.addInvoice({ + rHash, + units: takerUnits, + expiry: takerCltvDelta, + currency: takerCurrency, + }); + const swapRequestBody: packets.SwapRequestPacketBody = { takerCltvDelta, rHash, + payReq, orderId: maker.id, pairId: maker.pairId, proposedQuantity: taker.quantity, }; const deal: SwapDeal = { - ...swapRequestBody, + rHash, + orderId: maker.id, + pairId: maker.pairId, + proposedQuantity: taker.quantity, + takerCltvDelta, rPreimage, takerCurrency, makerCurrency, @@ -534,7 +549,7 @@ class Swaps extends EventEmitter { // 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 { rHash, proposedQuantity, pairId, takerCltvDelta, orderId, payReq } = requestPacket.body!; const reqId = requestPacket.header.id; if (this.usedHashes.has(rHash)) { await this.sendErrorToPeer({ @@ -610,6 +625,7 @@ class Swaps extends EventEmitter { makerUnits, takerUnits, takerCltvDelta, + payReq, takerPubKey: takerIdentifier, destination: takerIdentifier, peerPubKey: peer.nodePubKey!, @@ -718,8 +734,9 @@ class Swaps extends EventEmitter { return false; } + let makerPayReq: string | undefined; try { - await makerSwapClient.addInvoice({ + makerPayReq = await makerSwapClient.addInvoice({ rHash: deal.rHash, units: deal.makerUnits, expiry: deal.makerCltvDelta, @@ -744,6 +761,7 @@ class Swaps extends EventEmitter { rHash, makerCltvDelta: deal.makerCltvDelta || 1, quantity: proposedQuantity, + payReq: makerPayReq, }; this.emit('swap.accepted', { @@ -849,7 +867,7 @@ class Swaps extends EventEmitter { */ private handleSwapAccepted = async (responsePacket: packets.SwapAcceptedPacket, peer: Peer) => { assert(responsePacket.body, 'SwapAcceptedPacket does not contain a body'); - const { quantity, rHash, makerCltvDelta } = responsePacket.body; + const { quantity, rHash, makerCltvDelta, payReq } = responsePacket.body; const deal = this.getDeal(rHash); if (!deal) { this.logger.warn(`received swap accepted for unrecognized deal: ${rHash}`); @@ -878,6 +896,8 @@ class Swaps extends EventEmitter { // update deal with maker's cltv delta deal.makerCltvDelta = makerCltvDelta; + deal.payReq = payReq; + if (quantity) { deal.quantity = quantity; // set the accepted quantity for the deal if (quantity <= 0) { @@ -917,6 +937,9 @@ class Swaps extends EventEmitter { return; } + // TODO: re-enable add invoice *after* swap accepted once we are able to specify the + // PaymentAddr to lnd upon invoice creation + /* try { await takerSwapClient.addInvoice({ rHash: deal.rHash, @@ -934,6 +957,7 @@ class Swaps extends EventEmitter { }); return; } +*/ // persist the deal to the database before we attempt to send await this.setDealPhase(deal, SwapPhase.SendingPayment); @@ -1419,7 +1443,10 @@ class Swaps extends EventEmitter { const swapClient = this.swapClientManager.get(deal.makerCurrency)!; swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call } - } else if (deal.phase === SwapPhase.SendingPayment) { + // TODO: go back to only canceling invoice on SendingPayment phase as taker + // once we resume adding invoice *after* swap deal is accepted + // } else if (deal.phase === SwapPhase.SendingPayment) { + } else { const swapClient = this.swapClientManager.get(deal.takerCurrency)!; swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call } diff --git a/lib/swaps/types.ts b/lib/swaps/types.ts index 1f9f1898e..afe0ed48d 100644 --- a/lib/swaps/types.ts +++ b/lib/swaps/types.ts @@ -61,6 +61,8 @@ export type SwapDeal = { executeTime?: number; /** The time when the swap either completed successfully or failed. */ completeTime?: number; + /** The payment request from the counterparty. */ + payReq?: string; }; /** The result of a successful swap. */ diff --git a/proto/xudp2p.proto b/proto/xudp2p.proto index 2a10f74b8..472cb7a69 100644 --- a/proto/xudp2p.proto +++ b/proto/xudp2p.proto @@ -124,6 +124,7 @@ message SwapRequestPacket { string order_id = 4; string r_hash = 5; uint32 taker_cltv_delta = 6; + string pay_req = 7; } message SwapAcceptedPacket { @@ -132,6 +133,7 @@ message SwapAcceptedPacket { string r_hash = 3; double quantity = 4; uint32 maker_cltv_delta = 5; + string pay_req = 6; } message SwapFailedPacket { diff --git a/test/integration/Swaps.spec.ts b/test/integration/Swaps.spec.ts index 1ac149757..13b64ea40 100644 --- a/test/integration/Swaps.spec.ts +++ b/test/integration/Swaps.spec.ts @@ -101,11 +101,13 @@ describe('Swaps.Integration', () => { swapClientManager = sandbox.createStubInstance(SwapClientManager) as any; swapClientManager['swapClients'] = new Map(); const btcSwapClient = sandbox.createStubInstance(SwapClient) as any; + btcSwapClient['addInvoice'] = async () => {}; btcSwapClient['removeInvoice'] = async () => {}; btcSwapClient.getRoute = getRouteResponse; btcSwapClient.isConnected = () => true; swapClientManager['swapClients'].set('BTC', btcSwapClient); const ltcSwapClient = sandbox.createStubInstance(SwapClient) as any; + ltcSwapClient['addInvoice'] = async () => {}; ltcSwapClient['removeInvoice'] = async () => {}; ltcSwapClient.isConnected = () => true; ltcSwapClient.getRoute = getRouteResponse; diff --git a/test/jest/LndClient.spec.ts b/test/jest/LndClient.spec.ts index ba647eef1..b80957d27 100644 --- a/test/jest/LndClient.spec.ts +++ b/test/jest/LndClient.spec.ts @@ -270,13 +270,16 @@ describe('LndClient', () => { }); }); + const expectedOutbound = 100 + 80 + 110 - 2 - 2 - 20; // 266 + const expectedInbound = 200 + 220 + 300 - 5 - 5 - 5; // 705 + const swapCapacities = await lnd.swapCapacities(); - expect(swapCapacities.maxOutboundChannelCapacity).toEqual(98); - expect(swapCapacities.maxInboundChannelCapacity).toEqual(295); + expect(swapCapacities.maxOutboundChannelCapacity).toEqual(expectedOutbound); + expect(swapCapacities.maxInboundChannelCapacity).toEqual(expectedInbound); expect(lnd['listChannels']).toHaveBeenCalledTimes(1); - expect(lnd['maxChannelOutboundAmount']).toEqual(98); - expect(lnd['maxChannelInboundAmount']).toEqual(295); + expect(lnd['totalOutboundAmount']).toEqual(expectedOutbound); + expect(lnd['totalInboundAmount']).toEqual(expectedInbound); }); }); }); diff --git a/test/simulation/actions.go b/test/simulation/actions.go index 93060d330..e89aacbcb 100644 --- a/test/simulation/actions.go +++ b/test/simulation/actions.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + // "github.com/ExchangeUnion/xud-simulation/connexttest" // "math/big" "time" @@ -376,7 +377,6 @@ func (a *actions) placeOrderAndSwap(srcNode, destNode *xudtest.HarnessNode, // Verify the swap events info. a.assert.Equal(eMaker.swap.OrderId, eTaker.swap.OrderId) - a.assert.NotEqual(eMaker.swap.LocalId, eTaker.swap.LocalId) a.assert.Equal(eMaker.swap.PairId, eTaker.swap.PairId) a.assert.Equal(eMaker.swap.Quantity, eTaker.swap.Quantity) a.assert.Equal(eMaker.swap.RHash, eTaker.swap.RHash) diff --git a/test/simulation/custom-xud.patch b/test/simulation/custom-xud.patch index 67f7ed7e1..b7d5c0c9b 100644 --- a/test/simulation/custom-xud.patch +++ b/test/simulation/custom-xud.patch @@ -1,8 +1,8 @@ diff --git a/lib/Xud.ts b/lib/Xud.ts -index c639c804..1433238c 100644 +index 75e6ee84..d5564956 100644 --- a/lib/Xud.ts +++ b/lib/Xud.ts -@@ -92,6 +92,11 @@ class Xud extends EventEmitter { +@@ -91,6 +91,11 @@ class Xud extends EventEmitter { this.logger.info('config file loaded'); } @@ -39,10 +39,10 @@ index c4dedf26..2c998d72 100644 }; diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts -index 89a3c43b..d60fb4de 100644 +index f6fbb217..93ba314a 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts -@@ -791,6 +791,32 @@ class Swaps extends EventEmitter { +@@ -820,6 +820,32 @@ class Swaps extends EventEmitter { } else if (deal.state === SwapState.Active) { // we check that the deal is still active before we try to settle the invoice try { @@ -75,7 +75,7 @@ index 89a3c43b..d60fb4de 100644 await swapClient.settleInvoice(rHash, rPreimage, currency); } catch (err) { this.logger.error(`could not settle invoice for deal ${rHash}`, err); -@@ -804,16 +830,21 @@ class Swaps extends EventEmitter { +@@ -833,16 +859,21 @@ class Swaps extends EventEmitter { ); const settleRetryPromise = new Promise((resolve) => { @@ -107,7 +107,7 @@ index 89a3c43b..d60fb4de 100644 }); await settleRetryPromise; } else { -@@ -837,6 +868,16 @@ class Swaps extends EventEmitter { +@@ -866,6 +897,16 @@ class Swaps extends EventEmitter { * accepted, initiates the swap. */ private handleSwapAccepted = async (responsePacket: packets.SwapAcceptedPacket, peer: Peer) => { @@ -122,9 +122,9 @@ index 89a3c43b..d60fb4de 100644 + } + assert(responsePacket.body, 'SwapAcceptedPacket does not contain a body'); - const { quantity, rHash, makerCltvDelta } = responsePacket.body; + const { quantity, rHash, makerCltvDelta, payReq } = responsePacket.body; const deal = this.getDeal(rHash); -@@ -929,6 +970,11 @@ class Swaps extends EventEmitter { +@@ -964,6 +1005,11 @@ class Swaps extends EventEmitter { try { await makerSwapClient.sendPayment(deal); @@ -136,7 +136,7 @@ index 89a3c43b..d60fb4de 100644 } 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 -@@ -1124,6 +1170,18 @@ class Swaps extends EventEmitter { +@@ -1154,6 +1200,18 @@ class Swaps extends EventEmitter { this.logger.debug('Executing maker code to resolve hash'); @@ -155,7 +155,7 @@ index 89a3c43b..d60fb4de 100644 const swapClient = this.swapClientManager.get(deal.takerCurrency)!; // we update the phase persist the deal to the database before we attempt to send payment -@@ -1134,6 +1192,13 @@ class Swaps extends EventEmitter { +@@ -1164,6 +1222,13 @@ class Swaps extends EventEmitter { assert(deal.state !== SwapState.Error, `cannot send payment for failed swap ${deal.rHash}`); try { @@ -169,7 +169,7 @@ index 89a3c43b..d60fb4de 100644 deal.rPreimage = await swapClient.sendPayment(deal); } catch (err) { this.logger.debug(`sendPayment in resolveHash for swap ${deal.rHash} failed due to ${err.message}`); -@@ -1217,10 +1282,23 @@ class Swaps extends EventEmitter { +@@ -1247,10 +1312,23 @@ class Swaps extends EventEmitter { } } @@ -194,7 +194,7 @@ index 89a3c43b..d60fb4de 100644 return deal.rPreimage; } else { // If we are here we are the taker -@@ -1231,6 +1309,16 @@ class Swaps extends EventEmitter { +@@ -1261,6 +1339,16 @@ class Swaps extends EventEmitter { ); this.logger.debug('Executing taker code to resolve hash'); @@ -211,21 +211,17 @@ index 89a3c43b..d60fb4de 100644 return deal.rPreimage; } }; -@@ -1414,8 +1502,11 @@ class Swaps extends EventEmitter { - swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call - } - } else if (deal.phase === SwapPhase.SendingPayment) { -- const swapClient = this.swapClientManager.get(deal.takerCurrency)!; -- swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call +@@ -1446,7 +1534,8 @@ class Swaps extends EventEmitter { + // TODO: go back to only canceling invoice on SendingPayment phase as taker + // once we resume adding invoice *after* swap deal is accepted + // } else if (deal.phase === SwapPhase.SendingPayment) { +- } else { ++ } else if (process.env.CUSTOM_SCENARIO !== 'SECURITY::TAKER_2ND_HTLC_STALL') { + // don't cancel any invoices if the taker is stalling -+ if (process.env.CUSTOM_SCENARIO !== 'SECURITY::TAKER_2ND_HTLC_STALL') { -+ const swapClient = this.swapClientManager.get(deal.takerCurrency)!; -+ swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call -+ } + const swapClient = this.swapClientManager.get(deal.takerCurrency)!; + swapClient.removeInvoice(deal.rHash).catch(this.logger.error); // we don't need to await the remove invoice call } - - this.logger.trace(`emitting swap.failed event for ${deal.rHash}`); -@@ -1487,15 +1578,13 @@ class Swaps extends EventEmitter { +@@ -1526,15 +1615,13 @@ class Swaps extends EventEmitter { if (deal.role === SwapRole.Maker) { // the maker begins execution of the swap upon accepting the deal diff --git a/test/simulation/docker-lnd/install-lnd.sh b/test/simulation/docker-lnd/install-lnd.sh index b7c18ec48..a8f0d2093 100755 --- a/test/simulation/docker-lnd/install-lnd.sh +++ b/test/simulation/docker-lnd/install-lnd.sh @@ -32,7 +32,7 @@ else echo "finished lnd clone" echo "starting lnd make..." - if ! (cd ${LND_PATH} && GOPATH=${GO_PATH} make tags="invoicesrpc"); then + if ! (cd ${LND_PATH} && GOPATH=${GO_PATH} make tags="invoicesrpc routerrpc"); then echo "unable to make lnd" exit 1 fi diff --git a/test/simulation/lntest/harness.go b/test/simulation/lntest/harness.go index a0959dbb9..cfdf3d5b4 100644 --- a/test/simulation/lntest/harness.go +++ b/test/simulation/lntest/harness.go @@ -257,6 +257,9 @@ func (n *NetworkHarness) SetUp(lndArgs []string) error { if err := n.ConnectNodes(ctxb, n.Alice, n.Bob); err != nil { return err } + if err := n.ConnectNodes(ctxb, n.Alice, n.Carol); err != nil { + return err + } if err := n.ConnectNodes(ctxb, n.Bob, n.Carol); err != nil { return err } diff --git a/test/simulation/tests-integration.go b/test/simulation/tests-integration.go index 05c6f334a..b05d7d162 100644 --- a/test/simulation/tests-integration.go +++ b/test/simulation/tests-integration.go @@ -18,6 +18,10 @@ var integrationTestCases = []*testCase{ name: "order matching and swap", test: testOrderMatchingAndSwap, }, + { + name: "order matching and multi path swap", + test: testOrderMatchingAndMultiPathSwap, + }, { name: "dust order discarded", test: testDustOrderDiscarded, @@ -356,6 +360,35 @@ func testOrderMatchingAndSwap(net *xudtest.NetworkHarness, ht *harnessTest) { ht.act.disconnect(net.Alice, net.Bob) } +func testOrderMatchingAndMultiPathSwap(net *xudtest.NetworkHarness, ht *harnessTest) { + // Connect Alice to Bob. + ht.act.connect(net.Alice, net.Bob) + ht.act.verifyConnectivity(net.Alice, net.Bob) + + // Place an order on Alice. + req := &xudrpc.PlaceOrderRequest{ + OrderId: "multi_path_order", + Price: 0.02, + Quantity: 8600000, + PairId: "LTC/BTC", + Side: xudrpc.OrderSide_BUY, + } + ht.act.placeOrderAndBroadcast(net.Alice, net.Bob, req) + + // Place a matching order on Bob. + req = &xudrpc.PlaceOrderRequest{ + OrderId: "multi_path_order", + Price: req.Price, + Quantity: req.Quantity, + PairId: req.PairId, + Side: xudrpc.OrderSide_SELL, + } + ht.act.placeOrderAndSwap(net.Bob, net.Alice, req) + + // Cleanup. + ht.act.disconnect(net.Alice, net.Bob) +} + func testDustOrderDiscarded(net *xudtest.NetworkHarness, ht *harnessTest) { // Connect Alice to Bob. ht.act.connect(net.Alice, net.Bob) @@ -363,7 +396,7 @@ func testDustOrderDiscarded(net *xudtest.NetworkHarness, ht *harnessTest) { // Place an order on Alice. req := &xudrpc.PlaceOrderRequest{ - OrderId: "maker_order_id", + OrderId: "dust_order", Price: 0.02, Quantity: 10000, PairId: "LTC/BTC", @@ -373,7 +406,7 @@ func testDustOrderDiscarded(net *xudtest.NetworkHarness, ht *harnessTest) { // Place a matching order on Bob. req = &xudrpc.PlaceOrderRequest{ - OrderId: "taker_order_id", + OrderId: "dust_order", Price: req.Price, Quantity: 10099, PairId: req.PairId, diff --git a/test/simulation/xud_test.go b/test/simulation/xud_test.go index 9648e8710..474e4a07d 100644 --- a/test/simulation/xud_test.go +++ b/test/simulation/xud_test.go @@ -3,11 +3,7 @@ package main import ( "context" "fmt" - // "github.com/ExchangeUnion/xud-simulation/connexttest" - // "github.com/ethereum/go-ethereum/ethclient" - "github.com/stretchr/testify/require" "log" - // "net/http" "os" "strings" "testing" @@ -26,6 +22,7 @@ import ( btctest "github.com/roasbeef/btcd/integration/rpctest" btcclient "github.com/roasbeef/btcd/rpcclient" "github.com/roasbeef/btcutil" + "github.com/stretchr/testify/require" ) var ( @@ -75,6 +72,10 @@ func TestIntegration(t *testing.T) { carolDavidLtcChanPoint, err := openLtcChannel(ctx, xudNetwork.LndLtcNetwork, xudNetwork.Carol.LndLtcNode, xudNetwork.Dave.LndLtcNode, amt, pushAmt) assert.NoError(err) + // open a second LTC channel between alice and carol for multipath payments + aliceCarolLtcChanPoint, err := openLtcChannel(ctx, xudNetwork.LndLtcNetwork, xudNetwork.Alice.LndLtcNode, xudNetwork.Carol.LndLtcNode, amt, pushAmt) + assert.NoError(err) + initialStates := make(map[int]*xudrpc.GetInfoResponse) for i, testCase := range integrationTestCases { success := t.Run(testCase.name, func(t1 *testing.T) { @@ -130,6 +131,8 @@ func TestIntegration(t *testing.T) { assert.NoError(err) err = closeLtcChannel(ctx, xudNetwork.LndLtcNetwork, xudNetwork.Alice.LndLtcNode, aliceBobLtcChanPoint, false) assert.NoError(err) + err = closeLtcChannel(ctx, xudNetwork.LndLtcNetwork, xudNetwork.Alice.LndLtcNode, aliceCarolLtcChanPoint, false) + assert.NoError(err) err = closeBtcChannel(ctx, xudNetwork.LndBtcNetwork, xudNetwork.Bob.LndBtcNode, bobCarolBtcChanPoint, false) assert.NoError(err) err = closeLtcChannel(ctx, xudNetwork.LndLtcNetwork, xudNetwork.Bob.LndLtcNode, bobCarolLtcChanPoint, false) diff --git a/test/unit/Parser.spec.ts b/test/unit/Parser.spec.ts index e86e15fe3..c552200ef 100644 --- a/test/unit/Parser.spec.ts +++ b/test/unit/Parser.spec.ts @@ -442,8 +442,13 @@ describe('Parser', () => { pairId: uuid(), orderId: uuid(), takerCltvDelta: 10, + payReq: + 'lnbc420n1p06uxnfpp5hxq94vpge8kpxj76hghn4uk940w38hjy5wpzqdzupmyajczhgyssdqqcqzpgsp57nj39u8wz0hkjs2pszlvcqysgvttk9039dnw3zv6evlywu37uy7q9qy9qsqjp4vqexungwscl6mkdx5eejf6rf09c0red8g8hzh4qrh0yzwuudzssv3yndmpsacaczxfwkevp3h4yf5wk5xqkwnf0s8nlhdvedydpsqsa7a8y', }; testValidPacket(new packets.SwapRequestPacket(swapRequestPacketBody)); + testValidPacket( + new packets.SwapRequestPacket(removeUndefinedProps({ ...swapRequestPacketBody, payReq: undefined })), + ); testInvalidPacket(new packets.SwapRequestPacket(swapRequestPacketBody, uuid())); testInvalidPacket( new packets.SwapRequestPacket( @@ -478,26 +483,31 @@ describe('Parser', () => { rHash, quantity: 10, makerCltvDelta: 10, + payReq: + 'lnbc420n1p06uxnfpp5hxq94vpge8kpxj76hghn4uk940w38hjy5wpzqdzupmyajczhgyssdqqcqzpgsp57nj39u8wz0hkjs2pszlvcqysgvttk9039dnw3zv6evlywu37uy7q9qy9qsqjp4vqexungwscl6mkdx5eejf6rf09c0red8g8hzh4qrh0yzwuudzssv3yndmpsacaczxfwkevp3h4yf5wk5xqkwnf0s8nlhdvedydpsqsa7a8y', }; testValidPacket(new packets.SwapAcceptedPacket(swapAcceptedPacketBody, uuid())); + testValidPacket( + new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, payReq: undefined }), uuid()), + ); testInvalidPacket(new packets.SwapAcceptedPacket(swapAcceptedPacketBody)); testInvalidPacket( - new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, rHash: undefined })), + new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, rHash: undefined }), uuid()), ); testInvalidPacket( - new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, quantity: undefined })), + new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, quantity: undefined }), uuid()), + ); + testInvalidPacket( + new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, quantity: 0 }), uuid()), ); - testInvalidPacket(new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, quantity: 0 }))); testInvalidPacket( new packets.SwapAcceptedPacket( - removeUndefinedProps({ - ...swapAcceptedPacketBody, - makerCltvDelta: undefined, - }), + removeUndefinedProps({ ...swapAcceptedPacketBody, makerCltvDelta: undefined }), + uuid(), ), ); testInvalidPacket( - new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, makerCltvDelta: 0 })), + new packets.SwapAcceptedPacket(removeUndefinedProps({ ...swapAcceptedPacketBody, makerCltvDelta: 0 }), uuid()), ); const swapFailedPacketBody = {