diff --git a/hold b/hold index 81a3189c..f0409d01 160000 --- a/hold +++ b/hold @@ -1 +1 @@ -Subproject commit 81a3189c2b85bf45b65dbbfe3cfe629cbacc16d8 +Subproject commit f0409d013da25516867bcc0576d22708c466b2f0 diff --git a/lib/VersionCheck.ts b/lib/VersionCheck.ts index 262b63fd..3a4be83e 100644 --- a/lib/VersionCheck.ts +++ b/lib/VersionCheck.ts @@ -86,8 +86,8 @@ class VersionCheck { maximal: '24.08.2', }, [ClnClient.serviceNameHold]: { - minimal: '0.1.0', - maximal: '0.1.2', + minimal: '0.2.0', + maximal: '0.2.0', }, [MpayClient.serviceName]: { minimal: '0.1.0', diff --git a/lib/lightning/cln/ClnClient.ts b/lib/lightning/cln/ClnClient.ts index 7589e60d..8af5f8c5 100644 --- a/lib/lightning/cln/ClnClient.ts +++ b/lib/lightning/cln/ClnClient.ts @@ -76,6 +76,7 @@ class ClnClient private readonly holdMeta = new Metadata(); private trackAllSubscription?: ClientReadableStream; + private holdInvoicesToSubscribe: Set = new Set(); constructor( logger: Logger, @@ -138,7 +139,7 @@ class ClnClient public useMpay = () => this.mpay !== undefined && this.mpay.isConnected(); - public connect = async (startSubscriptions = true): Promise => { + public connect = async (): Promise => { if (!this.isConnected()) { this.nodeClient = new NodeClient(this.nodeUri, this.nodeCreds, { ...grpcOptions, @@ -154,11 +155,6 @@ class ClnClient try { await this.getInfo(); - - if (startSubscriptions) { - this.subscribeTrackHoldInvoices(); - } - this.setClientStatus(ClientStatus.Connected); } catch (error) { this.setClientStatus(ClientStatus.Disconnected); @@ -620,11 +616,14 @@ class ClnClient }; }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public subscribeSingleInvoice = (_: Buffer): void => { - // Just here for interface compatibility; - // with CLN we can subscribe to all hold invoices with one gRPC subscription - return; + public subscribeSingleInvoice = (preimageHash: Buffer): void => { + // That call is only used to get the last update of relevant invoices + // when starting the subscription for *all* hold invoices + if (this.trackAllSubscription !== undefined) { + return; + } + + this.holdInvoicesToSubscribe.add(preimageHash); }; private static routingHintsToGrpc = ( @@ -771,14 +770,17 @@ class ClnClient return undefined; }; - private subscribeTrackHoldInvoices = () => { + public subscribeTrackHoldInvoices = () => { if (this.trackAllSubscription) { this.trackAllSubscription.cancel(); } - this.trackAllSubscription = this.holdClient!.trackAll( - new holdrpc.TrackAllRequest(), - ); + const req = new holdrpc.TrackAllRequest(); + + req.setPaymentHashesList(Array.from(this.holdInvoicesToSubscribe.values())); + this.holdInvoicesToSubscribe.clear(); + + this.trackAllSubscription = this.holdClient!.trackAll(req); this.trackAllSubscription.on('data', (update: holdrpc.TrackAllResponse) => { switch (update.getState()) { diff --git a/lib/proto/hold/hold_grpc_pb.d.ts b/lib/proto/hold/hold_grpc_pb.d.ts index 4f668dad..d9d1d368 100644 --- a/lib/proto/hold/hold_grpc_pb.d.ts +++ b/lib/proto/hold/hold_grpc_pb.d.ts @@ -13,6 +13,7 @@ interface IHoldService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } +interface IHoldService_IClean extends grpc.MethodDefinition { + path: "/hold.Hold/Clean"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IHoldService_ITrack extends grpc.MethodDefinition { path: "/hold.Hold/Track"; requestStream: false; @@ -89,6 +99,7 @@ export interface IHoldServer extends grpc.UntypedServiceImplementation { list: grpc.handleUnaryCall; settle: grpc.handleUnaryCall; cancel: grpc.handleUnaryCall; + clean: grpc.handleUnaryCall; track: grpc.handleServerStreamingCall; trackAll: grpc.handleServerStreamingCall; } @@ -109,6 +120,9 @@ export interface IHoldClient { cancel(request: hold_pb.CancelRequest, callback: (error: grpc.ServiceError | null, response: hold_pb.CancelResponse) => void): grpc.ClientUnaryCall; cancel(request: hold_pb.CancelRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: hold_pb.CancelResponse) => void): grpc.ClientUnaryCall; cancel(request: hold_pb.CancelRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: hold_pb.CancelResponse) => void): grpc.ClientUnaryCall; + clean(request: hold_pb.CleanRequest, callback: (error: grpc.ServiceError | null, response: hold_pb.CleanResponse) => void): grpc.ClientUnaryCall; + clean(request: hold_pb.CleanRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: hold_pb.CleanResponse) => void): grpc.ClientUnaryCall; + clean(request: hold_pb.CleanRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: hold_pb.CleanResponse) => void): grpc.ClientUnaryCall; track(request: hold_pb.TrackRequest, options?: Partial): grpc.ClientReadableStream; track(request: hold_pb.TrackRequest, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; trackAll(request: hold_pb.TrackAllRequest, options?: Partial): grpc.ClientReadableStream; @@ -132,6 +146,9 @@ export class HoldClient extends grpc.Client implements IHoldClient { public cancel(request: hold_pb.CancelRequest, callback: (error: grpc.ServiceError | null, response: hold_pb.CancelResponse) => void): grpc.ClientUnaryCall; public cancel(request: hold_pb.CancelRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: hold_pb.CancelResponse) => void): grpc.ClientUnaryCall; public cancel(request: hold_pb.CancelRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: hold_pb.CancelResponse) => void): grpc.ClientUnaryCall; + public clean(request: hold_pb.CleanRequest, callback: (error: grpc.ServiceError | null, response: hold_pb.CleanResponse) => void): grpc.ClientUnaryCall; + public clean(request: hold_pb.CleanRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: hold_pb.CleanResponse) => void): grpc.ClientUnaryCall; + public clean(request: hold_pb.CleanRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: hold_pb.CleanResponse) => void): grpc.ClientUnaryCall; public track(request: hold_pb.TrackRequest, options?: Partial): grpc.ClientReadableStream; public track(request: hold_pb.TrackRequest, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; public trackAll(request: hold_pb.TrackAllRequest, options?: Partial): grpc.ClientReadableStream; diff --git a/lib/proto/hold/hold_grpc_pb.js b/lib/proto/hold/hold_grpc_pb.js index f4bce10b..c298bff3 100644 --- a/lib/proto/hold/hold_grpc_pb.js +++ b/lib/proto/hold/hold_grpc_pb.js @@ -26,6 +26,28 @@ function deserialize_hold_CancelResponse(buffer_arg) { return hold_pb.CancelResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_hold_CleanRequest(arg) { + if (!(arg instanceof hold_pb.CleanRequest)) { + throw new Error('Expected argument of type hold.CleanRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_hold_CleanRequest(buffer_arg) { + return hold_pb.CleanRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_hold_CleanResponse(arg) { + if (!(arg instanceof hold_pb.CleanResponse)) { + throw new Error('Expected argument of type hold.CleanResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_hold_CleanResponse(buffer_arg) { + return hold_pb.CleanResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_hold_GetInfoRequest(arg) { if (!(arg instanceof hold_pb.GetInfoRequest)) { throw new Error('Expected argument of type hold.GetInfoRequest'); @@ -215,6 +237,18 @@ var HoldService = exports.HoldService = { responseSerialize: serialize_hold_CancelResponse, responseDeserialize: deserialize_hold_CancelResponse, }, + // Cleans cancelled invoices +clean: { + path: '/hold.Hold/Clean', + requestStream: false, + responseStream: false, + requestType: hold_pb.CleanRequest, + responseType: hold_pb.CleanResponse, + requestSerialize: serialize_hold_CleanRequest, + requestDeserialize: deserialize_hold_CleanRequest, + responseSerialize: serialize_hold_CleanResponse, + responseDeserialize: deserialize_hold_CleanResponse, + }, track: { path: '/hold.Hold/Track', requestStream: false, diff --git a/lib/proto/hold/hold_pb.d.ts b/lib/proto/hold/hold_pb.d.ts index f5f98316..c2150795 100644 --- a/lib/proto/hold/hold_pb.d.ts +++ b/lib/proto/hold/hold_pb.d.ts @@ -431,6 +431,49 @@ export namespace CancelResponse { } } +export class CleanRequest extends jspb.Message { + + hasAge(): boolean; + clearAge(): void; + getAge(): number | undefined; + setAge(value: number): CleanRequest; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CleanRequest.AsObject; + static toObject(includeInstance: boolean, msg: CleanRequest): CleanRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CleanRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CleanRequest; + static deserializeBinaryFromReader(message: CleanRequest, reader: jspb.BinaryReader): CleanRequest; +} + +export namespace CleanRequest { + export type AsObject = { + age?: number, + } +} + +export class CleanResponse extends jspb.Message { + getCleaned(): number; + setCleaned(value: number): CleanResponse; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CleanResponse.AsObject; + static toObject(includeInstance: boolean, msg: CleanResponse): CleanResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CleanResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CleanResponse; + static deserializeBinaryFromReader(message: CleanResponse, reader: jspb.BinaryReader): CleanResponse; +} + +export namespace CleanResponse { + export type AsObject = { + cleaned: number, + } +} + export class TrackRequest extends jspb.Message { getPaymentHash(): Uint8Array | string; getPaymentHash_asU8(): Uint8Array; @@ -474,6 +517,12 @@ export namespace TrackResponse { } export class TrackAllRequest extends jspb.Message { + clearPaymentHashesList(): void; + getPaymentHashesList(): Array; + getPaymentHashesList_asU8(): Array; + getPaymentHashesList_asB64(): Array; + setPaymentHashesList(value: Array): TrackAllRequest; + addPaymentHashes(value: Uint8Array | string, index?: number): Uint8Array | string; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): TrackAllRequest.AsObject; @@ -487,6 +536,7 @@ export class TrackAllRequest extends jspb.Message { export namespace TrackAllRequest { export type AsObject = { + paymentHashesList: Array, } } diff --git a/lib/proto/hold/hold_pb.js b/lib/proto/hold/hold_pb.js index e52abbd6..17e7b291 100644 --- a/lib/proto/hold/hold_pb.js +++ b/lib/proto/hold/hold_pb.js @@ -23,6 +23,8 @@ var global = (function() { goog.exportSymbol('proto.hold.CancelRequest', null, global); goog.exportSymbol('proto.hold.CancelResponse', null, global); +goog.exportSymbol('proto.hold.CleanRequest', null, global); +goog.exportSymbol('proto.hold.CleanResponse', null, global); goog.exportSymbol('proto.hold.GetInfoRequest', null, global); goog.exportSymbol('proto.hold.GetInfoResponse', null, global); goog.exportSymbol('proto.hold.Hop', null, global); @@ -358,6 +360,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.hold.CancelResponse.displayName = 'proto.hold.CancelResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.hold.CleanRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.hold.CleanRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.hold.CleanRequest.displayName = 'proto.hold.CleanRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.hold.CleanResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.hold.CleanResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.hold.CleanResponse.displayName = 'proto.hold.CleanResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -411,7 +455,7 @@ if (goog.DEBUG && !COMPILED) { * @constructor */ proto.hold.TrackAllRequest = function(opt_data) { - jspb.Message.initialize(this, opt_data, 0, -1, null, null); + jspb.Message.initialize(this, opt_data, 0, -1, proto.hold.TrackAllRequest.repeatedFields_, null); }; goog.inherits(proto.hold.TrackAllRequest, jspb.Message); if (goog.DEBUG && !COMPILED) { @@ -3491,6 +3535,284 @@ proto.hold.CancelResponse.serializeBinaryToWriter = function(message, writer) { +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.hold.CleanRequest.prototype.toObject = function(opt_includeInstance) { + return proto.hold.CleanRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.hold.CleanRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.hold.CleanRequest.toObject = function(includeInstance, msg) { + var f, obj = { + age: jspb.Message.getFieldWithDefault(msg, 1, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.hold.CleanRequest} + */ +proto.hold.CleanRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.hold.CleanRequest; + return proto.hold.CleanRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.hold.CleanRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.hold.CleanRequest} + */ +proto.hold.CleanRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setAge(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.hold.CleanRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.hold.CleanRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.hold.CleanRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.hold.CleanRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = /** @type {number} */ (jspb.Message.getField(message, 1)); + if (f != null) { + writer.writeUint64( + 1, + f + ); + } +}; + + +/** + * optional uint64 age = 1; + * @return {number} + */ +proto.hold.CleanRequest.prototype.getAge = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.hold.CleanRequest} returns this + */ +proto.hold.CleanRequest.prototype.setAge = function(value) { + return jspb.Message.setField(this, 1, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.hold.CleanRequest} returns this + */ +proto.hold.CleanRequest.prototype.clearAge = function() { + return jspb.Message.setField(this, 1, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.hold.CleanRequest.prototype.hasAge = function() { + return jspb.Message.getField(this, 1) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.hold.CleanResponse.prototype.toObject = function(opt_includeInstance) { + return proto.hold.CleanResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.hold.CleanResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.hold.CleanResponse.toObject = function(includeInstance, msg) { + var f, obj = { + cleaned: jspb.Message.getFieldWithDefault(msg, 1, 0) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.hold.CleanResponse} + */ +proto.hold.CleanResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.hold.CleanResponse; + return proto.hold.CleanResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.hold.CleanResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.hold.CleanResponse} + */ +proto.hold.CleanResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setCleaned(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.hold.CleanResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.hold.CleanResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.hold.CleanResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.hold.CleanResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getCleaned(); + if (f !== 0) { + writer.writeUint64( + 1, + f + ); + } +}; + + +/** + * optional uint64 cleaned = 1; + * @return {number} + */ +proto.hold.CleanResponse.prototype.getCleaned = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.hold.CleanResponse} returns this + */ +proto.hold.CleanResponse.prototype.setCleaned = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. @@ -3773,6 +4095,13 @@ proto.hold.TrackResponse.prototype.setState = function(value) { +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.hold.TrackAllRequest.repeatedFields_ = [1]; + if (jspb.Message.GENERATE_TO_OBJECT) { @@ -3804,7 +4133,7 @@ proto.hold.TrackAllRequest.prototype.toObject = function(opt_includeInstance) { */ proto.hold.TrackAllRequest.toObject = function(includeInstance, msg) { var f, obj = { - + paymentHashesList: msg.getPaymentHashesList_asB64() }; if (includeInstance) { @@ -3841,6 +4170,10 @@ proto.hold.TrackAllRequest.deserializeBinaryFromReader = function(msg, reader) { } var field = reader.getFieldNumber(); switch (field) { + case 1: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.addPaymentHashes(value); + break; default: reader.skipField(); break; @@ -3870,6 +4203,74 @@ proto.hold.TrackAllRequest.prototype.serializeBinary = function() { */ proto.hold.TrackAllRequest.serializeBinaryToWriter = function(message, writer) { var f = undefined; + f = message.getPaymentHashesList_asU8(); + if (f.length > 0) { + writer.writeRepeatedBytes( + 1, + f + ); + } +}; + + +/** + * repeated bytes payment_hashes = 1; + * @return {!(Array|Array)} + */ +proto.hold.TrackAllRequest.prototype.getPaymentHashesList = function() { + return /** @type {!(Array|Array)} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** + * repeated bytes payment_hashes = 1; + * This is a type-conversion wrapper around `getPaymentHashesList()` + * @return {!Array} + */ +proto.hold.TrackAllRequest.prototype.getPaymentHashesList_asB64 = function() { + return /** @type {!Array} */ (jspb.Message.bytesListAsB64( + this.getPaymentHashesList())); +}; + + +/** + * repeated bytes payment_hashes = 1; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getPaymentHashesList()` + * @return {!Array} + */ +proto.hold.TrackAllRequest.prototype.getPaymentHashesList_asU8 = function() { + return /** @type {!Array} */ (jspb.Message.bytesListAsU8( + this.getPaymentHashesList())); +}; + + +/** + * @param {!(Array|Array)} value + * @return {!proto.hold.TrackAllRequest} returns this + */ +proto.hold.TrackAllRequest.prototype.setPaymentHashesList = function(value) { + return jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @param {number=} opt_index + * @return {!proto.hold.TrackAllRequest} returns this + */ +proto.hold.TrackAllRequest.prototype.addPaymentHashes = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.hold.TrackAllRequest} returns this + */ +proto.hold.TrackAllRequest.prototype.clearPaymentHashesList = function() { + return this.setPaymentHashesList([]); }; diff --git a/lib/swap/SwapManager.ts b/lib/swap/SwapManager.ts index 85324103..bc12d760 100644 --- a/lib/swap/SwapManager.ts +++ b/lib/swap/SwapManager.ts @@ -283,6 +283,14 @@ class SwapManager { await this.recreateFilters(pendingReverseSwaps, true); this.recreateChainSwapFilters(pendingChainSwaps); + for (const currency of this.currencies.values()) { + if (currency.clnClient === undefined) { + continue; + } + + currency.clnClient.subscribeTrackHoldInvoices(); + } + await this.chainSwapSigner.init(); this.logger.info( @@ -1181,7 +1189,10 @@ class SwapManager { ) { const reverseSwap = swap as ReverseSwap; - const { lndClient } = this.currencies.get(lightningCurrency)!; + const lightningClient = NodeSwitch.getReverseSwapNode( + this.currencies.get(lightningCurrency)!, + reverseSwap, + ); if ( reverseSwap.node === NodeType.LND && @@ -1191,13 +1202,13 @@ class SwapManager { const decoded = await this.sidecar.decodeInvoiceOrOffer( reverseSwap.minerFeeInvoice, ); - lndClient?.subscribeSingleInvoice(decoded.paymentHash!); + lightningClient.subscribeSingleInvoice(decoded.paymentHash!); } const decoded = await this.sidecar.decodeInvoiceOrOffer( reverseSwap.invoice, ); - lndClient?.subscribeSingleInvoice(decoded.paymentHash!); + lightningClient.subscribeSingleInvoice(decoded.paymentHash!); } else if ( (swap.status === SwapUpdateEvent.TransactionMempool || swap.status === SwapUpdateEvent.TransactionConfirmed) && diff --git a/test/integration/lightning/PendingPaymentTracker.spec.ts b/test/integration/lightning/PendingPaymentTracker.spec.ts index 989f9b3b..2604696d 100644 --- a/test/integration/lightning/PendingPaymentTracker.spec.ts +++ b/test/integration/lightning/PendingPaymentTracker.spec.ts @@ -57,7 +57,7 @@ describe('PendingPaymentTracker', () => { startSidecar(); await Promise.all([ db.init(), - clnClient.connect(true), + clnClient.connect(), bitcoinLndClient.connect(false), sidecar.connect( { on: jest.fn(), removeAllListeners: jest.fn() } as any, @@ -66,6 +66,8 @@ describe('PendingPaymentTracker', () => { ), ]); + clnClient.subscribeTrackHoldInvoices(); + await PairRepository.addPair({ id: 'BTC/BTC', base: 'BTC', diff --git a/test/integration/lightning/cln/ClnClient.spec.ts b/test/integration/lightning/cln/ClnClient.spec.ts index 23086d38..7a356c97 100644 --- a/test/integration/lightning/cln/ClnClient.spec.ts +++ b/test/integration/lightning/cln/ClnClient.spec.ts @@ -5,6 +5,7 @@ import { ClientStatus } from '../../../../lib/consts/Enums'; import Errors from '../../../../lib/lightning/Errors'; import { InvoiceFeature } from '../../../../lib/lightning/LightningClient'; import Sidecar from '../../../../lib/sidecar/Sidecar'; +import { wait } from '../../../Utils'; import { bitcoinClient, bitcoinLndClient, @@ -38,6 +39,8 @@ describe('ClnClient', () => { expect(clnClient.isConnected()).toEqual(false); await clnClient.connect(); expect(clnClient.isConnected()).toEqual(true); + + clnClient.subscribeTrackHoldInvoices(); }); test('should parse node URIs in getinfo', async () => { @@ -258,4 +261,36 @@ describe('ClnClient', () => { ).rejects.toEqual(expect.anything()); }); }); + + test('should subscribe to single invoices', async () => { + expect.assertions(4); + + const preimage = randomBytes(32); + const preimageHash = crypto.sha256(preimage); + + const invoice = await clnClient.addHoldInvoice(1_000, preimageHash); + + clnClient.disconnect(); + + clnClient.once('htlc.accepted', (acceptedInvoice) => { + expect(acceptedInvoice).toEqual(invoice); + }); + + const payPromise = bitcoinLndClient.sendPayment(invoice); + await wait(1_000); + + clnClient.subscribeSingleInvoice(preimageHash); + expect(clnClient['holdInvoicesToSubscribe'].size).toEqual(1); + expect(clnClient['holdInvoicesToSubscribe'].has(preimageHash)).toEqual( + true, + ); + + await clnClient.connect(); + clnClient.subscribeTrackHoldInvoices(); + + expect(clnClient['holdInvoicesToSubscribe'].size).toEqual(0); + + await clnClient.settleHoldInvoice(preimage); + await payPromise; + }); }); diff --git a/test/integration/service/cooperative/DeferredClaimer.spec.ts b/test/integration/service/cooperative/DeferredClaimer.spec.ts index 5abe9b96..694f656f 100644 --- a/test/integration/service/cooperative/DeferredClaimer.spec.ts +++ b/test/integration/service/cooperative/DeferredClaimer.spec.ts @@ -170,11 +170,13 @@ describe('DeferredClaimer', () => { await Promise.all([ setup(), bitcoinClient.connect(), - clnClient.connect(true), + clnClient.connect(), bitcoinLndClient.connect(false), bitcoinLndClient2.connect(false), ]); + clnClient.subscribeTrackHoldInvoices(); + await bitcoinClient.generate(1); });