Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/messaging/__tests__/messages/DlcAccept.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,78 @@ describe('DlcAccept', () => {
instance.validate();
}).to.throw(Error);
});

it('should allow single funded DLCs with zero acceptCollateral', () => {
// Set up single funded DLC with zero collateral
instance.acceptCollateral = BigInt(0);
instance.fundingInputs = [];
instance.markAsSingleFunded();

expect(function () {
instance.validate();
}).to.not.throw(Error);
});

it('should throw if single funded DLC funding amount is insufficient', () => {
// Set up single funded DLC with insufficient funding
instance.acceptCollateral = BigInt(300000000);
instance.fundingInputs = [];
instance.markAsSingleFunded();

expect(function () {
instance.validate();
}).to.throw(
'For single funded DLCs, fundingAmount must be at least acceptCollateral',
);
});
});

describe('Single Funded DLC Methods', () => {
it('should correctly identify single funded DLCs', () => {
// Initially not single funded
expect(instance.isSingleFunded()).to.be.false;

// Mark as single funded
instance.markAsSingleFunded();

expect(instance.isSingleFunded()).to.be.true;
});

it('should not identify regular DLCs as single funded', () => {
// Default test setup has non-zero acceptCollateral
expect(instance.isSingleFunded()).to.be.false;
});

it('should mark DLC as single funded', () => {
expect(function () {
instance.markAsSingleFunded();
}).to.not.throw(Error);

expect(instance.singleFunded).to.be.true;
});

it('should auto-detect single funded DLCs during deserialization', () => {
// Set up single funded DLC with zero collateral
instance.acceptCollateral = BigInt(0);

// Serialize and deserialize
const serialized = instance.serialize();
const deserialized = DlcAccept.deserialize(serialized);

// Should auto-detect as single funded
expect(deserialized.singleFunded).to.be.true;
expect(deserialized.isSingleFunded()).to.be.true;
});

it('should not auto-detect regular DLCs as single funded', () => {
// Default test setup has non-zero acceptCollateral
const serialized = instance.serialize();
const deserialized = DlcAccept.deserialize(serialized);

// Should not be detected as single funded
expect(deserialized.singleFunded).to.be.false;
expect(deserialized.isSingleFunded()).to.be.false;
});
});
});

Expand Down
102 changes: 94 additions & 8 deletions packages/messaging/__tests__/messages/DlcOffer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,25 +385,111 @@ describe('DlcOffer', () => {
}).to.throw(Error);
});

it('should throw if totalCollateral <= offerCollateral', () => {
instance.contractInfo.totalCollateral = BigInt(200000000);
instance.offerCollateral = BigInt(200000000);
it('should throw if funding amount less than offer collateral', () => {
instance.offerCollateral = BigInt(3e8);
expect(function () {
instance.validate();
}).to.throw(Error);
});

it('should allow single funded DLCs when totalCollateral equals offerCollateral', () => {
// Set up single funded DLC
instance.contractInfo.totalCollateral = BigInt(99999999);
instance.offerCollateral = BigInt(99999999);
instance.markAsSingleFunded();

expect(function () {
instance.validate();
}).to.not.throw(Error);
});

it('should throw if marked as single funded but collateral amounts do not match', () => {
instance.contractInfo.totalCollateral = BigInt(200000000);
instance.offerCollateral = BigInt(200000001);
instance.offerCollateral = BigInt(99999999);

expect(function () {
instance.markAsSingleFunded();
}).to.throw(
'Cannot mark as single funded: totalCollateral (200000000) must equal offerCollateral (99999999)',
);
});

it('should validate single funded DLC funding amount correctly', () => {
// Set up single funded DLC
instance.contractInfo.totalCollateral = BigInt(99999999);
instance.offerCollateral = BigInt(99999999);
instance.markAsSingleFunded();

// Funding amount should be at least totalCollateral for single funded DLCs
expect(function () {
instance.validate();
}).to.throw(Error);
}).to.not.throw(Error);
});

