@node-dlc/core
Version:
493 lines (428 loc) • 20.2 kB
text/typescript
import { Value } from '@node-dlc/bitcoin';
import {
ContractDescriptorV1,
ContractInfoV0,
OracleAnnouncement,
PayoutFunctionV0,
} from '@node-dlc/messaging';
import { BitcoinNetworks } from 'bitcoin-networks';
import { expect } from 'chai';
import sinon from 'sinon';
import {
buildCoveredCallOrderOffer,
buildCustomStrategyOrderOffer,
buildLongCallOrderOffer,
buildLongPutOrderOffer,
buildRoundingIntervalsFromIntervals,
buildShortPutOrderOffer,
computeRoundingModulus,
UNIT_MULTIPLIER,
} from '../../../lib/dlc/finance/Builder';
describe('OrderOffer Builder', () => {
describe('buildCoveredCallOrderOffer', () => {
const strikePrice = 50000;
const contractSize = Value.fromSats(100000000);
const premium = Value.fromSats(25000);
const threeXPremium = Value.fromSats(75000);
const totalCollateral = contractSize;
const oracleAnnouncementBuf = Buffer.from(
'fdd824fd02ab1efe41fa42ea1dcd103a0251929dd2b192d2daece8a4ce4d81f68a183b750d92d6f02d796965dc79adf4e7786e08f861a1ecc897afbba2dab9cff6eb0a81937eb8b005b07acf849ad2cec22107331dedbf5a607654fad4eafe39c278e27dde68fdd822fd02450011f9313f1edd903fab297d5350006b669506eb0ffda0bb58319b4df89ac24e14fd15f9791dc78d1596b06f4969bdb37d9e394dc9fedaa18d694027fa32b5ea2a5e60080c58e13727367c3a4ce1ad65dfb3c7e3ca1ea912b0299f6e383bab2875058aa96a1c74633130af6fbd008788de6ac9db76da4ecc7303383cc1a49f525316413850f7e3ac385019d560e84c5b3a3e9ae6c83f59fe4286ddfd23ea46d7ae04610a175cd28a9bf5f574e245c3dfe230dc4b0adf4daaea96780e594f6464f676505f4b74cfe3ffc33415a23de795bf939ce64c0c02033bbfc6c9ff26fb478943a1ece775f38f5db067ca4b2a9168b40792398def9164bfe5c46838472dc3c162af16c811b7a116e9417d5bccb9e5b8a5d7d26095aba993696188c3f85a02f7ab8d12ada171c352785eb63417228c7e248909fc2d673e1bb453140bf8bf429375819afb5e9556663b76ff09c2a7ba9779855ffddc6d360cb459cf8c42a2b949d0de19fe96163d336fd66a4ce2f1791110e679572a20036ffae50204ef520c01058ff4bef28218d1c0e362ee3694ad8b2ae83a51c86c4bc1630ed6202a158810096726f809fc828fafdcf053496affdf887ae8c54b6ca4323ccecf6a51121c4f0c60e790536dab41b221db1c6b35065dc19a9d31cf75901aa35eefecbb6fefd07296cda13cb34ce3b58eba20a0eb8f9614994ec7fee3cc290e30e6b1e3211ae1f3a85b6de6abdbb77d6d9ed33a1cee3bd5cd93a71f12c9c45e385d744ad0e7286660305100fdd80a11000200076274632f75736400000000001109425443205072696365',
'hex',
);
const oracleAnnouncement = OracleAnnouncement.deserialize(
oracleAnnouncementBuf,
);
it('should build a covered call OrderOffer correctly', () => {
sinon.stub(Date.prototype, 'getTime').returns(Date.UTC(2021, 1, 5));
const orderOffer = buildCoveredCallOrderOffer(
oracleAnnouncement,
contractSize,
strikePrice,
premium,
12,
10000,
'bitcoin',
);
expect(() => orderOffer.validate()).to.not.throw(Error);
});
it('should build a short put OrderOffer correctly', () => {
const orderOffer = buildShortPutOrderOffer(
oracleAnnouncement,
contractSize,
strikePrice,
totalCollateral,
premium,
12,
10000,
'bitcoin',
);
expect(() => orderOffer.validate()).to.not.throw(Error);
});
it('should build a long call OrderOffer correctly', () => {
const orderOffer = buildLongCallOrderOffer(
oracleAnnouncement,
contractSize,
strikePrice,
threeXPremium,
premium,
12,
10000,
'bitcoin',
);
expect(() => orderOffer.validate()).to.not.throw(Error);
});
it('should build a long put OrderOffer correctly', () => {
const orderOffer = buildLongPutOrderOffer(
oracleAnnouncement,
contractSize,
strikePrice,
threeXPremium,
premium,
12,
10000,
'bitcoin',
);
expect(() => orderOffer.validate()).to.not.throw(Error);
});
it('should fail to build an OrderOffer with an invalid oracleAnnouncement', () => {
oracleAnnouncement.announcementSig = Buffer.from('deadbeef', 'hex');
const orderOffer = buildShortPutOrderOffer(
oracleAnnouncement,
contractSize,
strikePrice,
totalCollateral,
premium,
12,
10000,
'bitcoin',
);
expect(() => orderOffer.validate()).to.throw(Error);
});
});
describe('buildCustomStrategyOrderOffer', () => {
const contractSizes: Value[] = [
Value.fromBitcoin(1),
Value.fromBitcoin(0.1),
Value.fromBitcoin(0.5),
Value.fromBitcoin(1.5),
Value.fromBitcoin(2),
];
const defaultContractSize = Value.fromBitcoin(1);
const maxLoss = Value.fromBitcoin(0.2);
const maxGain = Value.fromBitcoin(0.04);
const feeRate = 2n;
const highestPrecisionRounding = Value.fromSats(10000);
const highPrecisionRounding = Value.fromSats(25000);
const mediumPrecisionRounding = Value.fromSats(100000);
const lowPrecisionRounding = Value.fromSats(200000);
const network = BitcoinNetworks.bitcoin;
const oracleAnnouncementBuf = Buffer.from(
'fdd824fd032e4a2b121973a7d2be4784fb6fb0c7b97405b757fb078b58bb729756a7372bf1bdc05c373472fc57fde0a07dcdad027f48f313fa1e349ab8bb936472735fce5221ff9c4885aa0c5987046ada7e729dac33591777a37af8bf14dedb95997d0c7d4bfdd822fd02c800154a2b121973a7d2be4784fb6fb0c7b97405b757fb078b58bb729756a7372bf1bdd7cb55eee59faac9a590e7d3bab782276daa0744786aaff3f69a1e51de99012339492f00198932d8057eb6742bd5245e37fd22b44e0bfaa68577ce5654b478786eb7dc091559699292dc060ee82864cffd863eb8fa876682099af34a8f3eab9174773231e6950b077df212df3fc91c199c34200cd8ce00f1c8a050ad471bfc1478e90d06bd77f11fc3804435556ed5dc1eb19b95a978f595ba127ea225c4662e22ea614cab60054d9b21da904cd9eac5b514a6a011de635ad08e0c72766a7bbc9144df24fe6bf3e0d631a61a1da27e93c6f4ea6829795f4c63628747415a41ffe199129639d7a6c430184aa925b9ab224c9180b64c86959bd7abc9ffa08fe4421952114839d5120f4ea9c9437364b4956b25a141aeefbc30d3e6cc811f9424d266671dc330b7f3d00f1f7a521e14697a0259499e6cc8fa0da13a095279f827cfe2cf11508c6d8d1405f6cb337d68fbe29fde5acbb094ff397c14c24f084ac6ef466b1d85d5160de41ed8ed435ab52f948788dc45406e07f02683c9822431d91d95a04e11e4fdb521485aa8737bdd8c67cfefd0d103f86005491181e704adafc7f49cc9700ce8cffda9c9b265bc64ed19b42d4b5e4449f6b0da81342923c45f37b1381b3a29b9acce9818f4ec6af0ecbddc063e64868f350281054d13599e6e35d503532b6fa9c0db85db703dde1de21142b351ecce626128f98ddb5dab11f669c16de45d3dd054786aa5e3fb6369114b97bee6c26d21be0f71258dfa3e8ec8b8890719eef8e817b69072f74325088781a50b4cd5fe74493828dfe9cb21b698901e48f9f4d7b70a636794feae500aac237add2630bad1257b0cac1b71a8fcd5dbd7540a84ccac5d48b76afae2b3dfe3ea8eb8cd6e67febb6a1433504329948e426064108cfdd80a0e00020004626974730000000000150f73747261746567794f7574636f6d65',
'hex',
);
const oracleAnnouncement = OracleAnnouncement.deserialize(
oracleAnnouncementBuf,
);
const unit = 'bits';
for (const contractSize of contractSizes) {
it(`should build a CSO OrderOffer with contract size ${contractSize.bitcoin} BTC correctly`, () => {
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const orderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
);
const payoutCurvePieces = (((orderOffer.contractInfo as ContractInfoV0)
.contractDescriptor as ContractDescriptorV1)
.payoutFunction as PayoutFunctionV0).payoutFunctionPieces;
expect(() => orderOffer.validate()).to.not.throw(Error);
expect(orderOffer.contractInfo.totalCollateral).to.equal(
contractSize.sats,
);
expect(orderOffer.offerCollateralSatoshis).to.equal(
contractSize.sats - (contractSize.sats * maxGain.sats) / BigInt(1e8),
);
expect(payoutCurvePieces[0].endPoint.eventOutcome).to.equal(
(defaultContractSize.sats - maxLoss.sats) /
BigInt(UNIT_MULTIPLIER[unit]),
);
expect(payoutCurvePieces[1].endPoint.eventOutcome).to.equal(
(defaultContractSize.sats + maxGain.sats) /
BigInt(UNIT_MULTIPLIER[unit]),
);
expect(payoutCurvePieces[0].endPoint.outcomePayout).to.equal(
contractSize.sats -
(maxLoss.sats * contractSize.sats) / BigInt(1e8) -
(maxGain.sats * contractSize.sats) / BigInt(1e8),
);
expect(payoutCurvePieces[1].endPoint.outcomePayout).to.equal(
contractSize.sats,
);
});
}
it('should build a CSO OrderOffer and shift the payout curve correctly for offeror fees', () => {
const contractSize = contractSizes[0];
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const offerFees = Value.fromSats(10000);
const orderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
'offeror',
offerFees,
);
const payoutCurvePieces = (((orderOffer.contractInfo as ContractInfoV0)
.contractDescriptor as ContractDescriptorV1)
.payoutFunction as PayoutFunctionV0).payoutFunctionPieces;
expect(() => orderOffer.validate()).to.not.throw(Error);
expect(orderOffer.contractInfo.totalCollateral).to.equal(
contractSize.sats,
);
expect(orderOffer.offerCollateralSatoshis).to.equal(
contractSize.sats - (contractSize.sats * maxGain.sats) / BigInt(1e8),
);
expect(payoutCurvePieces[0].endPoint.eventOutcome).to.equal(
(defaultContractSize.sats - maxLoss.sats + offerFees.sats) /
BigInt(UNIT_MULTIPLIER[unit]),
);
expect(payoutCurvePieces[1].endPoint.eventOutcome).to.equal(
(defaultContractSize.sats + maxGain.sats + offerFees.sats) /
BigInt(UNIT_MULTIPLIER[unit]),
);
expect(payoutCurvePieces[0].endPoint.outcomePayout).to.equal(
contractSize.sats -
(maxLoss.sats * contractSize.sats) / BigInt(1e8) -
(maxGain.sats * contractSize.sats) / BigInt(1e8),
);
expect(payoutCurvePieces[1].endPoint.outcomePayout).to.equal(
contractSize.sats,
);
});
it('should build a CSO OrderOffer and shift the payout curve correctly for acceptor fees', () => {
const acceptFees = Value.fromSats(10000);
const contractSize = contractSizes[0];
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const orderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
'acceptor',
acceptFees,
);
const payoutCurvePieces = (((orderOffer.contractInfo as ContractInfoV0)
.contractDescriptor as ContractDescriptorV1)
.payoutFunction as PayoutFunctionV0).payoutFunctionPieces;
expect(() => orderOffer.validate()).to.not.throw(Error);
expect(orderOffer.contractInfo.totalCollateral).to.equal(
contractSize.sats,
);
expect(orderOffer.offerCollateralSatoshis).to.equal(
contractSize.sats - (contractSize.sats * maxGain.sats) / BigInt(1e8),
);
expect(payoutCurvePieces[0].endPoint.eventOutcome).to.equal(
(defaultContractSize.sats - maxLoss.sats - acceptFees.sats) /
BigInt(UNIT_MULTIPLIER[unit]),
);
expect(payoutCurvePieces[1].endPoint.eventOutcome).to.equal(
(defaultContractSize.sats + maxGain.sats - acceptFees.sats) /
BigInt(UNIT_MULTIPLIER[unit]),
);
expect(payoutCurvePieces[0].endPoint.outcomePayout).to.equal(
contractSize.sats -
(maxLoss.sats * contractSize.sats) / BigInt(1e8) -
(maxGain.sats * contractSize.sats) / BigInt(1e8),
);
expect(payoutCurvePieces[1].endPoint.outcomePayout).to.equal(
contractSize.sats,
);
});
it('should fail to build a CSO OrderOffer with contractSize 0', () => {
const contractSize = Value.zero();
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
try {
buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
);
// If the function call does not throw, fail the test
expect.fail(
'Expected buildCustomStrategyOrderOffer to throw an error due to contractSize being 0',
);
} catch (error) {
// Assert that the error message is as expected
expect(error.message).to.equal('contractSize must be greater than 0');
}
});
it('should fail to build a CSO OrderOffer with an invalid oracleAnnouncement', () => {
const contractSize = contractSizes[0];
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
oracleAnnouncement.announcementSig = Buffer.from(
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
'hex',
);
const skipValidation = true;
const orderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
undefined,
undefined,
undefined,
undefined,
undefined,
skipValidation,
);
expect(() => orderOffer.validate()).to.throw(Error);
});
});
describe('computeRoundingModulus', () => {
describe('with satoshi inputs (existing functionality)', () => {
it('should properly compute the rounding modulus for 0.0001 BTC', () => {
const modulus = computeRoundingModulus(BigInt(100000), BigInt(10000));
expect(modulus).to.equal(BigInt(10));
});
it('should properly compute the rounding modulus for 1 BTC', () => {
const modulus = computeRoundingModulus(
BigInt(100000),
BigInt(100000000),
);
expect(modulus).to.equal(BigInt(100000));
});
it('should properly compute the rounding modulus for 1.25 BTC', () => {
const modulus = computeRoundingModulus(BigInt(5000), BigInt(125000000));
expect(modulus).to.equal(BigInt(6250));
});
it('should properly compute the rounding modulus for 0.9 BTC', () => {
const modulus = computeRoundingModulus(
BigInt(100000),
BigInt(90000000),
);
expect(modulus).to.equal(BigInt(90000));
});
});
describe('with bitcoin amount inputs (new functionality)', () => {
it('should properly compute the rounding modulus for 0.001 BTC rounding with 0.1 BTC contract size', () => {
const modulus = computeRoundingModulus(0.001, 0.1); // 0.001 BTC rounding, 0.1 BTC contract
expect(modulus).to.equal(BigInt(10000)); // (0.001 * 1e8) * (0.1 * 1e8) / 1e8 = 10000
});
it('should properly compute the rounding modulus for 0.0001 BTC rounding with 1 BTC contract size', () => {
const modulus = computeRoundingModulus(0.0001, 1); // 0.0001 BTC rounding, 1 BTC contract
expect(modulus).to.equal(BigInt(10000)); // (0.0001 * 1e8) * (1 * 1e8) / 1e8 = 10000
});
it('should properly compute the rounding modulus for 0.001 BTC rounding with 1.5 BTC contract size', () => {
const modulus = computeRoundingModulus(0.001, 1.5); // 0.001 BTC rounding, 1.5 BTC contract
expect(modulus).to.equal(BigInt(150000)); // (0.001 * 1e8) * (1.5 * 1e8) / 1e8 = 150000
});
it('should properly compute the rounding modulus for 0.0005 BTC rounding with 0.5 BTC contract size', () => {
const modulus = computeRoundingModulus(0.0005, 0.5); // 0.0005 BTC rounding, 0.5 BTC contract
expect(modulus).to.equal(BigInt(25000)); // (0.0005 * 1e8) * (0.5 * 1e8) / 1e8 = 25000
});
it('should handle fractional bitcoin amounts correctly', () => {
const modulus = computeRoundingModulus(0.00001, 0.12345); // Very small amounts
expect(modulus).to.equal(BigInt(123)); // (0.00001 * 1e8) * (0.12345 * 1e8) / 1e8 = 123.45 -> 123 (rounded down)
});
});
describe('with Value object inputs', () => {
it('should properly compute the rounding modulus with Value objects', () => {
const rounding = Value.fromBitcoin(0.001);
const contractSize = Value.fromBitcoin(1);
const modulus = computeRoundingModulus(rounding, contractSize);
expect(modulus).to.equal(BigInt(100000)); // (0.001 * 1e8) * (1 * 1e8) / 1e8 = 100000
});
it('should properly compute the rounding modulus with mixed Value and number inputs', () => {
const rounding = Value.fromBitcoin(0.001);
const contractSize = 1.5; // number input
const modulus = computeRoundingModulus(rounding, contractSize);
expect(modulus).to.equal(BigInt(150000)); // (0.001 * 1e8) * (1.5 * 1e8) / 1e8 = 150000
});
});
describe('with BigInt inputs', () => {
it('should properly compute the rounding modulus with BigInt inputs', () => {
const modulus = computeRoundingModulus(
BigInt(100000),
BigInt(100000000),
);
expect(modulus).to.equal(BigInt(100000));
});
it('should properly compute the rounding modulus with mixed BigInt and number inputs', () => {
const modulus = computeRoundingModulus(BigInt(100000), 1); // BigInt rounding, number contract size
expect(modulus).to.equal(BigInt(100000)); // 100000 * (1 * 1e8) / 1e8 = 100000
});
});
describe('edge cases', () => {
it('should handle zero rounding correctly', () => {
const modulus = computeRoundingModulus(0, 1);
expect(modulus).to.equal(BigInt(0));
});
it('should handle zero contract size correctly', () => {
const modulus = computeRoundingModulus(0.001, 0);
expect(modulus).to.equal(BigInt(0));
});
it('should handle very small bitcoin amounts', () => {
const modulus = computeRoundingModulus(0.00000001, 0.00000001); // 1 satoshi each
expect(modulus).to.equal(BigInt(0)); // (1 * 1) / 1e8 = 0 (rounded down)
});
});
});
});