@node-dlc/messaging
Version:
DLC Messaging Protocol
306 lines (255 loc) • 10.9 kB
text/typescript
import { BufferReader, BufferWriter } from '@node-dlc/bufio';
import { MessageType, PROTOCOL_VERSION } from '../MessageType';
import { deserializeTlv } from '../serialize/deserializeTlv';
import { getTlv } from '../serialize/getTlv';
import { bigIntToNumber, toBigInt } from '../util';
import { IDlcMessage } from './DlcMessage';
import { FundingInput, IFundingInputJSON } from './FundingInput';
import { FundingSignatures, IFundingSignaturesJSON } from './FundingSignatures';
import { ScriptWitnessV0 } from './ScriptWitnessV0';
/**
* DlcClose message contains information about a node and indicates its
* desire to close an existing contract.
* Updated to follow DlcOffer architectural patterns.
*/
export class DlcClose implements IDlcMessage {
public static type = MessageType.DlcClose;
/**
* Creates a DlcClose from JSON data (e.g., from test vectors)
* Handles both our internal format and external test vector formats
* @param json JSON object representing a DLC close message
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
public static fromJSON(json: any): DlcClose {
const instance = new DlcClose();
// Basic fields with field name variations
instance.protocolVersion =
json.protocolVersion || json.protocol_version || PROTOCOL_VERSION;
// Basic fields with field name variations
instance.contractId = Buffer.from(
json.contractId || json.contract_id,
'hex',
);
instance.closeSignature = Buffer.from(
json.closeSignature || json.close_signature,
'hex',
);
// Use toBigInt helper to handle BigInt values from json-bigint
instance.offerPayoutSatoshis = toBigInt(
json.offerPayoutSatoshis || json.offer_payout_satoshis,
);
instance.acceptPayoutSatoshis = toBigInt(
json.acceptPayoutSatoshis || json.accept_payout_satoshis,
);
instance.fundInputSerialId = toBigInt(
json.fundInputSerialId || json.fund_input_serial_id,
);
// Use FundingInput.fromJSON() for each funding input - proper delegation
instance.fundingInputs = (json.fundingInputs || json.funding_inputs || [])
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((inputJson: any) => FundingInput.fromJSON(inputJson));
// Create FundingSignatures manually since it doesn't have fromJSON
if (json.fundingSignatures || json.funding_signatures) {
instance.fundingSignatures = new FundingSignatures();
const sigData = json.fundingSignatures || json.funding_signatures;
// Handle different possible structures
if (sigData.fundingSignatures) {
// Standard format
instance.fundingSignatures.witnessElements = sigData.fundingSignatures.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(sig: any) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sig.witnessElements?.map((elem: any) => {
const witness = new ScriptWitnessV0();
witness.length =
elem.length || Buffer.from(elem.witness || '', 'hex').length;
witness.witness = Buffer.from(elem.witness || '', 'hex');
return witness;
}) || [],
);
} else if (Array.isArray(sigData)) {
// Array format
instance.fundingSignatures.witnessElements = sigData.map(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(sig: any) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sig.witnessElements?.map((elem: any) => {
const witness = new ScriptWitnessV0();
witness.length =
elem.length || Buffer.from(elem.witness || '', 'hex').length;
witness.witness = Buffer.from(elem.witness || '', 'hex');
return witness;
}) || [],
);
}
}
return instance;
}
/**
* Deserializes a close_dlc message with backward compatibility
* Detects old format (without protocol_version) vs new format (with protocol_version)
* @param buf
*/
public static deserialize(buf: Buffer): DlcClose {
const instance = new DlcClose();
const reader = new BufferReader(buf);
const type = reader.readUInt16BE(); // read type
// Validate type matches expected DlcClose type
if (type !== MessageType.DlcClose) {
throw new Error(
`Invalid message type. Expected ${MessageType.DlcClose}, got ${type}`,
);
}
// Read protocol version
instance.protocolVersion = reader.readUInt32BE();
instance.contractId = reader.readBytes(32);
instance.closeSignature = reader.readBytes(64);
instance.offerPayoutSatoshis = reader.readUInt64BE();
instance.acceptPayoutSatoshis = reader.readUInt64BE();
instance.fundInputSerialId = reader.readUInt64BE();
// Changed from u16 to bigsize for consistency with DlcOffer
const fundingInputsLen = Number(reader.readBigSize());
for (let i = 0; i < fundingInputsLen; i++) {
// FundingInput body is serialized directly without TLV wrapper in rust-dlc format
const fundingInput = FundingInput.deserializeBody(
reader.buffer.subarray(reader.position),
);
instance.fundingInputs.push(fundingInput);
// Skip past the FundingInput we just read
const fundingInputLength = fundingInput.serializeBody().length;
reader.position += fundingInputLength;
}
// Handle FundingSignatures - deserialize raw data (no TLV wrapper) like DlcSign
instance.fundingSignatures = FundingSignatures.deserialize(
reader.buffer.subarray(reader.position),
);
// Skip past the funding signatures we just read
const fundingLength = instance.fundingSignatures.serialize().length;
reader.position += fundingLength;
// Parse any additional TLV stream (for future extensibility)
while (!reader.eof) {
const buf = getTlv(reader);
const tlvReader = new BufferReader(buf);
const { type } = deserializeTlv(tlvReader);
// Store unknown TLVs for future compatibility
if (!instance.unknownTlvs) {
instance.unknownTlvs = [];
}
instance.unknownTlvs.push({ type: Number(type), data: buf });
}
return instance;
}
/**
* The type for close_dlc message. close_dlc = 52170
*/
public type = DlcClose.type;
// New fields as per dlcspecs PR #163
public protocolVersion: number = PROTOCOL_VERSION; // Default to current protocol version
public contractId: Buffer;
public closeSignature: Buffer;
public offerPayoutSatoshis: bigint;
public acceptPayoutSatoshis: bigint;
public fundInputSerialId: bigint;
public fundingInputs: FundingInput[] = [];
public fundingSignatures: FundingSignatures;
// Store unknown TLVs for forward compatibility
public unknownTlvs?: Array<{ type: number; data: Buffer }>;
/**
* Validates correctness of all fields
* @throws Will throw an error if validation fails
*/
public validate(): void {
// Type is set automatically in class
// protocol_version validation
if (this.protocolVersion !== PROTOCOL_VERSION) {
throw new Error(
`Unsupported protocol version: ${this.protocolVersion}, expected: ${PROTOCOL_VERSION}`,
);
}
// contractId validation
if (!this.contractId || this.contractId.length !== 32) {
throw new Error('contractId must be 32 bytes');
}
// closeSignature validation
if (!this.closeSignature || this.closeSignature.length !== 64) {
throw new Error('closeSignature must be 64 bytes');
}
// Ensure input serial ids are unique
const inputSerialIds = this.fundingInputs.map(
(input: FundingInput) => input.inputSerialId,
);
if (new Set(inputSerialIds).size !== inputSerialIds.length) {
throw new Error('inputSerialIds must be unique');
}
// Ensure funding inputs are segwit
this.fundingInputs.forEach((input: FundingInput) => input.validate());
// Note: FundingSignatures doesn't have a validate method, so we skip validation
}
/**
* Converts dlc_close to JSON (canonical rust-dlc format)
*/
public toJSON(): IDlcCloseJSON {
// Include unknown TLVs for debugging
const tlvs = [];
if (this.unknownTlvs) {
this.unknownTlvs.forEach((tlv) =>
tlvs.push({ type: tlv.type, data: tlv.data.toString('hex') }),
);
}
// Return canonical rust-dlc format
return {
protocolVersion: this.protocolVersion,
contractId: this.contractId.toString('hex'),
closeSignature: this.closeSignature.toString('hex'),
offerPayoutSatoshis: bigIntToNumber(this.offerPayoutSatoshis),
acceptPayoutSatoshis: bigIntToNumber(this.acceptPayoutSatoshis),
fundInputSerialId: bigIntToNumber(this.fundInputSerialId),
fundingInputs: this.fundingInputs.map((input) => input.toJSON()),
fundingSignatures: this.fundingSignatures.toJSON(),
}; // Allow different field names from interface
}
/**
* Serializes the close_dlc message into a Buffer
* Updated serialization format to match DlcOffer patterns
*/
public serialize(): Buffer {
const writer = new BufferWriter();
writer.writeUInt16BE(this.type);
// New fields as per dlcspecs PR #163
writer.writeUInt32BE(this.protocolVersion);
writer.writeBytes(this.contractId);
writer.writeBytes(this.closeSignature);
writer.writeUInt64BE(this.offerPayoutSatoshis);
writer.writeUInt64BE(this.acceptPayoutSatoshis);
writer.writeUInt64BE(this.fundInputSerialId);
// Changed from u16 to bigsize for consistency with DlcOffer
writer.writeBigSize(this.fundingInputs.length);
for (const fundingInput of this.fundingInputs) {
// Use serializeBody() to match rust-dlc behavior - funding inputs in vec are serialized without TLV wrapper
writer.writeBytes(fundingInput.serializeBody());
}
// Serialize FundingSignatures directly (no TLV wrapper) like DlcSign
writer.writeBytes(this.fundingSignatures.serialize());
// Write unknown TLVs for forward compatibility
if (this.unknownTlvs) {
this.unknownTlvs.forEach((tlv) => {
writer.writeBytes(tlv.data);
});
}
return writer.toBuffer();
}
}
export interface IDlcCloseJSON {
type?: number; // Made optional for rust-dlc compatibility
protocolVersion: number;
contractId: string;
closeSignature: string;
offerPayoutSatoshis: number;
acceptPayoutSatoshis: number;
fundInputSerialId: number;
fundingInputs: IFundingInputJSON[];
fundingSignatures: IFundingSignaturesJSON;
serialized?: string; // Made optional - hex serialization for compatibility testing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tlvs?: any[]; // Made optional - for unknown TLVs
}