From 5ca4786bb4a0be5b1e487ff6cdd56c8f361f150c Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:48:49 +0200 Subject: [PATCH 01/12] multi: bump to lnd/tapd/lndclient v0.18.4-beta branches --- app/src/types/generated/lnd_pb.d.ts | 92 +++ app/src/types/generated/lnd_pb.js | 805 ++++++++++++++++++- app/src/types/generated/loop_pb.d.ts | 32 + app/src/types/generated/loop_pb.js | 246 ++++++ app/src/types/generated/loop_pb_service.d.ts | 19 + app/src/types/generated/loop_pb_service.js | 40 + app/src/util/tests/sampleData.ts | 3 + go.mod | 56 +- go.sum | 109 +-- itest/litd_firewall_test.go | 2 +- itest/litd_mode_integrated_test.go | 2 +- itest/litd_node.go | 7 +- itest/network_harness.go | 15 +- proto/lnd.proto | 77 +- proto/loop.proto | 12 + subservers/taproot-assets.go | 7 +- terminal.go | 5 +- 17 files changed, 1426 insertions(+), 103 deletions(-) diff --git a/app/src/types/generated/lnd_pb.d.ts b/app/src/types/generated/lnd_pb.d.ts index 137365bf0..e2dbe766c 100644 --- a/app/src/types/generated/lnd_pb.d.ts +++ b/app/src/types/generated/lnd_pb.d.ts @@ -1506,6 +1506,11 @@ export class Channel extends jspb.Message { getMemo(): string; setMemo(value: string): void; + getCustomChannelData(): Uint8Array | string; + getCustomChannelData_asU8(): Uint8Array; + getCustomChannelData_asB64(): string; + setCustomChannelData(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Channel.AsObject; static toObject(includeInstance: boolean, msg: Channel): Channel.AsObject; @@ -1554,6 +1559,7 @@ export namespace Channel { peerAlias: string, peerScidAlias: string, memo: string, + customChannelData: Uint8Array | string, } } @@ -2349,6 +2355,42 @@ export namespace ChannelOpenUpdate { } } +export class CloseOutput extends jspb.Message { + getAmountSat(): string; + setAmountSat(value: string): void; + + getPkScript(): Uint8Array | string; + getPkScript_asU8(): Uint8Array; + getPkScript_asB64(): string; + setPkScript(value: Uint8Array | string): void; + + getIsLocal(): boolean; + setIsLocal(value: boolean): void; + + getCustomChannelData(): Uint8Array | string; + getCustomChannelData_asU8(): Uint8Array; + getCustomChannelData_asB64(): string; + setCustomChannelData(value: Uint8Array | string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): CloseOutput.AsObject; + static toObject(includeInstance: boolean, msg: CloseOutput): CloseOutput.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: CloseOutput, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): CloseOutput; + static deserializeBinaryFromReader(message: CloseOutput, reader: jspb.BinaryReader): CloseOutput; +} + +export namespace CloseOutput { + export type AsObject = { + amountSat: string, + pkScript: Uint8Array | string, + isLocal: boolean, + customChannelData: Uint8Array | string, + } +} + export class ChannelCloseUpdate extends jspb.Message { getClosingTxid(): Uint8Array | string; getClosingTxid_asU8(): Uint8Array; @@ -2358,6 +2400,21 @@ export class ChannelCloseUpdate extends jspb.Message { getSuccess(): boolean; setSuccess(value: boolean): void; + hasLocalCloseOutput(): boolean; + clearLocalCloseOutput(): void; + getLocalCloseOutput(): CloseOutput | undefined; + setLocalCloseOutput(value?: CloseOutput): void; + + hasRemoteCloseOutput(): boolean; + clearRemoteCloseOutput(): void; + getRemoteCloseOutput(): CloseOutput | undefined; + setRemoteCloseOutput(value?: CloseOutput): void; + + clearAdditionalOutputsList(): void; + getAdditionalOutputsList(): Array; + setAdditionalOutputsList(value: Array): void; + addAdditionalOutputs(value?: CloseOutput, index?: number): CloseOutput; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ChannelCloseUpdate.AsObject; static toObject(includeInstance: boolean, msg: ChannelCloseUpdate): ChannelCloseUpdate.AsObject; @@ -2372,6 +2429,9 @@ export namespace ChannelCloseUpdate { export type AsObject = { closingTxid: Uint8Array | string, success: boolean, + localCloseOutput?: CloseOutput.AsObject, + remoteCloseOutput?: CloseOutput.AsObject, + additionalOutputsList: Array, } } @@ -3356,6 +3416,11 @@ export namespace PendingChannelsResponse { getMemo(): string; setMemo(value: string): void; + getCustomChannelData(): Uint8Array | string; + getCustomChannelData_asU8(): Uint8Array; + getCustomChannelData_asB64(): string; + setCustomChannelData(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): PendingChannel.AsObject; static toObject(includeInstance: boolean, msg: PendingChannel): PendingChannel.AsObject; @@ -3381,6 +3446,7 @@ export namespace PendingChannelsResponse { chanStatusFlags: string, pb_private: boolean, memo: string, + customChannelData: Uint8Array | string, } } @@ -3847,6 +3913,11 @@ export class ChannelBalanceResponse extends jspb.Message { getPendingOpenRemoteBalance(): Amount | undefined; setPendingOpenRemoteBalance(value?: Amount): void; + getCustomChannelData(): Uint8Array | string; + getCustomChannelData_asU8(): Uint8Array; + getCustomChannelData_asB64(): string; + setCustomChannelData(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ChannelBalanceResponse.AsObject; static toObject(includeInstance: boolean, msg: ChannelBalanceResponse): ChannelBalanceResponse.AsObject; @@ -3867,6 +3938,7 @@ export namespace ChannelBalanceResponse { unsettledRemoteBalance?: Amount.AsObject, pendingOpenLocalBalance?: Amount.AsObject, pendingOpenRemoteBalance?: Amount.AsObject, + customChannelData: Uint8Array | string, } } @@ -4221,6 +4293,14 @@ export class Route extends jspb.Message { getTotalAmtMsat(): string; setTotalAmtMsat(value: string): void; + getFirstHopAmountMsat(): string; + setFirstHopAmountMsat(value: string): void; + + getCustomChannelData(): Uint8Array | string; + getCustomChannelData_asU8(): Uint8Array; + getCustomChannelData_asB64(): string; + setCustomChannelData(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Route.AsObject; static toObject(includeInstance: boolean, msg: Route): Route.AsObject; @@ -4239,6 +4319,8 @@ export namespace Route { hopsList: Array, totalFeesMsat: string, totalAmtMsat: string, + firstHopAmountMsat: string, + customChannelData: Uint8Array | string, } } @@ -5354,6 +5436,11 @@ export class InvoiceHTLC extends jspb.Message { getAmp(): AMP | undefined; setAmp(value?: AMP): void; + getCustomChannelData(): Uint8Array | string; + getCustomChannelData_asU8(): Uint8Array; + getCustomChannelData_asB64(): string; + setCustomChannelData(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): InvoiceHTLC.AsObject; static toObject(includeInstance: boolean, msg: InvoiceHTLC): InvoiceHTLC.AsObject; @@ -5377,6 +5464,7 @@ export namespace InvoiceHTLC { customRecordsMap: Array<[number, Uint8Array | string]>, mppTotalAmtMsat: string, amp?: AMP.AsObject, + customChannelData: Uint8Array | string, } } @@ -5628,6 +5716,8 @@ export class Payment extends jspb.Message { getFailureReason(): PaymentFailureReasonMap[keyof PaymentFailureReasonMap]; setFailureReason(value: PaymentFailureReasonMap[keyof PaymentFailureReasonMap]): void; + getFirstHopCustomRecordsMap(): jspb.Map; + clearFirstHopCustomRecordsMap(): void; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): Payment.AsObject; static toObject(includeInstance: boolean, msg: Payment): Payment.AsObject; @@ -5655,6 +5745,7 @@ export namespace Payment { htlcsList: Array, paymentIndex: string, failureReason: PaymentFailureReasonMap[keyof PaymentFailureReasonMap], + firstHopCustomRecordsMap: Array<[number, Uint8Array | string]>, } export interface PaymentStatusMap { @@ -7439,6 +7530,7 @@ export interface CommitmentTypeMap { ANCHORS: 3; SCRIPT_ENFORCED_LEASE: 4; SIMPLE_TAPROOT: 5; + SIMPLE_TAPROOT_OVERLAY: 6; } export const CommitmentType: CommitmentTypeMap; diff --git a/app/src/types/generated/lnd_pb.js b/app/src/types/generated/lnd_pb.js index 740d82862..cb8672f31 100644 --- a/app/src/types/generated/lnd_pb.js +++ b/app/src/types/generated/lnd_pb.js @@ -75,6 +75,7 @@ goog.exportSymbol('proto.lnrpc.ChannelUpdate', null, global); goog.exportSymbol('proto.lnrpc.CheckMacPermRequest', null, global); goog.exportSymbol('proto.lnrpc.CheckMacPermResponse', null, global); goog.exportSymbol('proto.lnrpc.CloseChannelRequest', null, global); +goog.exportSymbol('proto.lnrpc.CloseOutput', null, global); goog.exportSymbol('proto.lnrpc.CloseStatusUpdate', null, global); goog.exportSymbol('proto.lnrpc.CloseStatusUpdate.UpdateCase', null, global); goog.exportSymbol('proto.lnrpc.ClosedChannelUpdate', null, global); @@ -1658,9 +1659,30 @@ if (goog.DEBUG && !COMPILED) { * @extends {jspb.Message} * @constructor */ -proto.lnrpc.ChannelCloseUpdate = function(opt_data) { +proto.lnrpc.CloseOutput = function(opt_data) { jspb.Message.initialize(this, opt_data, 0, -1, null, null); }; +goog.inherits(proto.lnrpc.CloseOutput, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.lnrpc.CloseOutput.displayName = 'proto.lnrpc.CloseOutput'; +} +/** + * 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.lnrpc.ChannelCloseUpdate = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.lnrpc.ChannelCloseUpdate.repeatedFields_, null); +}; goog.inherits(proto.lnrpc.ChannelCloseUpdate, jspb.Message); if (goog.DEBUG && !COMPILED) { /** @@ -14527,7 +14549,8 @@ proto.lnrpc.Channel.toObject = function(includeInstance, msg) { zeroConfConfirmedScid: jspb.Message.getFieldWithDefault(msg, 33, "0"), peerAlias: jspb.Message.getFieldWithDefault(msg, 34, ""), peerScidAlias: jspb.Message.getFieldWithDefault(msg, 35, "0"), - memo: jspb.Message.getFieldWithDefault(msg, 36, "") + memo: jspb.Message.getFieldWithDefault(msg, 36, ""), + customChannelData: msg.getCustomChannelData_asB64() }; if (includeInstance) { @@ -14713,6 +14736,10 @@ proto.lnrpc.Channel.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {string} */ (reader.readString()); msg.setMemo(value); break; + case 37: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setCustomChannelData(value); + break; default: reader.skipField(); break; @@ -14997,6 +15024,13 @@ proto.lnrpc.Channel.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getCustomChannelData_asU8(); + if (f.length > 0) { + writer.writeBytes( + 37, + f + ); + } }; @@ -15725,6 +15759,48 @@ proto.lnrpc.Channel.prototype.setMemo = function(value) { }; +/** + * optional bytes custom_channel_data = 37; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.Channel.prototype.getCustomChannelData = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 37, "")); +}; + + +/** + * optional bytes custom_channel_data = 37; + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {string} + */ +proto.lnrpc.Channel.prototype.getCustomChannelData_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getCustomChannelData())); +}; + + +/** + * optional bytes custom_channel_data = 37; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {!Uint8Array} + */ +proto.lnrpc.Channel.prototype.getCustomChannelData_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getCustomChannelData())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.Channel} returns this + */ +proto.lnrpc.Channel.prototype.setCustomChannelData = function(value) { + return jspb.Message.setProto3BytesField(this, 37, value); +}; + + @@ -21233,6 +21309,281 @@ proto.lnrpc.ChannelOpenUpdate.prototype.hasChannelPoint = function() { +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.lnrpc.CloseOutput.prototype.toObject = function(opt_includeInstance) { + return proto.lnrpc.CloseOutput.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.lnrpc.CloseOutput} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.lnrpc.CloseOutput.toObject = function(includeInstance, msg) { + var f, obj = { + amountSat: jspb.Message.getFieldWithDefault(msg, 1, "0"), + pkScript: msg.getPkScript_asB64(), + isLocal: jspb.Message.getBooleanFieldWithDefault(msg, 3, false), + customChannelData: msg.getCustomChannelData_asB64() + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.lnrpc.CloseOutput} + */ +proto.lnrpc.CloseOutput.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.lnrpc.CloseOutput; + return proto.lnrpc.CloseOutput.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.lnrpc.CloseOutput} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.lnrpc.CloseOutput} + */ +proto.lnrpc.CloseOutput.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readInt64String()); + msg.setAmountSat(value); + break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setPkScript(value); + break; + case 3: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setIsLocal(value); + break; + case 4: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setCustomChannelData(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.lnrpc.CloseOutput.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.lnrpc.CloseOutput.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.lnrpc.CloseOutput} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.lnrpc.CloseOutput.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAmountSat(); + if (parseInt(f, 10) !== 0) { + writer.writeInt64String( + 1, + f + ); + } + f = message.getPkScript_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } + f = message.getIsLocal(); + if (f) { + writer.writeBool( + 3, + f + ); + } + f = message.getCustomChannelData_asU8(); + if (f.length > 0) { + writer.writeBytes( + 4, + f + ); + } +}; + + +/** + * optional int64 amount_sat = 1; + * @return {string} + */ +proto.lnrpc.CloseOutput.prototype.getAmountSat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.lnrpc.CloseOutput} returns this + */ +proto.lnrpc.CloseOutput.prototype.setAmountSat = function(value) { + return jspb.Message.setProto3StringIntField(this, 1, value); +}; + + +/** + * optional bytes pk_script = 2; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.CloseOutput.prototype.getPkScript = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes pk_script = 2; + * This is a type-conversion wrapper around `getPkScript()` + * @return {string} + */ +proto.lnrpc.CloseOutput.prototype.getPkScript_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getPkScript())); +}; + + +/** + * optional bytes pk_script = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getPkScript()` + * @return {!Uint8Array} + */ +proto.lnrpc.CloseOutput.prototype.getPkScript_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getPkScript())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.CloseOutput} returns this + */ +proto.lnrpc.CloseOutput.prototype.setPkScript = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + +/** + * optional bool is_local = 3; + * @return {boolean} + */ +proto.lnrpc.CloseOutput.prototype.getIsLocal = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 3, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.lnrpc.CloseOutput} returns this + */ +proto.lnrpc.CloseOutput.prototype.setIsLocal = function(value) { + return jspb.Message.setProto3BooleanField(this, 3, value); +}; + + +/** + * optional bytes custom_channel_data = 4; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.CloseOutput.prototype.getCustomChannelData = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * optional bytes custom_channel_data = 4; + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {string} + */ +proto.lnrpc.CloseOutput.prototype.getCustomChannelData_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getCustomChannelData())); +}; + + +/** + * optional bytes custom_channel_data = 4; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {!Uint8Array} + */ +proto.lnrpc.CloseOutput.prototype.getCustomChannelData_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getCustomChannelData())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.CloseOutput} returns this + */ +proto.lnrpc.CloseOutput.prototype.setCustomChannelData = function(value) { + return jspb.Message.setProto3BytesField(this, 4, value); +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.lnrpc.ChannelCloseUpdate.repeatedFields_ = [5]; + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. @@ -21263,7 +21614,11 @@ proto.lnrpc.ChannelCloseUpdate.prototype.toObject = function(opt_includeInstance proto.lnrpc.ChannelCloseUpdate.toObject = function(includeInstance, msg) { var f, obj = { closingTxid: msg.getClosingTxid_asB64(), - success: jspb.Message.getBooleanFieldWithDefault(msg, 2, false) + success: jspb.Message.getBooleanFieldWithDefault(msg, 2, false), + localCloseOutput: (f = msg.getLocalCloseOutput()) && proto.lnrpc.CloseOutput.toObject(includeInstance, f), + remoteCloseOutput: (f = msg.getRemoteCloseOutput()) && proto.lnrpc.CloseOutput.toObject(includeInstance, f), + additionalOutputsList: jspb.Message.toObjectList(msg.getAdditionalOutputsList(), + proto.lnrpc.CloseOutput.toObject, includeInstance) }; if (includeInstance) { @@ -21308,6 +21663,21 @@ proto.lnrpc.ChannelCloseUpdate.deserializeBinaryFromReader = function(msg, reade var value = /** @type {boolean} */ (reader.readBool()); msg.setSuccess(value); break; + case 3: + var value = new proto.lnrpc.CloseOutput; + reader.readMessage(value,proto.lnrpc.CloseOutput.deserializeBinaryFromReader); + msg.setLocalCloseOutput(value); + break; + case 4: + var value = new proto.lnrpc.CloseOutput; + reader.readMessage(value,proto.lnrpc.CloseOutput.deserializeBinaryFromReader); + msg.setRemoteCloseOutput(value); + break; + case 5: + var value = new proto.lnrpc.CloseOutput; + reader.readMessage(value,proto.lnrpc.CloseOutput.deserializeBinaryFromReader); + msg.addAdditionalOutputs(value); + break; default: reader.skipField(); break; @@ -21351,6 +21721,30 @@ proto.lnrpc.ChannelCloseUpdate.serializeBinaryToWriter = function(message, write f ); } + f = message.getLocalCloseOutput(); + if (f != null) { + writer.writeMessage( + 3, + f, + proto.lnrpc.CloseOutput.serializeBinaryToWriter + ); + } + f = message.getRemoteCloseOutput(); + if (f != null) { + writer.writeMessage( + 4, + f, + proto.lnrpc.CloseOutput.serializeBinaryToWriter + ); + } + f = message.getAdditionalOutputsList(); + if (f.length > 0) { + writer.writeRepeatedMessage( + 5, + f, + proto.lnrpc.CloseOutput.serializeBinaryToWriter + ); + } }; @@ -21414,6 +21808,118 @@ proto.lnrpc.ChannelCloseUpdate.prototype.setSuccess = function(value) { }; +/** + * optional CloseOutput local_close_output = 3; + * @return {?proto.lnrpc.CloseOutput} + */ +proto.lnrpc.ChannelCloseUpdate.prototype.getLocalCloseOutput = function() { + return /** @type{?proto.lnrpc.CloseOutput} */ ( + jspb.Message.getWrapperField(this, proto.lnrpc.CloseOutput, 3)); +}; + + +/** + * @param {?proto.lnrpc.CloseOutput|undefined} value + * @return {!proto.lnrpc.ChannelCloseUpdate} returns this +*/ +proto.lnrpc.ChannelCloseUpdate.prototype.setLocalCloseOutput = function(value) { + return jspb.Message.setWrapperField(this, 3, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.lnrpc.ChannelCloseUpdate} returns this + */ +proto.lnrpc.ChannelCloseUpdate.prototype.clearLocalCloseOutput = function() { + return this.setLocalCloseOutput(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.lnrpc.ChannelCloseUpdate.prototype.hasLocalCloseOutput = function() { + return jspb.Message.getField(this, 3) != null; +}; + + +/** + * optional CloseOutput remote_close_output = 4; + * @return {?proto.lnrpc.CloseOutput} + */ +proto.lnrpc.ChannelCloseUpdate.prototype.getRemoteCloseOutput = function() { + return /** @type{?proto.lnrpc.CloseOutput} */ ( + jspb.Message.getWrapperField(this, proto.lnrpc.CloseOutput, 4)); +}; + + +/** + * @param {?proto.lnrpc.CloseOutput|undefined} value + * @return {!proto.lnrpc.ChannelCloseUpdate} returns this +*/ +proto.lnrpc.ChannelCloseUpdate.prototype.setRemoteCloseOutput = function(value) { + return jspb.Message.setWrapperField(this, 4, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.lnrpc.ChannelCloseUpdate} returns this + */ +proto.lnrpc.ChannelCloseUpdate.prototype.clearRemoteCloseOutput = function() { + return this.setRemoteCloseOutput(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.lnrpc.ChannelCloseUpdate.prototype.hasRemoteCloseOutput = function() { + return jspb.Message.getField(this, 4) != null; +}; + + +/** + * repeated CloseOutput additional_outputs = 5; + * @return {!Array} + */ +proto.lnrpc.ChannelCloseUpdate.prototype.getAdditionalOutputsList = function() { + return /** @type{!Array} */ ( + jspb.Message.getRepeatedWrapperField(this, proto.lnrpc.CloseOutput, 5)); +}; + + +/** + * @param {!Array} value + * @return {!proto.lnrpc.ChannelCloseUpdate} returns this +*/ +proto.lnrpc.ChannelCloseUpdate.prototype.setAdditionalOutputsList = function(value) { + return jspb.Message.setRepeatedWrapperField(this, 5, value); +}; + + +/** + * @param {!proto.lnrpc.CloseOutput=} opt_value + * @param {number=} opt_index + * @return {!proto.lnrpc.CloseOutput} + */ +proto.lnrpc.ChannelCloseUpdate.prototype.addAdditionalOutputs = function(opt_value, opt_index) { + return jspb.Message.addToRepeatedWrapperField(this, 5, opt_value, proto.lnrpc.CloseOutput, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.lnrpc.ChannelCloseUpdate} returns this + */ +proto.lnrpc.ChannelCloseUpdate.prototype.clearAdditionalOutputsList = function() { + return this.setAdditionalOutputsList([]); +}; + + @@ -28102,7 +28608,8 @@ proto.lnrpc.PendingChannelsResponse.PendingChannel.toObject = function(includeIn numForwardingPackages: jspb.Message.getFieldWithDefault(msg, 10, "0"), chanStatusFlags: jspb.Message.getFieldWithDefault(msg, 11, ""), pb_private: jspb.Message.getBooleanFieldWithDefault(msg, 12, false), - memo: jspb.Message.getFieldWithDefault(msg, 13, "") + memo: jspb.Message.getFieldWithDefault(msg, 13, ""), + customChannelData: msg.getCustomChannelData_asB64() }; if (includeInstance) { @@ -28191,6 +28698,10 @@ proto.lnrpc.PendingChannelsResponse.PendingChannel.deserializeBinaryFromReader = var value = /** @type {string} */ (reader.readString()); msg.setMemo(value); break; + case 34: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setCustomChannelData(value); + break; default: reader.skipField(); break; @@ -28311,6 +28822,13 @@ proto.lnrpc.PendingChannelsResponse.PendingChannel.serializeBinaryToWriter = fun f ); } + f = message.getCustomChannelData_asU8(); + if (f.length > 0) { + writer.writeBytes( + 34, + f + ); + } }; @@ -28548,6 +29066,48 @@ proto.lnrpc.PendingChannelsResponse.PendingChannel.prototype.setMemo = function( }; +/** + * optional bytes custom_channel_data = 34; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.PendingChannelsResponse.PendingChannel.prototype.getCustomChannelData = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 34, "")); +}; + + +/** + * optional bytes custom_channel_data = 34; + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {string} + */ +proto.lnrpc.PendingChannelsResponse.PendingChannel.prototype.getCustomChannelData_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getCustomChannelData())); +}; + + +/** + * optional bytes custom_channel_data = 34; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {!Uint8Array} + */ +proto.lnrpc.PendingChannelsResponse.PendingChannel.prototype.getCustomChannelData_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getCustomChannelData())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.PendingChannelsResponse.PendingChannel} returns this + */ +proto.lnrpc.PendingChannelsResponse.PendingChannel.prototype.setCustomChannelData = function(value) { + return jspb.Message.setProto3BytesField(this, 34, value); +}; + + @@ -31625,7 +32185,8 @@ proto.lnrpc.ChannelBalanceResponse.toObject = function(includeInstance, msg) { unsettledLocalBalance: (f = msg.getUnsettledLocalBalance()) && proto.lnrpc.Amount.toObject(includeInstance, f), unsettledRemoteBalance: (f = msg.getUnsettledRemoteBalance()) && proto.lnrpc.Amount.toObject(includeInstance, f), pendingOpenLocalBalance: (f = msg.getPendingOpenLocalBalance()) && proto.lnrpc.Amount.toObject(includeInstance, f), - pendingOpenRemoteBalance: (f = msg.getPendingOpenRemoteBalance()) && proto.lnrpc.Amount.toObject(includeInstance, f) + pendingOpenRemoteBalance: (f = msg.getPendingOpenRemoteBalance()) && proto.lnrpc.Amount.toObject(includeInstance, f), + customChannelData: msg.getCustomChannelData_asB64() }; if (includeInstance) { @@ -31700,6 +32261,10 @@ proto.lnrpc.ChannelBalanceResponse.deserializeBinaryFromReader = function(msg, r reader.readMessage(value,proto.lnrpc.Amount.deserializeBinaryFromReader); msg.setPendingOpenRemoteBalance(value); break; + case 9: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setCustomChannelData(value); + break; default: reader.skipField(); break; @@ -31791,6 +32356,13 @@ proto.lnrpc.ChannelBalanceResponse.serializeBinaryToWriter = function(message, w proto.lnrpc.Amount.serializeBinaryToWriter ); } + f = message.getCustomChannelData_asU8(); + if (f.length > 0) { + writer.writeBytes( + 9, + f + ); + } }; @@ -32052,6 +32624,48 @@ proto.lnrpc.ChannelBalanceResponse.prototype.hasPendingOpenRemoteBalance = funct }; +/** + * optional bytes custom_channel_data = 9; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.ChannelBalanceResponse.prototype.getCustomChannelData = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 9, "")); +}; + + +/** + * optional bytes custom_channel_data = 9; + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {string} + */ +proto.lnrpc.ChannelBalanceResponse.prototype.getCustomChannelData_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getCustomChannelData())); +}; + + +/** + * optional bytes custom_channel_data = 9; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {!Uint8Array} + */ +proto.lnrpc.ChannelBalanceResponse.prototype.getCustomChannelData_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getCustomChannelData())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.ChannelBalanceResponse} returns this + */ +proto.lnrpc.ChannelBalanceResponse.prototype.setCustomChannelData = function(value) { + return jspb.Message.setProto3BytesField(this, 9, value); +}; + + /** * List of repeated fields within this message type. @@ -34627,7 +35241,9 @@ proto.lnrpc.Route.toObject = function(includeInstance, msg) { hopsList: jspb.Message.toObjectList(msg.getHopsList(), proto.lnrpc.Hop.toObject, includeInstance), totalFeesMsat: jspb.Message.getFieldWithDefault(msg, 5, "0"), - totalAmtMsat: jspb.Message.getFieldWithDefault(msg, 6, "0") + totalAmtMsat: jspb.Message.getFieldWithDefault(msg, 6, "0"), + firstHopAmountMsat: jspb.Message.getFieldWithDefault(msg, 7, "0"), + customChannelData: msg.getCustomChannelData_asB64() }; if (includeInstance) { @@ -34689,6 +35305,14 @@ proto.lnrpc.Route.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {string} */ (reader.readInt64String()); msg.setTotalAmtMsat(value); break; + case 7: + var value = /** @type {string} */ (reader.readInt64String()); + msg.setFirstHopAmountMsat(value); + break; + case 8: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setCustomChannelData(value); + break; default: reader.skipField(); break; @@ -34761,6 +35385,20 @@ proto.lnrpc.Route.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getFirstHopAmountMsat(); + if (parseInt(f, 10) !== 0) { + writer.writeInt64String( + 7, + f + ); + } + f = message.getCustomChannelData_asU8(); + if (f.length > 0) { + writer.writeBytes( + 8, + f + ); + } }; @@ -34892,6 +35530,66 @@ proto.lnrpc.Route.prototype.setTotalAmtMsat = function(value) { }; +/** + * optional int64 first_hop_amount_msat = 7; + * @return {string} + */ +proto.lnrpc.Route.prototype.getFirstHopAmountMsat = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 7, "0")); +}; + + +/** + * @param {string} value + * @return {!proto.lnrpc.Route} returns this + */ +proto.lnrpc.Route.prototype.setFirstHopAmountMsat = function(value) { + return jspb.Message.setProto3StringIntField(this, 7, value); +}; + + +/** + * optional bytes custom_channel_data = 8; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.Route.prototype.getCustomChannelData = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 8, "")); +}; + + +/** + * optional bytes custom_channel_data = 8; + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {string} + */ +proto.lnrpc.Route.prototype.getCustomChannelData_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getCustomChannelData())); +}; + + +/** + * optional bytes custom_channel_data = 8; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {!Uint8Array} + */ +proto.lnrpc.Route.prototype.getCustomChannelData_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getCustomChannelData())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.Route} returns this + */ +proto.lnrpc.Route.prototype.setCustomChannelData = function(value) { + return jspb.Message.setProto3BytesField(this, 8, value); +}; + + @@ -42799,7 +43497,8 @@ proto.lnrpc.InvoiceHTLC.toObject = function(includeInstance, msg) { state: jspb.Message.getFieldWithDefault(msg, 8, 0), customRecordsMap: (f = msg.getCustomRecordsMap()) ? f.toObject(includeInstance, undefined) : [], mppTotalAmtMsat: jspb.Message.getFieldWithDefault(msg, 10, "0"), - amp: (f = msg.getAmp()) && proto.lnrpc.AMP.toObject(includeInstance, f) + amp: (f = msg.getAmp()) && proto.lnrpc.AMP.toObject(includeInstance, f), + customChannelData: msg.getCustomChannelData_asB64() }; if (includeInstance) { @@ -42883,6 +43582,10 @@ proto.lnrpc.InvoiceHTLC.deserializeBinaryFromReader = function(msg, reader) { reader.readMessage(value,proto.lnrpc.AMP.deserializeBinaryFromReader); msg.setAmp(value); break; + case 12: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setCustomChannelData(value); + break; default: reader.skipField(); break; @@ -42987,6 +43690,13 @@ proto.lnrpc.InvoiceHTLC.serializeBinaryToWriter = function(message, writer) { proto.lnrpc.AMP.serializeBinaryToWriter ); } + f = message.getCustomChannelData_asU8(); + if (f.length > 0) { + writer.writeBytes( + 12, + f + ); + } }; @@ -43212,6 +43922,48 @@ proto.lnrpc.InvoiceHTLC.prototype.hasAmp = function() { }; +/** + * optional bytes custom_channel_data = 12; + * @return {!(string|Uint8Array)} + */ +proto.lnrpc.InvoiceHTLC.prototype.getCustomChannelData = function() { + return /** @type {!(string|Uint8Array)} */ (jspb.Message.getFieldWithDefault(this, 12, "")); +}; + + +/** + * optional bytes custom_channel_data = 12; + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {string} + */ +proto.lnrpc.InvoiceHTLC.prototype.getCustomChannelData_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getCustomChannelData())); +}; + + +/** + * optional bytes custom_channel_data = 12; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getCustomChannelData()` + * @return {!Uint8Array} + */ +proto.lnrpc.InvoiceHTLC.prototype.getCustomChannelData_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getCustomChannelData())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.lnrpc.InvoiceHTLC} returns this + */ +proto.lnrpc.InvoiceHTLC.prototype.setCustomChannelData = function(value) { + return jspb.Message.setProto3BytesField(this, 12, value); +}; + + @@ -44724,7 +45476,8 @@ proto.lnrpc.Payment.toObject = function(includeInstance, msg) { htlcsList: jspb.Message.toObjectList(msg.getHtlcsList(), proto.lnrpc.HTLCAttempt.toObject, includeInstance), paymentIndex: jspb.Message.getFieldWithDefault(msg, 15, "0"), - failureReason: jspb.Message.getFieldWithDefault(msg, 16, 0) + failureReason: jspb.Message.getFieldWithDefault(msg, 16, 0), + firstHopCustomRecordsMap: (f = msg.getFirstHopCustomRecordsMap()) ? f.toObject(includeInstance, undefined) : [] }; if (includeInstance) { @@ -44822,6 +45575,12 @@ proto.lnrpc.Payment.deserializeBinaryFromReader = function(msg, reader) { var value = /** @type {!proto.lnrpc.PaymentFailureReason} */ (reader.readEnum()); msg.setFailureReason(value); break; + case 17: + var value = msg.getFirstHopCustomRecordsMap(); + reader.readMessage(value, function(message, reader) { + jspb.Map.deserializeBinary(message, reader, jspb.BinaryReader.prototype.readUint64, jspb.BinaryReader.prototype.readBytes, null, 0, ""); + }); + break; default: reader.skipField(); break; @@ -44957,6 +45716,10 @@ proto.lnrpc.Payment.serializeBinaryToWriter = function(message, writer) { f ); } + f = message.getFirstHopCustomRecordsMap(true); + if (f && f.getLength() > 0) { + f.serializeBinary(17, writer, jspb.BinaryWriter.prototype.writeUint64, jspb.BinaryWriter.prototype.writeBytes); + } }; @@ -45261,6 +46024,29 @@ proto.lnrpc.Payment.prototype.setFailureReason = function(value) { }; +/** + * map first_hop_custom_records = 17; + * @param {boolean=} opt_noLazyCreate Do not create the map if + * empty, instead returning `undefined` + * @return {!jspb.Map} + */ +proto.lnrpc.Payment.prototype.getFirstHopCustomRecordsMap = function(opt_noLazyCreate) { + return /** @type {!jspb.Map} */ ( + jspb.Message.getMapField(this, 17, opt_noLazyCreate, + null)); +}; + + +/** + * Clears values from the map. The map will be non-null. + * @return {!proto.lnrpc.Payment} returns this + */ +proto.lnrpc.Payment.prototype.clearFirstHopCustomRecordsMap = function() { + this.getFirstHopCustomRecordsMap().clear(); + return this; +}; + + @@ -57257,7 +58043,8 @@ proto.lnrpc.CommitmentType = { STATIC_REMOTE_KEY: 2, ANCHORS: 3, SCRIPT_ENFORCED_LEASE: 4, - SIMPLE_TAPROOT: 5 + SIMPLE_TAPROOT: 5, + SIMPLE_TAPROOT_OVERLAY: 6 }; /** diff --git a/app/src/types/generated/loop_pb.d.ts b/app/src/types/generated/loop_pb.d.ts index 3250adeed..60203512c 100644 --- a/app/src/types/generated/loop_pb.d.ts +++ b/app/src/types/generated/loop_pb.d.ts @@ -704,6 +704,38 @@ export namespace TokensResponse { } } +export class FetchL402TokenRequest extends jspb.Message { + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): FetchL402TokenRequest.AsObject; + static toObject(includeInstance: boolean, msg: FetchL402TokenRequest): FetchL402TokenRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: FetchL402TokenRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): FetchL402TokenRequest; + static deserializeBinaryFromReader(message: FetchL402TokenRequest, reader: jspb.BinaryReader): FetchL402TokenRequest; +} + +export namespace FetchL402TokenRequest { + export type AsObject = { + } +} + +export class FetchL402TokenResponse extends jspb.Message { + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): FetchL402TokenResponse.AsObject; + static toObject(includeInstance: boolean, msg: FetchL402TokenResponse): FetchL402TokenResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: FetchL402TokenResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): FetchL402TokenResponse; + static deserializeBinaryFromReader(message: FetchL402TokenResponse, reader: jspb.BinaryReader): FetchL402TokenResponse; +} + +export namespace FetchL402TokenResponse { + export type AsObject = { + } +} + export class L402Token extends jspb.Message { getBaseMacaroon(): Uint8Array | string; getBaseMacaroon_asU8(): Uint8Array; diff --git a/app/src/types/generated/loop_pb.js b/app/src/types/generated/loop_pb.js index bc2e8f94a..763d57b06 100644 --- a/app/src/types/generated/loop_pb.js +++ b/app/src/types/generated/loop_pb.js @@ -33,6 +33,8 @@ goog.exportSymbol('proto.looprpc.AutoReason', null, global); goog.exportSymbol('proto.looprpc.ClientReservation', null, global); goog.exportSymbol('proto.looprpc.Disqualified', null, global); goog.exportSymbol('proto.looprpc.FailureReason', null, global); +goog.exportSymbol('proto.looprpc.FetchL402TokenRequest', null, global); +goog.exportSymbol('proto.looprpc.FetchL402TokenResponse', null, global); goog.exportSymbol('proto.looprpc.GetInfoRequest', null, global); goog.exportSymbol('proto.looprpc.GetInfoResponse', null, global); goog.exportSymbol('proto.looprpc.GetLiquidityParamsRequest', null, global); @@ -475,6 +477,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.looprpc.TokensResponse.displayName = 'proto.looprpc.TokensResponse'; } +/** + * 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.looprpc.FetchL402TokenRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.looprpc.FetchL402TokenRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.looprpc.FetchL402TokenRequest.displayName = 'proto.looprpc.FetchL402TokenRequest'; +} +/** + * 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.looprpc.FetchL402TokenResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.looprpc.FetchL402TokenResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.looprpc.FetchL402TokenResponse.displayName = 'proto.looprpc.FetchL402TokenResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -6019,6 +6063,208 @@ proto.looprpc.TokensResponse.prototype.clearTokensList = function() { +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.looprpc.FetchL402TokenRequest.prototype.toObject = function(opt_includeInstance) { + return proto.looprpc.FetchL402TokenRequest.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.looprpc.FetchL402TokenRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.looprpc.FetchL402TokenRequest.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.looprpc.FetchL402TokenRequest} + */ +proto.looprpc.FetchL402TokenRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.looprpc.FetchL402TokenRequest; + return proto.looprpc.FetchL402TokenRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.looprpc.FetchL402TokenRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.looprpc.FetchL402TokenRequest} + */ +proto.looprpc.FetchL402TokenRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.looprpc.FetchL402TokenRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.looprpc.FetchL402TokenRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.looprpc.FetchL402TokenRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.looprpc.FetchL402TokenRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + +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.looprpc.FetchL402TokenResponse.prototype.toObject = function(opt_includeInstance) { + return proto.looprpc.FetchL402TokenResponse.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.looprpc.FetchL402TokenResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.looprpc.FetchL402TokenResponse.toObject = function(includeInstance, msg) { + var f, obj = { + + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.looprpc.FetchL402TokenResponse} + */ +proto.looprpc.FetchL402TokenResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.looprpc.FetchL402TokenResponse; + return proto.looprpc.FetchL402TokenResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.looprpc.FetchL402TokenResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.looprpc.FetchL402TokenResponse} + */ +proto.looprpc.FetchL402TokenResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.looprpc.FetchL402TokenResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.looprpc.FetchL402TokenResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.looprpc.FetchL402TokenResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.looprpc.FetchL402TokenResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. diff --git a/app/src/types/generated/loop_pb_service.d.ts b/app/src/types/generated/loop_pb_service.d.ts index a2b5a3969..4b4ef7532 100644 --- a/app/src/types/generated/loop_pb_service.d.ts +++ b/app/src/types/generated/loop_pb_service.d.ts @@ -121,6 +121,15 @@ type SwapClientGetLsatTokens = { readonly responseType: typeof loop_pb.TokensResponse; }; +type SwapClientFetchL402Token = { + readonly methodName: string; + readonly service: typeof SwapClient; + readonly requestStream: false; + readonly responseStream: false; + readonly requestType: typeof loop_pb.FetchL402TokenRequest; + readonly responseType: typeof loop_pb.FetchL402TokenResponse; +}; + type SwapClientGetInfo = { readonly methodName: string; readonly service: typeof SwapClient; @@ -208,6 +217,7 @@ export class SwapClient { static readonly Probe: SwapClientProbe; static readonly GetL402Tokens: SwapClientGetL402Tokens; static readonly GetLsatTokens: SwapClientGetLsatTokens; + static readonly FetchL402Token: SwapClientFetchL402Token; static readonly GetInfo: SwapClientGetInfo; static readonly GetLiquidityParams: SwapClientGetLiquidityParams; static readonly SetLiquidityParams: SwapClientSetLiquidityParams; @@ -359,6 +369,15 @@ export class SwapClientClient { requestMessage: loop_pb.TokensRequest, callback: (error: ServiceError|null, responseMessage: loop_pb.TokensResponse|null) => void ): UnaryResponse; + fetchL402Token( + requestMessage: loop_pb.FetchL402TokenRequest, + metadata: grpc.Metadata, + callback: (error: ServiceError|null, responseMessage: loop_pb.FetchL402TokenResponse|null) => void + ): UnaryResponse; + fetchL402Token( + requestMessage: loop_pb.FetchL402TokenRequest, + callback: (error: ServiceError|null, responseMessage: loop_pb.FetchL402TokenResponse|null) => void + ): UnaryResponse; getInfo( requestMessage: loop_pb.GetInfoRequest, metadata: grpc.Metadata, diff --git a/app/src/types/generated/loop_pb_service.js b/app/src/types/generated/loop_pb_service.js index ce0f802b4..406a53146 100644 --- a/app/src/types/generated/loop_pb_service.js +++ b/app/src/types/generated/loop_pb_service.js @@ -127,6 +127,15 @@ SwapClient.GetLsatTokens = { responseType: loop_pb.TokensResponse }; +SwapClient.FetchL402Token = { + methodName: "FetchL402Token", + service: SwapClient, + requestStream: false, + responseStream: false, + requestType: loop_pb.FetchL402TokenRequest, + responseType: loop_pb.FetchL402TokenResponse +}; + SwapClient.GetInfo = { methodName: "GetInfo", service: SwapClient, @@ -617,6 +626,37 @@ SwapClientClient.prototype.getLsatTokens = function getLsatTokens(requestMessage }; }; +SwapClientClient.prototype.fetchL402Token = function fetchL402Token(requestMessage, metadata, callback) { + if (arguments.length === 2) { + callback = arguments[1]; + } + var client = grpc.unary(SwapClient.FetchL402Token, { + request: requestMessage, + host: this.serviceHost, + metadata: metadata, + transport: this.options.transport, + debug: this.options.debug, + onEnd: function (response) { + if (callback) { + if (response.status !== grpc.Code.OK) { + var err = new Error(response.statusMessage); + err.code = response.status; + err.metadata = response.trailers; + callback(err, null); + } else { + callback(null, response.message); + } + } + } + }); + return { + cancel: function () { + callback = null; + client.close(); + } + }; +}; + SwapClientClient.prototype.getInfo = function getInfo(requestMessage, metadata, callback) { if (arguments.length === 2) { callback = arguments[1]; diff --git a/app/src/util/tests/sampleData.ts b/app/src/util/tests/sampleData.ts index 1dd8707cb..6dd8da4ba 100644 --- a/app/src/util/tests/sampleData.ts +++ b/app/src/util/tests/sampleData.ts @@ -74,6 +74,7 @@ export const lndGetNodeInfo: Required = { export const lndChannelBalance: LND.ChannelBalanceResponse.AsObject = { balance: '9990950', pendingOpenBalance: '0', + customChannelData: '', }; export const lndWalletBalance: LND.WalletBalanceResponse.AsObject = { @@ -132,6 +133,7 @@ export const lndChannel: LND.Channel.AsObject = { peerAlias: '', peerScidAlias: '', memo: 'test channel', + customChannelData: '', }; export const lndListChannelsMany: LND.ListChannelsResponse.AsObject = { @@ -172,6 +174,7 @@ export const lndPendingChannel: LND.PendingChannelsResponse.PendingChannel.AsObj chanStatusFlags: 'ChanStatusDefault', pb_private: false, memo: 'test channel', + customChannelData: '', }; export const lndPendingChannels: LND.PendingChannelsResponse.AsObject = { diff --git a/go.mod b/go.mod index e49b99a77..392a3d46c 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,12 @@ module github.com/lightninglabs/lightning-terminal require ( - github.com/btcsuite/btcd v0.24.2 - github.com/btcsuite/btcd/btcec/v2 v2.3.3 + github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53 + github.com/btcsuite/btcd/btcec/v2 v2.3.4 github.com/btcsuite/btcd/btcutil v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 - github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f - github.com/btcsuite/btcwallet/walletdb v1.4.2 + github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c + github.com/btcsuite/btcwallet/walletdb v1.4.4 github.com/go-errors/errors v1.0.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/improbable-eng/grpc-web v0.12.0 @@ -15,14 +15,15 @@ require ( github.com/lightninglabs/lightning-node-connect v0.3.2-alpha.0.20240822142323-ee4e7ff52f83 github.com/lightninglabs/lightning-terminal/autopilotserverrpc v0.0.1 github.com/lightninglabs/lightning-terminal/litrpc v1.0.0 - github.com/lightninglabs/lndclient v0.18.0-3 - github.com/lightninglabs/loop v0.28.8-beta.0.20241025130130-d5df72f56c8c - github.com/lightninglabs/loop/looprpc v1.0.0 + github.com/lightninglabs/lndclient v0.18.4-7 + github.com/lightninglabs/loop v0.28.9-beta + github.com/lightninglabs/loop/looprpc v1.0.1 github.com/lightninglabs/loop/swapserverrpc v1.0.10 - github.com/lightninglabs/pool v0.6.5-beta.0.20240531084722-4000ec802aaa + github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df github.com/lightninglabs/pool/auctioneerrpc v1.1.2 - github.com/lightninglabs/taproot-assets v0.4.2-0.20240725155459-2bf18437e945 - github.com/lightningnetwork/lnd v0.18.3-beta + github.com/lightninglabs/pool/poolrpc v1.0.0 + github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a + github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/kvdb v1.4.10 github.com/lightningnetwork/lnd/tlv v1.2.6 @@ -31,7 +32,7 @@ require ( github.com/mwitkow/grpc-proxy v0.0.0-20230212185441-f345521cb9c9 github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.9 - go.etcd.io/bbolt v1.3.8 + go.etcd.io/bbolt v1.3.11 golang.org/x/crypto v0.25.0 golang.org/x/net v0.27.0 golang.org/x/sync v0.8.0 @@ -42,7 +43,7 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect @@ -54,11 +55,11 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect - github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd // indirect - github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect - github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect - github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect - github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect + github.com/btcsuite/btcwallet v0.16.10-0.20240912233857-ffb143c77cc5 // indirect + github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 // indirect + github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect + github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect + github.com/btcsuite/btcwallet/wtxmgr v1.5.4 // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect @@ -86,7 +87,7 @@ require ( github.com/fortytw2/leaktest v1.3.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.1 // indirect github.com/golang-migrate/migrate/v4 v4.17.0 // indirect @@ -120,22 +121,22 @@ require ( github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad // indirect github.com/jedib0t/go-pretty/v6 v6.2.7 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect - github.com/jrick/logrotate v1.0.0 // indirect + github.com/jrick/logrotate v1.1.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/juju/loggo v1.0.0 // indirect github.com/kkdai/bstream v1.0.0 // indirect - github.com/klauspost/compress v1.15.11 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/lib/pq v1.10.9 // indirect github.com/libdns/libdns v0.2.1 // indirect - github.com/lightninglabs/aperture v0.3.2-beta // indirect + github.com/lightninglabs/aperture v0.3.4-beta // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 // indirect github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect - github.com/lightningnetwork/lnd/fn v1.2.0 // indirect + github.com/lightningnetwork/lnd/fn v1.2.3 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.5 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.4 // indirect @@ -158,7 +159,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -189,12 +190,12 @@ require ( go.etcd.io/etcd/raft/v3 v3.5.12 // indirect go.etcd.io/etcd/server/v3 v3.5.12 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.32.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/mock v0.4.0 // indirect @@ -222,6 +223,7 @@ require ( modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect nhooyr.io/websocket v1.8.7 // indirect + pgregory.net/rapid v1.1.0 // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) @@ -234,4 +236,4 @@ replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-d replace github.com/lightninglabs/lightning-terminal/litrpc => ./litrpc -go 1.22.3 +go 1.22.6 diff --git a/go.sum b/go.sum index 3b592aee2..85f2d62d6 100644 --- a/go.sum +++ b/go.sum @@ -596,8 +596,8 @@ cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoIS cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= @@ -651,12 +651,12 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= -github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53 h1:XOZ/wRGHkKv0AqxfDks5IkzaQ1Ge6fq322ZOOG5VIkU= +github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53/go.mod h1:zHK7t7sw8XbsCkD64WePHE3r3k9/XoGAcf6mXV14c64= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= -github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= +github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= @@ -667,21 +667,22 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtyd github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= +github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd h1:QDb8foTCRoXrfoZVEzSYgSde16MJh4gCtCin8OCS0kI= -github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd/go.mod h1:X2xDre+j1QphTRo54y2TikUzeSvreL1t1aMXrD8Kc5A= -github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 h1:poyHFf7+5+RdxNp5r2T6IBRD7RyraUsYARYbp/7t4D8= -github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4/go.mod h1:GETGDQuyq+VFfH1S/+/7slLM/9aNa4l7P4ejX6dJfb0= -github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 h1:UZo7YRzdHbwhK7Rhv3PO9bXgTxiOH45edK5qdsdiatk= -github.com/btcsuite/btcwallet/wallet/txrules v1.2.1/go.mod h1:MVSqRkju/IGxImXYPfBkG65FgEZYA4fXchheILMVl8g= -github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 h1:nmcKAVTv/cmYrs0A4hbiC6Qw+WTLYy/14SmTt3mLnCo= -github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4/go.mod h1:YqJR8WAAHiKIPesZTr9Cx9Az4fRhRLcJ6GcxzRUZCAc= -github.com/btcsuite/btcwallet/walletdb v1.4.2 h1:zwZZ+zaHo4mK+FAN6KeK85S3oOm+92x2avsHvFAhVBE= -github.com/btcsuite/btcwallet/walletdb v1.4.2/go.mod h1:7ZQ+BvOEre90YT7eSq8bLoxTsgXidUzA/mqbRS114CQ= -github.com/btcsuite/btcwallet/wtxmgr v1.5.3 h1:QrWCio9Leh3DwkWfp+A1SURj8pYn3JuTLv3waP5uEro= -github.com/btcsuite/btcwallet/wtxmgr v1.5.3/go.mod h1:M4nQpxGTXiDlSOODKXboXX7NFthmiBNjzAKKNS7Fhjg= +github.com/btcsuite/btcwallet v0.16.10-0.20240912233857-ffb143c77cc5 h1:zYy233eUBvkF3lq2MUkybEhxhDsrRDSgiToIKN57mtk= +github.com/btcsuite/btcwallet v0.16.10-0.20240912233857-ffb143c77cc5/go.mod h1:1HJXYbjJzgumlnxOC2+ViR1U+gnHWoOn7WeK5OfY1eU= +github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= +github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= +github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk= +github.com/btcsuite/btcwallet/wallet/txrules v1.2.2/go.mod h1:4v+grppsDpVn91SJv+mZT7B8hEV4nSmpREM4I8Uohws= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 h1:93o5Xz9dYepBP4RMFUc9RGIFXwqP2volSWRkYJFrNtI= +github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5/go.mod h1:lQ+e9HxZ85QP7r3kdxItkiMSloSLg1PEGis5o5CXUQw= +github.com/btcsuite/btcwallet/walletdb v1.4.4 h1:BDel6iT/ltYSIYKs0YbjwnEDi7xR3yzABIsQxN2F1L8= +github.com/btcsuite/btcwallet/walletdb v1.4.4/go.mod h1:jk/hvpLFINF0C1kfTn0bfx2GbnFT+Nvnj6eblZALfjs= +github.com/btcsuite/btcwallet/wtxmgr v1.5.4 h1:hJjHy1h/dJwSfD9uDsCwcH21D1iOrus6OrI5gR9E/O0= +github.com/btcsuite/btcwallet/wtxmgr v1.5.4/go.mod h1:lAv0b1Vj9Ig5U8QFm0yiJ9WqPl8yGO/6l7JxdHY1PKE= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc= @@ -854,8 +855,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -1081,8 +1082,9 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0= +github.com/jrick/logrotate v1.1.2/go.mod h1:f9tdWggSVK3iqavGpyvegq5IhNois7KXmasU6/N96OQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -1115,8 +1117,8 @@ github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+ github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= -github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= -github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= @@ -1145,8 +1147,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= -github.com/lightninglabs/aperture v0.3.2-beta h1:J2GQwBmSHxpr5VOatXbgrTogF/qN2l6UWLPHfIowq10= -github.com/lightninglabs/aperture v0.3.2-beta/go.mod h1:M/5dPzHjHvuYXQuxzicqaGiCclHUvKW6N0ay1t/HGiM= +github.com/lightninglabs/aperture v0.3.4-beta h1:TiQHw1+2CdW785U88uH0BmwUmv0R7nyfvmF9neQd56o= +github.com/lightninglabs/aperture v0.3.4-beta/go.mod h1:xuusZUPdKzQN8wKT5yL2eML8To9nz+AXoRoQa/njd4Q= github.com/lightninglabs/faraday v0.2.13-alpha h1:rpk3IM5WyyEd/wghGWpGcUyDazQhwdfkuj+D/AvDlgk= github.com/lightninglabs/faraday v0.2.13-alpha/go.mod h1:hzuTMntsY7X3gxeBLZ6kYduZlKtXUgqtk1WnnEF0aIg= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= @@ -1155,36 +1157,38 @@ github.com/lightninglabs/lightning-node-connect v0.3.2-alpha.0.20240822142323-ee github.com/lightninglabs/lightning-node-connect v0.3.2-alpha.0.20240822142323-ee4e7ff52f83/go.mod h1:+SasPOt0evcJdfApb/ALTaTz4x3a2/kWy5KqFoTpiX8= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= -github.com/lightninglabs/lndclient v0.18.0-3 h1:2k5NZJgtrcJlW3KFhvtquS7CWwKwjFN9rtLNa8MlqRA= -github.com/lightninglabs/lndclient v0.18.0-3/go.mod h1:D/0qn5s0JSdB3Kelkw4vl3bqnuZ0IHHORAv9sJjYJcQ= -github.com/lightninglabs/loop v0.28.8-beta.0.20241025130130-d5df72f56c8c h1:DQqiAv0xUErCOVtUeVDXKTafJFRcN7Ce32Mu51Ah8Ew= -github.com/lightninglabs/loop v0.28.8-beta.0.20241025130130-d5df72f56c8c/go.mod h1:30wla2TOtMyJ17OhPu3XjXZOKwm9ZZr2cpjcCDvU3ko= -github.com/lightninglabs/loop/looprpc v1.0.0 h1:xry4QPCZShPww660xJm1BVcNFj8etgNeN2vMpfsv3c4= -github.com/lightninglabs/loop/looprpc v1.0.0/go.mod h1:+hPlWT2LGxEUY9mMVB2FcbV3KJmd1cmEezmZQagVUtY= +github.com/lightninglabs/lndclient v0.18.4-7 h1:3lV3jeaL66wtxFeR+7YTo+1ZJ8YzD3gYHG8U9yas3YM= +github.com/lightninglabs/lndclient v0.18.4-7/go.mod h1:qaIx+eqEV+Bdf1j7GVeJiDqJbtZXsr9XTfHu/8HmgQU= +github.com/lightninglabs/loop v0.28.9-beta h1:JpAUpC7JEjYML36ZEJKwaTbtOfm1CgFuoykfYVok8Uc= +github.com/lightninglabs/loop v0.28.9-beta/go.mod h1:XnB5JYj+8Vo9UBsvuxmx8NBO3HzoZa7gWNmJAACrnww= +github.com/lightninglabs/loop/looprpc v1.0.1 h1:r/Nj9A26T/rZkbmUg6AttkK9n5r4jR4Hul4OOCM/5t0= +github.com/lightninglabs/loop/looprpc v1.0.1/go.mod h1:+hPlWT2LGxEUY9mMVB2FcbV3KJmd1cmEezmZQagVUtY= github.com/lightninglabs/loop/swapserverrpc v1.0.10 h1:ZLib2zUytlYOwCYOEyWUPJE16wyjR3UG0MmG/Kx/NUc= github.com/lightninglabs/loop/swapserverrpc v1.0.10/go.mod h1:Ml3gMwe/iTRLvu1QGGZzXcr0DYSa9sJGwKPktLaWtwE= github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd h1:D8aRocHpoCv43hL8egXEMYyPmyOiefFHZ66338KQB2s= github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd/go.mod h1:x3OmY2wsA18+Kc3TSV2QpSUewOCiscw2mKpXgZv2kZk= github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= -github.com/lightninglabs/pool v0.6.5-beta.0.20240531084722-4000ec802aaa h1:UYqz8O8teeSU1383NLUMIADCD2QplOeJVjiB4k1xeSI= -github.com/lightninglabs/pool v0.6.5-beta.0.20240531084722-4000ec802aaa/go.mod h1:x2+mFwSvKlBqUES3z5TdoXolS2dWD1ksueRcMORyAJA= +github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df h1:EdiN1GxUI+442K6xZcLg9mq/PekvRoSWUGuEKCjLDww= +github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df/go.mod h1:ONyfvFj3e3D0AkhpTTZPGDuIDAcY2ODnlj86aDfs2q4= github.com/lightninglabs/pool/auctioneerrpc v1.1.2 h1:Dbg+9Z9jXnhimR27EN37foc4aB1uQqndm/YOO+XAdMA= github.com/lightninglabs/pool/auctioneerrpc v1.1.2/go.mod h1:1wKDzN2zEP8srOi0B9iySlEsPdoPhw6oo3Vbm1v4Mhw= +github.com/lightninglabs/pool/poolrpc v1.0.0 h1:vvosrgNx9WXF4mcHGqLjZOW8wNM0q+BLVfdn897AFLw= +github.com/lightninglabs/pool/poolrpc v1.0.0/go.mod h1:ZqpEpBFRMMBAerMmilEjh27tqauSXDwLaLR0O3jvmMA= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightninglabs/taproot-assets v0.4.2-0.20240725155459-2bf18437e945 h1:vzr2NtI1M9/LQDv7malP0EmV9vVkh2ve5ZpDUcYFC6s= -github.com/lightninglabs/taproot-assets v0.4.2-0.20240725155459-2bf18437e945/go.mod h1:+mGg1ZNFzzcb55ZLd1UulW4iiq+ruzRHOTjCKHBeQ2g= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a h1:h1ha0sK9/3Y+bSg1qD/bDAvMgvKDh5KCwyxddk3dmFM= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.18.3-beta h1:I1Mcz79HGpVGPz0U2jSdxzzqzIi2cwUF0DXtzYJS7C8= -github.com/lightningnetwork/lnd v0.18.3-beta/go.mod h1:Xamph8AYM3iWyyn9w/tx+cLG6Tx1SSnSSPRFn71zuyQ= +github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43 h1:Oqqfo54xCWlKGeA5+i2RXr4I+LKYoMl6KwYmoSs/uQE= +github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43/go.mod h1:nPRQzLla5uHPQFyyZn8r9Vgddkd23PBUDa9rggEPOfY= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= -github.com/lightningnetwork/lnd/fn v1.2.0 h1:YTb2m8NN5ZiJAskHeBZAmR1AiPY8SXziIYPAX1VI/ZM= -github.com/lightningnetwork/lnd/fn v1.2.0/go.mod h1:SyFohpVrARPKH3XVAJZlXdVe+IwMYc4OMAvrDY32kw0= +github.com/lightningnetwork/lnd/fn v1.2.3 h1:Q1OrgNSgQynVheBNa16CsKVov1JI5N2AR6G07x9Mles= +github.com/lightningnetwork/lnd/fn v1.2.3/go.mod h1:SyFohpVrARPKH3XVAJZlXdVe+IwMYc4OMAvrDY32kw0= github.com/lightningnetwork/lnd/healthcheck v1.2.5 h1:aTJy5xeBpcWgRtW/PGBDe+LMQEmNm/HQewlQx2jt7OA= github.com/lightningnetwork/lnd/healthcheck v1.2.5/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I= github.com/lightningnetwork/lnd/kvdb v1.4.10 h1:vK89IVv1oVH9ubQWU+EmoCQFeVRaC8kfmOrqHbY5zoY= @@ -1297,8 +1301,9 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= @@ -1407,8 +1412,8 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= -go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= -go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= go.etcd.io/etcd/client/pkg/v3 v3.5.12 h1:EYDL6pWwyOsylrQyLp2w+HkQ46ATiOvoEdMarindU2A= @@ -1433,20 +1438,20 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= @@ -2264,6 +2269,8 @@ modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= +pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/itest/litd_firewall_test.go b/itest/litd_firewall_test.go index 8923631d3..ee263b1f4 100644 --- a/itest/litd_firewall_test.go +++ b/itest/litd_firewall_test.go @@ -202,7 +202,7 @@ func testFirewallRules(ctx context.Context, net *NetworkHarness, resp, err := net.Alice.GetInfo(ctx, &lnrpc.GetInfoRequest{}) require.NoError(t.t, err) require.NotEmpty(t.t, resp.Alias) - require.Contains(t.t, resp.Alias, "0") + require.Contains(t.t, resp.Alias, "Alice") // Open a channel between Alice and Bob so that we have something to // query later. diff --git a/itest/litd_mode_integrated_test.go b/itest/litd_mode_integrated_test.go index 50fda9318..65af53aa8 100644 --- a/itest/litd_mode_integrated_test.go +++ b/itest/litd_mode_integrated_test.go @@ -478,7 +478,7 @@ func integratedTestSuite(ctx context.Context, net *NetworkHarness, t *testing.T, resp, err := net.Alice.GetInfo(ctx, &lnrpc.GetInfoRequest{}) require.NoError(t, err) require.NotEmpty(t, resp.Alias) - require.Contains(t, resp.Alias, "0") + require.Contains(t, resp.Alias, "Alice") t.Run("certificate check", func(tt *testing.T) { runCertificateCheck(tt, net.Alice) diff --git a/itest/litd_node.go b/itest/litd_node.go index 76544959d..618ae17e1 100644 --- a/itest/litd_node.go +++ b/itest/litd_node.go @@ -150,7 +150,7 @@ func (l *litArgs) getArg(name string) (string, bool) { } // toArgList converts the litArgs map to an arguments string slice. -func (l *litArgs) toArgList() []string { +func (l *litArgs) toArgList(nodeName string) []string { l.mu.Lock() defer l.mu.Unlock() @@ -164,7 +164,7 @@ func (l *litArgs) toArgList() []string { args = append(args, fmt.Sprintf("--%s=%s", arg, setting)) } - return args + return append([]string{"--lnd.alias=" + nodeName}, args...) } // LitArgOption defines the signature of a functional option that can be used @@ -197,7 +197,7 @@ func (cfg *LitNodeConfig) GenArgs(opts ...LitArgOption) []string { cfg.ActiveArgs = args - return args.toArgList() + return args.toArgList(cfg.Name) } // defaultLitArgs generates the default arguments to be used with a Litd node. @@ -216,6 +216,7 @@ func (cfg *LitNodeConfig) defaultLitdArgs() *litArgs { "enablerest": "", "restcors": "*", "lnd.debuglevel": "trace,GRPC=error,PEER=info", + "lndconnectinterval": "200ms", } ) for _, arg := range cfg.LitArgs { diff --git a/itest/network_harness.go b/itest/network_harness.go index 9f72e48f6..271163441 100644 --- a/itest/network_harness.go +++ b/itest/network_harness.go @@ -285,11 +285,14 @@ func (n *NetworkHarness) litArgs() []string { // NewNode initializes a new HarnessNode. func (n *NetworkHarness) NewNode(t *testing.T, name string, extraArgs []string, - remoteMode bool, wait bool) (*HarnessNode, error) { + remoteMode bool, wait bool, additionalLitArgs ...string) (*HarnessNode, + error) { + + allLitArgs := append(n.litArgs(), additionalLitArgs...) return n.newNode( - t, name, extraArgs, n.litArgs(), false, remoteMode, nil, - wait, false, + t, name, extraArgs, allLitArgs, false, remoteMode, nil, wait, + false, ) } @@ -1048,7 +1051,9 @@ func (n *NetworkHarness) CloseChannel(lnNode *HarnessNode, err = wait.NoError(func() error { closeReq := &lnrpc.CloseChannelRequest{ - ChannelPoint: cp, Force: force, + ChannelPoint: cp, + Force: force, + SatPerVbyte: 5, } closeRespStream, err = lnNode.CloseChannel(ctx, closeReq) if err != nil { @@ -1076,7 +1081,7 @@ func (n *NetworkHarness) CloseChannel(lnNode *HarnessNode, return fmt.Errorf("unable to decode closeTxid: "+ "%v", err) } - n.LNDHarness.Miner().AssertTxInMempool(closeTxid) + n.LNDHarness.Miner().AssertTxInMempool(*closeTxid) return nil }, wait.ChannelCloseTimeout) diff --git a/proto/lnd.proto b/proto/lnd.proto index a02fdde65..95ee5ae73 100644 --- a/proto/lnd.proto +++ b/proto/lnd.proto @@ -1388,8 +1388,14 @@ enum CommitmentType { A channel that uses musig2 for the funding output, and the new tapscript features where relevant. */ - // TODO(roasbeef): need script enforce mirror type for the above as well? SIMPLE_TAPROOT = 5; + + /* + Identical to the SIMPLE_TAPROOT channel type, but with extra functionality. + This channel type also commits to additional meta data in the tapscript + leaves for the scripts in a channel. + */ + SIMPLE_TAPROOT_OVERLAY = 6; } message ChannelConstraints { @@ -1592,6 +1598,11 @@ message Channel { the channel's operation. */ string memo = 36; + + /* + Custom channel data that might be populated in custom channels. + */ + bytes custom_channel_data = 37; } message ListChannelsRequest { @@ -2028,10 +2039,38 @@ message ChannelOpenUpdate { ChannelPoint channel_point = 1; } +message CloseOutput { + // The amount in satoshi of this close output. This amount is the final + // commitment balance of the channel and the actual amount paid out on chain + // might be smaller due to subtracted fees. + int64 amount_sat = 1 [jstype = JS_STRING]; + + // The pkScript of the close output. + bytes pk_script = 2; + + // Whether this output is for the local or remote node. + bool is_local = 3; + + // The TLV encoded custom channel data records for this output, which might + // be set for custom channels. + bytes custom_channel_data = 4; +} + message ChannelCloseUpdate { bytes closing_txid = 1; bool success = 2; + + // The local channel close output. If the local channel balance was dust to + // begin with, this output will not be set. + CloseOutput local_close_output = 3; + + // The remote channel close output. If the remote channel balance was dust + // to begin with, this output will not be set. + CloseOutput remote_close_output = 4; + + // Any additional outputs that might be added for custom channel types. + repeated CloseOutput additional_outputs = 5; } message CloseChannelRequest { @@ -2709,6 +2748,11 @@ message PendingChannelsResponse { impacts the channel's operation. */ string memo = 13; + + /* + Custom channel data that might be populated in custom channels. + */ + bytes custom_channel_data = 34; } message PendingOpenChannel { @@ -2968,6 +3012,12 @@ message ChannelBalanceResponse { // Sum of channels pending remote balances. Amount pending_open_remote_balance = 8; + + /* + Custom channel data that might be populated if there are custom channels + present. + */ + bytes custom_channel_data = 9; } message QueryRoutesRequest { @@ -3293,6 +3343,20 @@ message Route { The total amount in millisatoshis. */ int64 total_amt_msat = 6 [jstype = JS_STRING]; + + /* + The actual on-chain amount that was sent out to the first hop. This value is + only different from the total_amt_msat field if this is a custom channel + payment and the value transported in the HTLC is different from the BTC + amount in the HTLC. If this value is zero, then this is an old payment that + didn't have this value yet and can be ignored. + */ + int64 first_hop_amount_msat = 7 [jstype = JS_STRING]; + + /* + Custom channel data that might be populated in custom channels. + */ + bytes custom_channel_data = 8; } message NodeInfoRequest { @@ -3922,6 +3986,11 @@ message InvoiceHTLC { // Details relevant to AMP HTLCs, only populated if this is an AMP HTLC. AMP amp = 11; + + /* + Custom channel data that might be populated in custom channels. + */ + bytes custom_channel_data = 12; } // Details specific to AMP HTLCs. @@ -4162,6 +4231,12 @@ message Payment { uint64 payment_index = 15 [jstype = JS_STRING]; PaymentFailureReason failure_reason = 16; + + /* + The custom TLV records that were sent to the first hop as part of the HTLC + wire message for this payment. + */ + map first_hop_custom_records = 17; } message HTLCAttempt { diff --git a/proto/loop.proto b/proto/loop.proto index 57254f94b..a2f9508dc 100644 --- a/proto/loop.proto +++ b/proto/loop.proto @@ -89,6 +89,12 @@ service SwapClient { */ rpc GetLsatTokens (TokensRequest) returns (TokensResponse); + /* loop: `fetchl402` + FetchL402Token fetches an L402 token from the server, this is required in + order to receive reservation notifications from the server. + */ + rpc FetchL402Token (FetchL402TokenRequest) returns (FetchL402TokenResponse); + /* loop: `getinfo` GetInfo gets basic information about the loop daemon. */ @@ -830,6 +836,12 @@ message TokensResponse { repeated L402Token tokens = 1; } +message FetchL402TokenRequest { +} + +message FetchL402TokenResponse { +} + message L402Token { /* The base macaroon that was baked by the auth server. diff --git a/subservers/taproot-assets.go b/subservers/taproot-assets.go index fc45a70ad..6c6a15fcc 100644 --- a/subservers/taproot-assets.go +++ b/subservers/taproot-assets.go @@ -103,9 +103,10 @@ func (t *taprootAssetsSubServer) Start(_ lnrpc.LightningClient, return err } - // The taproot asset channel feature is still experimental, so we - // disable it for now. - const enableChannelFeatures = false + // If we're being called here, it means tapd is running in integrated + // mode. But we can only offer Taproot Asset channel functionality if + // lnd is also running in integrated mode. + enableChannelFeatures := !t.lndRemote err = tapcfg.ConfigureSubServer( t.Server, t.cfg, log, &lndGrpc.LndServices, diff --git a/terminal.go b/terminal.go index 88a7d3f1f..ac698518d 100644 --- a/terminal.go +++ b/terminal.go @@ -1284,14 +1284,15 @@ func (g *LightningTerminal) Permissions() map[string][]bakery.Op { // // NOTE: This is part of the lnd.WalletConfigBuilder interface. func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, - dbs *lnd.DatabaseInstances, interceptorChain *rpcperms.InterceptorChain, + dbs *lnd.DatabaseInstances, auxComponents *lnd.AuxComponents, + interceptorChain *rpcperms.InterceptorChain, grpcListeners []*lnd.ListenerWithSignal) (*chainreg.PartialChainControl, *btcwallet.Config, func(), error) { g.lndInterceptorChain = interceptorChain return g.defaultImplCfg.WalletConfigBuilder.BuildWalletConfig( - ctx, dbs, interceptorChain, grpcListeners, + ctx, dbs, auxComponents, interceptorChain, grpcListeners, ) } From 1d69ecc1f3acb46e79e9136269eddae1c4d465fd Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:52:28 +0200 Subject: [PATCH 02/12] subservers: add Impl to subservers, GetServer to mgr --- subservers/faraday.go | 11 +++++++++++ subservers/interface.go | 5 +++++ subservers/loop.go | 11 +++++++++++ subservers/manager.go | 13 +++++++++++++ subservers/pool.go | 11 +++++++++++ subservers/taproot-assets.go | 25 +++++++++++++++++++++++++ 6 files changed, 76 insertions(+) diff --git a/subservers/faraday.go b/subservers/faraday.go index 9b9efd8e3..9b8498ffa 100644 --- a/subservers/faraday.go +++ b/subservers/faraday.go @@ -9,6 +9,7 @@ import ( "github.com/lightninglabs/faraday/frdrpcserver" "github.com/lightninglabs/faraday/frdrpcserver/perms" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightningnetwork/lnd/lnrpc" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" @@ -126,3 +127,13 @@ func (f *faradaySubServer) Permissions() map[string][]bakery.Op { func (f *faradaySubServer) WhiteListedURLs() map[string]struct{} { return nil } + +// Impl returns the actual implementation of the sub-server. This might not be +// set if the sub-server is running in remote mode. +func (f *faradaySubServer) Impl() fn.Option[any] { + if f.RPCServer == nil { + return fn.None[any]() + } + + return fn.Some[any](f.RPCServer) +} diff --git a/subservers/interface.go b/subservers/interface.go index cc7ed5051..4ff0083cf 100644 --- a/subservers/interface.go +++ b/subservers/interface.go @@ -5,6 +5,7 @@ import ( restProxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" @@ -62,4 +63,8 @@ type SubServer interface { // WhiteListedURLs returns a map of all the sub-server's URLs that can // be accessed without a macaroon. WhiteListedURLs() map[string]struct{} + + // Impl returns the actual implementation of the sub-server. This might + // not be set if the sub-server is running in remote mode. + Impl() fn.Option[any] } diff --git a/subservers/loop.go b/subservers/loop.go index 4281125fb..8a405eb18 100644 --- a/subservers/loop.go +++ b/subservers/loop.go @@ -9,6 +9,7 @@ import ( "github.com/lightninglabs/loop/loopd" "github.com/lightninglabs/loop/loopd/perms" "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightningnetwork/lnd/lnrpc" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" @@ -136,3 +137,13 @@ func (l *loopSubServer) Permissions() map[string][]bakery.Op { func (l *loopSubServer) WhiteListedURLs() map[string]struct{} { return nil } + +// Impl returns the actual implementation of the sub-server. This might not be +// set if the sub-server is running in remote mode. +func (l *loopSubServer) Impl() fn.Option[any] { + if l.Daemon == nil { + return fn.None[any]() + } + + return fn.Some[any](l.Daemon) +} diff --git a/subservers/manager.go b/subservers/manager.go index e81595174..8d9f6b1b3 100644 --- a/subservers/manager.go +++ b/subservers/manager.go @@ -90,6 +90,19 @@ func (s *Manager) AddServer(ss SubServer, enable bool) error { return nil } +// GetServer returns the sub-server with the given name if it exists. +func (s *Manager) GetServer(name string) (SubServer, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + + ss, ok := s.servers[name] + if !ok { + return nil, false + } + + return ss.SubServer, true +} + // StartIntegratedServers starts all the manager's sub-servers that should be // started in integrated mode. func (s *Manager) StartIntegratedServers(lndClient lnrpc.LightningClient, diff --git a/subservers/pool.go b/subservers/pool.go index 9c476705b..6a6576125 100644 --- a/subservers/pool.go +++ b/subservers/pool.go @@ -8,6 +8,7 @@ import ( "github.com/lightninglabs/pool" "github.com/lightninglabs/pool/perms" "github.com/lightninglabs/pool/poolrpc" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightningnetwork/lnd/lnrpc" "google.golang.org/grpc" "gopkg.in/macaroon-bakery.v2/bakery" @@ -126,3 +127,13 @@ func (p *poolSubServer) Permissions() map[string][]bakery.Op { func (p *poolSubServer) WhiteListedURLs() map[string]struct{} { return nil } + +// Impl returns the actual implementation of the sub-server. This might not be +// set if the sub-server is running in remote mode. +func (p *poolSubServer) Impl() fn.Option[any] { + if p.Server == nil { + return fn.None[any]() + } + + return fn.Some[any](p.Server) +} diff --git a/subservers/taproot-assets.go b/subservers/taproot-assets.go index 6c6a15fcc..039882f86 100644 --- a/subservers/taproot-assets.go +++ b/subservers/taproot-assets.go @@ -9,6 +9,7 @@ import ( "github.com/lightninglabs/lndclient" tap "github.com/lightninglabs/taproot-assets" "github.com/lightninglabs/taproot-assets/address" + "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/perms" "github.com/lightninglabs/taproot-assets/tapcfg" "github.com/lightninglabs/taproot-assets/taprpc" @@ -177,6 +178,20 @@ func (t *taprootAssetsSubServer) RegisterRestService(ctx context.Context, return err } + err = rfqrpc.RegisterRfqHandlerFromEndpoint( + ctx, mux, endpoint, dialOpts, + ) + if err != nil { + return err + } + + err = tchrpc.RegisterTaprootAssetChannelsHandlerFromEndpoint( + ctx, mux, endpoint, dialOpts, + ) + if err != nil { + return err + } + err = universerpc.RegisterUniverseHandlerFromEndpoint( ctx, mux, endpoint, dialOpts, ) @@ -239,3 +254,13 @@ func (t *taprootAssetsSubServer) WhiteListedURLs() map[string]struct{} { t.cfg.RpcConf.AllowPublicStats || t.remote, ) } + +// Impl returns the actual implementation of the sub-server. This might not be +// set if the sub-server is running in remote mode. +func (t *taprootAssetsSubServer) Impl() fn.Option[any] { + if t.Server == nil { + return fn.None[any]() + } + + return fn.Some[any](t.Server) +} From 228b70b57ba66520b36992fa62d2da4a07de147a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:54:35 +0200 Subject: [PATCH 03/12] litd: register tapd as aux component of lnd This commit represents the main integration between lnd running in integrated mode and tapd providing auxiliary components for custom channels. --- config.go | 14 +++++++++ go.mod | 2 +- terminal.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/config.go b/config.go index 12cf21f29..bc860914f 100644 --- a/config.go +++ b/config.go @@ -31,6 +31,7 @@ import ( "github.com/lightningnetwork/lnd/cert" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/signal" "github.com/mwitkow/go-conntrack/connhelpers" "golang.org/x/crypto/acme/autocert" @@ -627,6 +628,19 @@ func loadConfigFile(preCfg *Config, interceptor signal.Interceptor) (*Config, // configuration is fully valid. This also sets up the main logger that // logs to a sub-directory in the .lnd folder. case ModeIntegrated: + // For the integration of tapd with lnd, we need to allow tapd + // to send custom error messages to peers through the + // SendCustomMessage RPC in lnd. Since the error messages aren't + // in the custom range, we explicitly need to allow them. This + // isn't currently needed in remote mode, because custom + // channels are only available if both lnd and tapd are running + // in integrated mode. We need to set this value before we call + // lnd.ValidateConfig() below, because that's what's going to + // inject these values into the lnwire package. + cfg.Lnd.ProtocolOptions.CustomMessage = append( + cfg.Lnd.ProtocolOptions.CustomMessage, lnwire.MsgError, + ) + var err error cfg.Lnd, err = lnd.ValidateConfig( *cfg.Lnd, interceptor, fileParser, flagParser, diff --git a/go.mod b/go.mod index 392a3d46c..188bb9b48 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241213120005-7358c1b0b42a github.com/lightningnetwork/lnd v0.18.4-beta.rc2.0.20241216115224-04767fe78c43 github.com/lightningnetwork/lnd/cert v1.2.2 + github.com/lightningnetwork/lnd/fn v1.2.3 github.com/lightningnetwork/lnd/kvdb v1.4.10 github.com/lightningnetwork/lnd/tlv v1.2.6 github.com/lightningnetwork/lnd/tor v1.1.2 @@ -136,7 +137,6 @@ require ( github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect - github.com/lightningnetwork/lnd/fn v1.2.3 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.5 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/sqldb v1.0.4 // indirect diff --git a/terminal.go b/terminal.go index ac698518d..b013ff0a8 100644 --- a/terminal.go +++ b/terminal.go @@ -34,9 +34,13 @@ import ( "github.com/lightninglabs/lightning-terminal/status" "github.com/lightninglabs/lightning-terminal/subservers" "github.com/lightninglabs/lndclient" + taprootassets "github.com/lightninglabs/taproot-assets" "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/funding" + "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" @@ -49,10 +53,14 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc" "github.com/lightningnetwork/lnd/lnrpc/wtclientrpc" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet/btcwallet" + "github.com/lightningnetwork/lnd/lnwallet/chancloser" "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/msgmux" "github.com/lightningnetwork/lnd/rpcperms" "github.com/lightningnetwork/lnd/signal" + "github.com/lightningnetwork/lnd/sweep" grpcProxy "github.com/mwitkow/grpc-proxy/proxy" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -509,6 +517,25 @@ func (g *LightningTerminal) start() error { }}, } + var auxComponents lnd.AuxComponents + switch g.cfg.TaprootAssetsMode { + case ModeRemote, ModeDisable: + log.Warnf("Taproot Assets daemon is either disabled " + + "or running in remote mode. Taproot Asset " + + "channel functionality will NOT be " + + "available. To enable, set Taproot Assets " + + "mode to 'integrated' in the config file.") + + case ModeIntegrated: + components, err := g.buildAuxComponents() + if err != nil { + return fmt.Errorf("could not build aux "+ + "components: %w", err) + } + + auxComponents = *components + } + implCfg := &lnd.ImplementationCfg{ GrpcRegistrar: g, RestRegistrar: g, @@ -516,6 +543,7 @@ func (g *LightningTerminal) start() error { DatabaseBuilder: g.defaultImplCfg.DatabaseBuilder, WalletConfigBuilder: g, ChainControlBuilder: g.defaultImplCfg.ChainControlBuilder, + AuxComponents: auxComponents, } g.wg.Add(1) @@ -1296,6 +1324,59 @@ func (g *LightningTerminal) BuildWalletConfig(ctx context.Context, ) } +// buildAuxComponent builds the auxiliary components required by lnd when +// running in integrated mode with tapd being the service that provides the +// aux component implementations. +func (g *LightningTerminal) buildAuxComponents() (*lnd.AuxComponents, error) { + errNotAvailable := fmt.Errorf("tapd is not available, both lnd and " + + "tapd must be started in integrated mode for Taproot " + + "Assets Channels to be available") + + tapdWrapper, available := g.subServerMgr.GetServer(subservers.TAP) + if !available { + return nil, errNotAvailable + } + + if tapdWrapper.Remote() { + return nil, errNotAvailable + } + + tapdOpt := tapdWrapper.Impl() + tapdAny, err := tapdOpt.UnwrapOrErr(errors.New("tapd not available")) + if err != nil { + return nil, err + } + + tapd, ok := tapdAny.(*taprootassets.Server) + if !ok { + return nil, fmt.Errorf("tapd is not of the expected type") + } + + router := msgmux.NewMultiMsgRouter() + router.Start() + err = router.RegisterEndpoint(tapd) + if err != nil { + return nil, fmt.Errorf("error registering tapd endpoint: %w", + err) + } + + return &lnd.AuxComponents{ + AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](tapd), + MsgRouter: fn.Some[msgmux.Router](router), + AuxFundingController: fn.Some[funding.AuxFundingController]( + tapd, + ), + AuxSigner: fn.Some[lnwallet.AuxSigner](tapd), + TrafficShaper: fn.Some[htlcswitch.AuxTrafficShaper](tapd), + AuxDataParser: fn.Some[lnd.AuxDataParser](tapd), + AuxChanCloser: fn.Some[chancloser.AuxChanCloser](tapd), + AuxSweeper: fn.Some[sweep.AuxSweeper](tapd), + AuxContractResolver: fn.Some[lnwallet.AuxContractResolver]( + tapd, + ), + }, nil +} + // shutdownSubServers stops all subservers that were started and attached to // lnd. func (g *LightningTerminal) shutdownSubServers() error { From 4a7b89675b586bb6e05dd472afc2e6ae017da42b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:55:26 +0200 Subject: [PATCH 04/12] litd: allow faster startup by making re-try configurable This change will speed up integration tests by not waiting a full 5 seconds before re-trying the connection to lnd if it fails the first time. --- config.go | 8 +++++--- terminal.go | 17 ++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/config.go b/config.go index bc860914f..18a1ad264 100644 --- a/config.go +++ b/config.go @@ -190,9 +190,10 @@ type Config struct { // friendly. Because then we can reference the explicit modes in the // help descriptions of the section headers. We'll parse the mode into // a bool for internal use for better code readability. - LndMode string `long:"lnd-mode" description:"The mode to run lnd in, either 'remote' (default) or 'integrated'. 'integrated' means lnd is started alongside the UI and everything is stored in lnd's main data directory, configure everything by using the --lnd.* flags. 'remote' means the UI connects to an existing lnd node and acts as a proxy for gRPC calls to it. In the remote node LiT creates its own directory for log and configuration files, configure everything using the --remote.* flags." choice:"integrated" choice:"remote"` - Lnd *lnd.Config `group:"Integrated lnd (use when lnd-mode=integrated)" namespace:"lnd"` - LndRPCTimeout time.Duration `long:"lndrpctimeout" description:"The timeout for RPC calls to lnd from other sub servers. This can be adjusted for slow lnd instances to give loop/pool/faraday/taproot-assets more time when querying into lnd's RPC methods. This value should NOT be set to anything below 30 seconds to avoid problems."` + LndMode string `long:"lnd-mode" description:"The mode to run lnd in, either 'remote' (default) or 'integrated'. 'integrated' means lnd is started alongside the UI and everything is stored in lnd's main data directory, configure everything by using the --lnd.* flags. 'remote' means the UI connects to an existing lnd node and acts as a proxy for gRPC calls to it. In the remote node LiT creates its own directory for log and configuration files, configure everything using the --remote.* flags." choice:"integrated" choice:"remote"` + Lnd *lnd.Config `group:"Integrated lnd (use when lnd-mode=integrated)" namespace:"lnd"` + LndRPCTimeout time.Duration `long:"lndrpctimeout" description:"The timeout for RPC calls to lnd from other sub servers. This can be adjusted for slow lnd instances to give loop/pool/faraday/taproot-assets more time when querying into lnd's RPC methods. This value should NOT be set to anything below 30 seconds to avoid problems."` + LndConnectInterval time.Duration `long:"lndconnectinterval" hidden:"true" description:"The interval at which LiT tries to connect to the lnd node. This value should only be changed for development mode."` FaradayMode string `long:"faraday-mode" description:"The mode to run faraday in, either 'integrated' (default), 'remote' or 'disable'. 'integrated' means faraday is started alongside the UI and everything is stored in faraday's main data directory, configure everything by using the --faraday.* flags. 'remote' means the UI connects to an existing faraday node and acts as a proxy for gRPC calls to it. 'disable' means that LiT is started without faraday." choice:"integrated" choice:"remote" choice:"disable"` Faraday *faraday.Config `group:"Integrated faraday options (use when faraday-mode=integrated)" namespace:"faraday"` @@ -314,6 +315,7 @@ func defaultConfig() *Config { LndMode: DefaultLndMode, Lnd: &lndDefaultConfig, LndRPCTimeout: defaultRPCTimeout, + LndConnectInterval: defaultStartupTimeout, LitDir: DefaultLitDir, LetsEncryptListen: defaultLetsEncryptListen, LetsEncryptDir: defaultLetsEncryptDir, diff --git a/terminal.go b/terminal.go index b013ff0a8..b27e3b2c5 100644 --- a/terminal.go +++ b/terminal.go @@ -811,7 +811,7 @@ func (g *LightningTerminal) setUpLNDClients(lndQuit chan struct{}) error { case <-interceptor.ShutdownChannel(): return fmt.Errorf("received the shutdown signal") - case <-time.After(defaultStartupTimeout): + case <-time.After(g.cfg.LndConnectInterval): return nil } } @@ -893,17 +893,20 @@ func (g *LightningTerminal) setUpLNDClients(lndQuit chan struct{}) error { for { g.lndClient, err = lndclient.NewLndServices( &lndclient.LndServicesConfig{ - LndAddress: host, - Network: network, - TLSPath: tlsPath, - Insecure: insecure, - CustomMacaroonPath: macPath, - CustomMacaroonHex: hex.EncodeToString(macData), + LndAddress: host, + Network: network, + TLSPath: tlsPath, + Insecure: insecure, + CustomMacaroonPath: macPath, + CustomMacaroonHex: hex.EncodeToString( + macData, + ), BlockUntilChainSynced: true, BlockUntilUnlocked: true, CallerCtx: ctxc, CheckVersion: minimalCompatibleVersion, RPCTimeout: g.cfg.LndRPCTimeout, + ChainSyncPollInterval: g.cfg.LndConnectInterval, }, ) if err == nil { From 8fa91f13a752e5d18088b07ef034955e77907775 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:56:31 +0200 Subject: [PATCH 05/12] cmd/litcli: add ln sub commands for custom channels --- cmd/litcli/ln.go | 641 +++++++++++++++++++++++++++++++++++++++++++++ cmd/litcli/main.go | 94 ++++++- 2 files changed, 728 insertions(+), 7 deletions(-) create mode 100644 cmd/litcli/ln.go diff --git a/cmd/litcli/ln.go b/cmd/litcli/ln.go new file mode 100644 index 000000000..a1cfcca89 --- /dev/null +++ b/cmd/litcli/ln.go @@ -0,0 +1,641 @@ +package main + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "strconv" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" + "github.com/lightningnetwork/lnd/cmd/commands" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/record" + "github.com/urfave/cli" + "google.golang.org/grpc" +) + +const ( + // minAssetAmount is the minimum amount of an asset that can be put into + // a channel. We choose an arbitrary value that allows for at least a + // couple of HTLCs to be created without leading to fractions of assets + // (which doesn't exist). + minAssetAmount = 100 +) + +var lnCommands = []cli.Command{ + { + Name: "ln", + Usage: "Interact with the Lightning Network.", + Category: "Taproot Assets on LN", + Subcommands: []cli.Command{ + fundChannelCommand, + sendPaymentCommand, + payInvoiceCommand, + addInvoiceCommand, + }, + }, +} + +var fundChannelCommand = cli.Command{ + Name: "fundchannel", + Category: "Channels", + Usage: "Open a Taproot Asset channel with a node on the Lightning " + + "Network.", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "node_key", + Usage: "the identity public key of the target " + + "node/peer serialized in compressed format, " + + "must already be connected to", + }, + cli.Uint64Flag{ + Name: "sat_per_vbyte", + Usage: "(optional) a manual fee expressed in " + + "sat/vByte that should be used when crafting " + + "the transaction", + }, + cli.Uint64Flag{ + Name: "push_amt", + Usage: "the number of satoshis to give the remote " + + "side as part of the initial commitment " + + "state, this is equivalent to first opening " + + "a channel and then sending the remote party " + + "funds, but done all in one step; therefore, " + + "this is equivalent to a donation to the " + + "remote party, unless they reimburse the " + + "funds in another way (outside the protocol)", + }, + cli.Uint64Flag{ + Name: "asset_amount", + Usage: "The amount of the asset to commit to the " + + "channel.", + }, + cli.StringFlag{ + Name: "asset_id", + Usage: "The asset ID to commit to the channel.", + }, + }, + Action: fundChannel, +} + +func fundChannel(c *cli.Context) error { + tapdConn, cleanup, err := connectSuperMacClient(c) + if err != nil { + return fmt.Errorf("error creating tapd connection: %w", err) + } + + defer cleanup() + + ctxb := context.Background() + tapdClient := taprpc.NewTaprootAssetsClient(tapdConn) + tchrpcClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn) + assets, err := tapdClient.ListAssets(ctxb, &taprpc.ListAssetRequest{}) + if err != nil { + return fmt.Errorf("error fetching assets: %w", err) + } + + assetIDBytes, err := hex.DecodeString(c.String("asset_id")) + if err != nil { + return fmt.Errorf("error hex decoding asset ID: %w", err) + } + + requestedAmount := c.Uint64("asset_amount") + if requestedAmount < minAssetAmount { + return fmt.Errorf("requested amount must be at least %d", + minAssetAmount) + } + + nodePubBytes, err := hex.DecodeString(c.String("node_key")) + if err != nil { + return fmt.Errorf("unable to decode node public key: %w", err) + } + + assetFound := false + totalAmount := uint64(0) + for _, rpcAsset := range assets.Assets { + if !bytes.Equal(rpcAsset.AssetGenesis.AssetId, assetIDBytes) { + continue + } + + totalAmount += rpcAsset.Amount + if totalAmount >= requestedAmount { + assetFound = true + + break + } + + assetFound = true + } + + if !assetFound { + return fmt.Errorf("asset with ID %x not found or no combined "+ + "UTXOs with at least amount %d is available", + assetIDBytes, requestedAmount) + } + + resp, err := tchrpcClient.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: requestedAmount, + AssetId: assetIDBytes, + PeerPubkey: nodePubBytes, + FeeRateSatPerVbyte: uint32(c.Uint64("sat_per_vbyte")), + PushSat: c.Int64("push_amt"), + }, + ) + if err != nil { + return fmt.Errorf("error funding channel: %w", err) + } + + printJSON(resp) + + return nil +} + +var ( + assetIDFlag = cli.StringFlag{ + Name: "asset_id", + Usage: "the asset ID of the asset to use when sending " + + "payments with assets", + } + + assetAmountFlag = cli.Uint64Flag{ + Name: "asset_amount", + Usage: "the amount of the asset to send in the asset keysend " + + "payment", + } + + rfqPeerPubKeyFlag = cli.StringFlag{ + Name: "rfq_peer_pubkey", + Usage: "(optional) the public key of the peer to ask for a " + + "quote when converting from assets to sats; must be " + + "set if there are multiple channels with the same " + + "asset ID present", + } +) + +// resultStreamWrapper is a wrapper around the SendPaymentClient stream that +// implements the generic PaymentResultStream interface. +type resultStreamWrapper struct { + amountMsat int64 + stream tchrpc.TaprootAssetChannels_SendPaymentClient +} + +// Recv receives the next payment result from the stream. +// +// NOTE: This method is part of the PaymentResultStream interface. +func (w *resultStreamWrapper) Recv() (*lnrpc.Payment, error) { + resp, err := w.stream.Recv() + if err != nil { + return nil, err + } + + res := resp.Result + switch r := res.(type) { + // The very first response might be an accepted sell order, which we + // just print out. + case *tchrpc.SendPaymentResponse_AcceptedSellOrder: + quote := r.AcceptedSellOrder + rpcRate := quote.BidAssetRate + rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal fixed "+ + "point: %w", err) + } + + amountMsat := lnwire.MilliSatoshi(w.amountMsat) + milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate) + numUnits := milliSatsFP.ScaleTo(0).ToUint64() + + // If the calculated number of units is 0 then the asset rate + // was not sufficient to represent the value of this payment. + if numUnits == 0 { + // We will calculate the minimum amount that can be + // effectively sent with this asset by calculating the + // value of a single asset unit, based on the provided + // asset rate. + + // We create the single unit. + unit := rfqmath.FixedPointFromUint64[rfqmath.BigInt]( + 1, 0, + ) + + // We derive the minimum amount. + minAmt := rfqmath.UnitsToMilliSatoshi(unit, *rate) + + // We return the error to the user. + return nil, fmt.Errorf("smallest payment with asset "+ + "rate %v is %v, cannot send %v", + rate.ToUint64(), minAmt, amountMsat) + } + + msatPerUnit := uint64(w.amountMsat) / numUnits + + fmt.Printf("Got quote for %v asset units at %v msat/unit from "+ + "peer %s with SCID %d\n", numUnits, msatPerUnit, + quote.Peer, quote.Scid) + + resp, err = w.stream.Recv() + if err != nil { + return nil, err + } + + if resp.Result == nil { + return nil, errors.New("unexpected nil result") + } + + return resp.GetPaymentResult(), nil + + case *tchrpc.SendPaymentResponse_PaymentResult: + return r.PaymentResult, nil + + default: + return nil, fmt.Errorf("unexpected response type: %T", r) + } +} + +var sendPaymentCommand = cli.Command{ + Name: "sendpayment", + Category: commands.SendPaymentCommand.Category, + Usage: "Send a payment over Lightning, potentially using a " + + "mulit-asset channel as the first hop", + Description: commands.SendPaymentCommand.Description + ` + To send an multi-asset LN payment to a single hop, the --asset_id=X + argument should be used. + + Note that this will only work in concert with the --keysend argument. + `, + ArgsUsage: commands.SendPaymentCommand.ArgsUsage + " --asset_id=X " + + "--asset_amount=Y [--rfq_peer_pubkey=Z]", + Flags: append( + commands.SendPaymentCommand.Flags, assetIDFlag, assetAmountFlag, + rfqPeerPubKeyFlag, + ), + Action: sendPayment, +} + +func sendPayment(ctx *cli.Context) error { + // Show command help if no arguments provided + if ctx.NArg() == 0 && ctx.NumFlags() == 0 { + _ = cli.ShowCommandHelp(ctx, "sendpayment") + return nil + } + + lndConn, cleanup, err := connectClient(ctx, false) + if err != nil { + return fmt.Errorf("unable to make rpc conn: %w", err) + } + defer cleanup() + + tapdConn, cleanup, err := connectSuperMacClient(ctx) + if err != nil { + return fmt.Errorf("error creating tapd connection: %w", err) + } + defer cleanup() + + switch { + case !ctx.IsSet(assetIDFlag.Name): + return fmt.Errorf("the --asset_id flag must be set") + case !ctx.IsSet("keysend"): + return fmt.Errorf("the --keysend flag must be set") + case !ctx.IsSet(assetAmountFlag.Name): + return fmt.Errorf("--asset_amount must be set") + } + + assetIDStr := ctx.String(assetIDFlag.Name) + assetIDBytes, err := hex.DecodeString(assetIDStr) + if err != nil { + return fmt.Errorf("unable to decode assetID: %v", err) + } + + assetAmountToSend := ctx.Uint64(assetAmountFlag.Name) + if assetAmountToSend == 0 { + return fmt.Errorf("must specify asset amount to send") + } + + // With the asset specific work out of the way, we'll parse the rest of + // the command as normal. + var ( + destNode []byte + rHash []byte + ) + + switch { + case ctx.IsSet("dest"): + destNode, err = hex.DecodeString(ctx.String("dest")) + default: + return fmt.Errorf("destination txid argument missing") + } + if err != nil { + return err + } + + if len(destNode) != 33 { + return fmt.Errorf("dest node pubkey must be exactly 33 bytes, "+ + "is instead: %v", len(destNode)) + } + + rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name)) + if err != nil { + return fmt.Errorf("unable to decode RFQ peer public key: "+ + "%w", err) + } + + // We use a constant amount of 500 to carry the asset HTLCs. In the + // future, we can use the double HTLC trick here, though it consumes + // more commitment space. + const htlcCarrierAmt = 500 + req := &routerrpc.SendPaymentRequest{ + Dest: destNode, + Amt: htlcCarrierAmt, + DestCustomRecords: make(map[uint64][]byte), + } + + if ctx.IsSet("payment_hash") { + return errors.New("cannot set payment hash when using " + + "keysend") + } + + // Read out the custom preimage for the keysend payment. + var preimage lntypes.Preimage + if _, err := rand.Read(preimage[:]); err != nil { + return err + } + + // Set the preimage. If the user supplied a preimage with the data + // flag, the preimage that is set here will be overwritten later. + req.DestCustomRecords[record.KeySendType] = preimage[:] + + hash := preimage.Hash() + rHash = hash[:] + + req.PaymentHash = rHash + + return commands.SendPaymentRequest( + ctx, req, lndConn, tapdConn, func(ctx context.Context, + payConn grpc.ClientConnInterface, + req *routerrpc.SendPaymentRequest) ( + commands.PaymentResultStream, error) { + + tchrpcClient := tchrpc.NewTaprootAssetChannelsClient( + payConn, + ) + + stream, err := tchrpcClient.SendPayment( + ctx, &tchrpc.SendPaymentRequest{ + AssetId: assetIDBytes, + AssetAmount: assetAmountToSend, + PeerPubkey: rfqPeerKey, + PaymentRequest: req, + }, + ) + if err != nil { + return nil, err + } + + return &resultStreamWrapper{ + stream: stream, + }, nil + }, + ) +} + +var payInvoiceCommand = cli.Command{ + Name: "payinvoice", + Category: "Payments", + Usage: "Pay an invoice over lightning using an asset.", + Description: ` + This command attempts to pay an invoice using an asset channel as the + source of the payment. The asset ID of the channel must be specified + using the --asset_id flag. + `, + ArgsUsage: "pay_req --asset_id=X", + Flags: append(commands.PaymentFlags(), + cli.Int64Flag{ + Name: "amt", + Usage: "(optional) number of satoshis to fulfill the " + + "invoice", + }, + assetIDFlag, + rfqPeerPubKeyFlag, + ), + Action: payInvoice, +} + +func payInvoice(ctx *cli.Context) error { + args := ctx.Args() + ctxb := context.Background() + + var payReq string + switch { + case ctx.IsSet("pay_req"): + payReq = ctx.String("pay_req") + case args.Present(): + payReq = args.First() + default: + return fmt.Errorf("pay_req argument missing") + } + + superMacConn, cleanup, err := connectSuperMacClient(ctx) + if err != nil { + return fmt.Errorf("unable to make rpc con: %w", err) + } + + defer cleanup() + + lndClient := lnrpc.NewLightningClient(superMacConn) + + decodeReq := &lnrpc.PayReqString{PayReq: payReq} + decodeResp, err := lndClient.DecodePayReq(ctxb, decodeReq) + if err != nil { + return err + } + + if !ctx.IsSet(assetIDFlag.Name) { + return fmt.Errorf("the --asset_id flag must be set") + } + + assetIDStr := ctx.String(assetIDFlag.Name) + + assetIDBytes, err := hex.DecodeString(assetIDStr) + if err != nil { + return fmt.Errorf("unable to decode assetID: %v", err) + } + + var assetID asset.ID + copy(assetID[:], assetIDBytes) + + rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name)) + if err != nil { + return fmt.Errorf("unable to decode RFQ peer public key: "+ + "%w", err) + } + + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: commands.StripPrefix(payReq), + } + + return commands.SendPaymentRequest( + ctx, req, superMacConn, superMacConn, func(ctx context.Context, + payConn grpc.ClientConnInterface, + req *routerrpc.SendPaymentRequest) ( + commands.PaymentResultStream, error) { + + tchrpcClient := tchrpc.NewTaprootAssetChannelsClient( + payConn, + ) + + stream, err := tchrpcClient.SendPayment( + ctx, &tchrpc.SendPaymentRequest{ + AssetId: assetIDBytes, + PeerPubkey: rfqPeerKey, + PaymentRequest: req, + }, + ) + if err != nil { + return nil, err + } + + return &resultStreamWrapper{ + amountMsat: decodeResp.NumMsat, + stream: stream, + }, nil + }, + ) +} + +var addInvoiceCommand = cli.Command{ + Name: "addinvoice", + Category: commands.AddInvoiceCommand.Category, + Usage: "Add a new invoice to receive Taproot Assets.", + Description: ` + Add a new invoice, expressing intent for a future payment, received in + Taproot Assets. + `, + ArgsUsage: "asset_id asset_amount", + Flags: append( + commands.AddInvoiceCommand.Flags, + cli.StringFlag{ + Name: "asset_id", + Usage: "the asset ID of the asset to receive", + }, + cli.Uint64Flag{ + Name: "asset_amount", + Usage: "the amount of assets to receive", + }, + cli.StringFlag{ + Name: "rfq_peer_pubkey", + Usage: "(optional) the public key of the peer to ask " + + "for a quote when converting from assets to " + + "sats for the invoice; must be set if there " + + "are multiple channels with the same " + + "asset ID present", + }, + ), + Action: addInvoice, +} + +func addInvoice(ctx *cli.Context) error { + args := ctx.Args() + ctxb := context.Background() + + var assetIDStr string + switch { + case ctx.IsSet("asset_id"): + assetIDStr = ctx.String("asset_id") + case args.Present(): + assetIDStr = args.First() + args = args.Tail() + default: + return fmt.Errorf("asset_id argument missing") + } + + var ( + assetAmount uint64 + preimage []byte + descHash []byte + err error + ) + switch { + case ctx.IsSet("asset_amount"): + assetAmount = ctx.Uint64("asset_amount") + case args.Present(): + assetAmount, err = strconv.ParseUint(args.First(), 10, 64) + if err != nil { + return fmt.Errorf("unable to parse asset amount %w", + err) + } + default: + return fmt.Errorf("asset_amount argument missing") + } + + if ctx.IsSet("preimage") { + preimage, err = hex.DecodeString(ctx.String("preimage")) + if err != nil { + return fmt.Errorf("unable to parse preimage: %w", err) + } + } + + descHash, err = hex.DecodeString(ctx.String("description_hash")) + if err != nil { + return fmt.Errorf("unable to parse description_hash: %w", err) + } + + expirySeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + if ctx.IsSet("expiry") { + expirySeconds = ctx.Int64("expiry") + } + + assetIDBytes, err := hex.DecodeString(assetIDStr) + if err != nil { + return fmt.Errorf("unable to decode assetID: %v", err) + } + + var assetID asset.ID + copy(assetID[:], assetIDBytes) + + rfqPeerKey, err := hex.DecodeString(ctx.String(rfqPeerPubKeyFlag.Name)) + if err != nil { + return fmt.Errorf("unable to decode RFQ peer public key: "+ + "%w", err) + } + + tapdConn, cleanup, err := connectSuperMacClient(ctx) + if err != nil { + return fmt.Errorf("error creating tapd connection: %w", err) + } + defer cleanup() + + channelsClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn) + resp, err := channelsClient.AddInvoice(ctxb, &tchrpc.AddInvoiceRequest{ + AssetId: assetIDBytes, + AssetAmount: assetAmount, + PeerPubkey: rfqPeerKey, + InvoiceRequest: &lnrpc.Invoice{ + Memo: ctx.String("memo"), + RPreimage: preimage, + DescriptionHash: descHash, + FallbackAddr: ctx.String("fallback_addr"), + Expiry: expirySeconds, + Private: ctx.Bool("private"), + IsAmp: ctx.Bool("amp"), + }, + }) + if err != nil { + return fmt.Errorf("error adding invoice: %w", err) + } + + printRespJSON(resp) + + return nil +} diff --git a/cmd/litcli/main.go b/cmd/litcli/main.go index f8dc55747..934e2020e 100644 --- a/cmd/litcli/main.go +++ b/cmd/litcli/main.go @@ -1,14 +1,17 @@ package main import ( + "context" + "encoding/hex" "fmt" - "io/ioutil" "os" "path/filepath" "strings" terminal "github.com/lightninglabs/lightning-terminal" + "github.com/lightninglabs/lightning-terminal/litrpc" "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/cmd/commands" "github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" @@ -69,6 +72,23 @@ func main() { baseDirFlag, tlsCertFlag, macaroonPathFlag, + // The following two flags are only required for the 'litcli ln' + // sub commands, because they call into lnd's commands package + // that requires them. They only need to be _defined_, but + // they're not actually actively used. + cli.StringFlag{ + Name: "lnddir", + Usage: "Path to lnd's base directory", + Hidden: true, + Value: commands.DefaultLndDir, + }, + cli.Int64Flag{ + Name: "macaroontimeout", + Value: 60, + Hidden: true, + Usage: "Anti-replay macaroon validity time in " + + "seconds.", + }, } app.Commands = append(app.Commands, sessionCommands...) app.Commands = append(app.Commands, accountsCommands...) @@ -78,6 +98,7 @@ func main() { app.Commands = append(app.Commands, litCommands...) app.Commands = append(app.Commands, helperCommands) app.Commands = append(app.Commands, statusCommands...) + app.Commands = append(app.Commands, lnCommands...) err := app.Run(os.Args) if err != nil { @@ -98,7 +119,7 @@ func connectClient(ctx *cli.Context, noMac bool) (grpc.ClientConnInterface, if err != nil { return nil, nil, err } - conn, err := getClientConn(rpcServer, tlsCertPath, macPath, noMac) + conn, err := getClientConn(rpcServer, tlsCertPath, macPath, noMac, nil) if err != nil { return nil, nil, err } @@ -107,14 +128,42 @@ func connectClient(ctx *cli.Context, noMac bool) (grpc.ClientConnInterface, return conn, cleanup, nil } -func getClientConn(address, tlsCertPath, macaroonPath string, noMac bool) ( - *grpc.ClientConn, error) { +func connectClientWithMac(ctx *cli.Context, + mac []byte) (grpc.ClientConnInterface, func(), error) { + + rpcServer := ctx.GlobalString("rpcserver") + tlsCertPath, macPath, err := extractPathArgs(ctx) + if err != nil { + return nil, nil, err + } + conn, err := getClientConn(rpcServer, tlsCertPath, macPath, false, mac) + if err != nil { + return nil, nil, err + } + cleanup := func() { _ = conn.Close() } + + return conn, cleanup, nil +} + +func getClientConn(address, tlsCertPath, macaroonPath string, noMac bool, + customMac []byte) (*grpc.ClientConn, error) { opts := []grpc.DialOption{ grpc.WithDefaultCallOptions(maxMsgRecvSize), } - if !noMac { + switch { + case len(customMac) > 0: + macOption, err := macFromBytes(customMac) + if err != nil { + return nil, err + } + opts = append(opts, macOption) + + case noMac: + // Don't do anything. + + default: macOption, err := readMacaroon(macaroonPath) if err != nil { return nil, err @@ -196,13 +245,18 @@ func extractPathArgs(ctx *cli.Context) (string, string, error) { // gRPC dial options from it. func readMacaroon(macPath string) (grpc.DialOption, error) { // Load the specified macaroon file. - macBytes, err := ioutil.ReadFile(macPath) + macBytes, err := os.ReadFile(macPath) if err != nil { return nil, fmt.Errorf("unable to read macaroon path : %v", err) } + return macFromBytes(macBytes) +} + +// macFromBytes returns a macaroon from the given byte slice. +func macFromBytes(macBytes []byte) (grpc.DialOption, error) { mac := &macaroon.Macaroon{} - if err = mac.UnmarshalBinary(macBytes); err != nil { + if err := mac.UnmarshalBinary(macBytes); err != nil { return nil, fmt.Errorf("unable to decode macaroon: %v", err) } @@ -244,3 +298,29 @@ func printRespJSON(resp proto.Message) { // nolint fmt.Println(string(jsonBytes)) } + +func connectSuperMacClient(ctx *cli.Context) (grpc.ClientConnInterface, + func(), error) { + + litdConn, cleanup, err := connectClient(ctx, false) + if err != nil { + return nil, nil, fmt.Errorf("error connecting client: %w", err) + } + defer cleanup() + + ctxb := context.Background() + litClient := litrpc.NewProxyClient(litdConn) + macResp, err := litClient.BakeSuperMacaroon( + ctxb, &litrpc.BakeSuperMacaroonRequest{}, + ) + if err != nil { + return nil, nil, fmt.Errorf("error baking macaroon: %w", err) + } + + macBytes, err := hex.DecodeString(macResp.Macaroon) + if err != nil { + return nil, nil, fmt.Errorf("error decoding macaroon: %w", err) + } + + return connectClientWithMac(ctx, macBytes) +} From d1c7b207a92a51e0c050e337f3bb41d154cb2d0b Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Fri, 15 Nov 2024 17:39:54 -0800 Subject: [PATCH 06/12] itest: log pid of new nodes --- itest/litd_node.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/itest/litd_node.go b/itest/litd_node.go index 618ae17e1..7ec9a0296 100644 --- a/itest/litd_node.go +++ b/itest/litd_node.go @@ -653,6 +653,9 @@ func (hn *HarnessNode) Start(litdBinary string, litdError chan<- error, return err } + fmt.Printf("Starting node=%v, pid=%v\n", hn.Cfg.Name, + hn.cmd.Process.Pid) + // Launch a new goroutine which that bubbles up any potential fatal // process errors to the goroutine running the tests. hn.processExit = make(chan struct{}) From da30b3d4b9a9a932a3bbd201e3dbd4b6e51ecc8c Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Thu, 23 May 2024 13:56:41 +0200 Subject: [PATCH 07/12] itest: add custom channel integration test Co-authored-by: Olaoluwa Osuntokun Co-authored-by: Gijs van Dam Co-authored-by: George Tsagkarelis --- go.mod | 4 +- itest/assertions.go | 46 +- itest/assets_test.go | 2064 ++++++++++++++++ itest/litd_accounts_test.go | 42 + itest/litd_custom_channels_test.go | 3655 ++++++++++++++++++++++++++++ itest/litd_node.go | 38 + itest/litd_test.go | 8 +- itest/litd_test_list_on_test.go | 48 + itest/log.go | 24 + itest/network_harness.go | 103 +- itest/oracle_test.go | 279 +++ 11 files changed, 6297 insertions(+), 14 deletions(-) create mode 100644 itest/assets_test.go create mode 100644 itest/litd_custom_channels_test.go create mode 100644 itest/log.go create mode 100644 itest/oracle_test.go diff --git a/go.mod b/go.mod index 188bb9b48..bd1dc1707 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c github.com/btcsuite/btcwallet/walletdb v1.4.4 + github.com/davecgh/go-spew v1.1.1 github.com/go-errors/errors v1.0.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/improbable-eng/grpc-web v0.12.0 @@ -35,6 +36,7 @@ require ( github.com/urfave/cli v1.22.9 go.etcd.io/bbolt v1.3.11 golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 golang.org/x/net v0.27.0 golang.org/x/sync v0.8.0 google.golang.org/grpc v1.65.0 @@ -73,7 +75,6 @@ require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/decred/dcrd/lru v1.1.2 // indirect @@ -201,7 +202,6 @@ require ( go.uber.org/mock v0.4.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.23.0 // indirect - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect diff --git a/itest/assertions.go b/itest/assertions.go index 51a6feaf8..f6518fea7 100644 --- a/itest/assertions.go +++ b/itest/assertions.go @@ -3,14 +3,18 @@ package itest import ( "context" "fmt" + "testing" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest/wait" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) // shutdownAndAssert shuts down the given node and asserts that no errors @@ -172,24 +176,25 @@ func assertChannelClosed(ctx context.Context, t *harnessTest, // block. block := mineBlocks(t, net, 1, 1)[0] - closingTxid, err := net.WaitForChannelClose(closeUpdates) + closingUpdate, err := net.WaitForChannelClose(closeUpdates) require.NoError(t.t, err, "error while waiting for channel close") + closingTxid, err := chainhash.NewHash(closingUpdate.ClosingTxid) + require.NoError(t.t, err) assertTxInBlock(t, block, closingTxid) // Finally, the transaction should no longer be in the waiting close // state as we've just mined a block that should include the closing // transaction. err = wait.Predicate(func() bool { - pendingChansRequest := &lnrpc.PendingChannelsRequest{} - pendingChanResp, err := node.PendingChannels( - ctx, pendingChansRequest, + resp, err := node.PendingChannels( + ctx, &lnrpc.PendingChannelsRequest{}, ) if err != nil { return false } - for _, pendingClose := range pendingChanResp.WaitingCloseChannels { + for _, pendingClose := range resp.WaitingCloseChannels { if pendingClose.Channel.ChannelPoint == chanPointStr { return false } @@ -203,3 +208,34 @@ func assertChannelClosed(ctx context.Context, t *harnessTest, return closingTxid } + +func assertSweepExists(t *testing.T, node *HarnessNode, + witnessType walletrpc.WitnessType) { + + ctxb := context.Background() + err := wait.NoError(func() error { + pendingSweeps, err := node.WalletKitClient.PendingSweeps( + ctxb, &walletrpc.PendingSweepsRequest{}, + ) + if err != nil { + return err + } + + for _, sweep := range pendingSweeps.PendingSweeps { + if sweep.WitnessType == witnessType { + return nil + } + } + + return fmt.Errorf("failed to find second level sweep: %v", + toProtoJSON(t, pendingSweeps)) + }, defaultTimeout) + require.NoError(t, err) +} + +func toProtoJSON(t *testing.T, resp proto.Message) string { + jsonBytes, err := taprpc.ProtoJSONMarshalOpts.Marshal(resp) + require.NoError(t, err) + + return string(jsonBytes) +} diff --git a/itest/assets_test.go b/itest/assets_test.go new file mode 100644 index 000000000..dd66c747f --- /dev/null +++ b/itest/assets_test.go @@ -0,0 +1,2064 @@ +package itest + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/davecgh/go-spew/spew" + tapfn "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/itest" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/tapfreighter" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" + "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightninglabs/taproot-assets/tapscript" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest/rpc" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/macaroons" + "github.com/lightningnetwork/lnd/record" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "gopkg.in/macaroon.v2" +) + +// PaymentTimeout is the default payment timeout we use in our tests. +const ( + PaymentTimeout = 12 * time.Second + DefaultPushSat int64 = 1062 +) + +// nolint: lll +var ( + failureNoBalance = lnrpc.PaymentFailureReason_FAILURE_REASON_INSUFFICIENT_BALANCE + failureNoRoute = lnrpc.PaymentFailureReason_FAILURE_REASON_NO_ROUTE + failureIncorrectDetails = lnrpc.PaymentFailureReason_FAILURE_REASON_INCORRECT_PAYMENT_DETAILS + failureTimeout = lnrpc.PaymentFailureReason_FAILURE_REASON_TIMEOUT + failureNone = lnrpc.PaymentFailureReason_FAILURE_REASON_NONE +) + +// createTestAssetNetwork sends asset funds from Charlie to Dave and Erin, so +// they can fund asset channels with Yara and Fabia, respectively. So the asset +// channels created are Charlie->Dave, Dave->Yara, Erin->Fabia. The channels +// are then confirmed and balances asserted. +func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap, + daveTap, erinTap, fabiaTap, yaraTap, universeTap *tapClient, + mintedAsset *taprpc.Asset, assetSendAmount, charlieFundingAmount, + daveFundingAmount, + erinFundingAmount uint64, pushSat int64) (*lnrpc.ChannelPoint, + *lnrpc.ChannelPoint, *lnrpc.ChannelPoint) { + + ctxb := context.Background() + assetID := mintedAsset.AssetGenesis.AssetId + var groupKey []byte + if mintedAsset.AssetGroup != nil { + groupKey = mintedAsset.AssetGroup.TweakedGroupKey + } + + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + // We need to send some assets to Dave, so he can fund an asset channel + // with Yara. + daveAddr, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Dave...", assetSendAmount) + + // Send the assets to Dave. + itest.AssertAddrCreated(t.t, daveTap, mintedAsset, daveAddr) + sendResp, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{mintedAsset.Amount - assetSendAmount, assetSendAmount}, + 0, 1, + ) + itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 1) + + // We need to send some assets to Erin, so he can fund an asset channel + // with Fabia. + erinAddr, err := erinTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Erin...", assetSendAmount) + + // Send the assets to Erin. + itest.AssertAddrCreated(t.t, erinTap, mintedAsset, erinAddr) + sendResp, err = charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{erinAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{ + mintedAsset.Amount - 2*assetSendAmount, assetSendAmount, + }, 1, 2, + ) + itest.AssertNonInteractiveRecvComplete(t.t, erinTap, 1) + + t.Logf("Opening asset channels...") + + // The first channel we create has a push amount, so Charlie can receive + // payments immediately and not run into the channel reserve issue. + fundRespCD, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: charlieFundingAmount, + AssetId: assetID, + PeerPubkey: daveTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD) + + fundRespDY, err := daveTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: daveFundingAmount, + AssetId: assetID, + PeerPubkey: yaraTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Dave and Yara: %v", fundRespDY) + + fundRespEF, err := erinTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: erinFundingAmount, + AssetId: assetID, + PeerPubkey: fabiaTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: pushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Erin and Fabia: %v", fundRespEF) + + // Make sure the pending channel shows up in the list and has the + // custom records set as JSON. + assertPendingChannels( + t.t, charlieTap.node, mintedAsset, 1, charlieFundingAmount, 0, + ) + assertPendingChannels( + t.t, daveTap.node, mintedAsset, 2, daveFundingAmount, + charlieFundingAmount, + ) + assertPendingChannels( + t.t, erinTap.node, mintedAsset, 1, erinFundingAmount, 0, + ) + + // Now that we've looked at the pending channels, let's actually confirm + // all three of them. + mineBlocks(t, net, 6, 3) + + // We'll be tracking the expected asset balances throughout the test, so + // we can assert it after each action. + charlieAssetBalance := mintedAsset.Amount - 2*assetSendAmount - + charlieFundingAmount + daveAssetBalance := assetSendAmount - daveFundingAmount + erinAssetBalance := assetSendAmount - erinFundingAmount + + // After opening the channels, the asset balance of the funding nodes + // should have been decreased with the funding amount. The asset with + // the funding output was imported into the asset DB but are kept out of + // the balance reporting by tapd. + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + + // There should only be a single asset piece for Charlie, the one in the + // channel. + assertNumAssetOutputs(t.t, charlieTap, assetID, 1) + assertAssetExists( + t.t, charlieTap, assetID, charlieFundingAmount, + fundingScriptKey, false, true, true, + ) + + // Dave should just have one asset piece, since we used the full amount + // for the channel opening. + assertNumAssetOutputs(t.t, daveTap, assetID, 1) + assertAssetExists( + t.t, daveTap, assetID, daveFundingAmount, fundingScriptKey, + false, true, true, + ) + + // Erin should just have two equally sized asset pieces, the change and + // the funding transaction. + assertNumAssetOutputs(t.t, erinTap, assetID, 2) + assertAssetExists( + t.t, erinTap, assetID, assetSendAmount-erinFundingAmount, nil, + true, false, false, + ) + assertAssetExists( + t.t, erinTap, assetID, erinFundingAmount, fundingScriptKey, + false, true, true, + ) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex), + ) + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespDY.Txid, fundRespDY.OutputIndex), + ) + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespEF.Txid, fundRespEF.OutputIndex), + ) + + // Make sure the channel shows the correct asset information. + assertAssetChan( + t.t, charlieTap.node, daveTap.node, charlieFundingAmount, + mintedAsset, + ) + assertAssetChan( + t.t, daveTap.node, yaraTap.node, daveFundingAmount, mintedAsset, + ) + assertAssetChan( + t.t, erinTap.node, fabiaTap.node, erinFundingAmount, + mintedAsset, + ) + + chanPointCD := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + chanPointDY := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespDY.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespDY.Txid, + }, + } + chanPointEF := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespEF.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespEF.Txid, + }, + } + + return chanPointCD, chanPointDY, chanPointEF +} + +func assertNumAssetUTXOs(t *testing.T, tapdClient *tapClient, + numUTXOs int) *taprpc.ListUtxosResponse { + + ctxb := context.Background() + + var clientUTXOs *taprpc.ListUtxosResponse + err := wait.NoError(func() error { + var err error + clientUTXOs, err = tapdClient.ListUtxos( + ctxb, &taprpc.ListUtxosRequest{}, + ) + if err != nil { + return err + } + + if len(clientUTXOs.ManagedUtxos) != numUTXOs { + return fmt.Errorf("expected %v UTXO, got %d", numUTXOs, + len(clientUTXOs.ManagedUtxos)) + } + + return nil + }, defaultTimeout) + require.NoErrorf(t, err, "failed to assert UTXOs: %v, last state: %v", + err, clientUTXOs) + + return clientUTXOs +} + +func locateAssetTransfers(t *testing.T, tapdClient *tapClient, + txid chainhash.Hash) *taprpc.AssetTransfer { + + var transfer *taprpc.AssetTransfer + err := wait.NoError(func() error { + ctxb := context.Background() + forceCloseTransfer, err := tapdClient.ListTransfers( + ctxb, &taprpc.ListTransfersRequest{ + AnchorTxid: txid.String(), + }, + ) + if err != nil { + return fmt.Errorf("unable to list %v transfers: %w", + tapdClient.node.Name(), err) + } + if len(forceCloseTransfer.Transfers) != 1 { + return fmt.Errorf("%v is missing force close "+ + "transfer", tapdClient.node.Name()) + } + + transfer = forceCloseTransfer.Transfers[0] + + if transfer.AnchorTxBlockHash == nil { + return fmt.Errorf("missing anchor block hash, " + + "transfer not confirmed") + } + + return nil + }, defaultTimeout) + require.NoError(t, err) + + return transfer +} + +func connectAllNodes(t *testing.T, net *NetworkHarness, nodes []*HarnessNode) { + for i, node := range nodes { + for j := i + 1; j < len(nodes); j++ { + peer := nodes[j] + net.ConnectNodesPerm(t, node, peer) + } + } +} + +func fundAllNodes(t *testing.T, net *NetworkHarness, nodes []*HarnessNode) { + for _, node := range nodes { + net.SendCoins(t, btcutil.SatoshiPerBitcoin, node) + } +} + +func syncUniverses(t *testing.T, universe *tapClient, nodes ...*HarnessNode) { + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + for _, node := range nodes { + nodeTapClient := newTapClient(t, node) + + universeHostAddr := universe.node.Cfg.LitAddr() + t.Logf("Syncing node %v with universe %v", node.Cfg.Name, + universeHostAddr) + + itest.SyncUniverses( + ctxt, t, nodeTapClient, universe, universeHostAddr, + defaultTimeout, + ) + } +} + +func assertUniverseProofExists(t *testing.T, universe *tapClient, + assetID, groupKey, scriptKey []byte, outpoint string) *taprpc.Asset { + + t.Logf("Asserting proof outpoint=%v, script_key=%x", outpoint, + scriptKey) + + req := &universerpc.UniverseKey{ + Id: &universerpc.ID{ + ProofType: universerpc.ProofType_PROOF_TYPE_TRANSFER, + }, + LeafKey: &universerpc.AssetKey{ + Outpoint: &universerpc.AssetKey_OpStr{ + OpStr: outpoint, + }, + ScriptKey: &universerpc.AssetKey_ScriptKeyBytes{ + ScriptKeyBytes: scriptKey, + }, + }, + } + + switch { + case len(groupKey) > 0: + req.Id.Id = &universerpc.ID_GroupKey{ + GroupKey: groupKey, + } + + case len(assetID) > 0: + req.Id.Id = &universerpc.ID_AssetId{ + AssetId: assetID, + } + + default: + t.Fatalf("Need either asset ID or group key") + } + + ctxb := context.Background() + var proofResp *universerpc.AssetProofResponse + err := wait.NoError(func() error { + var pErr error + proofResp, pErr = universe.QueryProof(ctxb, req) + return pErr + }, defaultTimeout) + require.NoError( + t, err, "%v: outpoint=%v, script_key=%x", err, outpoint, + scriptKey, + ) + + if len(groupKey) > 0 { + require.NotNil(t, proofResp.AssetLeaf.Asset.AssetGroup) + require.Equal( + t, proofResp.AssetLeaf.Asset.AssetGroup.TweakedGroupKey, + groupKey, + ) + } else { + require.Equal( + t, proofResp.AssetLeaf.Asset.AssetGenesis.AssetId, + assetID, + ) + } + + a := proofResp.AssetLeaf.Asset + t.Logf("Proof found for scriptKey=%x, amount=%d", a.ScriptKey, a.Amount) + + return a +} + +func assertPendingChannels(t *testing.T, node *HarnessNode, + mintedAsset *taprpc.Asset, numChannels int, localSum, + remoteSum uint64) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + pendingChannelsResp, err := node.PendingChannels( + ctxt, &lnrpc.PendingChannelsRequest{}, + ) + require.NoError(t, err) + require.Len(t, pendingChannelsResp.PendingOpenChannels, numChannels) + + pendingChan := pendingChannelsResp.PendingOpenChannels[0] + var pendingJSON rfqmsg.JsonAssetChannel + err = json.Unmarshal( + pendingChan.Channel.CustomChannelData, &pendingJSON, + ) + require.NoError(t, err) + require.Len(t, pendingJSON.Assets, 1) + + require.NotZero(t, pendingJSON.Assets[0].Capacity) + + // Check the decimal display of the channel funding blob. If no explicit + // value was set, we assume and expect the value of 0. + var expectedDecimalDisplay uint8 + if mintedAsset.DecimalDisplay != nil { + expectedDecimalDisplay = uint8( + mintedAsset.DecimalDisplay.DecimalDisplay, + ) + } + + require.Equal( + t, expectedDecimalDisplay, + pendingJSON.Assets[0].AssetInfo.DecimalDisplay, + ) + + // Check the balance of the pending channel. + assetID := mintedAsset.AssetGenesis.AssetId + pendingLocalBalance, pendingRemoteBalance, _, _ := + getAssetChannelBalance( + t, node, assetID, true, + ) + require.EqualValues(t, localSum, pendingLocalBalance) + require.EqualValues(t, remoteSum, pendingRemoteBalance) +} + +func assertAssetChan(t *testing.T, src, dst *HarnessNode, fundingAmount uint64, + mintedAsset *taprpc.Asset) { + + assetID := mintedAsset.AssetGenesis.AssetId + assetIDStr := hex.EncodeToString(assetID) + err := wait.NoError(func() error { + a, err := getChannelCustomData(src, dst) + if err != nil { + return err + } + + if a.AssetInfo.AssetGenesis.AssetID != assetIDStr { + return fmt.Errorf("expected asset ID %s, got %s", + assetIDStr, a.AssetInfo.AssetGenesis.AssetID) + } + if a.Capacity != fundingAmount { + return fmt.Errorf("expected capacity %d, got %d", + fundingAmount, a.Capacity) + } + + // Check the decimal display of the channel funding blob. If no + // explicit value was set, we assume and expect the value of 0. + var expectedDecimalDisplay uint8 + if mintedAsset.DecimalDisplay != nil { + expectedDecimalDisplay = uint8( + mintedAsset.DecimalDisplay.DecimalDisplay, + ) + } + + if a.AssetInfo.DecimalDisplay != expectedDecimalDisplay { + return fmt.Errorf("expected decimal display %d, got %d", + expectedDecimalDisplay, + a.AssetInfo.DecimalDisplay) + } + + return nil + }, defaultTimeout) + require.NoError(t, err) +} + +func assertChannelKnown(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + txid, err := chainhash.NewHash(chanPoint.GetFundingTxidBytes()) + require.NoError(t, err) + targetChanPoint := fmt.Sprintf( + "%v:%d", txid.String(), chanPoint.OutputIndex, + ) + + err = wait.NoError(func() error { + graphResp, err := node.DescribeGraph( + ctxt, &lnrpc.ChannelGraphRequest{}, + ) + if err != nil { + return err + } + + found := false + for _, edge := range graphResp.Edges { + if edge.ChanPoint == targetChanPoint { + found = true + break + } + } + + if !found { + return fmt.Errorf("channel %v not found", + targetChanPoint) + } + + return nil + }, defaultTimeout) + require.NoError(t, err) +} + +func getChannelCustomData(src, dst *HarnessNode) (*rfqmsg.JsonAssetChanInfo, + error) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + srcDestChannels, err := src.ListChannels( + ctxt, &lnrpc.ListChannelsRequest{ + Peer: dst.PubKey[:], + }, + ) + if err != nil { + return nil, err + } + + assetChannels := fn.Filter(func(c *lnrpc.Channel) bool { + return len(c.CustomChannelData) > 0 + }, srcDestChannels.Channels) + + if len(assetChannels) != 1 { + return nil, fmt.Errorf("expected 1 asset channel, got %d: %v", + len(assetChannels), spew.Sdump(assetChannels)) + } + + targetChan := assetChannels[0] + + var assetData rfqmsg.JsonAssetChannel + err = json.Unmarshal(targetChan.CustomChannelData, &assetData) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal asset data: %w", + err) + } + + if len(assetData.Assets) != 1 { + return nil, fmt.Errorf("expected 1 asset, got %d", + len(assetData.Assets)) + } + + return &assetData.Assets[0], nil +} + +func getAssetChannelBalance(t *testing.T, node *HarnessNode, assetID []byte, + pending bool) (uint64, uint64, uint64, uint64) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + balance, err := node.ChannelBalance( + ctxt, &lnrpc.ChannelBalanceRequest{}, + ) + require.NoError(t, err) + + var assetBalance rfqmsg.JsonAssetChannelBalances + err = json.Unmarshal(balance.CustomChannelData, &assetBalance) + require.NoError(t, err) + + balances := assetBalance.OpenChannels + if pending { + balances = assetBalance.PendingChannels + } + + var localSum, remoteSum uint64 + for assetIDString := range balances { + if assetIDString != hex.EncodeToString(assetID) { + continue + } + + localSum += balances[assetIDString].LocalBalance + remoteSum += balances[assetIDString].RemoteBalance + } + + return localSum, remoteSum, balance.LocalBalance.Sat, + balance.RemoteBalance.Sat +} + +func fetchChannel(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint) *lnrpc.Channel { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + channelResp, err := node.ListChannels(ctxt, &lnrpc.ListChannelsRequest{ + ActiveOnly: true, + }) + require.NoError(t, err) + + chanFundingHash, err := lnrpc.GetChanPointFundingTxid(chanPoint) + require.NoError(t, err) + + chanPointStr := fmt.Sprintf("%v:%v", chanFundingHash, + chanPoint.OutputIndex) + + var targetChan *lnrpc.Channel + for _, channel := range channelResp.Channels { + if channel.ChannelPoint == chanPointStr { + targetChan = channel + + break + } + } + require.NotNil(t, targetChan) + + return targetChan +} + +func assertChannelSatBalance(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint, local, remote int64) { + + targetChan := fetchChannel(t, node, chanPoint) + + require.InDelta(t, local, targetChan.LocalBalance, 1) + require.InDelta(t, remote, targetChan.RemoteBalance, 1) +} + +func assertChannelAssetBalance(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint, local, remote uint64) { + + targetChan := fetchChannel(t, node, chanPoint) + + var assetBalance rfqmsg.JsonAssetChannel + err := json.Unmarshal(targetChan.CustomChannelData, &assetBalance) + require.NoError(t, err) + + require.Len(t, assetBalance.Assets, 1) + + require.InDelta(t, local, assetBalance.Assets[0].LocalBalance, 1) + require.InDelta(t, remote, assetBalance.Assets[0].RemoteBalance, 1) +} + +// addRoutingFee adds the default routing fee (1 part per million fee rate plus +// 1000 milli-satoshi base fee) to the given milli-satoshi amount. +func addRoutingFee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { + return amt + (amt / 1000_000) + 1000 +} + +func sendAssetKeySendPayment(t *testing.T, src, dst *HarnessNode, amt uint64, + assetID []byte, btcAmt fn.Option[int64], opts ...payOpt) { + + cfg := defaultPayConfig() + for _, opt := range opts { + opt(cfg) + } + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + srcTapd := newTapClient(t, src) + + // Read out the custom preimage for the keysend payment. + var preimage lntypes.Preimage + _, err := rand.Read(preimage[:]) + require.NoError(t, err) + + hash := preimage.Hash() + + // Set the preimage. If the user supplied a preimage with the data + // flag, the preimage that is set here will be overwritten later. + customRecords := make(map[uint64][]byte) + customRecords[record.KeySendType] = preimage[:] + + sendReq := &routerrpc.SendPaymentRequest{ + Dest: dst.PubKey[:], + Amt: btcAmt.UnwrapOr(500), + DestCustomRecords: customRecords, + PaymentHash: hash[:], + TimeoutSeconds: int32(PaymentTimeout.Seconds()), + } + + stream, err := srcTapd.SendPayment(ctxt, &tchrpc.SendPaymentRequest{ + AssetId: assetID, + AssetAmount: amt, + PaymentRequest: sendReq, + }) + require.NoError(t, err) + + result, err := getAssetPaymentResult(stream, false) + require.NoError(t, err) + if result.Status == lnrpc.Payment_FAILED { + t.Logf("Failure reason: %v", result.FailureReason) + } + require.Equal(t, cfg.payStatus, result.Status) + require.Equal(t, cfg.failureReason, result.FailureReason) +} + +func sendKeySendPayment(t *testing.T, src, dst *HarnessNode, + amt btcutil.Amount) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + // Read out the custom preimage for the keysend payment. + var preimage lntypes.Preimage + _, err := rand.Read(preimage[:]) + require.NoError(t, err) + + hash := preimage.Hash() + + // Set the preimage. If the user supplied a preimage with the data + // flag, the preimage that is set here will be overwritten later. + customRecords := make(map[uint64][]byte) + customRecords[record.KeySendType] = preimage[:] + + req := &routerrpc.SendPaymentRequest{ + Dest: dst.PubKey[:], + Amt: int64(amt), + DestCustomRecords: customRecords, + PaymentHash: hash[:], + TimeoutSeconds: int32(PaymentTimeout.Seconds()), + } + + stream, err := src.RouterClient.SendPaymentV2(ctxt, req) + require.NoError(t, err) + + result, err := getPaymentResult(stream) + require.NoError(t, err) + require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) +} + +func createAndPayNormalInvoiceWithBtc(t *testing.T, src, dst *HarnessNode, + amountSat btcutil.Amount) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + expirySeconds := 10 + invoiceResp, err := dst.AddInvoice(ctxt, &lnrpc.Invoice{ + Value: int64(amountSat), + Memo: "normal invoice", + Expiry: int64(expirySeconds), + }) + require.NoError(t, err) + + payInvoiceWithSatoshi(t, src, invoiceResp) +} + +func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode, + amountSat btcutil.Amount, assetID []byte, opts ...payOpt) uint64 { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + expirySeconds := 10 + invoiceResp, err := dst.AddInvoice(ctxt, &lnrpc.Invoice{ + Value: int64(amountSat), + Memo: "normal invoice", + Expiry: int64(expirySeconds), + }) + require.NoError(t, err) + + numUnits, _ := payInvoiceWithAssets( + t, src, rfqPeer, invoiceResp.PaymentRequest, assetID, opts..., + ) + + return numUnits +} + +func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, + invoice *lnrpc.AddInvoiceResponse, opts ...payOpt) { + + cfg := defaultPayConfig() + for _, opt := range opts { + opt(cfg) + } + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + sendReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: int32(PaymentTimeout.Seconds()), + MaxShardSizeMsat: 80_000_000, + FeeLimitMsat: 1_000_000, + } + stream, err := payer.RouterClient.SendPaymentV2(ctxt, sendReq) + require.NoError(t, err) + + result, err := getPaymentResult(stream) + if cfg.expectTimeout { + require.ErrorContains(t, err, "context deadline exceeded") + } else { + require.NoError(t, err) + require.Equal(t, cfg.payStatus, result.Status) + require.Equal(t, cfg.failureReason, result.FailureReason) + } +} + +func payInvoiceWithSatoshiLastHop(t *testing.T, payer *HarnessNode, + invoice *lnrpc.AddInvoiceResponse, hopPub []byte, + expectedStatus lnrpc.Payment_PaymentStatus) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + routeRes, err := payer.RouterClient.BuildRoute( + ctxb, &routerrpc.BuildRouteRequest{ + AmtMsat: 17800, + FinalCltvDelta: 80, + PaymentAddr: invoice.PaymentAddr, + HopPubkeys: [][]byte{hopPub}, + }, + ) + require.NoError(t, err) + + res, err := payer.RouterClient.SendToRouteV2( + ctxt, &routerrpc.SendToRouteRequest{ + PaymentHash: invoice.RHash, + Route: routeRes.Route, + }, + ) + + switch expectedStatus { + case lnrpc.Payment_FAILED: + require.NoError(t, err) + require.Equal(t, lnrpc.HTLCAttempt_FAILED, res.Status) + require.Nil(t, res.Preimage) + + case lnrpc.Payment_SUCCEEDED: + require.NoError(t, err) + require.Equal(t, lnrpc.HTLCAttempt_SUCCEEDED, res.Status) + } +} + +type payConfig struct { + smallShards bool + expectTimeout bool + payStatus lnrpc.Payment_PaymentStatus + failureReason lnrpc.PaymentFailureReason + rfq fn.Option[rfqmsg.ID] +} + +func defaultPayConfig() *payConfig { + return &payConfig{ + smallShards: false, + expectTimeout: false, + payStatus: lnrpc.Payment_SUCCEEDED, + failureReason: lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, + } +} + +type payOpt func(*payConfig) + +func withSmallShards() payOpt { + return func(c *payConfig) { + c.smallShards = true + } +} + +func withExpectTimeout() payOpt { + return func(c *payConfig) { + c.expectTimeout = true + } +} + +func withFailure(status lnrpc.Payment_PaymentStatus, + reason lnrpc.PaymentFailureReason) payOpt { + + return func(c *payConfig) { + c.payStatus = status + c.failureReason = reason + } +} + +func withRFQ(rfqID rfqmsg.ID) payOpt { + return func(c *payConfig) { + c.rfq = fn.Some(rfqID) + } +} + +func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, + payReq string, assetID []byte, + opts ...payOpt) (uint64, rfqmath.BigIntFixedPoint) { + + cfg := defaultPayConfig() + for _, opt := range opts { + opt(cfg) + } + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + payerTapd := newTapClient(t, payer) + + decodedInvoice, err := payer.DecodePayReq(ctxt, &lnrpc.PayReqString{ + PayReq: payReq, + }) + require.NoError(t, err) + + sendReq := &routerrpc.SendPaymentRequest{ + PaymentRequest: payReq, + TimeoutSeconds: int32(PaymentTimeout.Seconds()), + FeeLimitMsat: 1_000_000, + } + + if cfg.smallShards { + sendReq.MaxShardSizeMsat = 80_000_000 + } + + var rfqBytes []byte + cfg.rfq.WhenSome(func(i rfqmsg.ID) { + rfqBytes = make([]byte, len(i[:])) + copy(rfqBytes, i[:]) + }) + + stream, err := payerTapd.SendPayment(ctxt, &tchrpc.SendPaymentRequest{ + AssetId: assetID, + PeerPubkey: rfqPeer.PubKey[:], + PaymentRequest: sendReq, + RfqId: rfqBytes, + }) + require.NoError(t, err) + + var ( + numUnits uint64 + rateVal rfqmath.FixedPoint[rfqmath.BigInt] + ) + if cfg.rfq.IsNone() { + // We want to receive the accepted quote message first, so we + // know how many assets we're going to pay. + quoteMsg, err := stream.Recv() + require.NoError(t, err) + acceptedQuote := quoteMsg.GetAcceptedSellOrder() + require.NotNil(t, acceptedQuote) + + peerPubKey := acceptedQuote.Peer + require.Equal(t, peerPubKey, rfqPeer.PubKeyStr) + + rpcRate := acceptedQuote.BidAssetRate + rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) + require.NoError(t, err) + + rateVal = *rate + + t.Logf("Got quote for %v asset units per BTC", rate) + + amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat) + milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate) + numUnits = milliSatsFP.ScaleTo(0).ToUint64() + msatPerUnit := float64(decodedInvoice.NumMsat) / + float64(numUnits) + t.Logf("Got quote for %v asset units at %3f msat/unit from "+ + "peer %s with SCID %d", numUnits, msatPerUnit, + peerPubKey, acceptedQuote.Scid) + } + + result, err := getAssetPaymentResult( + stream, cfg.payStatus == lnrpc.Payment_IN_FLIGHT, + ) + require.NoError(t, err) + require.Equal(t, cfg.payStatus, result.Status) + require.Equal(t, cfg.failureReason, result.FailureReason) + + return numUnits, rateVal +} + +func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, + assetAmount uint64, assetID []byte) *lnrpc.AddInvoiceResponse { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + + t.Logf("Asking peer %x for quote to buy assets to receive for "+ + "invoice over %d units; waiting up to %ds", + dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + + dstTapd := newTapClient(t, dst) + + resp, err := dstTapd.AddInvoice(ctxt, &tchrpc.AddInvoiceRequest{ + AssetId: assetID, + AssetAmount: assetAmount, + PeerPubkey: dstRfqPeer.PubKey[:], + InvoiceRequest: &lnrpc.Invoice{ + Memo: fmt.Sprintf("this is an asset invoice over "+ + "%d units", assetAmount), + Expiry: timeoutSeconds, + }, + }) + require.NoError(t, err) + + decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{ + PayReq: resp.InvoiceResult.PaymentRequest, + }) + require.NoError(t, err) + + rpcRate := resp.AcceptedBuyQuote.AskAssetRate + rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) + require.NoError(t, err) + + t.Logf("Got quote for %v asset units per BTC", rate) + + assetUnits := rfqmath.NewBigIntFixedPoint(assetAmount, 0) + numMSats := rfqmath.UnitsToMilliSatoshi(assetUnits, *rate) + mSatPerUnit := float64(decodedInvoice.NumMsat) / float64(assetAmount) + + require.EqualValues(t, numMSats, decodedInvoice.NumMsat) + + t.Logf("Got quote for %d mSats at %3f msat/unit from peer %x with "+ + "SCID %d", decodedInvoice.NumMsat, mSatPerUnit, + dstRfqPeer.PubKey[:], resp.AcceptedBuyQuote.Scid) + + return resp.InvoiceResult +} + +// assertInvoiceHtlcAssets makes sure the invoice with the given hash shows the +// individual HTLCs that arrived for it and that they show the correct asset +// amounts for the given ID when decoded. +func assertInvoiceHtlcAssets(t *testing.T, node *HarnessNode, + addedInvoice *lnrpc.AddInvoiceResponse, assetID []byte, + assetAmount uint64) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + invoice, err := node.InvoicesClient.LookupInvoiceV2( + ctxt, &invoicesrpc.LookupInvoiceMsg{ + InvoiceRef: &invoicesrpc.LookupInvoiceMsg_PaymentAddr{ + PaymentAddr: addedInvoice.PaymentAddr, + }, + }, + ) + require.NoError(t, err) + require.NotEmpty(t, invoice.Htlcs) + + t.Logf("Asset invoice: %v", toProtoJSON(t, invoice)) + + targetID := hex.EncodeToString(assetID) + + var totalAssetAmount uint64 + for _, htlc := range invoice.Htlcs { + require.NotEmpty(t, htlc.CustomChannelData) + + jsonHtlc := &rfqmsg.JsonHtlc{} + err := json.Unmarshal(htlc.CustomChannelData, jsonHtlc) + require.NoError(t, err) + + for _, balance := range jsonHtlc.Balances { + if balance.AssetID != targetID { + continue + } + + totalAssetAmount += balance.Amount + } + } + + // Due to rounding we allow up to 1 unit of error. + require.InDelta(t, assetAmount, totalAssetAmount, 1) +} + +// assertPaymentHtlcAssets makes sure the payment with the given hash shows the +// individual HTLCs that arrived for it and that they show the correct asset +// amounts for the given ID when decoded. +func assertPaymentHtlcAssets(t *testing.T, node *HarnessNode, payHash []byte, + assetID []byte, assetAmount uint64) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + stream, err := node.RouterClient.TrackPaymentV2( + ctxt, &routerrpc.TrackPaymentRequest{ + PaymentHash: payHash, + NoInflightUpdates: true, + }, + ) + require.NoError(t, err) + + payment, err := stream.Recv() + require.NoError(t, err) + require.NotNil(t, payment) + require.NotEmpty(t, payment.Htlcs) + + t.Logf("Asset payment: %v", toProtoJSON(t, payment)) + + targetID := hex.EncodeToString(assetID) + + var totalAssetAmount uint64 + for _, htlc := range payment.Htlcs { + require.NotNil(t, htlc.Route) + require.NotEmpty(t, htlc.Route.CustomChannelData) + + jsonHtlc := &rfqmsg.JsonHtlc{} + err := json.Unmarshal(htlc.Route.CustomChannelData, jsonHtlc) + require.NoError(t, err) + + for _, balance := range jsonHtlc.Balances { + if balance.AssetID != targetID { + continue + } + + totalAssetAmount += balance.Amount + } + } + + // Due to rounding we allow up to 1 unit of error. + require.InDelta(t, assetAmount, totalAssetAmount, 1) +} + +type assetHodlInvoice struct { + preimage lntypes.Preimage + payReq string +} + +func createAssetHodlInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, + assetAmount uint64, assetID []byte) assetHodlInvoice { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + timeoutSeconds := int64(rfq.DefaultInvoiceExpiry.Seconds()) + + t.Logf("Asking peer %x for quote to buy assets to receive for "+ + "invoice over %d units; waiting up to %ds", + dstRfqPeer.PubKey[:], assetAmount, timeoutSeconds) + + dstTapd := newTapClient(t, dst) + + // As this is a hodl invoice, we'll also need to create a preimage + // external to lnd. + var preimage lntypes.Preimage + _, err := rand.Read(preimage[:]) + require.NoError(t, err) + + payHash := preimage.Hash() + + resp, err := dstTapd.AddInvoice(ctxt, &tchrpc.AddInvoiceRequest{ + AssetId: assetID, + AssetAmount: assetAmount, + PeerPubkey: dstRfqPeer.PubKey[:], + InvoiceRequest: &lnrpc.Invoice{ + Memo: fmt.Sprintf("this is an asset invoice over "+ + "%d units", assetAmount), + Expiry: timeoutSeconds, + }, + HodlInvoice: &tchrpc.HodlInvoice{ + PaymentHash: payHash[:], + }, + }) + require.NoError(t, err) + + decodedInvoice, err := dst.DecodePayReq(ctxt, &lnrpc.PayReqString{ + PayReq: resp.InvoiceResult.PaymentRequest, + }) + require.NoError(t, err) + + rpcRate := resp.AcceptedBuyQuote.AskAssetRate + rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) + require.NoError(t, err) + + assetUnits := rfqmath.NewBigIntFixedPoint(assetAmount, 0) + numMSats := rfqmath.UnitsToMilliSatoshi(assetUnits, *rate) + mSatPerUnit := float64(decodedInvoice.NumMsat) / float64(assetAmount) + + require.EqualValues(t, uint64(numMSats), uint64(decodedInvoice.NumMsat)) + + t.Logf("Got quote for %d sats at %v msat/unit from peer %x with SCID "+ + "%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:], + resp.AcceptedBuyQuote.Scid) + + return assetHodlInvoice{ + preimage: preimage, + payReq: resp.InvoiceResult.PaymentRequest, + } +} + +func waitForSendEvent(t *testing.T, + sendEvents taprpc.TaprootAssets_SubscribeSendEventsClient, + expectedState tapfreighter.SendState) { + + t.Helper() + + for { + sendEvent, err := sendEvents.Recv() + require.NoError(t, err) + + t.Logf("Received send event: %v", sendEvent.SendState) + if sendEvent.SendState == expectedState.String() { + return + } + } +} + +// coOpCloseBalanceCheck is a function type that can be passed into +// closeAssetChannelAndAsset to asset the final balance of the closing +// transaction. +type coOpCloseBalanceCheck func(t *testing.T, local, remote *HarnessNode, + closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate, + assetID, groupKey []byte, universeTap *tapClient) + +// noOpCoOpCloseBalanceCheck is a no-op implementation of the co-op close +// balance check that can be used in tests. +func noOpCoOpCloseBalanceCheck(_ *testing.T, _, _ *HarnessNode, _ *wire.MsgTx, + _ *lnrpc.ChannelCloseUpdate, _, _ []byte, _ *tapClient) { + + // This is a no-op function. +} + +// closeAssetChannelAndAssert closes the channel between the local and remote +// node and asserts the final balances of the closing transaction. +func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, + local, remote *HarnessNode, chanPoint *lnrpc.ChannelPoint, + assetID, groupKey []byte, universeTap *tapClient, + balanceCheck coOpCloseBalanceCheck) { + + t.t.Helper() + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + closeStream, _, err := t.lndHarness.CloseChannel( + local, chanPoint, false, + ) + require.NoError(t.t, err) + + localTapd := newTapClient(t.t, local) + sendEvents, err := localTapd.SubscribeSendEvents( + ctxt, &taprpc.SubscribeSendEventsRequest{}, + ) + require.NoError(t.t, err) + + mineBlocks(t, net, 1, 1) + + closeUpdate, err := t.lndHarness.WaitForChannelClose(closeStream) + require.NoError(t.t, err) + + closeTxid, err := chainhash.NewHash(closeUpdate.ClosingTxid) + require.NoError(t.t, err) + + closeTransaction := t.lndHarness.Miner.GetRawTransaction(*closeTxid) + closeTx := closeTransaction.MsgTx() + t.Logf("Channel closed with txid: %v", closeTxid) + t.Logf("Close transaction: %v", spew.Sdump(closeTx)) + + waitForSendEvent(t.t, sendEvents, tapfreighter.SendStateComplete) + + // Check the final balance of the closing transaction. + balanceCheck( + t.t, local, remote, closeTx, closeUpdate, assetID, groupKey, + universeTap, + ) +} + +// assertDefaultCoOpCloseBalance returns a default implementation of the co-op +// close balance check that can be used in tests. It assumes the initiator has +// both an asset and BTC balance left, while the responder's balance can be +// specified with the boolean variables. +func assertDefaultCoOpCloseBalance(remoteBtcBalance, + remoteAssetBalance bool) coOpCloseBalanceCheck { + + return func(t *testing.T, local, remote *HarnessNode, + closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate, + assetID, groupKey []byte, universeTap *tapClient) { + + defaultCoOpCloseBalanceCheck( + t, local, remote, closeTx, closeUpdate, assetID, + groupKey, universeTap, remoteBtcBalance, + remoteAssetBalance, + ) + } +} + +// defaultCoOpCloseBalanceCheck is a default implementation of the co-op close +// balance check that can be used in tests. It assumes the initiator has both +// an asset and BTC balance left, while the responder's balance can be specified +// with the boolean variables. +func defaultCoOpCloseBalanceCheck(t *testing.T, local, remote *HarnessNode, + closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate, + assetID, groupKey []byte, universeTap *tapClient, remoteBtcBalance, + remoteAssetBalance bool) { + + // With the channel closed, we'll now assert that the co-op close + // transaction was inserted into the local universe. + // + // We expect that at most four outputs exist: one for the local asset + // output, one for the remote asset output, one for the remote BTC + // channel balance and one for the remote BTC channel balance. + // + // Those outputs are only present if the respective party has a + // non-dust balance. + numOutputs := 2 + additionalOutputs := 1 + if remoteBtcBalance { + numOutputs++ + } + if remoteAssetBalance { + numOutputs++ + additionalOutputs++ + } + + closeTxid := closeTx.TxHash() + require.Len(t, closeTx.TxOut, numOutputs) + + outIdx := 0 + dummyAmt := int64(1000) + require.LessOrEqual(t, closeTx.TxOut[outIdx].Value, dummyAmt) + + if remoteAssetBalance { + outIdx++ + require.LessOrEqual(t, closeTx.TxOut[outIdx].Value, dummyAmt) + } + + // We also require there to be at most two additional outputs, one for + // each of the asset outputs with balance. + require.Len(t, closeUpdate.AdditionalOutputs, additionalOutputs) + + var remoteCloseOut *lnrpc.CloseOutput + if remoteBtcBalance { + // The remote node has received a couple of HTLCs with an above + // dust value, so it should also have accumulated a non-dust + // balance, even after subtracting 1k sats for the asset output. + remoteCloseOut = closeUpdate.RemoteCloseOutput + require.NotNil(t, remoteCloseOut) + + outIdx++ + require.EqualValues( + t, remoteCloseOut.AmountSat-dummyAmt, + closeTx.TxOut[outIdx].Value, + ) + } else if remoteAssetBalance { + // The remote node has received a couple of HTLCs but not enough + // to go above dust. So it should still have an asset balance + // that we can verify. + remoteCloseOut = closeUpdate.RemoteCloseOutput + require.NotNil(t, remoteCloseOut) + } + + // The local node should have received the local BTC balance minus the + // TX fees and 1k sats for the asset output. + localCloseOut := closeUpdate.LocalCloseOutput + require.NotNil(t, localCloseOut) + outIdx++ + require.Greater( + t, closeTx.TxOut[outIdx].Value, + localCloseOut.AmountSat-dummyAmt, + ) + + // Find out which of the additional outputs is the local one and which + // is the remote. + localAuxOut := closeUpdate.AdditionalOutputs[0] + + var remoteAuxOut *lnrpc.CloseOutput + if remoteAssetBalance { + remoteAuxOut = closeUpdate.AdditionalOutputs[1] + } + if !localAuxOut.IsLocal && remoteAuxOut != nil { + localAuxOut, remoteAuxOut = remoteAuxOut, localAuxOut + } + + // The first two transaction outputs should be the additional outputs + // as identified by the pk scripts in the close update. + localAssetIndex, remoteAssetIndex := 1, 0 + if bytes.Equal(closeTx.TxOut[0].PkScript, localAuxOut.PkScript) { + localAssetIndex, remoteAssetIndex = 0, 1 + } + + if remoteAuxOut != nil { + require.Equal( + t, remoteAuxOut.PkScript, + closeTx.TxOut[remoteAssetIndex].PkScript, + ) + } + + require.Equal( + t, localAuxOut.PkScript, + closeTx.TxOut[localAssetIndex].PkScript, + ) + + // We now verify the arrival of the local balance asset proof at the + // universe server. + var localAssetCloseOut rfqmsg.JsonCloseOutput + err := json.Unmarshal( + localCloseOut.CustomChannelData, &localAssetCloseOut, + ) + require.NoError(t, err) + + for assetIDStr, scriptKeyStr := range localAssetCloseOut.ScriptKeys { + scriptKeyBytes, err := hex.DecodeString(scriptKeyStr) + require.NoError(t, err) + + require.Equal(t, hex.EncodeToString(assetID), assetIDStr) + + a := assertUniverseProofExists( + t, universeTap, assetID, groupKey, scriptKeyBytes, + fmt.Sprintf("%v:%v", closeTxid, localAssetIndex), + ) + + localTapd := newTapClient(t, local) + + scriptKey, err := btcec.ParsePubKey(scriptKeyBytes) + require.NoError(t, err) + assertAssetExists( + t, localTapd, assetID, a.Amount, scriptKey, true, + true, false, + ) + } + + // If there is no remote asset balance, we're done. + if !remoteAssetBalance { + return + } + + // At this point the remote close output should be defined, otherwise + // something went wrong. + require.NotNil(t, remoteCloseOut) + + // And then we verify the arrival of the remote balance asset proof at + // the universe server as well. + var remoteAssetCloseOut rfqmsg.JsonCloseOutput + err = json.Unmarshal( + remoteCloseOut.CustomChannelData, &remoteAssetCloseOut, + ) + require.NoError(t, err) + + for assetIDStr, scriptKeyStr := range remoteAssetCloseOut.ScriptKeys { + scriptKeyBytes, err := hex.DecodeString(scriptKeyStr) + require.NoError(t, err) + + require.Equal(t, hex.EncodeToString(assetID), assetIDStr) + + a := assertUniverseProofExists( + t, universeTap, assetID, groupKey, scriptKeyBytes, + fmt.Sprintf("%v:%v", closeTxid, remoteAssetIndex), + ) + + remoteTapd := newTapClient(t, remote) + + scriptKey, err := btcec.ParsePubKey(scriptKeyBytes) + require.NoError(t, err) + assertAssetExists( + t, remoteTapd, assetID, a.Amount, scriptKey, true, + true, false, + ) + } +} + +// initiatorZeroAssetBalanceCoOpBalanceCheck is a co-op close balance check +// function that can be used when the initiator has a zero asset balance. +func initiatorZeroAssetBalanceCoOpBalanceCheck(t *testing.T, _, + remote *HarnessNode, closeTx *wire.MsgTx, + closeUpdate *lnrpc.ChannelCloseUpdate, assetID, groupKey []byte, + universeTap *tapClient) { + + // With the channel closed, we'll now assert that the co-op close + // transaction was inserted into the local universe. + // + // Since the initiator has a zero asset balance, we expect that at most + // three outputs exist: one for the remote asset output, one for the + // remote BTC channel balance and one for the initiator's BTC channel + // balance (which cannot be zero or below dust due to the mandatory + // channel reserve). + numOutputs := 3 + + closeTxid := closeTx.TxHash() + require.Len(t, closeTx.TxOut, numOutputs) + + // We assume that the local node has a non-zero BTC balance left. + localOut, _ := closeTxOut(t, closeTx, closeUpdate, true) + require.Greater(t, localOut.Value, int64(1000)) + + // We also require there to be exactly one additional output, which is + // the remote asset output. + require.Len(t, closeUpdate.AdditionalOutputs, 1) + assetTxOut, assetOutputIndex := findTxOut( + t, closeTx, closeUpdate.AdditionalOutputs[0].PkScript, + ) + require.LessOrEqual(t, assetTxOut.Value, int64(1000)) + + // The remote node has received a couple of HTLCs with an above + // dust value, so it should also have accumulated a non-dust + // balance, even after subtracting 1k sats for the asset output. + remoteCloseOut := closeUpdate.RemoteCloseOutput + require.NotNil(t, remoteCloseOut) + + // Find out which of the additional outputs is the local one and which + // is the remote. + remoteAuxOut := closeUpdate.AdditionalOutputs[0] + require.False(t, remoteAuxOut.IsLocal) + + // And then we verify the arrival of the remote balance asset proof at + // the universe server as well. + var remoteAssetCloseOut rfqmsg.JsonCloseOutput + err := json.Unmarshal( + remoteCloseOut.CustomChannelData, &remoteAssetCloseOut, + ) + require.NoError(t, err) + + for assetIDStr, scriptKeyStr := range remoteAssetCloseOut.ScriptKeys { + scriptKeyBytes, err := hex.DecodeString(scriptKeyStr) + require.NoError(t, err) + + require.Equal(t, hex.EncodeToString(assetID), assetIDStr) + + a := assertUniverseProofExists( + t, universeTap, assetID, groupKey, scriptKeyBytes, + fmt.Sprintf("%v:%v", closeTxid, assetOutputIndex), + ) + + remoteTapd := newTapClient(t, remote) + + scriptKey, err := btcec.ParsePubKey(scriptKeyBytes) + require.NoError(t, err) + assertAssetExists( + t, remoteTapd, assetID, a.Amount, scriptKey, true, + true, false, + ) + } +} + +// closeTxOut returns either the local or remote output from the close +// transaction, based on the information given in the close update. +func closeTxOut(t *testing.T, closeTx *wire.MsgTx, + closeUpdate *lnrpc.ChannelCloseUpdate, local bool) (*wire.TxOut, int) { + + var targetPkScript []byte + if local { + require.NotNil(t, closeUpdate.LocalCloseOutput) + targetPkScript = closeUpdate.LocalCloseOutput.PkScript + } else { + require.NotNil(t, closeUpdate.RemoteCloseOutput) + targetPkScript = closeUpdate.RemoteCloseOutput.PkScript + } + + return findTxOut(t, closeTx, targetPkScript) +} + +// findTxOut returns the transaction output with the target pk script from the +// given transaction. +func findTxOut(t *testing.T, tx *wire.MsgTx, targetPkScript []byte) ( + *wire.TxOut, int) { + + for i, txOut := range tx.TxOut { + if bytes.Equal(txOut.PkScript, targetPkScript) { + return txOut, i + } + } + + t.Fatalf("close output (targetPkScript=%x) not found in close "+ + "transaction", targetPkScript) + + return &wire.TxOut{}, 0 +} + +type tapClient struct { + node *HarnessNode + lnd *rpc.HarnessRPC + taprpc.TaprootAssetsClient + assetwalletrpc.AssetWalletClient + tapdevrpc.TapDevClient + mintrpc.MintClient + rfqrpc.RfqClient + tchrpc.TaprootAssetChannelsClient + universerpc.UniverseClient +} + +func newTapClient(t *testing.T, node *HarnessNode) *tapClient { + cfg := node.Cfg + superMacFile := bakeSuperMacaroon(t, cfg, getLiTMacFromFile, false) + + t.Cleanup(func() { + require.NoError(t, os.Remove(superMacFile)) + }) + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + rawConn, err := connectRPCWithMac( + ctxt, cfg.LitAddr(), cfg.LitTLSCertPath, superMacFile, + ) + require.NoError(t, err) + + t.Cleanup(func() { + _ = rawConn.Close() + }) + + assetsClient := taprpc.NewTaprootAssetsClient(rawConn) + assetWalletClient := assetwalletrpc.NewAssetWalletClient(rawConn) + devClient := tapdevrpc.NewTapDevClient(rawConn) + mintMintClient := mintrpc.NewMintClient(rawConn) + rfqClient := rfqrpc.NewRfqClient(rawConn) + tchClient := tchrpc.NewTaprootAssetChannelsClient(rawConn) + universeClient := universerpc.NewUniverseClient(rawConn) + + return &tapClient{ + node: node, + TaprootAssetsClient: assetsClient, + AssetWalletClient: assetWalletClient, + TapDevClient: devClient, + MintClient: mintMintClient, + RfqClient: rfqClient, + TaprootAssetChannelsClient: tchClient, + UniverseClient: universeClient, + } +} + +func connectRPCWithMac(ctx context.Context, hostPort, tlsCertPath, + macFilePath string) (*grpc.ClientConn, error) { + + tlsCreds, err := credentials.NewClientTLSFromFile(tlsCertPath, "") + if err != nil { + return nil, err + } + + opts := []grpc.DialOption{ + grpc.WithBlock(), + grpc.WithTransportCredentials(tlsCreds), + } + + macOption, err := readMacaroon(macFilePath) + if err != nil { + return nil, err + } + + opts = append(opts, macOption) + + return grpc.DialContext(ctx, hostPort, opts...) +} + +func assertAssetBalance(t *testing.T, client *tapClient, assetID []byte, + expectedBalance uint64) { + + t.Helper() + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, shortTimeout) + defer cancel() + + req := &taprpc.ListBalancesRequest{ + GroupBy: &taprpc.ListBalancesRequest_AssetId{ + AssetId: true, + }, + } + + err := wait.NoError(func() error { + assetIDBalances, err := client.ListBalances(ctxt, req) + if err != nil { + return err + } + + assetIDFound := false + for _, balance := range assetIDBalances.AssetBalances { + if !bytes.Equal(balance.AssetGenesis.AssetId, assetID) { + continue + } + + assetIDFound = true + if expectedBalance != balance.Balance { + return fmt.Errorf("expected balance %d, got %d", + expectedBalance, balance.Balance) + } + } + + if expectedBalance > 0 && !assetIDFound { + return fmt.Errorf("expected balance %d, got 0", + expectedBalance) + } + return nil + }, shortTimeout) + if err != nil { + r, err2 := client.ListAssets(ctxb, &taprpc.ListAssetRequest{}) + require.NoError(t, err2) + + t.Logf("Failed to assert expected balance of %d, current "+ + "assets: %v", expectedBalance, toProtoJSON(t, r)) + + utxos, err3 := client.ListUtxos( + ctxb, &taprpc.ListUtxosRequest{}, + ) + require.NoError(t, err3) + + t.Logf("Current UTXOs: %v", toProtoJSON(t, utxos)) + + t.Fatalf("Failed to assert balance: %v", err) + } +} + +// assertSpendableBalance differs from assertAssetBalance in that it asserts +// that the entire balance is spendable. We consider something spendable if we +// have a local script key for it. +func assertSpendableBalance(t *testing.T, client *tapClient, assetID []byte, + expectedBalance uint64) { + + t.Helper() + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, shortTimeout) + defer cancel() + + err := wait.NoError(func() error { + utxos, err := client.ListUtxos(ctxt, &taprpc.ListUtxosRequest{}) + if err != nil { + return err + } + + assets := tapfn.FlatMap( + maps.Values(utxos.ManagedUtxos), + func(utxo *taprpc.ManagedUtxo) []*taprpc.Asset { + return utxo.Assets + }, + ) + + relevantAssets := fn.Filter(func(utxo *taprpc.Asset) bool { + return bytes.Equal(utxo.AssetGenesis.AssetId, assetID) + }, assets) + + var assetSum uint64 + for _, asset := range relevantAssets { + if asset.ScriptKeyIsLocal { + assetSum += asset.Amount + } + } + + if assetSum != expectedBalance { + return fmt.Errorf("expected balance %d, got %d", + expectedBalance, assetSum) + } + + return nil + }, shortTimeout) + if err != nil { + r, err2 := client.ListAssets(ctxb, &taprpc.ListAssetRequest{}) + require.NoError(t, err2) + + t.Logf("Failed to assert expected balance of %d, current "+ + "assets: %v", expectedBalance, toProtoJSON(t, r)) + + utxos, err3 := client.ListUtxos( + ctxb, &taprpc.ListUtxosRequest{}, + ) + require.NoError(t, err3) + + t.Logf("Current UTXOs: %v", toProtoJSON(t, utxos)) + + t.Fatalf("Failed to assert balance: %v", err) + } +} + +func assertNumAssetOutputs(t *testing.T, client *tapClient, assetID []byte, + numPieces int) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, shortTimeout) + defer cancel() + + resp, err := client.ListAssets(ctxt, &taprpc.ListAssetRequest{ + IncludeLeased: true, + }) + require.NoError(t, err) + + var outputs []*taprpc.Asset + for _, a := range resp.Assets { + if !bytes.Equal(a.AssetGenesis.AssetId, assetID) { + continue + } + + outputs = append(outputs, a) + } + + require.Len(t, outputs, numPieces) +} + +func assertAssetExists(t *testing.T, client *tapClient, assetID []byte, + amount uint64, scriptKey *btcec.PublicKey, scriptKeyLocal, + scriptKeyKnown, scriptKeyHasScript bool) *taprpc.Asset { + + t.Helper() + + var a *taprpc.Asset + err := wait.NoError(func() error { + var err error + a, err = assetExists( + t, client, assetID, amount, scriptKey, scriptKeyLocal, + scriptKeyKnown, scriptKeyHasScript, + ) + return err + }, shortTimeout) + require.NoError(t, err) + + return a +} + +func assetExists(t *testing.T, client *tapClient, assetID []byte, + amount uint64, scriptKey *btcec.PublicKey, scriptKeyLocal, + scriptKeyKnown, scriptKeyHasScript bool) (*taprpc.Asset, error) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, shortTimeout) + defer cancel() + + resp, err := client.ListAssets(ctxt, &taprpc.ListAssetRequest{ + IncludeLeased: true, + }) + if err != nil { + return nil, err + } + + for _, a := range resp.Assets { + if !bytes.Equal(a.AssetGenesis.AssetId, assetID) { + continue + } + + if amount != a.Amount { + continue + } + + if scriptKey != nil { + xOnlyKey, _ := schnorr.ParsePubKey( + schnorr.SerializePubKey(scriptKey), + ) + xOnlyKeyBytes := xOnlyKey.SerializeCompressed() + if !bytes.Equal(xOnlyKeyBytes, a.ScriptKey) { + continue + } + } + + if scriptKeyLocal != a.ScriptKeyIsLocal { + continue + } + + if scriptKeyKnown != a.ScriptKeyDeclaredKnown { + continue + } + + if scriptKeyHasScript != a.ScriptKeyHasScriptPath { + continue + } + + // Success, we have found the asset we're looking for. + return a, nil + } + + return nil, fmt.Errorf("asset with given criteria (amount=%d) not "+ + "found in list, got: %v", amount, toProtoJSON(t, resp)) +} + +func logBalance(t *testing.T, nodes []*HarnessNode, assetID []byte, + occasion string) { + + t.Helper() + + time.Sleep(time.Millisecond * 250) + + for _, node := range nodes { + local, remote, localSat, remoteSat := + getAssetChannelBalance(t, node, assetID, false) + + t.Logf("%-7s balance: local=%-9d remote=%-9d, localSat=%-9d, "+ + "remoteSat=%-9d (%v)", node.Cfg.Name, local, remote, + localSat, remoteSat, occasion) + } +} + +// readMacaroon tries to read the macaroon file at the specified path and create +// gRPC dial options from it. +func readMacaroon(macPath string) (grpc.DialOption, error) { + // Load the specified macaroon file. + macBytes, err := os.ReadFile(macPath) + if err != nil { + return nil, fmt.Errorf("unable to read macaroon path : %w", err) + } + + return macFromBytes(macBytes) +} + +// macFromBytes returns a macaroon from the given byte slice. +func macFromBytes(macBytes []byte) (grpc.DialOption, error) { + mac := &macaroon.Macaroon{} + if err := mac.UnmarshalBinary(macBytes); err != nil { + return nil, fmt.Errorf("unable to decode macaroon: %w", err) + } + + // Now we append the macaroon credentials to the dial options. + cred, err := macaroons.NewMacaroonCredential(mac) + if err != nil { + return nil, fmt.Errorf("error creating macaroon credential: %w", + err) + } + return grpc.WithPerRPCCredentials(cred), nil +} + +func assertNumHtlcs(t *testing.T, node *HarnessNode, expected int) { + t.Helper() + + ctxb := context.Background() + + err := wait.NoError(func() error { + listChansRequest := &lnrpc.ListChannelsRequest{} + listChansResp, err := node.ListChannels(ctxb, listChansRequest) + if err != nil { + return err + } + + var numHtlcs int + for _, channel := range listChansResp.Channels { + numHtlcs += len(channel.PendingHtlcs) + } + + if numHtlcs != expected { + return fmt.Errorf("expected %v HTLCs, got %v, %v", + expected, numHtlcs, + spew.Sdump(toProtoJSON(t, listChansResp))) + } + + return nil + }, defaultTimeout) + require.NoError(t, err) +} + +type forceCloseExpiryInfo struct { + currentHeight uint32 + csvDelay uint32 + + cltvDelays map[lntypes.Hash]uint32 + + localAssetBalance uint64 + remoteAssetBalance uint64 + + t *testing.T + + node *HarnessNode +} + +func (f *forceCloseExpiryInfo) blockTillExpiry(hash lntypes.Hash) uint32 { + ctxb := context.Background() + nodeInfo, err := f.node.GetInfo(ctxb, &lnrpc.GetInfoRequest{}) + require.NoError(f.t, err) + + cltv, ok := f.cltvDelays[hash] + require.True(f.t, ok) + + f.t.Logf("current_height=%v, expiry=%v, mining %v blocks", + nodeInfo.BlockHeight, cltv, cltv-nodeInfo.BlockHeight) + + return cltv - nodeInfo.BlockHeight +} + +func newCloseExpiryInfo(t *testing.T, node *HarnessNode) forceCloseExpiryInfo { + ctxb := context.Background() + + listChansRequest := &lnrpc.ListChannelsRequest{} + listChansResp, err := node.ListChannels(ctxb, listChansRequest) + require.NoError(t, err) + + mainChan := listChansResp.Channels[0] + + nodeInfo, err := node.GetInfo(ctxb, &lnrpc.GetInfoRequest{}) + require.NoError(t, err) + + cltvs := make(map[lntypes.Hash]uint32) + for _, htlc := range mainChan.PendingHtlcs { + var payHash lntypes.Hash + copy(payHash[:], htlc.HashLock) + cltvs[payHash] = htlc.ExpirationHeight + } + + var assetData rfqmsg.JsonAssetChannel + err = json.Unmarshal(mainChan.CustomChannelData, &assetData) + require.NoError(t, err) + + return forceCloseExpiryInfo{ + csvDelay: mainChan.CsvDelay, + currentHeight: nodeInfo.BlockHeight, + cltvDelays: cltvs, + localAssetBalance: assetData.Assets[0].LocalBalance, + remoteAssetBalance: assetData.Assets[0].RemoteBalance, + t: t, + node: node, + } +} diff --git a/itest/litd_accounts_test.go b/itest/litd_accounts_test.go index f1677c255..b678723f0 100644 --- a/itest/litd_accounts_test.go +++ b/itest/litd_accounts_test.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/lightning-terminal/litrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/routerrpc" "github.com/lightningnetwork/lnd/lntest" @@ -433,3 +434,44 @@ func getPaymentResult(stream routerrpc.Router_SendPaymentV2Client) ( } } } + +func getAssetPaymentResult( + s tapchannelrpc.TaprootAssetChannels_SendPaymentClient, + isHodl bool) (*lnrpc.Payment, error) { + + // No idea why it makes a difference whether we wait before calling + // s.Recv() or not, but it does. Without the sleep, the test will fail + // with "insufficient local balance"... ¯\_(ツ)_/¯ + // Probably something weird within lnd itself. + time.Sleep(time.Second) + + for { + msg, err := s.Recv() + if err != nil { + return nil, err + } + + // Ignore RFQ quote acceptance messages read from the send + // payment stream, as they are not relevant. + quote := msg.GetAcceptedSellOrder() + if quote != nil { + continue + } + + payment := msg.GetPaymentResult() + if payment == nil { + return nil, fmt.Errorf("unexpected message: %v", msg) + } + + // If this is a hodl payment, then we'll return the first + // expected response. Otherwise, we'll wait until the in flight + // clears to we can observe the other payment states. + switch { + case isHodl: + return payment, nil + + case payment.Status != lnrpc.Payment_IN_FLIGHT: + return payment, nil + } + } +} diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go new file mode 100644 index 000000000..77789c496 --- /dev/null +++ b/itest/litd_custom_channels_test.go @@ -0,0 +1,3655 @@ +package itest + +import ( + "bytes" + "context" + "fmt" + "math" + "math/big" + "slices" + "time" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/itest" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/tapchannel" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" + "github.com/lightninglabs/taproot-assets/taprpc/universerpc" + "github.com/lightninglabs/taproot-assets/tapscript" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/walletrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/port" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +var ( + dummyMetaData = &taprpc.AssetMeta{ + Data: []byte("some metadata"), + } + + itestAsset = &mintrpc.MintAsset{ + AssetType: taprpc.AssetType_NORMAL, + Name: "itest-asset-cents", + AssetMeta: dummyMetaData, + Amount: 1_000_000, + } + + shortTimeout = time.Second * 5 +) + +var ( + lndArgsTemplate = []string{ + "--trickledelay=50", + "--gossip.sub-batch-delay=5ms", + "--caches.rpc-graph-cache-duration=100ms", + "--default-remote-max-htlcs=483", + "--dust-threshold=5000000", + "--rpcmiddleware.enable", + "--protocol.anchors", + "--protocol.option-scid-alias", + "--protocol.zero-conf", + "--protocol.simple-taproot-chans", + "--protocol.simple-taproot-overlay-chans", + "--protocol.custom-message=17", + "--accept-keysend", + "--debuglevel=trace,GRPC=error,BTCN=info", + } + litdArgsTemplateNoOracle = []string{ + "--taproot-assets.allow-public-uni-proof-courier", + "--taproot-assets.universe.public-access=rw", + "--taproot-assets.universe.sync-all-assets", + "--taproot-assets.universerpccourier.skipinitdelay", + "--taproot-assets.universerpccourier.backoffresetwait=1s", + "--taproot-assets.universerpccourier.numtries=5", + "--taproot-assets.universerpccourier.initialbackoff=300ms", + "--taproot-assets.universerpccourier.maxbackoff=600ms", + "--taproot-assets.universerpccourier.skipinitdelay", + "--taproot-assets.universerpccourier.backoffresetwait=100ms", + "--taproot-assets.universerpccourier.initialbackoff=300ms", + "--taproot-assets.universerpccourier.maxbackoff=600ms", + "--taproot-assets.custodianproofretrievaldelay=500ms", + } + litdArgsTemplate = append(litdArgsTemplateNoOracle, []string{ + "--taproot-assets.experimental.rfq.priceoracleaddress=" + + "use_mock_price_oracle_service_promise_to_" + + "not_use_on_mainnet", + "--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" + + "5820600", + }...) +) + +const ( + fundingAmount = 50_000 + startAmount = fundingAmount * 2 +) + +// testCustomChannelsLarge tests that we can create a network with custom +// channels and send large asset payments over them. +func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, + t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + channelOp := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, channelOp, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, channelOp) + assertChannelKnown(t.t, fabia, channelOp) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + daveFundingAmount = uint64(400_000) + erinFundingAmount = uint64(200_000) + ) + charlieFundingAmount := cents.Amount - uint64(2*400_000) + + chanPointCD, _, _ := createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, cents, 400_000, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, DefaultPushSat, + ) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + // Print initial channel balances. + logBalance(t.t, nodes, assetID, "initial") + + // Try larger invoice payments, first from Charlie to Fabia, then half + // of the amount back in the other direction. + const fabiaInvoiceAssetAmount = 20_000 + invoiceResp := createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + ) + logBalance(t.t, nodes, assetID, "after invoice") + + invoiceResp2 := createAssetInvoice( + t.t, dave, charlie, fabiaInvoiceAssetAmount/2, assetID, + ) + + // Sleep for a second to make sure the balances fully propagated before + // we make the payment. Otherwise, we'll make an RFQ order with a max + // amount of zero. + time.Sleep(time.Second * 1) + + payInvoiceWithAssets( + t.t, fabia, erin, invoiceResp2.PaymentRequest, assetID, + ) + logBalance(t.t, nodes, assetID, "after invoice 2") + + // Now we send a large invoice from Charlie to Dave. + const largeInvoiceAmount = 100_000 + invoiceResp3 := createAssetInvoice( + t.t, charlie, dave, largeInvoiceAmount, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp3.PaymentRequest, assetID, + ) + logBalance(t.t, nodes, assetID, "after invoice 3") + + // Make sure the invoice on the receiver side and the payment on the + // sender side show the individual HTLCs that arrived for it and that + // they show the correct asset amounts when decoded. + assertInvoiceHtlcAssets( + t.t, dave, invoiceResp3, assetID, largeInvoiceAmount, + ) + assertPaymentHtlcAssets( + t.t, charlie, invoiceResp3.RHash, assetID, largeInvoiceAmount, + ) + + // We keysend the rest, so that all the balance is on Dave's side. + charlieRemainingBalance := charlieFundingAmount - largeInvoiceAmount - + fabiaInvoiceAssetAmount/2 + sendAssetKeySendPayment( + t.t, charlie, dave, charlieRemainingBalance, + assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend") + + // And now we close the channel to test how things look if all the + // balance is on the non-initiator (recipient) side. + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, nil, + universeTap, initiatorZeroAssetBalanceCoOpBalanceCheck, + ) +} + +// testCustomChannels tests that we can create a network with custom channels +// and send asset payments over them. +func testCustomChannels(_ context.Context, net *NetworkHarness, + t *harnessTest) { + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + channelOp := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: 5_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, channelOp, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, channelOp) + assertChannelKnown(t.t, fabia, channelOp) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + daveFundingAmount = uint64(startAmount) + erinFundingAmount = uint64(fundingAmount) + ) + charlieFundingAmount := cents.Amount - 2*startAmount + + chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, cents, startAmount, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, DefaultPushSat, + ) + + // We'll be tracking the expected asset balances throughout the test, so + // we can assert it after each action. + charlieAssetBalance := charlieFundingAmount + daveAssetBalance := uint64(startAmount) + erinAssetBalance := uint64(startAmount) + fabiaAssetBalance := uint64(0) + yaraAssetBalance := uint64(0) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + // Print initial channel balances. + logBalance(t.t, nodes, assetID, "initial") + + // ------------ + // Test case 1: Send a direct keysend payment from Charlie to Dave, + // sending the whole balance. + // ------------ + keySendAmount := charlieFundingAmount + sendAssetKeySendPayment( + t.t, charlie, dave, charlieFundingAmount, assetID, + fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend") + + charlieAssetBalance -= keySendAmount + daveAssetBalance += keySendAmount + + // We should be able to send 1000 assets back immediately, because + // there is enough on-chain balance on Dave's side to be able to create + // an HTLC. We use an invoice to execute another code path. + const charlieInvoiceAmount = 1_000 + invoiceResp := createAssetInvoice( + t.t, dave, charlie, charlieInvoiceAmount, assetID, + ) + payInvoiceWithAssets( + t.t, dave, charlie, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice back") + + // Make sure the invoice on the receiver side and the payment on the + // sender side show the individual HTLCs that arrived for it and that + // they show the correct asset amounts when decoded. + assertInvoiceHtlcAssets( + t.t, charlie, invoiceResp, assetID, charlieInvoiceAmount, + ) + assertPaymentHtlcAssets( + t.t, dave, invoiceResp.RHash, assetID, charlieInvoiceAmount, + ) + + charlieAssetBalance += charlieInvoiceAmount + daveAssetBalance -= charlieInvoiceAmount + + // We should also be able to do a non-asset (BTC only) keysend payment + // from Charlie to Dave. This'll also replenish the BTC balance of + // Dave, making it possible to send another asset HTLC below, sending + // all assets back to Charlie (so we have enough balance for further + // tests). + sendKeySendPayment(t.t, charlie, dave, 2000) + logBalance(t.t, nodes, assetID, "after BTC only keysend") + + // Let's keysend the rest of the balance back to Charlie. + sendAssetKeySendPayment( + t.t, dave, charlie, charlieFundingAmount-charlieInvoiceAmount, + assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend back") + + charlieAssetBalance += charlieFundingAmount - charlieInvoiceAmount + daveAssetBalance -= charlieFundingAmount - charlieInvoiceAmount + + // ------------ + // Test case 2: Pay a normal invoice from Dave by Charlie, making it + // a direct channel invoice payment with no RFQ SCID present in the + // invoice. + // ------------ + paidAssetAmount := createAndPayNormalInvoice( + t.t, charlie, dave, dave, 20_000, assetID, withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= paidAssetAmount + daveAssetBalance += paidAssetAmount + + // We should also be able to do a multi-hop BTC only payment, paying an + // invoice from Erin by Charlie. + createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) + logBalance(t.t, nodes, assetID, "after BTC only invoice") + + // ------------ + // Test case 3: Pay an asset invoice from Dave by Charlie, making it + // a direct channel invoice payment with an RFQ SCID present in the + // invoice. + // ------------ + const daveInvoiceAssetAmount = 2_000 + invoiceResp = createAssetInvoice( + t.t, charlie, dave, daveInvoiceAssetAmount, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= daveInvoiceAssetAmount + daveAssetBalance += daveInvoiceAssetAmount + + // ------------ + // Test case 3.5: Pay an asset invoice from Dave by Charlie with normal + // satoshi payment flow. We expect that payment to fail, since it's a + // direct channel payment and the invoice is for assets, not sats. So + // without a conversion, it is rejected by the receiver. + // ------------ + invoiceResp = createAssetInvoice( + t.t, charlie, dave, daveInvoiceAssetAmount, assetID, + ) + payInvoiceWithSatoshi( + t.t, charlie, invoiceResp, withFailure( + lnrpc.Payment_FAILED, failureIncorrectDetails, + ), + ) + logBalance(t.t, nodes, assetID, "after asset invoice paid with sats") + + // We don't need to update the asset balances of Charlie and Dave here + // as the invoice payment failed. + + // ------------ + // Test case 4: Pay a normal invoice from Erin by Charlie. + // ------------ + paidAssetAmount = createAndPayNormalInvoice( + t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= paidAssetAmount + daveAssetBalance += paidAssetAmount + + // ------------ + // Test case 5: Create an asset invoice on Fabia and pay it from + // Charlie. + // ------------ + const fabiaInvoiceAssetAmount1 = 1000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount1, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= fabiaInvoiceAssetAmount1 + daveAssetBalance += fabiaInvoiceAssetAmount1 + erinAssetBalance -= fabiaInvoiceAssetAmount1 + fabiaAssetBalance += fabiaInvoiceAssetAmount1 + + // ------------ + // Test case 6: Create an asset invoice on Fabia and pay it with just + // BTC from Dave, making sure it ends up being a multipart payment (we + // set the maximum shard size to 80k sat and 15k asset units will be + // more than a single shard). + // ------------ + const fabiaInvoiceAssetAmount2 = 15_000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, + ) + payInvoiceWithSatoshi(t.t, dave, invoiceResp) + logBalance(t.t, nodes, assetID, "after invoice") + + erinAssetBalance -= fabiaInvoiceAssetAmount2 + fabiaAssetBalance += fabiaInvoiceAssetAmount2 + + // ------------ + // Test case 7: Create an asset invoice on Fabia and pay it with assets + // from Charlie, making sure it ends up being a multipart payment as + // well, with the high amount of asset units to send and the hard coded + // 80k sat max shard size. + // ------------ + const fabiaInvoiceAssetAmount3 = 10_000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount3, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= fabiaInvoiceAssetAmount3 + daveAssetBalance += fabiaInvoiceAssetAmount3 + erinAssetBalance -= fabiaInvoiceAssetAmount3 + fabiaAssetBalance += fabiaInvoiceAssetAmount3 + + // ------------ + // Test case 8: An invoice payment over two channels that are both asset + // channels. + // ------------ + logBalance(t.t, nodes, assetID, "before asset-to-asset") + + const yaraInvoiceAssetAmount1 = 1000 + invoiceResp = createAssetInvoice( + t.t, dave, yara, yaraInvoiceAssetAmount1, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after asset-to-asset") + + charlieAssetBalance -= yaraInvoiceAssetAmount1 + yaraAssetBalance += yaraInvoiceAssetAmount1 + + // ------------ + // Test case 8: Now we'll close each of the channels, starting with the + // Charlie -> Dave custom channel. + // ------------ + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, nil, + universeTap, assertDefaultCoOpCloseBalance(true, true), + ) + + t.Logf("Closing Dave -> Yara channel, close initiated by Yara") + closeAssetChannelAndAssert( + t, net, yara, dave, chanPointDY, assetID, nil, + universeTap, assertDefaultCoOpCloseBalance(false, true), + ) + + t.Logf("Closing Erin -> Fabia channel") + closeAssetChannelAndAssert( + t, net, erin, fabia, chanPointEF, assetID, nil, + universeTap, assertDefaultCoOpCloseBalance(true, true), + ) + + // We've been tracking the off-chain channel balances all this time, so + // now that we have the assets on-chain again, we can assert them. Due + // to rounding errors that happened when sending multiple shards with + // MPP, we need to do some slight adjustments. + charlieAssetBalance += 1 + erinAssetBalance += 4 + fabiaAssetBalance -= 4 + yaraAssetBalance -= 1 + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, fabiaTap, assetID, fabiaAssetBalance) + assertAssetBalance(t.t, yaraTap, assetID, yaraAssetBalance) + + // ------------ + // Test case 10: We now open a new asset channel and close it again, to + // make sure that a non-existent remote balance is handled correctly. + t.Logf("Opening new asset channel between Charlie and Dave...") + fundRespCD, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded second channel between Charlie and Dave: %v", fundRespCD) + + mineBlocks(t, net, 6, 1) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, assetID, nil, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex), + ) + assertAssetChan(t.t, charlie, dave, fundingAmount, cents) + + // And let's just close the channel again. + chanPointCD = &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, nil, + universeTap, assertDefaultCoOpCloseBalance(false, false), + ) + + // Charlie should still have four asset pieces, two with the same size. + assertNumAssetOutputs(t.t, charlieTap, assetID, 2) + assertAssetExists( + t.t, charlieTap, assetID, charlieAssetBalance-fundingAmount, + nil, true, false, false, + ) + assertAssetExists( + t.t, charlieTap, assetID, fundingAmount, nil, true, true, + false, + ) + + // Dave should have two outputs, one from the initial channel with Yara + // and one from the remaining amount of the channel with Charlie. + assertNumAssetOutputs(t.t, daveTap, assetID, 2) + daveFirstChannelRemainder := daveFundingAmount - + yaraInvoiceAssetAmount1 + 1 + assertAssetExists( + t.t, daveTap, assetID, daveFirstChannelRemainder, nil, true, + true, false, + ) + assertAssetExists( + t.t, daveTap, assetID, + daveAssetBalance-daveFirstChannelRemainder, nil, true, true, + false, + ) + + // Fabia and Yara should all have a single output each, just what was + // left over from the initial channel. + assertNumAssetOutputs(t.t, fabiaTap, assetID, 1) + assertAssetExists( + t.t, fabiaTap, assetID, fabiaAssetBalance, nil, true, true, + false, + ) + assertNumAssetOutputs(t.t, yaraTap, assetID, 1) + assertAssetExists( + t.t, yaraTap, assetID, yaraAssetBalance, nil, true, true, false, + ) + + // Erin didn't use all of his assets when opening the channel, so he + // should have two outputs, the change from the channel opening and the + // remaining amount after closing the channel. + assertNumAssetOutputs(t.t, erinTap, assetID, 2) + erinChange := startAmount - erinFundingAmount + assertAssetExists( + t.t, erinTap, assetID, erinAssetBalance-erinChange, nil, true, + true, false, + ) + assertAssetExists( + t.t, erinTap, assetID, erinChange, nil, true, false, false, + ) + + // The asset balances should still remain unchanged. + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, fabiaTap, assetID, fabiaAssetBalance) +} + +// testCustomChannelsGroupedAsset tests that we can create a network with custom +// channels that use grouped assets and send asset payments over them. +func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, + t *harnessTest) { + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + channelOp := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: 5_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, channelOp, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, channelOp) + assertChannelKnown(t.t, fabia, channelOp) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + groupAssetReq := itest.CopyRequest(&mintrpc.MintAssetRequest{ + Asset: itestAsset, + }) + groupAssetReq.Asset.NewGroupedAsset = true + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{groupAssetReq}, + ) + + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + groupID := cents.GetAssetGroup().GetTweakedGroupKey() + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + daveFundingAmount = uint64(startAmount) + erinFundingAmount = uint64(fundingAmount) + ) + charlieFundingAmount := cents.Amount - 2*startAmount + + chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, cents, startAmount, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, DefaultPushSat, + ) + + // We'll be tracking the expected asset balances throughout the test, so + // we can assert it after each action. + charlieAssetBalance := charlieFundingAmount + daveAssetBalance := uint64(startAmount) + erinAssetBalance := uint64(startAmount) + fabiaAssetBalance := uint64(0) + yaraAssetBalance := uint64(0) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + // Print initial channel balances. + logBalance(t.t, nodes, assetID, "initial") + + // ------------ + // Test case 1: Send a direct keysend payment from Charlie to Dave. + // ------------ + const keySendAmount = 100 + sendAssetKeySendPayment( + t.t, charlie, dave, keySendAmount, assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend") + + charlieAssetBalance -= keySendAmount + daveAssetBalance += keySendAmount + + // We should be able to send the 100 assets back immediately, because + // there is enough on-chain balance on Dave's side to be able to create + // an HTLC. + sendAssetKeySendPayment( + t.t, dave, charlie, keySendAmount, assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after keysend back") + + charlieAssetBalance += keySendAmount + daveAssetBalance -= keySendAmount + + // We should also be able to do a non-asset (BTC only) keysend payment. + sendKeySendPayment(t.t, charlie, dave, 2000) + logBalance(t.t, nodes, assetID, "after BTC only keysend") + + // ------------ + // Test case 2: Pay a normal invoice from Dave by Charlie, making it + // a direct channel invoice payment with no RFQ SCID present in the + // invoice. + // ------------ + paidAssetAmount := createAndPayNormalInvoice( + t.t, charlie, dave, dave, 20_000, assetID, withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= paidAssetAmount + daveAssetBalance += paidAssetAmount + + // We should also be able to do a multi-hop BTC only payment, paying an + // invoice from Erin by Charlie. + createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) + logBalance(t.t, nodes, assetID, "after BTC only invoice") + + // ------------ + // Test case 3: Pay an asset invoice from Dave by Charlie, making it + // a direct channel invoice payment with an RFQ SCID present in the + // invoice. + // ------------ + const daveInvoiceAssetAmount = 2_000 + invoiceResp := createAssetInvoice( + t.t, charlie, dave, daveInvoiceAssetAmount, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + // Make sure the invoice on the receiver side and the payment on the + // sender side show the individual HTLCs that arrived for it and that + // they show the correct asset amounts when decoded. + assertInvoiceHtlcAssets( + t.t, dave, invoiceResp, assetID, daveInvoiceAssetAmount, + ) + assertPaymentHtlcAssets( + t.t, charlie, invoiceResp.RHash, assetID, + daveInvoiceAssetAmount, + ) + + charlieAssetBalance -= daveInvoiceAssetAmount + daveAssetBalance += daveInvoiceAssetAmount + + // ------------ + // Test case 4: Pay a normal invoice from Erin by Charlie. + // ------------ + paidAssetAmount = createAndPayNormalInvoice( + t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= paidAssetAmount + daveAssetBalance += paidAssetAmount + + // ------------ + // Test case 5: Create an asset invoice on Fabia and pay it from + // Charlie. + // ------------ + const fabiaInvoiceAssetAmount1 = 1000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount1, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= fabiaInvoiceAssetAmount1 + daveAssetBalance += fabiaInvoiceAssetAmount1 + erinAssetBalance -= fabiaInvoiceAssetAmount1 + fabiaAssetBalance += fabiaInvoiceAssetAmount1 + + // ------------ + // Test case 6: Create an asset invoice on Fabia and pay it with just + // BTC from Dave, making sure it ends up being a multipart payment (we + // set the maximum shard size to 80k sat and 15k asset units will be + // more than a single shard). + // ------------ + const fabiaInvoiceAssetAmount2 = 15_000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount2, assetID, + ) + payInvoiceWithSatoshi(t.t, dave, invoiceResp) + logBalance(t.t, nodes, assetID, "after invoice") + + erinAssetBalance -= fabiaInvoiceAssetAmount2 + fabiaAssetBalance += fabiaInvoiceAssetAmount2 + + // ------------ + // Test case 7: Create an asset invoice on Fabia and pay it with assets + // from Charlie, making sure it ends up being a multipart payment as + // well, with the high amount of asset units to send and the hard coded + // 80k sat max shard size. + // ------------ + const fabiaInvoiceAssetAmount3 = 10_000 + invoiceResp = createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount3, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after invoice") + + charlieAssetBalance -= fabiaInvoiceAssetAmount3 + daveAssetBalance += fabiaInvoiceAssetAmount3 + erinAssetBalance -= fabiaInvoiceAssetAmount3 + fabiaAssetBalance += fabiaInvoiceAssetAmount3 + + // ------------ + // Test case 8: An invoice payment over two channels that are both asset + // channels. + // ------------ + logBalance(t.t, nodes, assetID, "before asset-to-asset") + + const yaraInvoiceAssetAmount1 = 1000 + invoiceResp = createAssetInvoice( + t.t, dave, yara, yaraInvoiceAssetAmount1, assetID, + ) + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), + ) + logBalance(t.t, nodes, assetID, "after asset-to-asset") + + charlieAssetBalance -= yaraInvoiceAssetAmount1 + yaraAssetBalance += yaraInvoiceAssetAmount1 + + // ------------ + // Test case 8: Now we'll close each of the channels, starting with the + // Charlie -> Dave custom channel. + // ------------ + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, groupID, + universeTap, assertDefaultCoOpCloseBalance(true, true), + ) + + t.Logf("Closing Dave -> Yara channel, close initiated by Yara") + closeAssetChannelAndAssert( + t, net, yara, dave, chanPointDY, assetID, groupID, + universeTap, assertDefaultCoOpCloseBalance(false, true), + ) + + t.Logf("Closing Erin -> Fabia channel") + closeAssetChannelAndAssert( + t, net, erin, fabia, chanPointEF, assetID, groupID, + universeTap, assertDefaultCoOpCloseBalance(true, true), + ) + + // We've been tracking the off-chain channel balances all this time, so + // now that we have the assets on-chain again, we can assert them. Due + // to rounding errors that happened when sending multiple shards with + // MPP, we need to do some slight adjustments. + charlieAssetBalance += 2 + daveAssetBalance -= 1 + erinAssetBalance += 4 + fabiaAssetBalance -= 4 + yaraAssetBalance -= 1 + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, fabiaTap, assetID, fabiaAssetBalance) + assertAssetBalance(t.t, yaraTap, assetID, yaraAssetBalance) + + // ------------ + // Test case 10: We now open a new asset channel and close it again, to + // make sure that a non-existent remote balance is handled correctly. + t.Logf("Opening new asset channel between Charlie and Dave...") + fundRespCD, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded second channel between Charlie and Dave: %v", fundRespCD) + + mineBlocks(t, net, 6, 1) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, nil, groupID, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex), + ) + assertAssetChan(t.t, charlie, dave, fundingAmount, cents) + + // And let's just close the channel again. + chanPointCD = &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, groupID, + universeTap, assertDefaultCoOpCloseBalance(false, false), + ) + + // Charlie should still have four asset pieces, two with the same size. + assertAssetExists( + t.t, charlieTap, assetID, charlieAssetBalance-fundingAmount, + nil, true, false, false, + ) + assertAssetExists( + t.t, charlieTap, assetID, fundingAmount, nil, true, true, + false, + ) + + // Charlie should have asset outputs: the leftover change from the + // channel funding, and the new close output. + assertNumAssetOutputs(t.t, charlieTap, assetID, 2) + + // The asset balances should still remain unchanged. + assertAssetBalance(t.t, charlieTap, assetID, charlieAssetBalance) + assertAssetBalance(t.t, daveTap, assetID, daveAssetBalance) + assertAssetBalance(t.t, erinTap, assetID, erinAssetBalance) + assertAssetBalance(t.t, fabiaTap, assetID, fabiaAssetBalance) +} + +// testCustomChannelsForceClose tests a force close scenario after both parties +// have an active asset balance. +func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness, + t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + // For our litd args, make sure that they all seen Zane as the main + // Universe server. + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // For this simple test, we'll just have Carol -> Dave as an assets + // channel. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + + // Next we'll connect all the nodes and also fund them with some coins. + nodes := []*HarnessNode{charlie, dave} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + + ctxb := context.Background() + + // Now we'll make an asset for Charlie that we'll use in the test to + // open a channel. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave) + t.Logf("Universes synced between all nodes, distributing assets...") + + // Before we actually create the asset channel, we want to make sure + // that failed attempts of creating a channel (e.g. due to insufficient + // on-chain funds) are cleaned up properly on the recipient side. + // We do this by sending all of Charlie's coins to a burn address then + // just sending him 50k sats, which isn't enough to fund a channel. + _, err = charlie.LightningClient.SendCoins( + ctxb, &lnrpc.SendCoinsRequest{ + Addr: burnAddr, + SendAll: true, + MinConfs: 0, + SpendUnconfirmed: true, + }, + ) + require.NoError(t.t, err) + net.SendCoins(t.t, 50_000, charlie) + + // The attempt should fail. But the recipient should receive the error, + // clean up the state and allow Charlie to try again after acquiring + // more funds. + _, err = charlieTap.FundChannel(ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + }) + require.ErrorContains(t.t, err, "not enough witness outputs to create") + + // Now we'll fund the channel with the correct amount. + net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, charlie) + + // Next we can open an asset channel from Charlie -> Dave, then kick + // off the main scenario. + t.Logf("Opening asset channels...") + assetFundResp, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp) + + // With the channel open, mine a block to confirm it. + mineBlocks(t, net, 6, 1) + + // A transfer for the funding transaction should be found in Charlie's + // DB. + fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid) + require.NoError(t.t, err) + assetFundingTransfer := locateAssetTransfers( + t.t, charlieTap, *fundingTxid, + ) + + t.Logf("Channel funding transfer: %v", + toProtoJSON(t.t, assetFundingTransfer)) + + // Charlie's balance should reflect that the funding asset is now + // excluded from balance reporting by tapd. + assertAssetBalance( + t.t, charlieTap, assetID, itestAsset.Amount-fundingAmount, + ) + + // Make sure that Charlie properly uploaded funding proof to the + // Universe server. + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + assertUniverseProofExists( + t.t, universeTap, assetID, nil, fundingScriptTreeBytes, + fmt.Sprintf( + "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, + ), + ) + + // Make sure the channel shows the correct asset information. + assertAssetChan(t.t, charlie, dave, fundingAmount, cents) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + + // We'll also have dave sync with Charlie+Zane to ensure he has the + // proof for the funding output. We sync the transfers as well so he + // has all the proofs needed. + mode := universerpc.UniverseSyncMode_SYNC_FULL + diff, err := daveTap.SyncUniverse(ctxb, &universerpc.SyncRequest{ + UniverseHost: zane.Cfg.LitAddr(), + SyncMode: mode, + }) + require.NoError(t.t, err) + + t.Logf("Synced Dave w/ Zane, universe_diff=%v", toProtoJSON(t.t, diff)) + + // With the channel confirmed, we'll push over some keysend payments + // from Carol to Dave. We'll send over a bit more BTC each time so Dave + // will go to chain sweep his output (default fee rate is 50 sat/vb). + const ( + numPayments = 5 + keySendAmount = 100 + btcAmt = int64(5_000) + ) + for i := 0; i < numPayments; i++ { + sendAssetKeySendPayment( + t.t, charlie, dave, keySendAmount, assetID, + fn.Some(btcAmt), + ) + } + + logBalance(t.t, nodes, assetID, "after keysend") + + // With the payments sent, we'll now go on chain with a force close + // from Carol. + t.Logf("Force closing channel...") + charlieChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(assetFundResp.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: assetFundResp.Txid, + }, + } + _, closeTxid, err := net.CloseChannel(charlie, charlieChanPoint, true) + require.NoError(t.t, err) + + t.Logf("Channel closed! Mining blocks, close_txid=%v", closeTxid) + + // Next, we'll mine a block to confirm the force close. + mineBlocks(t, net, 1, 1) + + // At this point, we should have the force close transaction in the set + // of transfers for both nodes. + var forceCloseTransfer *taprpc.ListTransfersResponse + fErr := wait.NoError(func() error { + forceCloseTransfer, err = charlieTap.ListTransfers( + ctxb, &taprpc.ListTransfersRequest{ + AnchorTxid: closeTxid.String(), + }, + ) + if err != nil { + return fmt.Errorf("unable to list charlie transfers: "+ + "%w", err) + } + if len(forceCloseTransfer.Transfers) != 1 { + return fmt.Errorf("charlie is missing force close " + + "transfer") + } + + forceCloseTransfer2, err := daveTap.ListTransfers( + ctxb, &taprpc.ListTransfersRequest{ + AnchorTxid: closeTxid.String(), + }, + ) + if err != nil { + return fmt.Errorf("unable to list dave transfers: %w", + err) + } + if len(forceCloseTransfer2.Transfers) != 1 { + return fmt.Errorf("dave is missing force close " + + "transfer") + } + + return nil + }, defaultTimeout) + require.NoError(t.t, fErr) + + t.Logf("Force close transfer: %v", toProtoJSON(t.t, forceCloseTransfer)) + + // Now that we have the transfer on disk, we'll also assert that the + // universe also has proof for both the relevant transfer outputs. + for _, transfer := range forceCloseTransfer.Transfers { + for _, transferOut := range transfer.Outputs { + assertUniverseProofExists( + t.t, universeTap, assetID, nil, + transferOut.ScriptKey, + transferOut.Anchor.Outpoint, + ) + } + } + + t.Logf("Universe proofs located!") + + time.Sleep(time.Second * 1) + + // We'll mine one more block, which triggers the 1 CSV needed for Dave + // to sweep his output. + mineBlocks(t, net, 1, 0) + + // We should also have a new sweep transaction in the mempool. + daveSweepTxid, err := waitForNTxsInMempool( + net.Miner.Client, 1, time.Second*5, + ) + require.NoError(t.t, err) + + t.Logf("Dave sweep txid: %v", daveSweepTxid) + + // Next, we'll mine a block to confirm Dave's sweep transaction. + // This'll sweep his non-delay commitment output. + mineBlocks(t, net, 1, 1) + + // At this point, a transfer should have been created for Dave's sweep + // transaction. + daveSweepTransfer := locateAssetTransfers( + t.t, daveTap, *daveSweepTxid[0], + ) + + t.Logf("Dave sweep transfer: %v", toProtoJSON(t.t, daveSweepTransfer)) + + time.Sleep(time.Second * 1) + + // Next, we'll mine three additional blocks to trigger the CSV delay + // for Charlie. + mineBlocks(t, net, 3, 0) + + // We expect that Charlie's sweep transaction has been broadcast. + charlieSweepTxid, err := waitForNTxsInMempool( + net.Miner.Client, 1, time.Second*5, + ) + require.NoError(t.t, err) + + t.Logf("Charlie sweep txid: %v", charlieSweepTxid) + + // Now we'll mine a block to confirm Charlie's sweep transaction. + mineBlocks(t, net, 1, 0) + + // Charlie should now have an asset transfer for his sweep transaction. + charlieSweepTransfer := locateAssetTransfers( + t.t, charlieTap, *charlieSweepTxid[0], + ) + + t.Logf("Charlie sweep transfer: %v", toProtoJSON( + t.t, charlieSweepTransfer, + )) + + // Both sides should now reflect their updated asset balances. + daveBalance := uint64(numPayments * keySendAmount) + charlieBalance := itestAsset.Amount - daveBalance + assertAssetBalance(t.t, daveTap, assetID, daveBalance) + assertAssetBalance(t.t, charlieTap, assetID, charlieBalance) + + // Dave should have a single managed UTXO that shows he has a new asset + // UTXO he can use. + assertNumAssetUTXOs(t.t, daveTap, 1) + assertNumAssetUTXOs(t.t, charlieTap, 2) + + // We'll make sure Dave can spend his asset UTXO by sending it all but + // one unit to Zane (the universe). + assetSendAmount := daveBalance - 1 + zaneAddr, err := universeTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset from Dave units to Zane...", assetSendAmount) + + // Send the assets to Zane. We expect Dave to have 3 transfers: the + // funding txn, their force close sweep, and now this new send. + itest.AssertAddrCreated(t.t, universeTap, cents, zaneAddr) + sendResp, err := daveTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{zaneAddr.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, daveTap, sendResp, assetID, + []uint64{1, assetSendAmount}, 2, 3, + ) + itest.AssertNonInteractiveRecvComplete(t.t, universeTap, 1) + + // And now we also send all assets but one from Charlie to the universe + // to make sure the time lock sweep output can also be spent correctly. + assetSendAmount = charlieBalance - 1 + zaneAddr2, err := universeTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: assetSendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset from Charlie units to Zane...", + assetSendAmount) + + itest.AssertAddrCreated(t.t, universeTap, cents, zaneAddr2) + sendResp2, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{zaneAddr2.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp2, assetID, + []uint64{1, assetSendAmount}, 3, 4, + ) + itest.AssertNonInteractiveRecvComplete(t.t, universeTap, 2) +} + +// testCustomChannelsBreach tests a force close scenario that breaches an old +// state, after both parties have an active asset balance. +func testCustomChannelsBreach(_ context.Context, net *NetworkHarness, + t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + // For our litd args, make sure that they all seen Zane as the main + // Universe server. + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // Charlie will be the breached party. We set --nolisten to ensure Dave + // won't be able to connect to him and trigger the channel protection + // logic automatically. We also can't have Charlie automatically + // reconnect too early, otherwise DLP would be initiated instead of the + // breach we want to provoke. + charlieFlags := append( + slices.Clone(lndArgs), "--nolisten", "--minbackoff=1h", + ) + + // For this simple test, we'll just have Carol -> Dave as an assets + // channel. + charlie, err := net.NewNode( + t.t, "Charlie", charlieFlags, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + + // Next we'll connect all the nodes and also fund them with some coins. + nodes := []*HarnessNode{charlie, dave} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + + ctxb := context.Background() + + // Now we'll make an asset for Charlie that we'll use in the test to + // open a channel. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave) + t.Logf("Universes synced between all nodes, distributing assets...") + + // Next we can open an asset channel from Charlie -> Dave, then kick + // off the main scenario. + t.Logf("Opening asset channels...") + assetFundResp, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Charlie and Dave: %v", assetFundResp) + + // With the channel open, mine a block to confirm it. + mineBlocks(t, net, 6, 1) + + // A transfer for the funding transaction should be found in Charlie's + // DB. + fundingTxid, err := chainhash.NewHashFromStr(assetFundResp.Txid) + require.NoError(t.t, err) + assetFundingTransfer := locateAssetTransfers( + t.t, charlieTap, *fundingTxid, + ) + + t.Logf("Channel funding transfer: %v", + toProtoJSON(t.t, assetFundingTransfer)) + + // Charlie's balance should reflect that the funding asset is now + // excluded from balance reporting by tapd. + assertAssetBalance( + t.t, charlieTap, assetID, itestAsset.Amount-fundingAmount, + ) + + // Make sure that Charlie properly uploaded funding proof to the + // Universe server. + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + assertUniverseProofExists( + t.t, universeTap, assetID, nil, fundingScriptTreeBytes, + fmt.Sprintf( + "%v:%v", assetFundResp.Txid, assetFundResp.OutputIndex, + ), + ) + + // Make sure the channel shows the correct asset information. + assertAssetChan(t.t, charlie, dave, fundingAmount, cents) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + + // Next, we'll make keysend payments from Charlie to Dave. we'll use + // this to reach a state where both parties have funds in the channel. + const ( + numPayments = 5 + keySendAmount = 100 + btcAmt = int64(5_000) + ) + for i := 0; i < numPayments; i++ { + sendAssetKeySendPayment( + t.t, charlie, dave, keySendAmount, assetID, + fn.Some(btcAmt), + ) + } + + logBalance(t.t, nodes, assetID, "after keysend -- breach state") + + // Now we'll create an on disk snapshot that we'll use to restore back + // to as our breached state. + require.NoError(t.t, net.StopAndBackupDB(dave)) + connectAllNodes(t.t, net, nodes) + + // We'll send one more keysend payment now to revoke the state we were + // just at above. + sendAssetKeySendPayment( + t.t, charlie, dave, keySendAmount, assetID, fn.Some(btcAmt), + ) + logBalance(t.t, nodes, assetID, "after keysend -- final state") + + // With the final state achieved, we'll now restore Dave (who will be + // force closing) to that old state, the breach state. + require.NoError(t.t, net.StopAndRestoreDB(dave)) + + // With Dave restored, we'll now execute the force close. + t.Logf("Force close by Dave to breach...") + daveChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(assetFundResp.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: assetFundResp.Txid, + }, + } + _, breachTxid, err := net.CloseChannel(dave, daveChanPoint, true) + require.NoError(t.t, err) + + t.Logf("Channel closed! Mining blocks, close_txid=%v", breachTxid) + + // Next, we'll mine a block to confirm the breach transaction. + mineBlocks(t, net, 1, 1) + + // We should be able to find the transfer of the breach for both + // parties. + charlieBreachTransfer := locateAssetTransfers( + t.t, charlieTap, *breachTxid, + ) + daveBreachTransfer := locateAssetTransfers( + t.t, daveTap, *breachTxid, + ) + + t.Logf("Charlie breach transfer: %v", + toProtoJSON(t.t, charlieBreachTransfer)) + t.Logf("Dave breach transfer: %v", + toProtoJSON(t.t, daveBreachTransfer)) + + // With the breach transaction mined, Charlie should now have a + // transaction in the mempool sweeping the *both* commitment outputs. + charlieJusticeTxid, err := waitForNTxsInMempool( + net.Miner.Client, 1, time.Second*5, + ) + require.NoError(t.t, err) + + t.Logf("Charlie justice txid: %v", charlieJusticeTxid) + + // Next, we'll mine a block to confirm Charlie's justice transaction. + mineBlocks(t, net, 1, 1) + + // Charlie should now have a transfer for his justice transaction. + charlieJusticeTransfer := locateAssetTransfers( + t.t, charlieTap, *charlieJusticeTxid[0], + ) + + t.Logf("Charlie justice transfer: %v", + toProtoJSON(t.t, charlieJusticeTransfer)) + + // Charlie's balance should now be the same as before the breach + // attempt: the amount he minted at the very start. + charlieBalance := itestAsset.Amount + assertAssetBalance(t.t, charlieTap, assetID, charlieBalance) + + t.Logf("Charlie balance after breach: %d", charlieBalance) + + // Charlie should now have 2 total UTXOs: the change from the funding + // output, and now the sweep output from the justice transaction. + charlieUTXOs := assertNumAssetUTXOs(t.t, charlieTap, 2) + + t.Logf("Charlie UTXOs after breach: %v", toProtoJSON(t.t, charlieUTXOs)) +} + +// testCustomChannelsLiquidityEdgeCases is a test that runs through some +// taproot asset channel liquidity related edge cases. +func testCustomChannelsLiquidityEdgeCases(ctxb context.Context, + net *NetworkHarness, t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + channelOp := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, channelOp, true) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, channelOp) + assertChannelKnown(t.t, fabia, channelOp) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + daveFundingAmount = uint64(400_000) + erinFundingAmount = uint64(200_000) + ) + charlieFundingAmount := cents.Amount - uint64(2*400_000) + + _, _, _ = createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, cents, 400_000, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, 0, + ) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + logBalance(t.t, nodes, assetID, "initial") + + // Normal case. + // Send 50 assets from Charlie to Dave. + sendAssetKeySendPayment( + t.t, charlie, dave, 50, assetID, fn.None[int64](), + ) + + logBalance(t.t, nodes, assetID, "after 50 assets") + + // Normal case. + // Send 1k sats from Charlie to Dave. + sendKeySendPayment(t.t, charlie, dave, 1000) + + logBalance(t.t, nodes, assetID, "after 1k sats") + + // Edge case: The channel reserve check should trigger, and we should + // get a payment failure, not a timeout. + // + // Now Dave tries to send 50 assets to Charlie. There shouldn't be + // enough sats in the channel. + // + // Assume an acceptable completion window which is half the payment + // timeout. If the payment succeeds within this duration this means we + // didn't fall into a routing loop. + timeoutChan := time.After(PaymentTimeout / 2) + done := make(chan bool, 1) + + go func() { + sendAssetKeySendPayment( + t.t, dave, charlie, 50, assetID, fn.None[int64](), + withFailure(lnrpc.Payment_FAILED, failureNoRoute), + ) + + done <- true + }() + + select { + case <-done: + case <-timeoutChan: + t.Fatalf("Payment didn't fail within expected time duration") + } + + logBalance(t.t, nodes, assetID, "after failed 50 assets") + + // Send 10k sats from Charlie to Dave. + sendKeySendPayment(t.t, charlie, dave, 10000) + + logBalance(t.t, nodes, assetID, "10k sats") + + // Now Dave tries to send 50 assets again, this time he should have + // enough sats. + sendAssetKeySendPayment( + t.t, dave, charlie, 50, assetID, fn.None[int64](), + ) + + logBalance(t.t, nodes, assetID, "after 50 sats backwards") + + // Edge case: This refers to a bug where an asset allocation would be + // expected for this HTLC. This is a dust HTLC and it can not carry + // assets. + // + // Send 1 sat from Charlie to Dave. + sendKeySendPayment(t.t, charlie, dave, 1) + + logBalance(t.t, nodes, assetID, "after 1 sat") + + // Pay a normal bolt11 invoice involving RFQ flow. + _ = createAndPayNormalInvoice( + t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), + ) + + logBalance(t.t, nodes, assetID, "after 20k sat asset payment") + + // Edge case: There was a bug when paying an asset invoice that would + // evaluate to more than the channel capacity, causing a payment failure + // even though enough asset balance exists. + // + // Pay a bolt11 invoice with assets, which evaluates to more than the + // channel btc capacity. + _ = createAndPayNormalInvoice( + t.t, charlie, dave, erin, 1_000_000, assetID, withSmallShards(), + ) + + logBalance(t.t, nodes, assetID, "after big asset payment (btc "+ + "invoice, multi-hop)") + + // Edge case: Big asset invoice paid by direct peer with assets. + const bigAssetAmount = 100_000 + invoiceResp := createAssetInvoice( + t.t, charlie, dave, bigAssetAmount, assetID, + ) + + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + ) + + logBalance(t.t, nodes, assetID, "after big asset payment (asset "+ + "invoice, direct)") + + // Make sure the invoice on the receiver side and the payment on the + // sender side show the individual HTLCs that arrived for it and that + // they show the correct asset amounts when decoded. + assertInvoiceHtlcAssets( + t.t, dave, invoiceResp, assetID, bigAssetAmount, + ) + assertPaymentHtlcAssets( + t.t, charlie, invoiceResp.RHash, assetID, bigAssetAmount, + ) + + // Edge case: Big normal invoice, paid by direct channel peer with + // assets. + const hugeAssetAmount = 1_000_000 + _ = createAndPayNormalInvoice( + t.t, dave, charlie, charlie, hugeAssetAmount, assetID, + withSmallShards(), + ) + + logBalance(t.t, nodes, assetID, "after big asset payment (btc "+ + "invoice, direct)") + + // Dave sends 200k assets and 5k sats to Yara. + sendAssetKeySendPayment( + t.t, dave, yara, 2*bigAssetAmount, assetID, fn.None[int64](), + ) + sendKeySendPayment(t.t, dave, yara, 5_000) + + logBalance(t.t, nodes, assetID, "after 200k assets to Yara") + + // Edge case: Now Charlie creates a big asset invoice to be paid for by + // Yara with assets. This is a multi-hop payment going over 2 asset + // channels, where the total asset value exceeds the btc capacity of the + // channels. + invoiceResp = createAssetInvoice( + t.t, dave, charlie, bigAssetAmount, assetID, + ) + + payInvoiceWithAssets( + t.t, yara, dave, invoiceResp.PaymentRequest, assetID, + ) + + logBalance(t.t, nodes, assetID, "after big asset payment (asset "+ + "invoice, multi-hop)") + + // Edge case: Now Charlie creates a tiny asset invoice to be paid for by + // Yara with satoshi. This is a multi-hop payment going over 2 asset + // channels, where the total asset value is less than the default anchor + // amount of 354 sats. + invoiceResp = createAssetInvoice(t.t, dave, charlie, 1, assetID) + payInvoiceWithSatoshi(t.t, yara, invoiceResp, withFailure( + lnrpc.Payment_FAILED, failureNoRoute, + )) + + logBalance(t.t, nodes, assetID, "after small payment (asset "+ + "invoice, <354sats)") + + // Edge case: Now Dave creates an asset invoice to be paid for by + // Yara with satoshi. For the last hop we try to settle the invoice in + // satoshi, where we will check whether Dave's strict forwarding works + // as expected. Charlie is only used as a dummy RFQ peer in this case, + // Yara totally ignored the RFQ hint and pays agnostically with sats. + invoiceResp = createAssetInvoice(t.t, charlie, dave, 1, assetID) + + stream, err := dave.InvoicesClient.SubscribeSingleInvoice( + ctxb, &invoicesrpc.SubscribeSingleInvoiceRequest{ + RHash: invoiceResp.RHash, + }, + ) + require.NoError(t.t, err) + + // Yara pays Dave with enough satoshis, but Charlie will not settle as + // he expects assets. + payInvoiceWithSatoshiLastHop( + t.t, yara, invoiceResp, dave.PubKey[:], lnrpc.Payment_FAILED, + ) + + t.lndHarness.LNDHarness.AssertInvoiceState(stream, lnrpc.Invoice_OPEN) + + logBalance(t.t, nodes, assetID, "after failed payment (asset "+ + "invoice, strict forwarding)") + + // Edge case: Check if the RFQ HTLC tracking accounts for cancelled + // HTLCs. We achieve this by manually creating & using an RFQ quote with + // a set max amount. We first pay to a hodl invoice that we eventually + // cancel, then pay to a normal invoice which should succeed. + + // We start by sloshing some funds in the Erin<->Fabia. + sendAssetKeySendPayment( + t.t, erin, fabia, 100_000, assetID, fn.Some[int64](20_000), + ) + + logBalance(t.t, nodes, assetID, "balance after 1st slosh") + + // We create the RFQ order. We set the max amt to ~180k sats which is + // going to evaluate to about 10k assets. + inOneHour := time.Now().Add(time.Hour) + resQ, err := charlieTap.RfqClient.AddAssetSellOrder( + ctxb, &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + PaymentMaxAmt: 180_000_000, + Expiry: uint64(inOneHour.Unix()), + PeerPubKey: dave.PubKey[:], + TimeoutSeconds: 100, + }, + ) + require.NoError(t.t, err) + + // We now create a hodl invoice on Fabia, for 10k assets. + hodlInv := createAssetHodlInvoice(t.t, erin, fabia, 10_000, assetID) + + // Charlie tries to pay via Dave, by providing the RFQ quote ID that was + // manually created above. + var quoteID rfqmsg.ID + copy(quoteID[:], resQ.GetAcceptedQuote().Id) + payInvoiceWithAssets( + t.t, charlie, dave, hodlInv.payReq, assetID, withSmallShards(), + withFailure(lnrpc.Payment_IN_FLIGHT, failureNone), + withRFQ(quoteID), + ) + + // We now assert that the expected numbers of HTLCs are present on each + // node. + // Reminder, topology looks like this: + // + // Charlie <-> Dave <-> Erin <-> Fabia + // + // Therefore the routing nodes should have double the number of HTLCs + // required for the payment present. + assertNumHtlcs(t.t, charlie, 3) + assertNumHtlcs(t.t, dave, 6) + assertNumHtlcs(t.t, erin, 6) + assertNumHtlcs(t.t, fabia, 3) + + // Now let's cancel the invoice on Fabia. + payHash := hodlInv.preimage.Hash() + _, err = fabia.InvoicesClient.CancelInvoice( + ctxb, &invoicesrpc.CancelInvoiceMsg{ + PaymentHash: payHash[:], + }, + ) + require.NoError(t.t, err) + + // There should be no HTLCs present on any channel. + assertNumHtlcs(t.t, charlie, 0) + assertNumHtlcs(t.t, dave, 0) + assertNumHtlcs(t.t, erin, 0) + assertNumHtlcs(t.t, fabia, 0) + + // Now Fabia creates the normal invoice. + invoiceResp = createAssetInvoice( + t.t, erin, fabia, 10_000, assetID, + ) + + // Now Charlie pays the invoice, again by using the manually specified + // RFQ quote ID. This payment should succeed. + payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + withSmallShards(), withRFQ(quoteID), + ) + + logBalance(t.t, nodes, assetID, "after manual rfq hodl") + + // Edge case: Charlie negotiates a quote with Dave which has a low max + // amount (~170k sats). Then Charlie creates an invoice with a total + // amount slightly larger than the max allowed in the quote (200k sats). + // Erin will try to pay that invoice with sats, in shards of max size + // 80k sats. Dave will eventually stop forwarding HTLCs as the RFQ HTLC + // tracking mechanism should stop them from being forwarded, as they + // violate the maximum allowed amount of the quote. + + // Charlie starts by negotiating the quote. + inOneHour = time.Now().Add(time.Hour) + res, err := charlieTap.RfqClient.AddAssetBuyOrder( + ctxb, &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + AssetMaxAmt: 10_000, + Expiry: uint64(inOneHour.Unix()), + PeerPubKey: dave.PubKey[:], + TimeoutSeconds: 10, + }, + ) + require.NoError(t.t, err) + + type acceptedQuote = *rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote + quote, ok := res.Response.(acceptedQuote) + require.True(t.t, ok) + + // We now manually add the invoice in order to inject the above, + // manually generated, quote. + iResp, err := charlie.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "", + Value: 200_000, + RPreimage: bytes.Repeat([]byte{11}, 32), + CltvExpiry: 60, + RouteHints: []*lnrpc.RouteHint{{ + HopHints: []*lnrpc.HopHint{{ + NodeId: dave.PubKeyStr, + ChanId: quote.AcceptedQuote.Scid, + }}, + }}, + }) + require.NoError(t.t, err) + + // Now Erin tries to pay the invoice. Since rfq quote cannot satisfy the + // total amount of the invoice this payment will fail. + payInvoiceWithSatoshi( + t.t, erin, iResp, withExpectTimeout(), + withFailure(lnrpc.Payment_FAILED, failureNone), + ) + + logBalance(t.t, nodes, assetID, "after small manual rfq") +} + +// testCustomChannelsBalanceConsistency is a test that test the balance of nodes +// under channel opening circumstances. +func testCustomChannelsBalanceConsistency(_ context.Context, + net *NetworkHarness, t *harnessTest) { + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + universeTap := newTapClient(t.t, zane) + + // Mint an asset on Charlie and sync Dave to Charlie as the universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + var groupKey []byte + if cents.AssetGroup != nil { + groupKey = cents.AssetGroup.TweakedGroupKey + } + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave) + t.Logf("Universes synced between all nodes, distributing assets...") + + charlieBalance := cents.Amount + + // Charlie should have a single balance output with the full balance. + assertAssetBalance(t.t, charlieTap, assetID, cents.Amount) + + // The script key should be local to charlie, and the script key should + // be known. It is after all the asset he just minted himself. + scriptKeyLocal := true + scriptKeyKnown := false + scriptKeyHasScriptPath := false + + scriptKey, err := schnorr.ParsePubKey(cents.ScriptKey[1:]) + require.NoError(t.t, err) + assertAssetExists( + t.t, charlieTap, assetID, charlieBalance, + scriptKey, scriptKeyLocal, scriptKeyKnown, + scriptKeyHasScriptPath, + ) + + fundingScriptTree := tapscript.NewChannelFundingScriptTree() + fundingScriptKey := fundingScriptTree.TaprootKey + fundingScriptTreeBytes := fundingScriptKey.SerializeCompressed() + + fundRespCD, err := charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: charlieBalance, + AssetId: assetID, + PeerPubkey: daveTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: 0, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD) + + // Make sure the pending channel shows up in the list and has the + // custom records set as JSON. + assertPendingChannels( + t.t, charlieTap.node, cents, 1, charlieBalance, 0, + ) + + // Let's confirm the channel. + mineBlocks(t, net, 6, 1) + + // Tapd should not report any balance for Charlie, since the asset is + // used in a funding transaction. It should also not report any balance + // for Dave. All those balances are reported through channel balances. + assertAssetBalance(t.t, charlieTap, assetID, 0) + assertAssetBalance(t.t, daveTap, assetID, 0) + + // There should only be a single asset piece for Charlie, the one in the + // channel. + assertNumAssetOutputs(t.t, charlieTap, assetID, 1) + + // The script key should now not be local anymore, since he funded a + // channel with it. Charlie does still know the script key though. + scriptKeyLocal = false + scriptKeyKnown = true + scriptKeyHasScriptPath = true + assertAssetExists( + t.t, charlieTap, assetID, charlieBalance, + fundingScriptKey, scriptKeyLocal, scriptKeyKnown, + scriptKeyHasScriptPath, + ) + + // Assert that the proofs for both channels has been uploaded to the + // designated Universe server. + assertUniverseProofExists( + t.t, universeTap, assetID, groupKey, fundingScriptTreeBytes, + fmt.Sprintf("%v:%v", fundRespCD.Txid, fundRespCD.OutputIndex), + ) + + // Make sure the channel shows the correct asset information. + assertAssetChan( + t.t, charlieTap.node, daveTap.node, charlieBalance, cents, + ) + + logBalance(t.t, nodes, assetID, "initial") + + // Normal case. + // Send 500 assets from Charlie to Dave. + sendAssetKeySendPayment( + t.t, charlie, dave, 500, assetID, fn.None[int64](), + ) + + logBalance(t.t, nodes, assetID, "after 500 assets") + + // Tapd should still not report balances for Charlie and Dave, since + // they are still locked up in the funding transaction. + assertAssetBalance(t.t, charlieTap, assetID, 0) + assertAssetBalance(t.t, daveTap, assetID, 0) + + // Send 10k sats from Charlie to Dave. Dave needs the sats to be able to + // send assets. + sendKeySendPayment(t.t, charlie, dave, 10000) + + // Now Dave tries to send 250 assets. + sendAssetKeySendPayment( + t.t, dave, charlie, 250, assetID, fn.None[int64](), + ) + + logBalance(t.t, nodes, assetID, "after 250 sats backwards") + + // Tapd should still not report balances for Charlie and Dave, since + // they are still locked up in the funding transaction. + assertAssetBalance(t.t, charlieTap, assetID, 0) + assertAssetBalance(t.t, daveTap, assetID, 0) + + // We will now close the channel. + t.Logf("Close the channel between Charlie and Dave...") + charlieChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + + closeChannelAndAssert(t, net, charlie, charlieChanPoint, false) + + // Charlie should have a single balance output with the balance 250 less + // than the total amount minted. + assertAssetBalance(t.t, charlieTap, assetID, charlieBalance-250) + assertAssetBalance(t.t, daveTap, assetID, 250) + + // The script key should now be local to both Charlie and Dave, since + // the channel was closed. + scriptKeyLocal = true + scriptKeyKnown = true + scriptKeyHasScriptPath = false + assertAssetExists( + t.t, charlieTap, assetID, charlieBalance-250, + nil, scriptKeyLocal, scriptKeyKnown, scriptKeyHasScriptPath, + ) + assertAssetExists( + t.t, daveTap, assetID, 250, + nil, scriptKeyLocal, scriptKeyKnown, scriptKeyHasScriptPath, + ) + + assertNumAssetOutputs(t.t, charlieTap, assetID, 1) + assertNumAssetOutputs(t.t, daveTap, assetID, 1) +} + +// testCustomChannelsSingleAssetMultiInput tests whether it is possible to fund +// a channel using FundChannel that uses multiple inputs from the same asset. +func testCustomChannelsSingleAssetMultiInput(_ context.Context, + net *NetworkHarness, t *harnessTest) { + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + + // Mint an assets on Charlie and sync Dave to Charlie as the universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", + cents.Amount) + syncUniverses(t.t, charlieTap, dave) + t.Logf("Universes synced between all nodes, distributing assets...") + + // Charlie should have two balance outputs with the full balance. + assertAssetBalance(t.t, charlieTap, assetID, cents.Amount) + + // Send assets to Dave so he can fund a channel. + halfCentsAmount := cents.Amount / 2 + daveAddr1, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: halfCentsAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + daveAddr2, err := daveTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: halfCentsAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlieTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + t.Logf("Sending %v asset units to Dave twice...", halfCentsAmount) + + // Send the assets to Dave. + itest.AssertAddrCreated(t.t, daveTap, cents, daveAddr1) + itest.AssertAddrCreated(t.t, daveTap, cents, daveAddr2) + sendResp, err := charlieTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr1.Encoded, daveAddr2.Encoded}, + }) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransferWithOutputs( + t.t, t.lndHarness.Miner.Client, charlieTap, sendResp, assetID, + []uint64{ + cents.Amount - 2*halfCentsAmount, halfCentsAmount, + halfCentsAmount, + }, 0, 1, 3, + ) + itest.AssertNonInteractiveRecvComplete(t.t, daveTap, 2) + + // Fund a channel using multiple inputs from the same asset. + fundRespCD, err := daveTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: 2 * halfCentsAmount, + AssetId: assetID, + PeerPubkey: charlieTap.node.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: 0, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Charlie and Dave: %v", fundRespCD) + + // Let's confirm the channel. + mineBlocks(t, net, 6, 1) + + // Tapd should not report any balance for Charlie, since the asset is + // used in a funding transaction. It should also not report any balance + // for Dave. All those balances are reported through channel balances. + assertAssetBalance(t.t, charlieTap, assetID, 0) + assertAssetBalance(t.t, daveTap, assetID, 0) + + // Make sure the channel shows the correct asset information. + assertAssetChan( + t.t, charlieTap.node, daveTap.node, 2*halfCentsAmount, cents, + ) +} + +// testCustomChannelsOraclePricing tests that all asset transfers are correctly +// priced when using an oracle that isn't tapd's mock oracle. +func testCustomChannelsOraclePricing(_ context.Context, + net *NetworkHarness, t *harnessTest) { + + usdMetaData := &taprpc.AssetMeta{ + Data: []byte(`{ +"description":"this is a USD stablecoin with decimal display of 6" +}`), + Type: taprpc.AssetMetaType_META_TYPE_JSON, + } + + const decimalDisplay = 6 + itestAsset = &mintrpc.MintAsset{ + AssetType: taprpc.AssetType_NORMAL, + Name: "USD", + AssetMeta: usdMetaData, + // We mint 1 million USD with a decimal display of 6, which + // results in 1 trillion asset units. + Amount: 1_000_000_000_000, + DecimalDisplay: decimalDisplay, + } + + oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort()) + oracle := newOracleHarness(oracleAddr) + oracle.start(t.t) + t.t.Cleanup(oracle.stop) + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplateNoOracle) + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.experimental.rfq.priceoracleaddress="+ + "rfqrpc://%s", oracleAddr, + )) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + const btcChannelFundingAmount = 10_000_000 + chanPointDE := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: btcChannelFundingAmount, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, chanPointDE, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, chanPointDE) + assertChannelKnown(t.t, fabia, chanPointDE) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + // Mint an asset on Charlie and sync Dave to Charlie as the universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + usdAsset := mintedAssets[0] + assetID := usdAsset.AssetGenesis.AssetId + + // Now that we've minted the asset, we can set the price in the oracle. + var id asset.ID + copy(id[:], assetID) + + // Let's assume the current USD price for 1 BTC is 66,548.40. We'll take + // that price and add a 4% spread, 2% on each side (buy/sell) to earn + // money as the oracle. 2% is 1,330.97, so we'll set the sell price to + // 65,217.43 and the purchase price to 67,879.37. + // The following numbers are to help understand the magic numbers below. + // They're the price in USD/BTC, the price of 1 USD in sats and the + // expected price in asset units per BTC. + // 65,217.43 => 1533.332 => 65_217_430_000 + // 66,548.40 => 1502.666 => 66_548_400_000 + // 67,879.37 => 1473.202 => 67_879_370_000 + salePrice := rfqmath.NewBigIntFixedPoint(65_217_43, 2) + purchasePrice := rfqmath.NewBigIntFixedPoint(67_879_37, 2) + + // We now have the prices defined in USD. But the asset has a decimal + // display of 6, so we need to multiply them by 10^6. + factor := rfqmath.NewBigInt( + big.NewInt(int64(math.Pow10(decimalDisplay))), + ) + salePrice.Coefficient = salePrice.Coefficient.Mul(factor) + purchasePrice.Coefficient = purchasePrice.Coefficient.Mul(factor) + oracle.setPrice(id, purchasePrice, salePrice) + + t.Logf("Minted %d USD assets, syncing universes...", usdAsset.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + sendAmount = uint64(400_000_000) + daveFundingAmount = uint64(400_000_000) + erinFundingAmount = uint64(200_000_000) + ) + charlieFundingAmount := usdAsset.Amount - 2*sendAmount + + chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, usdAsset, sendAmount, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, 0, + ) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + // We now create an invoice at Fabia for 100 USD, which is 100_000_000 + // asset units with decimal display of 6. + const fabiaInvoiceAssetAmount = 100_000_000 + invoiceResp := createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount, assetID, + ) + decodedInvoice, err := fabia.DecodePayReq(ctxb, &lnrpc.PayReqString{ + PayReq: invoiceResp.PaymentRequest, + }) + require.NoError(t.t, err) + + // The invoice amount should come out as 100 * 1533.332. + require.EqualValues(t.t, 153_333_242, decodedInvoice.NumMsat) + + numUnits, rate := payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp.PaymentRequest, assetID, + ) + logBalance(t.t, nodes, assetID, "after invoice") + + // The calculated amount Charlie has to pay should come out as + // 153_333_242 / 1473.202, which is quite exactly 4% more than will + // arrive at the destination (which is the oracle's configured spread). + // This is before routing fees though. + const charlieInvoiceAmount = 104_081_638 + require.EqualValues(t.t, charlieInvoiceAmount, numUnits) + + // The default routing fees are 1ppm + 1msat per hop, and we have 2 + // hops in total. + charliePaidMSat := addRoutingFee(addRoutingFee(lnwire.MilliSatoshi( + decodedInvoice.NumMsat, + ))) + charliePaidAmount := rfqmath.MilliSatoshiToUnits( + charliePaidMSat, rate, + ).ScaleTo(0).ToUint64() + assertPaymentHtlcAssets( + t.t, charlie, invoiceResp.RHash, assetID, charliePaidAmount, + ) + + // We now make sure the asset and satoshi channel balances are exactly + // what we expect them to be. + var ( + // channelFundingAmount is the hard coded satoshi amount that + // currently goes into asset channels. + channelFundingAmount int64 = 100_000 + + // commitFeeP2TR is the default commit fee for a P2TR channel + // commitment with 4 outputs (to_local, to_remote, 2 anchors). + commitFeeP2TR int64 = 2420 + commitFeeP2WSH int64 = 2810 + anchorAmount int64 = 330 + assetHtlcCarryAmount = int64( + tapchannel.DefaultOnChainHtlcAmount, + ) + unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR - + anchorAmount + balancedLocalAmount = unbalancedLocalAmount - anchorAmount + ) + + // Checking Charlie's sat and asset balances in channel Charlie->Dave. + assertChannelSatBalance( + t.t, charlie, chanPointCD, + balancedLocalAmount-assetHtlcCarryAmount, assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, charlie, chanPointCD, + charlieFundingAmount-charliePaidAmount, charliePaidAmount, + ) + + // Checking Dave's sat and asset balances in channel Charlie->Dave. + assertChannelSatBalance( + t.t, dave, chanPointCD, + assetHtlcCarryAmount, balancedLocalAmount-assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, dave, chanPointCD, + charliePaidAmount, charlieFundingAmount-charliePaidAmount, + ) + + // Checking Dave's sat balance in channel Dave->Erin. + forwardAmountDave := addRoutingFee( + lnwire.MilliSatoshi(decodedInvoice.NumMsat), + ).ToSatoshis() + assertChannelSatBalance( + t.t, dave, chanPointDE, + btcChannelFundingAmount-commitFeeP2WSH-2*anchorAmount- + int64(forwardAmountDave), + int64(forwardAmountDave), + ) + + // Checking Erin's sat balance in channel Dave->Erin. + assertChannelSatBalance( + t.t, erin, chanPointDE, + int64(forwardAmountDave), + btcChannelFundingAmount-commitFeeP2WSH-2*anchorAmount- + int64(forwardAmountDave), + ) + + // Checking Erin's sat and asset balances in channel Erin->Fabia. + assertChannelSatBalance( + t.t, erin, chanPointEF, + balancedLocalAmount-assetHtlcCarryAmount, assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, erin, chanPointEF, + erinFundingAmount-fabiaInvoiceAssetAmount, + fabiaInvoiceAssetAmount, + ) + + // Checking Fabia's sat and asset balances in channel Erin->Fabia. + assertChannelSatBalance( + t.t, fabia, chanPointEF, + assetHtlcCarryAmount, balancedLocalAmount-assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, erin, chanPointEF, + fabiaInvoiceAssetAmount, + erinFundingAmount-fabiaInvoiceAssetAmount, + ) + + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, nil, universeTap, + noOpCoOpCloseBalanceCheck, + ) + + t.Logf("Closing Dave -> Yara channel, close initiated by Yara") + closeAssetChannelAndAssert( + t, net, yara, dave, chanPointDY, assetID, nil, universeTap, + noOpCoOpCloseBalanceCheck, + ) + + t.Logf("Closing Erin -> Fabia channel") + closeAssetChannelAndAssert( + t, net, erin, fabia, chanPointEF, assetID, nil, universeTap, + noOpCoOpCloseBalanceCheck, + ) +} + +// testCustomChannelsFee tests whether the custom channel funding process +// fails if the proposed fee rate is lower than the minimum relay fee. +func testCustomChannelsFee(_ context.Context, + net *NetworkHarness, t *harnessTest) { + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + + // Mint an assets on Charlie and sync Dave to Charlie as the universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave) + t.Logf("Universes synced between all nodes, distributing assets...") + + // Fund a channel with a fee rate of zero. + zeroFeeRate := uint32(0) + + _, err = charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: cents.Amount, + AssetId: assetID, + PeerPubkey: daveTap.node.PubKey[:], + FeeRateSatPerVbyte: zeroFeeRate, + PushSat: 0, + }, + ) + + errSpecifyFeerate := "fee rate must be specified" + require.ErrorContains(t.t, err, errSpecifyFeerate) + + // Fund a channel with a fee rate that is too low. + tooLowFeeRate := uint32(1) + tooLowFeeRateAmount := chainfee.SatPerVByte(tooLowFeeRate) + + _, err = charlieTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: cents.Amount, + AssetId: assetID, + PeerPubkey: daveTap.node.PubKey[:], + FeeRateSatPerVbyte: tooLowFeeRate, + PushSat: 0, + }, + ) + + errFeeRateTooLow := fmt.Sprintf("fee rate %s too low, "+ + "min_relay_fee: ", tooLowFeeRateAmount.FeePerKWeight()) + require.ErrorContains(t.t, err, errFeeRateTooLow) +} + +// testCustomChannelsHtlcForceClose tests that we can force close a channel +// with HTLCs in both directions and that the HTLC outputs are correctly +// swept. +func testCustomChannelsHtlcForceClose(ctxb context.Context, net *NetworkHarness, + t *harnessTest) { + + runCustomChannelsHtlcForceClose(ctxb, t, net, false) + runCustomChannelsHtlcForceClose(ctxb, t, net, true) +} + +// runCustomChannelsHtlcForceClose is a helper function that runs the HTLC force +// close test with the given MPP setting. +func runCustomChannelsHtlcForceClose(ctxb context.Context, t *harnessTest, + net *NetworkHarness, mpp bool) { + + t.Logf("Running test with MPP: %v", mpp) + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Zane will serve as our designated Universe node. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // Next, we'll make Alice and Bob, who will be the main nodes under + // test. + alice, err := net.NewNode( + t.t, "Alice", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + bob, err := net.NewNode( + t.t, "Bob", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + // Now we'll connect all nodes, and also fund them with some coins. + nodes := []*HarnessNode{alice, bob} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + aliceTap := newTapClient(t.t, alice) + bobTap := newTapClient(t.t, bob) + + // Next, we'll mint an asset for Alice, who will be the node that opens + // the channel outbound. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, aliceTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, aliceTap, bob) + t.Logf("Universes synced between all nodes, distributing assets...") + + // With the assets created, and synced -- we'll now open the channel + // between Alice and Bob. + t.Logf("Opening asset channels...") + assetFundResp, err := aliceTap.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: fundingAmount, + AssetId: assetID, + PeerPubkey: bob.PubKey[:], + FeeRateSatPerVbyte: 5, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel between Alice and Bob: %v", assetFundResp) + + // With the channel open, mine a block to confirm it. + mineBlocks(t, net, 6, 1) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(alice, bob)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(bob, alice)) + + // First, we'll send over some funds from Alice to Bob, as we want Bob + // to be able to extend HTLCs in the other direction. + const ( + numPayments = 10 + keySendAmount = 2_500 + ) + for i := 0; i < numPayments; i++ { + sendAssetKeySendPayment( + t.t, alice, bob, keySendAmount, assetID, + fn.None[int64](), + ) + } + + // Now that both parties have some funds, we'll move onto the main test. + // + // We'll make 2 hodl invoice for each peer, so 4 total. From Alice's + // PoV, she'll have two outgoing HTLCs (or +4 with MPP), and two + // incoming HTLCs. + var ( + bobHodlInvoices []assetHodlInvoice + aliceHodlInvoices []assetHodlInvoice + + // The default oracle rate is 17_180 mSat/asset unit, so 10_000 + // will be equal to 171_800_000 mSat. When we use the mpp bool + // for the smallShards param of payInvoiceWithAssets, that + // means we'll split the payment into shards of 80_000_000 mSat + // max. So we'll get three shards per payment. + assetInvoiceAmt = 10_000 + assetsPerMPPShard = 4656 + ) + for i := 0; i < 2; i++ { + bobHodlInvoices = append( + bobHodlInvoices, createAssetHodlInvoice( + t.t, alice, bob, uint64(assetInvoiceAmt), + assetID, + ), + ) + aliceHodlInvoices = append( + aliceHodlInvoices, createAssetHodlInvoice( + t.t, bob, alice, uint64(assetInvoiceAmt), + assetID, + ), + ) + } + + // Now we'll have both Bob and Alice pay each other's invoices. We only + // care that they're in flight at this point, as they won't be settled + // yet. + for _, aliceInvoice := range aliceHodlInvoices { + opts := []payOpt{ + withFailure( + lnrpc.Payment_IN_FLIGHT, + lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, + ), + } + if mpp { + opts = append(opts, withSmallShards()) + } + payInvoiceWithAssets( + t.t, bob, alice, aliceInvoice.payReq, assetID, opts..., + ) + } + for _, bobInvoice := range bobHodlInvoices { + payInvoiceWithAssets( + t.t, alice, bob, bobInvoice.payReq, assetID, + withFailure( + lnrpc.Payment_IN_FLIGHT, + lnrpc.PaymentFailureReason_FAILURE_REASON_NONE, + ), + ) + } + + // At this point, both sides should have 4 (or +4 with MPP) HTLCs + // active. + numHtlcs := 4 + if mpp { + numAdditionalShards := assetInvoiceAmt / assetsPerMPPShard + numHtlcs += numAdditionalShards * 2 + } + assertNumHtlcs(t.t, alice, numHtlcs) + assertNumHtlcs(t.t, bob, numHtlcs) + + // Before we force close, we'll grab the current height, the CSV delay + // needed, and also the absolute timeout of the set of active HTLCs. + closeExpiryInfo := newCloseExpiryInfo(t.t, alice) + + // With all of the HTLCs established, we'll now force close the channel + // with Alice. + t.Logf("Force close by Alice w/ HTLCs...") + aliceChanPoint := &lnrpc.ChannelPoint{ + OutputIndex: uint32(assetFundResp.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: assetFundResp.Txid, + }, + } + _, closeTxid, err := net.CloseChannel(alice, aliceChanPoint, true) + require.NoError(t.t, err) + + t.Logf("Channel closed! Mining blocks, close_txid=%v", closeTxid) + + // Next, we'll mine a block which should start the clock ticking on the + // relative timeout for the Alice, and Bob. + // + // After this next block, both of them can start to sweep. + // + // For Alice, she'll go to the second level, revealing her preimage in + // the process. She'll then need to wait for the relative timeout to + // expire before she can sweep her output. + // + // For Bob, since the remote party (Alice) closed, he can try to sweep + // right away after initial confirmation. + mineBlocks(t, net, 1, 1) + + // After force closing, Bob should now have a transfer that tracks the + // force closed commitment transaction. + locateAssetTransfers(t.t, bobTap, *closeTxid) + + t.Logf("Settling Bob's hodl invoice") + + // At this point, the commitment transaction has been mined, and we have + // 4 total HTLCs on Alice's commitment transaction: + // + // * 2x outgoing HTLCs from Alice to Bob + // * 2x incoming HTLCs from Bob to Alice (+2 with MPP) + // + // We'll leave half the HTLCs timeout, while pulling the other half. + // To start, we'll signal Bob to settle one of his incoming HTLCs on + // Alice's commitment transaction. For him, this is a remote success + // spend, so there's no CSV delay other than the 1 CSV (carve out), and + // he can spend directly from the commitment transaction. + _, err = bob.InvoicesClient.SettleInvoice( + ctxb, &invoicesrpc.SettleInvoiceMsg{ + Preimage: bobHodlInvoices[0].preimage[:], + }, + ) + require.NoError(t.t, err) + + // We'll pause here for Bob to extend the sweep request to the sweeper. + assertSweepExists( + t.t, bob, + walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_REMOTE_SUCCESS, + ) + + // We'll mine an empty block to get the sweeper to tick. + mineBlocks(t, net, 1, 0) + + bobSweepTx1, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + // Next, we'll mine an additional block, this should allow Bob to sweep + // both his commitment output, and the incoming HTLC that we just + // settled above. + mineBlocks(t, net, 1, 1) + + // At this point, we should have the next sweep transaction in the + // mempool: Bob's incoming HTLC sweep directly off the commitment + // transaction. + bobSweepTx2, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + // We'll now mine the next block, which should confirm Bob's HTLC sweep + // transaction. + mineBlocks(t, net, 1, 1) + + bobSweepTransfer1 := locateAssetTransfers(t.t, bobTap, *bobSweepTx1[0]) + bobSweepTransfer2 := locateAssetTransfers(t.t, bobTap, *bobSweepTx2[0]) + t.Logf("Bob's sweep transfer 1: %v", + toProtoJSON(t.t, bobSweepTransfer1)) + t.Logf("Bob's sweep transfer 2: %v", + toProtoJSON(t.t, bobSweepTransfer2)) + + t.Logf("Confirming Bob's remote HTLC success sweep") + + // Bob's balance should now reflect that he's gained the value of the + // HTLC, in addition to his settled balance. We need to subtract 1 from + // the final balance due to the rounding down of the asset amount during + // RFQ conversion. + bobExpectedBalance := closeExpiryInfo.remoteAssetBalance + + uint64(assetInvoiceAmt-1) + t.Logf("Expecting Bob's balance to be %d", bobExpectedBalance) + assertSpendableBalance(t.t, bobTap, assetID, bobExpectedBalance) + + // With Bob's HTLC settled, we'll now have Alice do the same. For her, + // it'll be a 2nd level sweep, which requires an extra transaction. + // + // Before, we do that though, enough blocks have passed so Alice can now + // sweep her to-local output. So we'll mine an extra block, then assert + // that she's swept everything properly. With the way the sweeper works, + // we need to mine one extra block before the sweeper picks things up. + mineBlocks(t, net, 1, 0) + + aliceSweepTx1, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + mineBlocks(t, net, 1, 1) + + aliceSweepTransfer1 := locateAssetTransfers( + t.t, aliceTap, *aliceSweepTx1[0], + ) + t.Logf("Alice's sweep transfer 1: %v", + toProtoJSON(t.t, aliceSweepTransfer1)) + + t.Logf("Confirming Alice's to-local sweep") + + // With this extra block mined, Alice's settled balance should be the + // starting balance, minus the 2 HTLCs, plus her settled balance. + aliceExpectedBalance := itestAsset.Amount - fundingAmount + aliceExpectedBalance += closeExpiryInfo.localAssetBalance + assertSpendableBalance( + t.t, aliceTap, assetID, aliceExpectedBalance, + ) + + t.Logf("Settling Alice's hodl invoice") + + // With her commitment output swept above, we'll now settle one of + // Alice's incoming HTLCs. + _, err = alice.InvoicesClient.SettleInvoice( + ctxb, &invoicesrpc.SettleInvoiceMsg{ + Preimage: aliceHodlInvoices[0].preimage[:], + }, + ) + require.NoError(t.t, err) + + // We'll pause here for Alice to extend the sweep request to the + // sweeper. + assertSweepExists( + t.t, alice, + walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_LOCAL_SUCCESS, + ) + + // We'll now mine a block, which should trigger Alice's broadcast of the + // second level sweep transaction. + sweepBlocks := mineBlocks(t, net, 1, 0) + + // If the block mined above didn't also mine our sweep, then we'll mine + // one final block which will confirm Alice's sweep transaction. + if len(sweepBlocks[0].Transactions) == 1 { + sweepTx, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + // With the sweep transaction in the mempool, we'll mine a block + // to confirm the sweep. + mineBlocks(t, net, 1, 1) + + aliceSweepTransfer := locateAssetTransfers( + t.t, aliceTap, *sweepTx[0], + ) + t.Logf("Alice's first-level sweep transfer: %v", + toProtoJSON(t.t, aliceSweepTransfer)) + } else { + sweepTx := sweepBlocks[0].Transactions[1] + aliceSweepTransfer := locateAssetTransfers( + t.t, aliceTap, sweepTx.TxHash(), + ) + t.Logf("Alice's first-level sweep transfer: %v", + toProtoJSON(t.t, aliceSweepTransfer)) + } + + t.Logf("Confirming Alice's second level remote HTLC success sweep") + + // Next, we'll mine enough blocks to trigger the CSV expiry so Alice can + // sweep the HTLC into her wallet. + mineBlocks(t, net, closeExpiryInfo.csvDelay, 0) + + // We'll pause here and wait until the sweeper recognizes that we've + // offered the second level sweep transaction. + assertSweepExists( + t.t, alice, + //nolint: lll + walletrpc.WitnessType_TAPROOT_HTLC_ACCEPTED_SUCCESS_SECOND_LEVEL, + ) + + t.Logf("Confirming Alice's local HTLC success sweep") + + // Now that we know the sweep was offered, we'll mine an extra block to + // actually trigger a sweeper broadcast. Due to an internal block race + // condition, the sweep transaction may have already been + // published+mined. If so, we don't need to mine the extra block. + sweepBlocks = mineBlocks(t, net, 1, 0) + + // If the block mined above didn't also mine our sweep, then we'll mine + // one final block which will confirm Alice's sweep transaction. + if len(sweepBlocks[0].Transactions) == 1 { + sweepTx, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + mineBlocks(t, net, 1, 1) + + aliceSweepTransfer := locateAssetTransfers( + t.t, aliceTap, *sweepTx[0], + ) + t.Logf("Alice's second-level sweep transfer: %v", + toProtoJSON(t.t, aliceSweepTransfer)) + } else { + sweepTx := sweepBlocks[0].Transactions[1] + aliceSweepTransfer := locateAssetTransfers( + t.t, aliceTap, sweepTx.TxHash(), + ) + t.Logf("Alice's second-level sweep transfer: %v", + toProtoJSON(t.t, aliceSweepTransfer)) + } + + // With the sweep transaction confirmed, Alice's balance should have + // incremented by the amt of the HTLC. + aliceExpectedBalance += uint64(assetInvoiceAmt - 1) + assertSpendableBalance( + t.t, aliceTap, assetID, aliceExpectedBalance, + ) + + t.Logf("Mining enough blocks to time out the remaining HTLCs") + + // At this point, we've swept two HTLCs: one from the remote commit, and + // one via the second layer. We'll now mine the remaining amount of + // blocks to time out the HTLCs. + blockToMine := closeExpiryInfo.blockTillExpiry( + aliceHodlInvoices[1].preimage.Hash(), + ) + mineBlocks(t, net, blockToMine, 0) + + // We'll wait for both Alice and Bob to present their respective sweeps + // to the sweeper. + assertSweepExists( + t.t, alice, + walletrpc.WitnessType_TAPROOT_HTLC_LOCAL_OFFERED_TIMEOUT, + ) + assertSweepExists( + t.t, bob, + walletrpc.WitnessType_TAPROOT_HTLC_OFFERED_REMOTE_TIMEOUT, + ) + + // We'll mine an extra block to trigger the sweeper. + mineBlocks(t, net, 1, 0) + + t.Logf("Confirming initial HTLC timeout txns") + + // Finally, we'll mine a single block to confirm them. + mineBlocks(t, net, 1, 2) + + // At this point, Bob's balance should be incremented by an additional + // HTLC value. + bobExpectedBalance += uint64(assetInvoiceAmt - 1) + assertSpendableBalance( + t.t, bobTap, assetID, bobExpectedBalance, + ) + + t.Logf("Mining extra blocks for Alice's CSV to expire on 2nd level txn") + + // Next, we'll mine 4 additional blocks to Alice's CSV delay expires for + // the second level timeout output. + mineBlocks(t, net, closeExpiryInfo.csvDelay, 0) + + // Wait for Alice to extend the second level output to the sweeper + // before we mine the next block to the sweeper. + assertSweepExists( + t.t, alice, + walletrpc.WitnessType_TAPROOT_HTLC_OFFERED_TIMEOUT_SECOND_LEVEL, + ) + + t.Logf("Confirming Alice's final timeout sweep") + + // With the way the sweeper works, we'll now need to mine an extra block + // to trigger the sweep. + sweepBlocks = mineBlocks(t, net, 1, 0) + + // If the block mined above didn't also mine our sweep, then we'll mine + // one final block which will confirm Alice's sweep transaction. + if len(sweepBlocks[0].Transactions) == 1 { + sweepTx, err := waitForNTxsInMempool( + net.Miner.Client, 1, shortTimeout, + ) + require.NoError(t.t, err) + + // We'll mine one final block which will confirm Alice's sweep + // transaction. + mineBlocks(t, net, 1, 1) + + aliceSweepTransfer := locateAssetTransfers( + t.t, aliceTap, *sweepTx[0], + ) + t.Logf("Alice's final timeout sweep transfer: %v", + toProtoJSON(t.t, aliceSweepTransfer)) + } else { + sweepTx := sweepBlocks[0].Transactions[1] + aliceSweepTransfer := locateAssetTransfers( + t.t, aliceTap, sweepTx.TxHash(), + ) + t.Logf("Alice's final timeout sweep transfer: %v", + toProtoJSON(t.t, aliceSweepTransfer)) + } + + // Finally, we'll assert that Alice's balance has been incremented by + // the timeout value. + aliceExpectedBalance += uint64(assetInvoiceAmt - 1) + t.Logf("Expecting Alice's balance to be %d", aliceExpectedBalance) + assertSpendableBalance( + t.t, aliceTap, assetID, aliceExpectedBalance, + ) + + t.Logf("Sending all settled funds to Zane") + + // As a final sanity check, both Alice and Bob should be able to send + // their entire balances to Zane, our 3rd party. + // + // We'll make two addrs for Zane, one for Alice, and one for bob. + zaneTap := newTapClient(t.t, zane) + aliceAddr, err := zaneTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: aliceExpectedBalance, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + zaneTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + bobAddr, err := zaneTap.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: bobExpectedBalance, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + zaneTap.node.Cfg.LitAddr(), + ), + }) + require.NoError(t.t, err) + + _, err = aliceTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{aliceAddr.Encoded}, + }) + require.NoError(t.t, err) + mineBlocks(t, net, 1, 1) + + itest.AssertNonInteractiveRecvComplete(t.t, zaneTap, 1) + + _, err = bobTap.SendAsset(ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{bobAddr.Encoded}, + }) + require.NoError(t.t, err) + mineBlocks(t, net, 1, 1) + + itest.AssertNonInteractiveRecvComplete(t.t, zaneTap, 2) + + // Zane's balance should now be the sum of Alice's and Bob's balances. + zaneExpectedBalance := aliceExpectedBalance + bobExpectedBalance + assertSpendableBalance( + t.t, zaneTap, assetID, zaneExpectedBalance, + ) +} + +// testCustomChannelsForwardBandwidth is a test that runs through some Taproot +// Assets Channel liquidity edge cases, specifically related to forwarding HTLCs +// into channels with no available asset bandwidth. +func testCustomChannelsForwardBandwidth(ctxb context.Context, + net *NetworkHarness, t *harnessTest) { + + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplate) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + channelOp := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: 10_000_000, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, channelOp, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, channelOp) + assertChannelKnown(t.t, fabia, channelOp) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + // Mint an asset on Charlie and sync all nodes to Charlie as the + // universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + cents := mintedAssets[0] + assetID := cents.AssetGenesis.AssetId + + t.Logf("Minted %d lightning cents, syncing universes...", cents.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + daveFundingAmount = uint64(400_000) + erinFundingAmount = uint64(200_000) + ) + charlieFundingAmount := cents.Amount - uint64(2*400_000) + + _, _, chanPointEF := createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, cents, 400_000, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, 0, + ) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + logBalance(t.t, nodes, assetID, "initial") + + // We now deplete the channel between Erin and Fabia by moving all + // assets to Fabia. + sendAssetKeySendPayment( + t.t, erin, fabia, erinFundingAmount, assetID, fn.None[int64](), + ) + logBalance(t.t, nodes, assetID, "after moving assets to Fabia") + + // Test case 1: We cannot keysend more assets from Erin to Fabia. + sendAssetKeySendPayment( + t.t, erin, fabia, 1, assetID, fn.None[int64](), + withFailure(lnrpc.Payment_FAILED, failureNoBalance), + ) + + // Test case 2: We cannot pay an invoice from Charlie to Fabia. + invoiceResp := createAssetInvoice(t.t, erin, fabia, 123, assetID) + payInvoiceWithSatoshi( + t.t, charlie, invoiceResp, + withFailure(lnrpc.Payment_FAILED, failureNoRoute), + ) + + // Test case 3: We now create an asset buy order for a normal amount of + // assets. We then "fake" an invoice referencing that buy order that + // is for an amount that is too small to be paid with a single asset + // unit. This should be handled gracefully and not lead to a crash. + // Ideally such an invoice shouldn't be created in the first place, but + // we want to make sure that the system doesn't crash in this case. + numUnits := uint64(10) + buyOrderResp, err := fabiaTap.RfqClient.AddAssetBuyOrder( + ctxb, &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + AssetMaxAmt: numUnits, + Expiry: uint64( + time.Now().Add(time.Hour).Unix(), + ), + PeerPubKey: erin.PubKey[:], + TimeoutSeconds: 10, + }, + ) + require.NoError(t.t, err) + + quoteResp := buyOrderResp.Response + quote, ok := quoteResp.(*rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote) + require.True(t.t, ok) + + // We calculate the milli-satoshi amount one below the equivalent of a + // single asset unit. + rate, err := oraclerpc.UnmarshalFixedPoint(&oraclerpc.FixedPoint{ + Coefficient: quote.AcceptedQuote.AskAssetRate.Coefficient, + Scale: quote.AcceptedQuote.AskAssetRate.Scale, + }) + require.NoError(t.t, err) + + oneUnit := uint64(1) + oneUnitFP := rfqmath.NewBigIntFixedPoint(oneUnit, 0) + oneUnitMilliSat := rfqmath.UnitsToMilliSatoshi(oneUnitFP, *rate) + + t.Logf("Got quote for %v asset units per BTC", rate) + msatPerUnit := float64(oneUnitMilliSat) / float64(oneUnit) + t.Logf("Got quote for %v asset units at %3f msat/unit from peer %s "+ + "with SCID %d", numUnits, msatPerUnit, erin.PubKeyStr, + quote.AcceptedQuote.Scid) + + // We now manually add the invoice in order to inject the above, + // manually generated, quote. + invoiceResp2, err := fabia.AddInvoice(ctxb, &lnrpc.Invoice{ + Memo: "too small invoice", + ValueMsat: int64(oneUnitMilliSat - 1), + RouteHints: []*lnrpc.RouteHint{{ + HopHints: []*lnrpc.HopHint{{ + NodeId: erin.PubKeyStr, + ChanId: quote.AcceptedQuote.Scid, + }}, + }}, + }) + require.NoError(t.t, err) + + payInvoiceWithSatoshi(t.t, dave, invoiceResp2, withFailure( + lnrpc.Payment_FAILED, failureNoRoute, + )) + + // Let's make sure we can still use the channel between Erin and Fabia + // by doing a satoshi keysend payment. + sendKeySendPayment(t.t, erin, fabia, 2000) + logBalance(t.t, nodes, assetID, "after BTC only keysend") + + // Finally, we close the channel between Erin and Fabia to make sure + // everything is settled correctly. + closeAssetChannelAndAssert( + t, net, erin, fabia, chanPointEF, assetID, nil, + universeTap, noOpCoOpCloseBalanceCheck, + ) +} diff --git a/itest/litd_node.go b/itest/litd_node.go index 7ec9a0296..e9be55972 100644 --- a/itest/litd_node.go +++ b/itest/litd_node.go @@ -89,6 +89,9 @@ type LitNodeConfig struct { LitPort int LitRESTPort int + + // backupDBDir is the path where a database backup is stored, if any. + backupDBDir string } func (cfg *LitNodeConfig) LitAddr() string { @@ -2087,3 +2090,38 @@ func connectLitRPC(ctx context.Context, hostPort, tlsCertPath, return grpc.DialContext(ctx, hostPort, opts...) } + +// copyAll copies all files and directories from srcDir to dstDir recursively. +// Note that this function does not support links. +func copyAll(dstDir, srcDir string) error { + entries, err := os.ReadDir(srcDir) + if err != nil { + return err + } + + for _, entry := range entries { + srcPath := filepath.Join(srcDir, entry.Name()) + dstPath := filepath.Join(dstDir, entry.Name()) + + info, err := os.Stat(srcPath) + if err != nil { + return err + } + + if info.IsDir() { + err := os.Mkdir(dstPath, info.Mode()) + if err != nil && !os.IsExist(err) { + return err + } + + err = copyAll(dstPath, srcPath) + if err != nil { + return err + } + } else if err := CopyFile(dstPath, srcPath); err != nil { + return err + } + } + + return nil +} diff --git a/itest/litd_test.go b/itest/litd_test.go index a87a0a426..2736baf6c 100644 --- a/itest/litd_test.go +++ b/itest/litd_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/btcsuite/btclog" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/signal" @@ -56,7 +57,8 @@ func TestLightningTerminal(t *testing.T) { // Start a chain backend. chainBackend, _, err := lntest.NewBackend( - lndHarness.Miner().P2PAddress(), harnessNetParams, + lndHarness.Miner().P2PAddress(), + harnessNetParams, ) require.NoError(t1, err, "new backend") @@ -130,6 +132,10 @@ func (h *harnessTest) setupLogging() { require.NoError(h.t, err) interceptor = &ic + UseLogger(build.NewSubLogger(Subsystem, func(tag string) btclog.Logger { + return logWriter.GenSubLogger(tag, func() {}) + })) + err = build.ParseAndSetDebugLevels("debug", logWriter) require.NoError(h.t, err) } diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 1ddc5a2d6..4d2cef88b 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -24,4 +24,52 @@ var allTestCases = []*testCase{ name: "test large http header", test: testLargeHttpHeader, }, + { + name: "test custom channels", + test: testCustomChannels, + }, + { + name: "test custom channels large", + test: testCustomChannelsLarge, + }, + { + name: "test custom channels grouped asset", + test: testCustomChannelsGroupedAsset, + }, + { + name: "test custom channels force close", + test: testCustomChannelsForceClose, + }, + { + name: "test custom channels breach", + test: testCustomChannelsBreach, + }, + { + name: "test custom channels liquidity", + test: testCustomChannelsLiquidityEdgeCases, + }, + { + name: "test custom channels htlc force close", + test: testCustomChannelsHtlcForceClose, + }, + { + name: "test custom channels balance consistency", + test: testCustomChannelsBalanceConsistency, + }, + { + name: "test custom channels single asset multi input", + test: testCustomChannelsSingleAssetMultiInput, + }, + { + name: "test custom channels oracle pricing", + test: testCustomChannelsOraclePricing, + }, + { + name: "test custom channels fee", + test: testCustomChannelsFee, + }, + { + name: "test custom channels forward bandwidth", + test: testCustomChannelsForwardBandwidth, + }, } diff --git a/itest/log.go b/itest/log.go new file mode 100644 index 000000000..67211af57 --- /dev/null +++ b/itest/log.go @@ -0,0 +1,24 @@ +package itest + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "ITST" + +// log is a logger that is initialized with no output filters. This means the +// package will not perform any logging by default until the caller requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/itest/network_harness.go b/itest/network_harness.go index 271163441..5b562c5ef 100644 --- a/itest/network_harness.go +++ b/itest/network_harness.go @@ -46,7 +46,8 @@ type NetworkHarness struct { // Miner is a reference to a running full node that can be used to create // new blocks on the network. - Miner *miner.HarnessMiner + Miner *miner.HarnessMiner + LNDHarness *lntest.HarnessTest // server is an instance of the local Loop/Pool mock server. @@ -435,6 +436,12 @@ tryconnect: "finish syncing") } } + + // Ignore "already connected to peer" errors. + if strings.Contains(err.Error(), "already connected to peer") { + return nil + } + return err } @@ -767,6 +774,58 @@ func (n *NetworkHarness) StopNode(node *HarnessNode) error { return node.Stop() } +// StopAndBackupDB backs up the database of the target node. +func (n *NetworkHarness) StopAndBackupDB(node *HarnessNode) error { + restart, err := n.SuspendNode(node) + if err != nil { + return err + } + + // Backup files. + tempDir, err := os.MkdirTemp("", "past-state") + if err != nil { + return fmt.Errorf("unable to create temp db folder: %w", + err) + } + + if err := copyAll(tempDir, node.Cfg.DBDir()); err != nil { + return fmt.Errorf("unable to copy database files: %w", + err) + } + + node.Cfg.backupDBDir = tempDir + + return restart() +} + +// StopAndRestoreDB stops the target node, restores the database from a backup +// and starts the node again. +func (n *NetworkHarness) StopAndRestoreDB(node *HarnessNode) error { + restart, err := n.SuspendNode(node) + if err != nil { + return err + } + + // Restore files. + if node.Cfg.backupDBDir == "" { + return fmt.Errorf("no database backup created") + } + + err = copyAll(node.Cfg.DBDir(), node.Cfg.backupDBDir) + if err != nil { + return fmt.Errorf("unable to copy database files: %w", + err) + } + + if err := os.RemoveAll(node.Cfg.backupDBDir); err != nil { + return fmt.Errorf("unable to remove backup dir: %w", + err) + } + node.Cfg.backupDBDir = "" + + return restart() +} + // OpenChannel attempts to open a channel between srcNode and destNode with the // passed channel funding parameters. If the passed context has a timeout, then // if the timeout is reached before the channel pending notification is @@ -1053,8 +1112,11 @@ func (n *NetworkHarness) CloseChannel(lnNode *HarnessNode, closeReq := &lnrpc.CloseChannelRequest{ ChannelPoint: cp, Force: force, - SatPerVbyte: 5, } + if !force { + closeReq.SatPerVbyte = 5 + } + closeRespStream, err = lnNode.CloseChannel(ctx, closeReq) if err != nil { return fmt.Errorf("unable to close channel: %v", err) @@ -1097,7 +1159,8 @@ func (n *NetworkHarness) CloseChannel(lnNode *HarnessNode, // passed context has a timeout, then if the timeout is reached before the // notification is received then an error is returned. func (n *NetworkHarness) WaitForChannelClose( - closeChanStream lnrpc.Lightning_CloseChannelClient) (*chainhash.Hash, error) { + stream lnrpc.Lightning_CloseChannelClient) (*lnrpc.ChannelCloseUpdate, + error) { ctxb := context.Background() ctx, cancel := context.WithTimeout(ctxb, wait.ChannelCloseTimeout) @@ -1106,13 +1169,14 @@ func (n *NetworkHarness) WaitForChannelClose( errChan := make(chan error) updateChan := make(chan *lnrpc.CloseStatusUpdate_ChanClose) go func() { - closeResp, err := closeChanStream.Recv() + closeResp, err := stream.Recv() if err != nil { errChan <- err return } - closeFin, ok := closeResp.Update.(*lnrpc.CloseStatusUpdate_ChanClose) + update := closeResp.Update + closeFin, ok := update.(*lnrpc.CloseStatusUpdate_ChanClose) if !ok { errChan <- fmt.Errorf("expected channel close update, "+ "instead got %v", closeFin) @@ -1130,7 +1194,7 @@ func (n *NetworkHarness) WaitForChannelClose( case err := <-errChan: return nil, err case update := <-updateChan: - return chainhash.NewHash(update.ChanClose.ClosingTxid) + return update.ChanClose, nil } } @@ -1177,6 +1241,33 @@ func (n *NetworkHarness) AssertChannelExists(node *HarnessNode, }, lntest.DefaultTimeout) } +// AssertNodeKnown makes sure the given node knows about the target node in the +// network graph. +func (n *NetworkHarness) AssertNodeKnown(node, target *HarnessNode) error { + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, wait.DefaultTimeout) + defer cancel() + + req := &lnrpc.NodeInfoRequest{ + PubKey: hex.EncodeToString( + target.PubKey[:], + ), + } + return wait.NoError(func() error { + info, err := node.GetNodeInfo(ctxt, req) + if err != nil { + return err + } + + if info.Node == nil { + return fmt.Errorf("node %x has no info about %x", + node.PubKey[:], target.PubKey[:]) + } + + return nil + }, lntest.DefaultTimeout) +} + // DumpLogs reads the current logs generated by the passed node, and returns // the logs as a single string. This function is useful for examining the logs // of a particular node in the case of a test failure. diff --git a/itest/oracle_test.go b/itest/oracle_test.go new file mode 100644 index 000000000..8f7cfd0c5 --- /dev/null +++ b/itest/oracle_test.go @@ -0,0 +1,279 @@ +package itest + +import ( + "context" + "crypto/tls" + "encoding/hex" + "fmt" + "net" + "testing" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" + "github.com/lightningnetwork/lnd/cert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// oracleHarness is a basic integration test RPC price oracle server harness. +type oracleHarness struct { + oraclerpc.UnimplementedPriceOracleServer + + listenAddr string + + grpcListener net.Listener + grpcServer *grpc.Server + + purchasePrices map[asset.ID]rfqmath.BigIntFixedPoint + salePrices map[asset.ID]rfqmath.BigIntFixedPoint +} + +func newOracleHarness(listenAddr string) *oracleHarness { + return &oracleHarness{ + listenAddr: listenAddr, + purchasePrices: make(map[asset.ID]rfqmath.BigIntFixedPoint), + salePrices: make(map[asset.ID]rfqmath.BigIntFixedPoint), + } +} + +func (o *oracleHarness) setPrice(assetID asset.ID, purchasePrice, + salePrice rfqmath.BigIntFixedPoint) { + + o.purchasePrices[assetID] = purchasePrice + o.salePrices[assetID] = salePrice +} + +func (o *oracleHarness) start(t *testing.T) { + // Start the mock RPC price oracle service. + // + // Generate self-signed certificate. This allows us to use TLS for the + // gRPC server. + tlsCert, err := generateSelfSignedCert() + require.NoError(t, err) + + // Create the gRPC server with TLS + transportCredentials := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + }) + o.grpcServer = grpc.NewServer(grpc.Creds(transportCredentials)) + + serviceAddr := fmt.Sprintf("rfqrpc://%s", o.listenAddr) + log.Infof("Starting RPC price oracle service at address: %s\n", + serviceAddr) + + oraclerpc.RegisterPriceOracleServer(o.grpcServer, o) + + go func() { + var err error + o.grpcListener, err = net.Listen("tcp", o.listenAddr) + if err != nil { + log.Errorf("Error oracle listening: %v", err) + return + } + if err := o.grpcServer.Serve(o.grpcListener); err != nil { + log.Errorf("Error oracle serving: %v", err) + } + }() +} + +func (o *oracleHarness) stop() { + if o.grpcServer != nil { + o.grpcServer.Stop() + } + if o.grpcListener != nil { + _ = o.grpcListener.Close() + } +} + +// getAssetRates returns the asset rates for a given transaction type and +// subject asset max amount. +func (o *oracleHarness) getAssetRates(id asset.ID, + transactionType oraclerpc.TransactionType) (oraclerpc.AssetRates, + error) { + + // Determine the rate based on the transaction type. + var subjectAssetRate rfqmath.BigIntFixedPoint + if transactionType == oraclerpc.TransactionType_PURCHASE { + rate, ok := o.purchasePrices[id] + if !ok { + return oraclerpc.AssetRates{}, fmt.Errorf("purchase "+ + "price not found for asset ID=%v", id) + } + subjectAssetRate = rate + } else { + rate, ok := o.salePrices[id] + if !ok { + return oraclerpc.AssetRates{}, fmt.Errorf("sale "+ + "price not found for asset ID=%v", id) + } + subjectAssetRate = rate + } + + // Marshal subject asset rate to RPC format. + rpcSubjectAssetToBtcRate, err := oraclerpc.MarshalBigIntFixedPoint( + subjectAssetRate, + ) + if err != nil { + return oraclerpc.AssetRates{}, err + } + + // Marshal payment asset rate to RPC format. + rpcPaymentAssetToBtcRate, err := oraclerpc.MarshalBigIntFixedPoint( + rfqmsg.MilliSatPerBtc, + ) + if err != nil { + return oraclerpc.AssetRates{}, err + } + + expiry := time.Now().Add(5 * time.Minute).Unix() + return oraclerpc.AssetRates{ + SubjectAssetRate: rpcSubjectAssetToBtcRate, + PaymentAssetRate: rpcPaymentAssetToBtcRate, + ExpiryTimestamp: uint64(expiry), + }, nil +} + +// QueryAssetRates queries the asset rates for a given transaction type, subject +// asset, and payment asset. An asset rate is the number of asset units per +// BTC. +// +// Example use case: +// +// Alice is trying to pay an invoice by spending an asset. Alice therefore +// requests that Bob (her asset channel counterparty) purchase the asset from +// her. Bob's payment, in BTC, will pay the invoice. +// +// Alice requests a bid quote from Bob. Her request includes an asset rates hint +// (ask). Alice obtains the asset rates hint by calling this endpoint. She sets: +// - `SubjectAsset` to the asset she is trying to sell. +// - `SubjectAssetMaxAmount` to the max channel asset outbound. +// - `PaymentAsset` to BTC. +// - `TransactionType` to SALE. +// - `AssetRateHint` to nil. +// +// Bob calls this endpoint to get the bid quote asset rates that he will send as +// a response to Alice's request. He sets: +// - `SubjectAsset` to the asset that Alice is trying to sell. +// - `SubjectAssetMaxAmount` to the value given in Alice's quote request. +// - `PaymentAsset` to BTC. +// - `TransactionType` to PURCHASE. +// - `AssetRateHint` to the value given in Alice's quote request. +func (o *oracleHarness) QueryAssetRates(_ context.Context, + req *oraclerpc.QueryAssetRatesRequest) ( + *oraclerpc.QueryAssetRatesResponse, error) { + + // Ensure that the payment asset is BTC. We only support BTC as the + // payment asset in this example. + if !oraclerpc.IsAssetBtc(req.PaymentAsset) { + log.Infof("Payment asset is not BTC: %v", req.PaymentAsset) + + return &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Error{ + Error: &oraclerpc.QueryAssetRatesErrResponse{ + Message: "unsupported payment asset, " + + "only BTC is supported", + }, + }, + }, nil + } + + // Ensure that the subject asset is set correctly. + subjectAssetID, err := parseSubjectAsset(req.SubjectAsset) + if err != nil { + log.Errorf("Error parsing subject asset: %v", err) + return nil, fmt.Errorf("error parsing subject asset: %w", err) + } + + _, hasPurchase := o.purchasePrices[subjectAssetID] + _, hasSale := o.salePrices[subjectAssetID] + + log.Infof("Have for asset=%x, purchase=%v, sale=%v", subjectAssetID[:], + hasPurchase, hasSale) + + // Ensure that the subject asset is supported. + if !hasPurchase || !hasSale { + log.Infof("Unsupported subject asset ID str: %v\n", + req.SubjectAsset) + + return &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Error{ + Error: &oraclerpc.QueryAssetRatesErrResponse{ + Message: "unsupported subject asset", + }, + }, + }, nil + } + + assetRates, err := o.getAssetRates(subjectAssetID, req.TransactionType) + if err != nil { + return nil, err + } + + log.Infof("QueryAssetRates returning rates (subject_asset_rate=%v, "+ + "payment_asset_rate=%v)", assetRates.SubjectAssetRate, + assetRates.PaymentAssetRate) + + return &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Ok{ + Ok: &oraclerpc.QueryAssetRatesOkResponse{ + AssetRates: &assetRates, + }, + }, + }, nil +} + +// parseSubjectAsset parses the subject asset from the given asset specifier. +func parseSubjectAsset(subjectAsset *oraclerpc.AssetSpecifier) (asset.ID, + error) { + + // Ensure that the subject asset is set. + if subjectAsset == nil { + return asset.ID{}, fmt.Errorf("subject asset is not set (nil)") + } + + // Check the subject asset bytes if set. + var subjectAssetID asset.ID + switch { + case len(subjectAsset.GetAssetId()) > 0: + copy(subjectAssetID[:], subjectAsset.GetAssetId()) + + case len(subjectAsset.GetAssetIdStr()) > 0: + assetIDBytes, err := hex.DecodeString( + subjectAsset.GetAssetIdStr(), + ) + if err != nil { + return asset.ID{}, fmt.Errorf("error decoding asset "+ + "ID hex string: %w", err) + } + + copy(subjectAssetID[:], assetIDBytes) + + default: + return asset.ID{}, fmt.Errorf("subject asset ID bytes and ID " + + "str not set") + } + + return subjectAssetID, nil +} + +// generateSelfSignedCert generates a self-signed TLS certificate and private +// key. +func generateSelfSignedCert() (tls.Certificate, error) { + certBytes, keyBytes, err := cert.GenCertPair( + "itest price oracle", nil, nil, false, 24*time.Hour, + ) + if err != nil { + return tls.Certificate{}, err + } + + tlsCert, err := tls.X509KeyPair(certBytes, keyBytes) + if err != nil { + return tls.Certificate{}, err + } + + return tlsCert, nil +} From 8e37256046f9c4956b4e4d0d2ddad3e4aa2a54c4 Mon Sep 17 00:00:00 2001 From: jamaljsr <1356600+jamaljsr@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:55:39 -0500 Subject: [PATCH 08/12] litclient: add taprpc packages to `Registrations` Add the `priceoraclerpc`, `rfqrpc`, and the `tapchannelrpc` JSON callbacks to the litclient's `Registrations` array. This allows the litclient to use the rpc functions contained in these JSON callbacks. --- litclient/jsoncallbacks.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/litclient/jsoncallbacks.go b/litclient/jsoncallbacks.go index 2a6e4028d..d28080cb5 100644 --- a/litclient/jsoncallbacks.go +++ b/litclient/jsoncallbacks.go @@ -10,6 +10,9 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" @@ -55,4 +58,7 @@ var Registrations = []StubPackageRegistration{ assetwalletrpc.RegisterAssetWalletJSONCallbacks, universerpc.RegisterUniverseJSONCallbacks, mintrpc.RegisterMintJSONCallbacks, + priceoraclerpc.RegisterPriceOracleJSONCallbacks, + rfqrpc.RegisterRfqJSONCallbacks, + tapchannelrpc.RegisterTaprootAssetChannelsJSONCallbacks, } From 8721fe4417c3b942bca63266362e0a9cf03b05ce Mon Sep 17 00:00:00 2001 From: ZZiigguurraatt Date: Thu, 5 Dec 2024 18:06:31 -0500 Subject: [PATCH 09/12] dev.Dockerfile: allow forcing a specific taproot-assets version through a build argument --- dev.Dockerfile | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index 5a70f97a0..f7a5e1c2e 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -23,10 +23,19 @@ COPY --from=nodejsbuilder /go/src/github.com/lightninglabs/lightning-terminal /g # queries required to connect to linked containers succeed. ENV GODEBUG netdns=cgo +# Allow forcing a specific taproot-assets version through a build argument. +# Please see https://go.dev/ref/mod#version-queries for the types of +# queries that can be used to define a version. +ARG TAPROOT_ASSETS_VERSION + # Install dependencies and install/build lightning-terminal. -RUN apk add --no-cache --update alpine-sdk \ - make \ +RUN apk add --no-cache --update alpine-sdk make \ && cd /go/src/github.com/lightninglabs/lightning-terminal \ + # If a custom taproot-assets version is supplied, force it now. + && if [ -n "$TAPROOT_ASSETS_VERSION" ]; then \ + go get -v github.com/lightninglabs/taproot-assets@$TAPROOT_ASSETS_VERSION \ + && go mod tidy; \ + fi \ && make go-install \ && make go-install-cli From d59c717350101f16ee73788a8bdc92938f909202 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 21 Oct 2024 16:02:53 +0200 Subject: [PATCH 10/12] version+docs: add release notes, bump version to v0.14.0-alpha.rc3 --- README.md | 7 +++++++ docs/release-notes/release-notes-0.13.5.md | 2 ++ ...-notes-0.13.7.md => release-notes-0.14.0.md} | 17 ++++++++++++++++- version.go | 6 +++--- 4 files changed, 28 insertions(+), 4 deletions(-) rename docs/release-notes/{release-notes-0.13.7.md => release-notes-0.14.0.md} (70%) diff --git a/README.md b/README.md index 37e521cd7..09b060c49 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ in remote mode (meaning that `lnd-mode=remote` is set). It shows the | LiT | LND | |-------------------|--------------| +| **v0.14.0-alpha** | v0.18.4-beta | | **v0.13.6-alpha** | v0.17.1-beta | | **v0.13.5-alpha** | v0.17.1-beta | | **v0.13.4-alpha** | v0.17.1-beta | @@ -158,10 +159,16 @@ The following table shows the supported combinations: | `taproot-assets-mode=disable` | X | X | | `lnd` running in "stateless init" mode | X | | +NOTE: Taproot Assets **Channel** functionality is only available when both `lnd` +and `tapd` are running in the same process (by setting both +`lnd-mode=integrated` and `taproot-assets-mode=integrated`). Remote mode support +will be added in the future. + ## Daemon Versions packaged with LiT | LiT | LND | Loop | Faraday | Pool | Taproot Assets | |-------------------|--------------|--------------|---------------|--------------|----------------| +| **v0.14.0-alpha** | v0.18.4-beta | v0.28.9-beta | v0.2.13-alpha | v0.6.5-beta | v0.5.0-alpha | | **v0.13.6-alpha** | v0.18.3-beta | v0.28.8-beta | v0.2.13-alpha | v0.6.5-beta | v0.4.1-alpha | | **v0.13.5-alpha** | v0.18.3-beta | v0.28.8-beta | v0.2.13-alpha | v0.6.5-beta | v0.4.1-alpha | | **v0.13.4-alpha** | v0.18.3-beta | v0.28.7-beta | v0.2.13-alpha | v0.6.5-beta | v0.4.1-alpha | diff --git a/docs/release-notes/release-notes-0.13.5.md b/docs/release-notes/release-notes-0.13.5.md index 05c3eb004..627b7dcf2 100644 --- a/docs/release-notes/release-notes-0.13.5.md +++ b/docs/release-notes/release-notes-0.13.5.md @@ -29,6 +29,8 @@ especially useful in stateless-init mode where users will not have access to a LiT macaroon to perform this call with. +* [Convert litrpc package into a module](https://github.com/lightninglabs/lightning-terminal/pull/823). + ### Technical and Architectural Updates * [Convert litrpc package into a module](https://github.com/lightninglabs/lightning-terminal/pull/823). diff --git a/docs/release-notes/release-notes-0.13.7.md b/docs/release-notes/release-notes-0.14.0.md similarity index 70% rename from docs/release-notes/release-notes-0.13.7.md rename to docs/release-notes/release-notes-0.14.0.md index 0e30c51f4..9ca71291f 100644 --- a/docs/release-notes/release-notes-0.13.7.md +++ b/docs/release-notes/release-notes-0.14.0.md @@ -34,6 +34,16 @@ network](https://github.com/lightninglabs/lightning-terminal/pull/902). This can be done using the `--network=signet` config option. +* Add [custom channel + functionality](https://github.com/lightninglabs/lightning-terminal/pull/848) + to `litd`. Custom channels with Taproot Assets can now be created when `litd` + runs in integrated `lnd` mode (`lnd-mode=integrated`) with the Taproot Assets + daemon also running in integrated mode (`taproot-assets-mode=integrated`). + +* [Add itest](https://github.com/lightninglabs/lightning-terminal/pull/892) for + the MinRelayFee check added in Taproot Assets. The test ensures that + transactions with fees below the minimum relay fee are rejected. + ### Technical and Architectural Updates ## Integrated Binary Updates @@ -51,4 +61,9 @@ # Contributors (Alphabetical Order) * Elle Mouton -* Oliver Gugger \ No newline at end of file +* George Tsagkarelis +* Gijs van Dam +* Jamal James +* Jonathan Harvey-Buschel +* Olaoluwa Osuntokun +* Oliver Gugger diff --git a/version.go b/version.go index d3ed75142..10f07fcd1 100644 --- a/version.go +++ b/version.go @@ -22,12 +22,12 @@ const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr // versioning 2.0.0 spec (http://semver.org/). const ( appMajor uint = 0 - appMinor uint = 13 - appPatch uint = 6 + appMinor uint = 14 + appPatch uint = 0 // appPreRelease MUST only contain characters from semanticAlphabet per // the semantic versioning spec. - appPreRelease = "alpha" + appPreRelease = "alpha.rc3" ) // Version returns the application version as a properly formed string per the From 868d058b05d4d19143a20ebfa1f4309314fa69f9 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Wed, 11 Dec 2024 12:08:05 +0100 Subject: [PATCH 11/12] build: bump tapd, lndclient --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index bd1dc1707..f1aa51344 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/lightninglabs/lightning-node-connect v0.3.2-alpha.0.20240822142323-ee4e7ff52f83 github.com/lightninglabs/lightning-terminal/autopilotserverrpc v0.0.1 github.com/lightninglabs/lightning-terminal/litrpc v1.0.0 - github.com/lightninglabs/lndclient v0.18.4-7 + github.com/lightninglabs/lndclient v1.0.1-0.20241212185726-f8f7e3fa3ad8 github.com/lightninglabs/loop v0.28.9-beta github.com/lightninglabs/loop/looprpc v1.0.1 github.com/lightninglabs/loop/swapserverrpc v1.0.10 diff --git a/go.sum b/go.sum index 85f2d62d6..7e4c3eadc 100644 --- a/go.sum +++ b/go.sum @@ -1157,8 +1157,8 @@ github.com/lightninglabs/lightning-node-connect v0.3.2-alpha.0.20240822142323-ee github.com/lightninglabs/lightning-node-connect v0.3.2-alpha.0.20240822142323-ee4e7ff52f83/go.mod h1:+SasPOt0evcJdfApb/ALTaTz4x3a2/kWy5KqFoTpiX8= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 h1:Er1miPZD2XZwcfE4xoS5AILqP1mj7kqnhbBSxW9BDxY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2/go.mod h1:antQGRDRJiuyQF6l+k6NECCSImgCpwaZapATth2Chv4= -github.com/lightninglabs/lndclient v0.18.4-7 h1:3lV3jeaL66wtxFeR+7YTo+1ZJ8YzD3gYHG8U9yas3YM= -github.com/lightninglabs/lndclient v0.18.4-7/go.mod h1:qaIx+eqEV+Bdf1j7GVeJiDqJbtZXsr9XTfHu/8HmgQU= +github.com/lightninglabs/lndclient v1.0.1-0.20241212185726-f8f7e3fa3ad8 h1:aOa99sBtrwSvnAED/BVTVjRYY/KoUtltuQmTF2FARJg= +github.com/lightninglabs/lndclient v1.0.1-0.20241212185726-f8f7e3fa3ad8/go.mod h1:qaIx+eqEV+Bdf1j7GVeJiDqJbtZXsr9XTfHu/8HmgQU= github.com/lightninglabs/loop v0.28.9-beta h1:JpAUpC7JEjYML36ZEJKwaTbtOfm1CgFuoykfYVok8Uc= github.com/lightninglabs/loop v0.28.9-beta/go.mod h1:XnB5JYj+8Vo9UBsvuxmx8NBO3HzoZa7gWNmJAACrnww= github.com/lightninglabs/loop/looprpc v1.0.1 h1:r/Nj9A26T/rZkbmUg6AttkK9n5r4jR4Hul4OOCM/5t0= From ec7a3e674fed5aaf430365b83d30079d01c09f26 Mon Sep 17 00:00:00 2001 From: George Tsagkarelis Date: Wed, 11 Dec 2024 12:07:48 +0100 Subject: [PATCH 12/12] itest: expect failure on direct rfq peer btc invoice Previously we'd consider it acceptable to settle direct rfq peer invoices, which included no rfq scid, with asset HTLCs. This behavior has been updated on the tapd invoice manager and we no longer accept asset HTLCs on invoices that do not expect assets. This commit updates such payments in our itests to instead expect a failure. --- itest/litd_custom_channels_test.go | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 77789c496..bfb810017 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -482,14 +482,12 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, // a direct channel invoice payment with no RFQ SCID present in the // invoice. // ------------ - paidAssetAmount := createAndPayNormalInvoice( + createAndPayNormalInvoice( t.t, charlie, dave, dave, 20_000, assetID, withSmallShards(), + withFailure(lnrpc.Payment_FAILED, failureIncorrectDetails), ) logBalance(t.t, nodes, assetID, "after invoice") - charlieAssetBalance -= paidAssetAmount - daveAssetBalance += paidAssetAmount - // We should also be able to do a multi-hop BTC only payment, paying an // invoice from Erin by Charlie. createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) @@ -535,7 +533,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, // ------------ // Test case 4: Pay a normal invoice from Erin by Charlie. // ------------ - paidAssetAmount = createAndPayNormalInvoice( + paidAssetAmount := createAndPayNormalInvoice( t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), ) logBalance(t.t, nodes, assetID, "after invoice") @@ -921,14 +919,12 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, // a direct channel invoice payment with no RFQ SCID present in the // invoice. // ------------ - paidAssetAmount := createAndPayNormalInvoice( + createAndPayNormalInvoice( t.t, charlie, dave, dave, 20_000, assetID, withSmallShards(), + withFailure(lnrpc.Payment_FAILED, failureIncorrectDetails), ) logBalance(t.t, nodes, assetID, "after invoice") - charlieAssetBalance -= paidAssetAmount - daveAssetBalance += paidAssetAmount - // We should also be able to do a multi-hop BTC only payment, paying an // invoice from Erin by Charlie. createAndPayNormalInvoiceWithBtc(t.t, charlie, erin, 2000) @@ -966,7 +962,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, // ------------ // Test case 4: Pay a normal invoice from Erin by Charlie. // ------------ - paidAssetAmount = createAndPayNormalInvoice( + paidAssetAmount := createAndPayNormalInvoice( t.t, charlie, dave, erin, 20_000, assetID, withSmallShards(), ) logBalance(t.t, nodes, assetID, "after invoice") @@ -1960,17 +1956,6 @@ func testCustomChannelsLiquidityEdgeCases(ctxb context.Context, t.t, charlie, invoiceResp.RHash, assetID, bigAssetAmount, ) - // Edge case: Big normal invoice, paid by direct channel peer with - // assets. - const hugeAssetAmount = 1_000_000 - _ = createAndPayNormalInvoice( - t.t, dave, charlie, charlie, hugeAssetAmount, assetID, - withSmallShards(), - ) - - logBalance(t.t, nodes, assetID, "after big asset payment (btc "+ - "invoice, direct)") - // Dave sends 200k assets and 5k sats to Yara. sendAssetKeySendPayment( t.t, dave, yara, 2*bigAssetAmount, assetID, fn.None[int64](),