UNPKG

@node-dlc/messaging

Version:
590 lines (508 loc) 21.6 kB
import { Value } from '@node-dlc/bitcoin'; import { BitcoinNetworks } from 'bitcoin-networks'; import { expect } from 'chai'; import { BatchFundingGroup, OrderIrcInfoV0, OrderMetadataV0, OrderPositionInfo, } from '../../lib'; import { EnumeratedDescriptor } from '../../lib/messages/ContractDescriptor'; import { SingleContractInfo } from '../../lib/messages/ContractInfo'; import { DlcOffer, DlcOfferContainer, LOCKTIME_THRESHOLD, } from '../../lib/messages/DlcOffer'; import { EnumEventDescriptorV0 } from '../../lib/messages/EventDescriptor'; import { FundingInput } from '../../lib/messages/FundingInput'; import { OracleAnnouncement } from '../../lib/messages/OracleAnnouncement'; import { OracleEvent } from '../../lib/messages/OracleEvent'; import { SingleOracleInfo } from '../../lib/messages/OracleInfo'; import { MessageType, PROTOCOL_VERSION } from '../../lib/MessageType'; describe('DlcOffer', () => { const bitcoinNetwork = BitcoinNetworks.bitcoin_regtest; let instance: DlcOffer; const contractFlags = Buffer.from('00', 'hex'); const chainHash = Buffer.from( '06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f', 'hex', ); const temporaryContractId = Buffer.from( '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', // 32 bytes 'hex', ); // Create ContractInfo programmatically for new dlcspecs PR #163 format function createTestContractInfo(): SingleContractInfo { const contractInfo = new SingleContractInfo(); contractInfo.totalCollateral = BigInt(200000000); // Create enumerated contract descriptor const contractDescriptor = new EnumeratedDescriptor(); contractDescriptor.outcomes = [ { outcome: 'win', localPayout: BigInt(0) }, { outcome: 'draw', localPayout: BigInt(153666723) }, { outcome: 'lose', localPayout: BigInt(200000000) }, ]; // Create oracle info (simplified) const oracleInfo = new SingleOracleInfo(); const announcement = new OracleAnnouncement(); announcement.announcementSig = Buffer.from( 'fab22628f6e2602e1671c286a2f63a9246794008627a1749639217f4214cb4a9494c93d1a852221080f44f697adb4355df59eb339f6ba0f9b01ba661a8b108d4', 'hex', ); announcement.oraclePublicKey = Buffer.from( 'da078bbb1d34e7729e38e2ae34236e776da121af442626fa31e31ae55a279a0b', 'hex', ); const oracleEvent = new OracleEvent(); oracleEvent.oracleNonces = [ Buffer.from( '3cfba011378411b20a5ab773cb95daab93e9bcd1e4cce44986a7dda84e01841b', 'hex', ), ]; oracleEvent.eventMaturityEpoch = 0; // Use proper EnumEventDescriptorV0 for new dlcspecs PR #163 format const eventDescriptor = new EnumEventDescriptorV0(); eventDescriptor.outcomes = ['dummy1', 'dummy2']; oracleEvent.eventDescriptor = eventDescriptor; oracleEvent.eventId = 'dummy'; announcement.oracleEvent = oracleEvent; oracleInfo.announcement = announcement; contractInfo.contractDescriptor = contractDescriptor; contractInfo.oracleInfo = oracleInfo; return contractInfo; } const fundingPubkey = Buffer.from( '0327efea09ff4dfb13230e887cbab8821d5cc249c7ff28668c6633ff9f4b4c08e3', 'hex', ); const payoutSpk = Buffer.from( '00142bbdec425007dc360523b0294d2c64d2213af498', 'hex', ); const fundingInput = Buffer.from( 'fda714' + '3f' + // length '000000000000fa51' + // input_serial_id '0029' + // prevtx_len '02000000000100c2eb0b00000000160014369d63a82ed846f4d47ad55045e594ab95539d6000000000' + // prevtx '00000000' + // prevtx_vout 'ffffffff' + // sequence '006b' + // max_witness_len '0000', // redeemscript_len 'hex', ); const changeSpk = Buffer.from( '0014afa16f949f3055f38bd3a73312bed00b61558884', 'hex', ); beforeEach(() => { instance = new DlcOffer(); instance.protocolVersion = PROTOCOL_VERSION; // New field instance.contractFlags = contractFlags; instance.chainHash = chainHash; instance.temporaryContractId = temporaryContractId; // New field instance.contractInfo = createTestContractInfo(); instance.fundingPubkey = fundingPubkey; instance.payoutSpk = payoutSpk; instance.payoutSerialId = BigInt(11555292); instance.offerCollateral = BigInt(99999999); instance.fundingInputs = [FundingInput.deserialize(fundingInput)]; instance.changeSpk = changeSpk; instance.changeSerialId = BigInt(2008045); instance.fundOutputSerialId = BigInt(5411962); instance.feeRatePerVb = BigInt(1); instance.cetLocktime = 100; instance.refundLocktime = 200; }); describe('deserialize', () => { it('should throw if incorrect type', () => { // Create buffer with incorrect type (0x123 instead of 42778) const incorrectTypeBuffer = Buffer.concat([ Buffer.from([0x01, 0x23]), // incorrect type instance.serialize().slice(2), // rest of the data ]); expect(function () { DlcOffer.deserialize(incorrectTypeBuffer); }).to.throw(Error); }); it('has correct type', () => { expect(DlcOffer.deserialize(instance.serialize()).type).to.equal( instance.type, ); }); }); describe('DlcOffer', () => { describe('serialize', () => { it('serializes', () => { // Test that it serializes without errors (new dlcspecs PR #163 format) const serialized = instance.serialize(); expect(serialized).to.be.instanceof(Buffer); expect(serialized.length).to.be.greaterThan(0); }); it('serializes with positioninfo', () => { const positionInfo = new OrderPositionInfo(); positionInfo.shiftForFees = 'acceptor'; positionInfo.fees = BigInt(1000); instance.positionInfo = positionInfo; // Test that it serializes without errors (new dlcspecs PR #163 format) const serialized = instance.serialize(); expect(serialized).to.be.instanceof(Buffer); expect(serialized.length).to.be.greaterThan(0); }); }); describe('deserialize', () => { it('deserializes', () => { // Use round-trip testing approach for consistency const serialized = instance.serialize(); const deserialized = DlcOffer.deserialize(serialized); expect(deserialized.protocolVersion).to.equal(PROTOCOL_VERSION); expect(deserialized.contractFlags).to.deep.equal(contractFlags); expect(deserialized.chainHash).to.deep.equal(chainHash); expect(deserialized.temporaryContractId).to.deep.equal( temporaryContractId, ); expect(deserialized.fundingPubkey).to.deep.equal(fundingPubkey); expect(deserialized.payoutSpk).to.deep.equal(payoutSpk); expect(Number(deserialized.payoutSerialId)).to.equal(11555292); expect(Number(deserialized.offerCollateral)).to.equal(99999999); expect(deserialized.changeSpk).to.deep.equal(changeSpk); expect(Number(deserialized.changeSerialId)).to.equal(2008045); expect(Number(deserialized.fundOutputSerialId)).to.equal(5411962); expect(Number(deserialized.feeRatePerVb)).to.equal(1); expect(deserialized.cetLocktime).to.equal(100); expect(deserialized.refundLocktime).to.equal(200); }); it('has correct type', () => { const serialized = instance.serialize(); expect(DlcOffer.deserialize(serialized).type).to.equal( MessageType.DlcOffer, ); }); it('deserializes with positioninfo', () => { const positionInfo = new OrderPositionInfo(); positionInfo.shiftForFees = 'acceptor'; positionInfo.fees = BigInt(1000); instance.positionInfo = positionInfo; const serialized = instance.serialize(); const deserialized = DlcOffer.deserialize(serialized); expect(deserialized.positionInfo).to.be.instanceof(OrderPositionInfo); expect(deserialized.positionInfo?.serialize()).to.deep.equal( positionInfo.serialize(), ); }); }); describe('toJSON', () => { it('converts to JSON', async () => { const json = instance.toJSON(); // Basic structure validation - detailed field testing is done in cross-language tests expect(json).to.be.an('object'); expect(json.protocolVersion).to.be.a('number'); expect(json.temporaryContractId).to.be.a('string'); expect(json.contractFlags).to.be.a('number'); expect(json.chainHash).to.be.a('string'); expect(json.contractInfo).to.be.an('object'); }); }); describe('getAddresses', () => { it('should get addresses', () => { const expectedFundingAddress = 'bcrt1qayylp95g2tzq2a60x2l7f8mclnx5y2jxm0yt09'; const expectedChangeAddress = 'bcrt1q47skl9ylxp2l8z7n5ue390kspds4tzyy5jdxs8'; const expectedPayoutAddress = 'bcrt1q9w77csjsqlwrvpfrkq556try6gsn4ayc2kn0kl'; // Use round-trip approach for consistency const serialized = instance.serialize(); const deserialized = DlcOffer.deserialize(serialized); const { fundingAddress, changeAddress, payoutAddress, } = deserialized.getAddresses(bitcoinNetwork); expect(fundingAddress).to.equal(expectedFundingAddress); expect(changeAddress).to.equal(expectedChangeAddress); expect(payoutAddress).to.equal(expectedPayoutAddress); }); }); describe('validate', () => { it('should throw if protocol version is invalid', () => { instance.protocolVersion = 999; expect(function () { instance.validate(); }).to.throw('Unsupported protocol version: 999, expected: 1'); }); it('should throw if temporaryContractId is invalid', () => { instance.temporaryContractId = Buffer.from('invalid', 'hex'); expect(function () { instance.validate(); }).to.throw('temporaryContractId must be 32 bytes'); }); it('should throw if payout_spk is invalid', () => { instance.payoutSpk = Buffer.from('fff', 'hex'); expect(function () { instance.validate(); }).to.throw(Error); }); it('should throw if change_spk is invalid', () => { instance.changeSpk = Buffer.from('fff', 'hex'); expect(function () { instance.validate(); }).to.throw(Error); }); it('should throw if fundingpubkey is not a valid pubkey', () => { instance.fundingPubkey = Buffer.from( '00f003aa11f2a97b6be755a86b9fd798a7451c670196a5245b7bae971306b7c87e', 'hex', ); expect(function () { instance.validate(); }).to.throw(Error); }); it('should throw if fundingpubkey is not in compressed secp256k1 format', () => { instance.fundingPubkey = Buffer.from( '045162991c7299223973cabc99ef5087d7bab2dafe61f78e5388b2f9492f7978123f51fd05ef0693790c0b2d4f30848363a3f3fbcf2bd53a05ba0fd5bb708c3184', 'hex', ); expect(function () { instance.validate(); }).to.throw(Error); }); it('should throw if offerCollateral is less than 1000', () => { instance.offerCollateral = BigInt(999); expect(function () { instance.validate(); }).to.throw(Error); // boundary check instance.offerCollateral = BigInt(1000); expect(function () { instance.validate(); }).to.not.throw(Error); }); it('should throw if cet_locktime is less than 0', () => { instance.cetLocktime = -1; expect(() => { instance.validate(); }).to.throw('cet_locktime must be greater than or equal to 0'); }); it('should throw if refund_locktime is less than 0', () => { instance.refundLocktime = -1; expect(() => { instance.validate(); }).to.throw('refund_locktime must be greater than or equal to 0'); }); it('should throw if cet_locktime and refund_locktime are not in same units', () => { instance.cetLocktime = 100; instance.refundLocktime = LOCKTIME_THRESHOLD + 200; expect(function () { instance.validate(); }).to.throw(Error); }); it('should not throw if cet_locktime and refund_locktime are in same units', () => { instance.cetLocktime = 100; instance.refundLocktime = 200; expect(function () { instance.validate(); }).to.not.throw(Error); instance.cetLocktime = LOCKTIME_THRESHOLD + 100; instance.refundLocktime = LOCKTIME_THRESHOLD + 200; expect(function () { instance.validate(); }).to.not.throw(Error); }); it('should throw if cet_locktime >= refund_locktime', () => { instance.cetLocktime = 200; instance.refundLocktime = 100; expect(function () { instance.validate(); }).to.throw(Error); instance.cetLocktime = 100; instance.refundLocktime = 100; expect(function () { instance.validate(); }).to.throw(Error); }); it('should throw if inputSerialIds arent unique', () => { instance.fundingInputs = [ FundingInput.deserialize(fundingInput), FundingInput.deserialize(fundingInput), ]; expect(function () { instance.validate(); }).to.throw(Error); }); it('should throw if changeSerialId == fundOutputSerialId', () => { instance.changeSerialId = instance.fundOutputSerialId; expect(function () { instance.validate(); }).to.throw(Error); }); 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(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.not.throw(Error); }); 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( '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; }); }); }); describe('DlcOfferContainer', () => { it('should serialize and deserialize', () => { // Create test offers using round-trip approach const dlcOffer = createTestDlcOffer(); const dlcOffer2 = createTestDlcOffer(); // swap payout and change spk to differentiate between dlcoffers dlcOffer2.payoutSpk = dlcOffer.changeSpk; dlcOffer2.changeSpk = dlcOffer.payoutSpk; const container = new DlcOfferContainer(); container.addOffer(dlcOffer); container.addOffer(dlcOffer2); const serialized = container.serialize(); const deserialized = DlcOfferContainer.deserialize(serialized); expect(deserialized.serialize()).to.deep.equal(container.serialize()); }); function createTestDlcOffer(): DlcOffer { const testOffer = new DlcOffer(); testOffer.protocolVersion = PROTOCOL_VERSION; testOffer.contractFlags = contractFlags; testOffer.chainHash = chainHash; testOffer.temporaryContractId = temporaryContractId; testOffer.contractInfo = createTestContractInfo(); testOffer.fundingPubkey = fundingPubkey; testOffer.payoutSpk = payoutSpk; testOffer.payoutSerialId = BigInt(11555292); testOffer.offerCollateral = BigInt(99999999); testOffer.fundingInputs = [FundingInput.deserialize(fundingInput)]; testOffer.changeSpk = changeSpk; testOffer.changeSerialId = BigInt(2008045); testOffer.fundOutputSerialId = BigInt(5411962); testOffer.feeRatePerVb = BigInt(1); testOffer.cetLocktime = 100; testOffer.refundLocktime = 200; return testOffer; } }); describe('TLVs', () => { it('should deserialize with all TLV types present', () => { const dlcOffer = new DlcOffer(); dlcOffer.protocolVersion = PROTOCOL_VERSION; dlcOffer.contractFlags = contractFlags; dlcOffer.chainHash = chainHash; dlcOffer.temporaryContractId = temporaryContractId; dlcOffer.contractInfo = createTestContractInfo(); dlcOffer.fundingPubkey = fundingPubkey; dlcOffer.payoutSpk = payoutSpk; dlcOffer.payoutSerialId = BigInt(29829); dlcOffer.offerCollateral = BigInt(16649967); dlcOffer.fundingInputs = [FundingInput.deserialize(fundingInput)]; dlcOffer.changeSpk = changeSpk; dlcOffer.changeSerialId = BigInt(94880); dlcOffer.fundOutputSerialId = BigInt(44394); dlcOffer.feeRatePerVb = BigInt(45); dlcOffer.cetLocktime = 1712689645; dlcOffer.refundLocktime = 1719255498; // Create TLV components programmatically for new dlcspecs PR #163 format const metadata = new OrderMetadataV0(); metadata.offerId = 'test-offer-id'; // Required property metadata.createdAt = 1640995200; // Optional but set for consistency metadata.goodTill = 1640995260; // Optional but set for consistency const ircInfo = new OrderIrcInfoV0(); ircInfo.nick = 'test-nick'; // Required property ircInfo.pubKey = Buffer.alloc(33); // Required property const positionInfo = new OrderPositionInfo(); positionInfo.shiftForFees = 'acceptor'; positionInfo.fees = BigInt(6930); const batchFundingGroup = new BatchFundingGroup(); batchFundingGroup.eventIds = ['test-event']; // Required property batchFundingGroup.allocatedCollateral = Value.fromSats(BigInt(1000)); // Required property dlcOffer.metadata = metadata; dlcOffer.ircInfo = ircInfo; dlcOffer.positionInfo = positionInfo; dlcOffer.batchFundingGroups = [batchFundingGroup]; // Test round-trip consistency with TLVs const serialized = dlcOffer.serialize(); const deserialized = DlcOffer.deserialize(serialized); expect(deserialized.toJSON()).to.deep.equal(dlcOffer.toJSON()); }); }); });