diff --git a/packages/messaging/__tests__/messages/DlcCloseV0.spec.ts b/packages/messaging/__tests__/messages/DlcCloseV0.spec.ts new file mode 100644 index 00000000..34456975 --- /dev/null +++ b/packages/messaging/__tests__/messages/DlcCloseV0.spec.ts @@ -0,0 +1,130 @@ +import { expect } from 'chai'; + +import { DlcClose, DlcCloseV0 } from '../../lib/messages/DlcClose'; +import { FundingInputV0 } from '../../lib/messages/FundingInput'; +import { MessageType } from '../../lib/MessageType'; + +describe('DlcClose', () => { + let instance: DlcCloseV0; + + const type = Buffer.from('cbca', 'hex'); + + const contractId = Buffer.from( + 'c1c79e1e9e2fa2840b2514902ea244f39eb3001a4037a52ea43c797d4f841269', + 'hex', + ); + + const closeSignature = Buffer.from( + '7c8ad6de287b62a1ed1d74ed9116a5158abc7f97376d201caa88e0f9daad68fcda4c271cc003512e768f403a57e5242bd1f6aa1750d7f3597598094a43b1c7bb', + 'hex', + ); + + const offerPayoutSatoshis = Buffer.from('0000000005f5e100', 'hex'); + const acceptPayoutSatoshis = Buffer.from('0000000005f5e100', 'hex'); + + const fundingInputsLen = Buffer.from('0001', 'hex'); + const fundingInputV0 = Buffer.from( + 'fda714' + // type funding_input_v0 + '3f' + // length + '000000000000dae8' + // input_serial_id + '0029' + // prevtx_len + '02000000000100c2eb0b000000001600149ea3bf2d6eb9c2ffa35e36f41e117403ed7fafe900000000' + // prevtx + '00000000' + // prevtx_vout + 'ffffffff' + // sequence + '006b' + // max_witness_len + '0000', // redeem_script_len + 'hex', + ); + + const dlcCloseHex = Buffer.concat([ + type, + contractId, + closeSignature, + offerPayoutSatoshis, + acceptPayoutSatoshis, + fundingInputsLen, + fundingInputV0, + ]); + + beforeEach(() => { + instance = new DlcCloseV0(); + instance.contractId = contractId; + instance.closeSignature = closeSignature; + instance.offerPayoutSatoshis = BigInt(100000000); + instance.acceptPayoutSatoshis = BigInt(100000000); + instance.fundingInputs = [FundingInputV0.deserialize(fundingInputV0)]; + }); + + describe('deserialize', () => { + it('should throw if incorrect type', () => { + instance.type = 0x123; + expect(function () { + DlcClose.deserialize(instance.serialize()); + }).to.throw(Error); + }); + + it('has correct type', () => { + expect(DlcClose.deserialize(instance.serialize()).type).to.equal( + instance.type, + ); + }); + }); + + describe('DlcCloseV0', () => { + describe('serialize', () => { + it('serializes', () => { + expect(instance.serialize().toString('hex')).to.equal( + dlcCloseHex.toString('hex'), + ); + }); + }); + + describe('deserialize', () => { + it('deserializes', () => { + const instance = DlcCloseV0.deserialize(dlcCloseHex); + expect(instance.contractId).to.deep.equal(contractId); + expect(instance.closeSignature).to.deep.equal(closeSignature); + expect(Number(instance.offerPayoutSatoshis)).to.equal(100000000); + expect(Number(instance.acceptPayoutSatoshis)).to.equal(100000000); + expect(instance.fundingInputs[0].serialize().toString('hex')).to.equal( + fundingInputV0.toString('hex'), + ); + }); + + it('has correct type', () => { + expect(DlcCloseV0.deserialize(dlcCloseHex).type).to.equal( + MessageType.DlcCloseV0, + ); + }); + }); + + describe('toJSON', () => { + it('convert to JSON', async () => { + const json = instance.toJSON(); + expect(json.contractId).to.equal(contractId.toString('hex')); + expect(json.closeSignature).to.equal(closeSignature.toString('hex')); + expect(json.fundingInputs[0].prevTx).to.equal( + instance.fundingInputs[0].prevTx.serialize().toString('hex'), + ); + }); + }); + + describe('validate', () => { + it('should throw if inputSerialIds arent unique', () => { + instance.fundingInputs = [ + FundingInputV0.deserialize(fundingInputV0), + FundingInputV0.deserialize(fundingInputV0), + ]; + expect(function () { + instance.validate(); + }).to.throw(Error); + }); + it('should ensure funding inputs are segwit', () => { + instance.fundingInputs = [FundingInputV0.deserialize(fundingInputV0)]; + expect(function () { + instance.validate(); + }).to.throw(Error); + }); + }); + }); +}); diff --git a/packages/messaging/lib/MessageType.ts b/packages/messaging/lib/MessageType.ts index a6e25507..d5d82e36 100644 --- a/packages/messaging/lib/MessageType.ts +++ b/packages/messaging/lib/MessageType.ts @@ -46,6 +46,7 @@ export enum MessageType { DlcAcceptV0 = 42780, DlcSignV0 = 42782, + DlcCloseV0 = 52170, // TODO: Temporary type DlcCancelV0 = 52172, /** diff --git a/packages/messaging/lib/messages/DlcClose.ts b/packages/messaging/lib/messages/DlcClose.ts new file mode 100644 index 00000000..26c3ccfa --- /dev/null +++ b/packages/messaging/lib/messages/DlcClose.ts @@ -0,0 +1,133 @@ +import { BufferReader, BufferWriter } from '@node-lightning/bufio'; + +import { MessageType } from '../MessageType'; +import { getTlv } from '../serialize/getTlv'; +import { IDlcMessage } from './DlcMessage'; +import { FundingInputV0, IFundingInputV0JSON } from './FundingInput'; + +export abstract class DlcClose { + public static deserialize(buf: Buffer): DlcCloseV0 { + const reader = new BufferReader(buf); + + const type = Number(reader.readUInt16BE()); + + switch (type) { + case MessageType.DlcCloseV0: + return DlcCloseV0.deserialize(buf); + default: + throw new Error(`DLC Close message type must be DlcCloseV0`); // This is a temporary measure while protocol is being developed + } + } + + public abstract type: number; + + public abstract toJSON(): IDlcCloseV0JSON; + + public abstract serialize(): Buffer; +} + +/** + * DlcClose message contains information about a node and indicates its + * desire to close an existing contract. + */ +export class DlcCloseV0 extends DlcClose implements IDlcMessage { + public static type = MessageType.DlcCloseV0; + + /** + * Deserializes an close_dlc_v0 message + * @param buf + */ + public static deserialize(buf: Buffer): DlcCloseV0 { + const instance = new DlcCloseV0(); + const reader = new BufferReader(buf); + + reader.readUInt16BE(); // read type + instance.contractId = reader.readBytes(32); + instance.closeSignature = reader.readBytes(64); + instance.offerPayoutSatoshis = reader.readUInt64BE(); + instance.acceptPayoutSatoshis = reader.readUInt64BE(); + const fundingInputsLen = reader.readUInt16BE(); + for (let i = 0; i < fundingInputsLen; i++) { + instance.fundingInputs.push(FundingInputV0.deserialize(getTlv(reader))); + } + + return instance; + } + + /** + * The type for close_dlc_v0 message. close_dlc_v0 = 52170 // TODO + */ + public type = DlcCloseV0.type; + + public contractId: Buffer; + + public closeSignature: Buffer; + + public offerPayoutSatoshis: bigint; + + public acceptPayoutSatoshis: bigint; + + public fundingInputs: FundingInputV0[] = []; + + /** + * Serializes the close_dlc_v0 message into a Buffer + */ + public serialize(): Buffer { + const writer = new BufferWriter(); + writer.writeUInt16BE(this.type); + writer.writeBytes(this.contractId); + writer.writeBytes(this.closeSignature); + writer.writeUInt64BE(this.offerPayoutSatoshis); + writer.writeUInt64BE(this.acceptPayoutSatoshis); + writer.writeUInt16BE(this.fundingInputs.length); + + for (const fundingInput of this.fundingInputs) { + writer.writeBytes(fundingInput.serialize()); + } + + return writer.toBuffer(); + } + + /** + * Validates correctness of all fields + * @throws Will throw an error if validation fails + */ + public validate(): void { + // Type is set automatically in class + + // Ensure input serial ids are unique + const inputSerialIds = this.fundingInputs.map( + (input: FundingInputV0) => 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: FundingInputV0) => input.validate()); + } + + /** + * Converts dlc_close_v0 to JSON + */ + public toJSON(): IDlcCloseV0JSON { + return { + type: this.type, + contractId: this.contractId.toString('hex'), + closeSignature: this.closeSignature.toString('hex'), + offerPayoutSatoshis: Number(this.offerPayoutSatoshis), + acceptPayoutSatoshis: Number(this.acceptPayoutSatoshis), + fundingInputs: this.fundingInputs.map((input) => input.toJSON()), + }; + } +} + +export interface IDlcCloseV0JSON { + type: number; + contractId: string; + closeSignature: string; + offerPayoutSatoshis: number; + acceptPayoutSatoshis: number; + fundingInputs: IFundingInputV0JSON[]; +}