it('should throw if funding amount less than offer collateral', () => {
instance.offerCollateral = BigInt(3e8);
it('should throw if single funded DLC funding amount is insufficient', () => {
// Set up single funded DLC with insufficient funding
instance.contractInfo.totalCollateral = BigInt(300000000);
instance.offerCollateral = BigInt(300000000);
instance.markAsSingleFunded();

expect(function () {
instance.validate();
}).to.throw(Error);
}).to.throw(
'For single funded DLCs, fundingAmount must be at least totalCollateral',
);
});
});

describe('Single Funded DLC Methods', () => {
it('should correctly identify single funded DLCs', () => {
// Initially not single funded
expect(instance.isSingleFunded()).to.be.false;

// Mark as single funded
instance.contractInfo.totalCollateral = BigInt(99999999);
instance.offerCollateral = BigInt(99999999);
instance.markAsSingleFunded();

expect(instance.isSingleFunded()).to.be.true;
});

it('should auto-detect single funded DLCs based on collateral amounts', () => {
// Set equal collateral amounts
instance.contractInfo.totalCollateral = BigInt(99999999);
instance.offerCollateral = BigInt(99999999);

// Should be detected as single funded even without explicit flag
expect(instance.isSingleFunded()).to.be.true;
});

it('should not identify regular DLCs as single funded', () => {
// Default test setup has different collateral amounts
expect(instance.isSingleFunded()).to.be.false;
});

it('should mark DLC as single funded when collateral amounts are equal', () => {
instance.contractInfo.totalCollateral = BigInt(99999999);
instance.offerCollateral = BigInt(99999999);

expect(function () {
instance.markAsSingleFunded();
}).to.not.throw(Error);

expect(instance.singleFunded).to.be.true;
});

it('should auto-detect single funded DLCs during deserialization', () => {
// Set up single funded DLC
instance.contractInfo.totalCollateral = BigInt(99999999);
instance.offerCollateral = BigInt(99999999);

// Serialize and deserialize
const serialized = instance.serialize();
const deserialized = DlcOffer.deserialize(serialized);

// Should auto-detect as single funded
expect(deserialized.singleFunded).to.be.true;
expect(deserialized.isSingleFunded()).to.be.true;
});
});
});
Expand Down
44 changes: 42 additions & 2 deletions packages/messaging/lib/messages/DlcAccept.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ export class DlcAccept implements IDlcMessage {
}
}

// Auto-detect single funded DLCs based on minimal acceptCollateral
// In single funded DLCs, acceptCollateral is typically 0 or very small
if (instance.acceptCollateral === BigInt(0)) {
instance.singleFunded = true;
}

return instance;
}

Expand Down Expand Up @@ -321,6 +327,28 @@ export class DlcAccept implements IDlcMessage {
// Store unknown TLVs for forward compatibility
public unknownTlvs?: Array<{ type: number; data: Buffer }>;

/**
* Flag to indicate if this is a single funded DLC
* In single funded DLCs, the acceptor provides minimal or no collateral
*/
public singleFunded = false;

/**
* Marks this DLC accept as single funded
* For single funded DLCs, acceptCollateral is typically 0 or minimal
*/
public markAsSingleFunded(): void {
this.singleFunded = true;
}

/**
* Checks if this DLC accept is for a single funded DLC
* @returns True if this is a single funded DLC
*/
public isSingleFunded(): boolean {
return this.singleFunded;
}

/**
* Get funding, change and payout address from DlcAccept
* @param network Bitcoin Network
Expand Down Expand Up @@ -402,8 +430,20 @@ export class DlcAccept implements IDlcMessage {
const input = fundingInput as FundingInput;
return acc + input.prevTx.outputs[input.prevTxVout].value.sats;
}, BigInt(0));
if (this.acceptCollateral >= fundingAmount) {
throw new Error('fundingAmount must be greater than acceptCollateral');

if (this.isSingleFunded()) {
// For single funded DLCs, acceptor may provide minimal or no collateral
// fundingAmount should be >= acceptCollateral (allowing for 0 collateral case)
if (this.acceptCollateral > 0 && fundingAmount < this.acceptCollateral) {
throw new Error(
'For single funded DLCs, fundingAmount must be at least acceptCollateral',
);
}
} else {
// For regular DLCs, funding amount must be greater than accept collateral
if (this.acceptCollateral >= fundingAmount) {
throw new Error('fundingAmount must be greater than acceptCollateral');
}
}
}

Expand Down
68 changes: 64 additions & 4 deletions packages/messaging/lib/messages/DlcOffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ export class DlcOffer implements IDlcMessage {
}
}

// Auto-detect single funded DLCs
if (
instance.contractInfo.getTotalCollateral() === instance.offerCollateral
) {
instance.singleFunded = true;
}

return instance;
}

Expand Down Expand Up @@ -279,6 +286,37 @@ export class DlcOffer implements IDlcMessage {
// Store unknown TLVs for forward compatibility
public unknownTlvs?: Array<{ type: number; data: Buffer }>;

/**
* Flag to indicate if this is a single funded DLC
* In single funded DLCs, totalCollateral equals offerCollateral
*/
public singleFunded = false;

/**
* Marks this DLC offer as single funded and validates that collateral amounts are correct
* @throws Will throw an error if totalCollateral doesn't equal offerCollateral
*/
public markAsSingleFunded(): void {
const totalCollateral = this.contractInfo.getTotalCollateral();
if (totalCollateral !== this.offerCollateral) {
throw new Error(
`Cannot mark as single funded: totalCollateral (${totalCollateral}) must equal offerCollateral (${this.offerCollateral})`,
);
}
this.singleFunded = true;
}

/**
* Checks if this DLC offer is single funded (totalCollateral == offerCollateral)
* @returns True if this is a single funded DLC
*/
public isSingleFunded(): boolean {
return (
this.singleFunded ||
this.contractInfo.getTotalCollateral() === this.offerCollateral
);
}

/**
* Get funding, change and payout address from DlcOffer
* @param network Bitcoin Network
Expand Down Expand Up @@ -400,17 +438,39 @@ export class DlcOffer implements IDlcMessage {
this.contractInfo.validate();

// totalCollateral should be > offerCollateral (logical validation)
if (this.contractInfo.getTotalCollateral() <= this.offerCollateral) {
throw new Error('totalCollateral should be greater than offerCollateral');
// Exception: for single funded DLCs, totalCollateral == offerCollateral is allowed
if (this.isSingleFunded()) {
if (this.contractInfo.getTotalCollateral() !== this.offerCollateral) {
throw new Error(
'For single funded DLCs, totalCollateral must equal offerCollateral',
);
}
} else {
if (this.contractInfo.getTotalCollateral() <= this.offerCollateral) {
throw new Error(
'totalCollateral should be greater than offerCollateral',
);
}
}

// validate funding amount
const fundingAmount = this.fundingInputs.reduce((acc, fundingInput) => {
const input = fundingInput as FundingInput;
return acc + input.prevTx.outputs[input.prevTxVout].value.sats;
}, BigInt(0));
if (this.offerCollateral >= fundingAmount) {
throw new Error('fundingAmount must be greater than offerCollateral');

if (this.isSingleFunded()) {
// For single funded DLCs, funding amount must cover the full total collateral plus fees
if (fundingAmount < this.contractInfo.getTotalCollateral()) {
throw new Error(
'For single funded DLCs, fundingAmount must be at least totalCollateral',
);
}
} else {
// For regular DLCs, funding amount must be greater than offer collateral
if (this.offerCollateral >= fundingAmount) {
throw new Error('fundingAmount must be greater than offerCollateral');
}
}
}

Expand Down
Loading