@node-dlc/core
Version:
1,125 lines (970 loc) • 40 kB
text/typescript
import { Value } from '@node-dlc/bitcoin';
import {
ContractDescriptorV1,
ContractInfoV0,
DigitDecompositionEventDescriptorV0,
DlcOffer,
OracleAnnouncement,
OracleEvent,
OracleInfoV0,
PayoutFunctionV0,
PolynomialPayoutCurvePiece,
} from '@node-dlc/messaging';
import { BitcoinNetworks } from 'bitcoin-networks';
import { expect } from 'chai';
import {
buildCustomStrategyOrderOffer,
buildRoundingIntervalsFromIntervals,
DlcParty,
dustThreshold,
getFinalizerByCount,
LinearPayout,
roundDownToNearestMultiplier,
roundUpToNearestMultiplier,
} from '../../../lib';
import {
getCsoInfoFromContractInfo,
getCsoInfoFromOffer,
validateCsoPayoutFunction,
} from '../../../lib/dlc/finance/CsoInfo';
const buildOracleAnnouncement = (oracleDigits: number, expiry: Date) => {
const eventDescriptor = new DigitDecompositionEventDescriptorV0();
eventDescriptor.base = 2;
eventDescriptor.isSigned = false;
eventDescriptor.unit = 'bits';
eventDescriptor.precision = 0;
eventDescriptor.nbDigits = oracleDigits;
const oracleEvent = new OracleEvent();
oracleEvent.eventMaturityEpoch = Math.floor(expiry.getTime() / 1000);
oracleEvent.eventDescriptor = eventDescriptor;
const oracleAnnouncement = new OracleAnnouncement();
oracleAnnouncement.oracleEvent = oracleEvent;
return oracleAnnouncement;
};
const buildCsoDlcOfferFixture = (
oracleDigits: number,
expiry: Date,
payoutFunction: PayoutFunctionV0,
totalCollateral: bigint,
offerCollateral: bigint,
): DlcOffer => {
const oracleAnnouncement = buildOracleAnnouncement(oracleDigits, expiry);
const oracleInfo = new OracleInfoV0();
oracleInfo.announcement = oracleAnnouncement;
const contractDescriptor = new ContractDescriptorV1();
contractDescriptor.numDigits = oracleDigits;
contractDescriptor.payoutFunction = payoutFunction;
const contractInfo = new ContractInfoV0();
contractInfo.totalCollateral = totalCollateral;
contractInfo.contractDescriptor = contractDescriptor;
contractInfo.oracleInfo = oracleInfo;
const dlcOffer = new DlcOffer();
// Set all required properties following DlcOffer.spec.ts pattern
dlcOffer.contractFlags = Buffer.from('00', 'hex');
dlcOffer.chainHash = Buffer.from(
'06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f',
'hex',
);
dlcOffer.temporaryContractId = Buffer.from(
'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
'hex',
);
dlcOffer.fundingPubkey = Buffer.from(
'0327efea09ff4dfb13230e887cbab8821d5cc249c7ff28668c6633ff9f4b4c08e3',
'hex',
);
dlcOffer.payoutSpk = Buffer.from(
'00142bbdec425007dc360523b0294d2c64d2213af498',
'hex',
);
dlcOffer.payoutSerialId = BigInt(11555292);
dlcOffer.changeSpk = Buffer.from(
'0014afa16f949f3055f38bd3a73312bed00b61558884',
'hex',
);
dlcOffer.changeSerialId = BigInt(2008045);
dlcOffer.fundOutputSerialId = BigInt(5411962);
dlcOffer.feeRatePerVb = BigInt(1);
dlcOffer.cetLocktime = 100;
dlcOffer.refundLocktime = 200;
dlcOffer.contractInfo = contractInfo;
dlcOffer.offerCollateral = offerCollateral;
return dlcOffer;
};
describe('CsoInfo', () => {
const minPayout = 60000000n;
const maxPayout = 100000000n;
const startOutcome = 800000n;
const endOutcome = 1200000n;
const offerCollateralValue = 80000000n;
const oracleBase = 2;
const oracleDigits = 21;
const expiry = new Date(1620014750000);
const maxGain = Value.fromBitcoin(0.2);
const maxLoss = Value.fromBitcoin(0.2);
const maxGainForContractSize = Value.fromBitcoin(0.2);
const maxLossForContractSize = Value.fromBitcoin(0.2);
const contractSize = Value.fromBitcoin(1);
const totalCollateral = Value.fromBitcoin(1);
const offerCollateral = Value.fromBitcoin(0.8);
const network = BitcoinNetworks.bitcoin;
const highestPrecisionRounding = Value.fromSats(10000);
const highPrecisionRounding = Value.fromSats(25000);
const mediumPrecisionRounding = Value.fromSats(100000);
const lowPrecisionRounding = Value.fromSats(200000);
const oracleAnnouncement = OracleAnnouncement.deserialize(
Buffer.from(
'fdd824fd0344340d5e431a385a8bb3819c0410c749b2a11ae5bebdd363e2dc23af057c6cbd3fd097fffa8e6beef46cb02389fb6ef102d3653a281dda5ff0d6e3f4d42a14df6830bbf19aa3a986ed4e5640240b507901d6e03d6bbd71a281ed356a145516c655fdd822fd02de0015b4ced0696e0c7b8636b816e8742944cfd652f3366d659694add4d186da20f06e1750ce25afb96aee7aedbc5a3981b7e62a22baf768f94fe0ffd455cb6ccb4da1a07fadc76aada50705e583aa77b664f1217212d4537f9d5398630716cd26e5fcdba1a300388860a9f5feed894ca9d5caf9daffaca23f53fd46071f69007b286050e798293f7524223f1882667d5e282be7b09faada3179496630053a060c1af1d2c80d212dc871bf56a846a6b70cfca17b47d2fcb86e57cef165215f2a526cc40215c3c6002fbeb5f0a9d545379004fd108a8a5ee96ac0bd33d1495c609724e6554fe1bf90fe92e679df8883df6df9173e8c476b5e83493f55bf2e20e9c85acd21f7f710a9a380233fc28254077fb751c4ffbd5124639421cc05ae2e3886bbc29177c02ad5ab569b5a89c4dd5da2e131ab41ac4ce2347f5425230b8822198cd805047b1a35fa00cd3877fdc902857293f94fcad3f0e418448b0756e99ce6cc46d1a4cf8673823a89819a9bd9d4c3506975d37835a5310a1bee056887124b6d0fc0ae8974371e1e12e13d68f844878bd9fa51bfdb2455f6bb6c93e49998b37e4fa2b01ff31c665b16313015311007d624fe77df48ac69d8475ca06500d618fbff38ab6f50575d4b0fd3efe4fd6e561797070ec365ecda8a62e6dcb993b8eff16a2e5af7ad7b93cb6a028cdab351251e368c4f188d620ad86fa43886ee57f91be033feab89cd381c486b9cb9c102ae2d3d3cc15068ecb5e0822e8f5ff5a2faef2598308ec40acc4f122023722e8e6758034f98bb45136117154cb7690a2c04b9504a747ce3f69be7d29299462181fcf721d795233aacf010309af5b1709309cb5c2ceae7926ea0868b1c8a2573554284f86310d1df7ed82e6be49d417ec494da9f15f9fac757bdee7cb49c3e7fdcfabc28801bde8f90b90cf3eb37784bb91d9c35662b5f00fdd80a0e00020004626974730000000000152561746f6d69632d656e67696e652d6d6f6e74686c792d323646454232342d32364150523234',
'hex',
),
);
describe('CsoInfo from linear curve message', () => {
const { payoutFunction } = LinearPayout.buildPayoutFunction(
minPayout,
maxPayout,
startOutcome,
endOutcome,
oracleBase,
oracleDigits,
);
const dlcOffer = buildCsoDlcOfferFixture(
oracleDigits,
expiry,
payoutFunction,
maxPayout,
offerCollateralValue,
);
const contractInfo = dlcOffer.contractInfo;
it('should get correct CsoInfo from ContractInfo', () => {
const csoInfo = getCsoInfoFromContractInfo(
contractInfo,
'neither',
Value.fromSats(0),
undefined,
'v0',
);
expect(csoInfo).to.deep.equal({
normalizedMaxGain: maxGain,
normalizedMaxLoss: maxLoss,
maxGainForContractSize,
maxLossForContractSize,
minPayout,
maxPayout,
contractSize,
offerCollateral,
totalCollateral,
expiry,
});
});
it('should get correct CsoInfo from ContractInfo', () => {
const csoInfo = getCsoInfoFromOffer(dlcOffer, 'v0');
expect(csoInfo).to.deep.equal({
normalizedMaxGain: maxGain,
normalizedMaxLoss: maxLoss,
maxGainForContractSize,
maxLossForContractSize,
minPayout,
maxPayout,
contractSize,
offerCollateral,
totalCollateral,
expiry,
});
});
it('should throw if offerCollateralSatoshis does not match calculated offerCollateral', () => {
const { payoutFunction } = LinearPayout.buildPayoutFunction(
minPayout,
maxPayout,
startOutcome,
endOutcome,
oracleBase,
oracleDigits,
);
const dlcOffer = buildCsoDlcOfferFixture(
oracleDigits,
expiry,
payoutFunction,
maxPayout,
offerCollateralValue,
);
dlcOffer.offerCollateral -= BigInt(10000);
expect(() => getCsoInfoFromOffer(dlcOffer)).to.throw(Error);
});
it('should get correct CsoInfo from ContractInfo with fees shifted', () => {
const contractSize = Value.fromBitcoin(1);
const maxLoss = Value.fromBitcoin(0.2);
const maxGain = Value.fromBitcoin(0.04);
const feeRate = 10n;
const highestPrecisionRounding = Value.fromSats(10000);
const highPrecisionRounding = Value.fromSats(25000);
const mediumPrecisionRounding = Value.fromSats(100000);
const lowPrecisionRounding = Value.fromSats(200000);
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const network = BitcoinNetworks.bitcoin;
const shiftForFees: DlcParty = 'offeror';
const fees = Value.fromSats(10000);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
shiftForFees,
fees,
);
const csoInfoFromContractInfo = getCsoInfoFromContractInfo(
csoOrderOffer.contractInfo,
shiftForFees,
fees,
);
const csoInfoFromOffer = getCsoInfoFromOffer(csoOrderOffer);
expect(csoInfoFromContractInfo.normalizedMaxGain.sats).to.equal(
maxGain.sats,
);
expect(csoInfoFromContractInfo.normalizedMaxLoss.sats).to.equal(
maxLoss.sats,
);
expect(csoInfoFromOffer.normalizedMaxGain.sats).to.equal(maxGain.sats);
expect(csoInfoFromOffer.normalizedMaxLoss.sats).to.equal(maxLoss.sats);
});
it('should get correct ContractInfo with smaller max gain', () => {
const contractSize = Value.fromBitcoin(1);
const maxLoss = Value.fromBitcoin(0.95);
const maxGain = Value.fromBitcoin(0.005);
const feeRate = 10n;
const highestPrecisionRounding = Value.fromSats(10000);
const highPrecisionRounding = Value.fromSats(25000);
const mediumPrecisionRounding = Value.fromSats(100000);
const lowPrecisionRounding = Value.fromSats(200000);
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const network = BitcoinNetworks.bitcoin;
const shiftForFees: DlcParty = 'offeror';
const fees = Value.fromSats(10000);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
shiftForFees,
fees,
);
const csoInfoFromContractInfo = getCsoInfoFromContractInfo(
csoOrderOffer.contractInfo,
shiftForFees,
fees,
);
const csoInfoFromOffer = getCsoInfoFromOffer(csoOrderOffer);
expect(csoInfoFromContractInfo.normalizedMaxGain.sats).to.equal(
maxGain.sats,
);
expect(csoInfoFromContractInfo.normalizedMaxLoss.sats).to.equal(
maxLoss.sats,
);
expect(csoInfoFromOffer.normalizedMaxGain.sats).to.equal(maxGain.sats);
expect(csoInfoFromOffer.normalizedMaxLoss.sats).to.equal(maxLoss.sats);
});
it('should get correct CsoInfo from ContractInfo with fees shifted contract size 0.01', () => {
const contractSize = Value.fromBitcoin(0.01);
const maxLoss = Value.fromBitcoin(0.2);
const maxGain = Value.fromBitcoin(0.04);
const feeRate = 10n;
const highestPrecisionRounding = Value.fromSats(10000);
const highPrecisionRounding = Value.fromSats(25000);
const mediumPrecisionRounding = Value.fromSats(100000);
const lowPrecisionRounding = Value.fromSats(200000);
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const network = BitcoinNetworks.bitcoin;
const shiftForFees: DlcParty = 'neither';
const fees = Value.fromSats(0);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
shiftForFees,
fees,
);
const csoInfoFromContractInfo = getCsoInfoFromContractInfo(
csoOrderOffer.contractInfo,
shiftForFees,
fees,
);
const csoInfoFromOffer = getCsoInfoFromOffer(csoOrderOffer);
expect(csoInfoFromContractInfo.normalizedMaxGain.sats).to.equal(
maxGain.sats,
);
expect(csoInfoFromContractInfo.normalizedMaxLoss.sats).to.equal(
maxLoss.sats,
);
expect(csoInfoFromOffer.normalizedMaxGain.sats).to.equal(maxGain.sats);
expect(csoInfoFromOffer.normalizedMaxLoss.sats).to.equal(maxLoss.sats);
});
const fees = [0, 1116, 29384, 34, 245, 11293, 2223, 10410];
const contractSizes = [0.01, 0.1, 0.5, 1, 2, 5, 10, 50];
contractSizes.forEach((contractSizeNum) => {
fees.forEach((fee) => {
it(`should get correct CsoInfo from ContractInfo with fees shifted contract size ${contractSizeNum} and fee ${fee}`, () => {
const contractSize = Value.fromBitcoin(contractSizeNum);
// const eventDescriptor = new DigitDecompositionEventDescriptorV0();
// eventDescriptor.base = 2;
// eventDescriptor.isSigned = false;
// eventDescriptor.unit = 'bits';
// eventDescriptor.precision = 0;
// eventDescriptor.nbDigits = oracleDigits;
// const oracleEvent = new OracleEventV0();
// oracleEvent.eventMaturityEpoch = Math.floor(expiry.getTime() / 1000);
// oracleEvent.eventDescriptor = eventDescriptor;
// const oracleAnnouncement = new OracleAnnouncementV0();
// oracleAnnouncement.oracleEvent = oracleEvent;
const maxLoss = Value.fromBitcoin(0.2);
const maxGain = Value.fromBitcoin(0.04);
const feeRate = 4n;
const highestPrecisionRounding = Value.fromSats(10000);
const highPrecisionRounding = Value.fromSats(25000);
const mediumPrecisionRounding = Value.fromSats(100000);
const lowPrecisionRounding = Value.fromSats(200000);
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
const network = BitcoinNetworks.bitcoin;
const shiftForFees: DlcParty = 'acceptor';
const fees = Value.fromSats(fee);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
shiftForFees,
fees,
);
const csoInfo = getCsoInfoFromOffer(csoOrderOffer);
expect(csoInfo.normalizedMaxGain.sats).to.equal(maxGain.sats);
expect(csoInfo.normalizedMaxLoss.sats).to.equal(maxLoss.sats);
});
});
});
});
describe('validateCsoPayoutFunction', () => {
it('should throw if event outcome is not an ascending line', () => {
const startOutcome = 900000n;
const endOutcome = 900000n;
const { payoutFunction } = LinearPayout.buildPayoutFunction(
minPayout,
maxPayout,
startOutcome,
endOutcome,
oracleBase,
oracleDigits,
);
expect(() => validateCsoPayoutFunction(payoutFunction)).to.throw(Error);
});
});
it('should throw if maxLoss to maxGain is not an ascending line', () => {
const minPayout = 100000000n;
const maxPayout = 100000000n;
const { payoutFunction } = LinearPayout.buildPayoutFunction(
minPayout,
maxPayout,
startOutcome,
endOutcome,
oracleBase,
oracleDigits,
);
expect(() => validateCsoPayoutFunction(payoutFunction)).to.throw(Error);
});
it('should throw if Payout Function point outcome payout not continuous', () => {
const { payoutFunction } = LinearPayout.buildPayoutFunction(
minPayout,
maxPayout,
startOutcome,
endOutcome,
oracleBase,
oracleDigits,
);
// Subtract 1 from outcomePayout to create gap in point outcome payout
(payoutFunction.payoutFunctionPieces[0]
.payoutCurvePiece as PolynomialPayoutCurvePiece).points[1].outcomePayout -= BigInt(
1,
);
expect(() => validateCsoPayoutFunction(payoutFunction)).to.throw(Error);
});
describe('CsoInfo collateral', () => {
const oracleAnnouncement = OracleAnnouncement.deserialize(
Buffer.from(
'fdd824fd0344340d5e431a385a8bb3819c0410c749b2a11ae5bebdd363e2dc23af057c6cbd3fd097fffa8e6beef46cb02389fb6ef102d3653a281dda5ff0d6e3f4d42a14df6830bbf19aa3a986ed4e5640240b507901d6e03d6bbd71a281ed356a145516c655fdd822fd02de0015b4ced0696e0c7b8636b816e8742944cfd652f3366d659694add4d186da20f06e1750ce25afb96aee7aedbc5a3981b7e62a22baf768f94fe0ffd455cb6ccb4da1a07fadc76aada50705e583aa77b664f1217212d4537f9d5398630716cd26e5fcdba1a300388860a9f5feed894ca9d5caf9daffaca23f53fd46071f69007b286050e798293f7524223f1882667d5e282be7b09faada3179496630053a060c1af1d2c80d212dc871bf56a846a6b70cfca17b47d2fcb86e57cef165215f2a526cc40215c3c6002fbeb5f0a9d545379004fd108a8a5ee96ac0bd33d1495c609724e6554fe1bf90fe92e679df8883df6df9173e8c476b5e83493f55bf2e20e9c85acd21f7f710a9a380233fc28254077fb751c4ffbd5124639421cc05ae2e3886bbc29177c02ad5ab569b5a89c4dd5da2e131ab41ac4ce2347f5425230b8822198cd805047b1a35fa00cd3877fdc902857293f94fcad3f0e418448b0756e99ce6cc46d1a4cf8673823a89819a9bd9d4c3506975d37835a5310a1bee056887124b6d0fc0ae8974371e1e12e13d68f844878bd9fa51bfdb2455f6bb6c93e49998b37e4fa2b01ff31c665b16313015311007d624fe77df48ac69d8475ca06500d618fbff38ab6f50575d4b0fd3efe4fd6e561797070ec365ecda8a62e6dcb993b8eff16a2e5af7ad7b93cb6a028cdab351251e368c4f188d620ad86fa43886ee57f91be033feab89cd381c486b9cb9c102ae2d3d3cc15068ecb5e0822e8f5ff5a2faef2598308ec40acc4f122023722e8e6758034f98bb45136117154cb7690a2c04b9504a747ce3f69be7d29299462181fcf721d795233aacf010309af5b1709309cb5c2ceae7926ea0868b1c8a2573554284f86310d1df7ed82e6be49d417ec494da9f15f9fac757bdee7cb49c3e7fdcfabc28801bde8f90b90cf3eb37784bb91d9c35662b5f00fdd80a0e00020004626974730000000000152561746f6d69632d656e67696e652d6d6f6e74686c792d323646454232342d32364150523234',
'hex',
),
);
const roundingIntervals = buildRoundingIntervalsFromIntervals(
contractSize,
[
{ beginInterval: 0n, rounding: lowPrecisionRounding },
{ beginInterval: 750000n, rounding: mediumPrecisionRounding },
{ beginInterval: 850000n, rounding: highPrecisionRounding },
{ beginInterval: 950000n, rounding: highestPrecisionRounding },
],
);
it('should get cso info from offer with contract size greater than collateral', () => {
const contractSize = Value.fromBitcoin(0.03);
const collateral = Value.fromBitcoin(0.01);
const numOfferInputs = 3;
const numContracts = 1;
const normalizedMaxGain = Value.fromBitcoin(0.005);
const normalizedMaxLoss = Value.fromBitcoin(0.04);
const feePerByte = BigInt(100);
const shiftForFees: DlcParty = 'offeror'; // 'offeror pays network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 1, numContracts)
.offerFees,
);
const skipValidation = false;
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
getCsoInfoFromOffer(csoOrderOffer, 'v1');
});
it('should get cso info from offer with contract size equal to collateral with both v0 and v1', () => {
const contractSize = Value.fromBitcoin(0.01);
const collateral = Value.fromBitcoin(0.01);
const numOfferInputs = 3;
const numContracts = 1;
const normalizedMaxGain = Value.fromBitcoin(0.005);
const normalizedMaxLoss = Value.fromBitcoin(0.04);
const feePerByte = BigInt(100);
const shiftForFees: DlcParty = 'offeror'; // 'offeror pays network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 1, numContracts)
.offerFees,
);
const skipValidation = false;
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
getCsoInfoFromOffer(csoOrderOffer, 'v0');
getCsoInfoFromOffer(csoOrderOffer, 'v1');
});
describe('high leverage with high fees', () => {
const contractSize = Value.fromBitcoin(0.36);
const collateral = Value.fromBitcoin(0.02);
const numOfferInputs = 5;
const numContracts = 5;
const normalizedMaxGain = Value.fromBitcoin(0.005);
const normalizedMaxLoss = Value.fromBitcoin(0.04);
const feePerByte = BigInt(450);
const skipValidation = false;
it('should build and decode shift for fees offeror', () => {
const shiftForFees: DlcParty = 'offeror'; // 'offeror' pays network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.acceptFees,
);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
const {
normalizedMaxGain: actualNormalizedMaxGain,
normalizedMaxLoss: actualNormalizedMaxLoss,
maxGainForContractSize: actualMaxGainForContractSize,
maxLossForContractSize: actualMaxLossForContractSize,
minPayout,
maxPayout,
contractSize: actualContractSize,
offerCollateral: actualOfferCollateral,
totalCollateral: actualTotalCollateral,
} = getCsoInfoFromOffer(csoOrderOffer, 'v1');
// Fees are very high, so use dust threshold for max gain
const offerFees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const expectedMaxGainForContractSize_ = offerFees.addn(
Value.fromSats(dustThreshold(BigInt(feePerByte))),
);
const expectedMaxGainForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
expectedMaxGainForContractSize_.sats,
BigInt(100),
),
);
const expectedMaxLossForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedNormalizedMaxGain = Value.fromSats(
roundDownToNearestMultiplier(
(expectedMaxGainForContractSize.sats * BigInt(1e8)) /
contractSize.sats,
BigInt(100),
),
);
expect(actualMaxGainForContractSize.sats).to.equal(
expectedMaxGainForContractSize.sats,
);
expect(actualMaxLossForContractSize.sats).to.equal(
expectedMaxLossForContractSize.sats,
);
expect(actualNormalizedMaxLoss.sats).to.equal(normalizedMaxLoss.sats);
expect(actualNormalizedMaxGain.sats).to.equal(
expectedNormalizedMaxGain.sats,
); // TODO: Fix issue with this line
expect(minPayout).to.equal(BigInt(121200));
expect(maxPayout).to.equal(collateral.sats);
expect(actualContractSize.sats).to.equal(contractSize.sats);
expect(actualOfferCollateral.sats).to.equal(
collateral.subn(expectedMaxGainForContractSize).sats,
);
expect(actualTotalCollateral.sats).to.equal(collateral.sats);
});
it('should build and decode shift for fees acceptor', () => {
const shiftForFees: DlcParty = 'acceptor'; // 'acceptor' pays network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
const {
normalizedMaxGain: actualNormalizedMaxGain,
normalizedMaxLoss: actualNormalizedMaxLoss,
maxGainForContractSize: actualMaxGainForContractSize,
maxLossForContractSize: actualMaxLossForContractSize,
minPayout,
maxPayout,
contractSize: actualContractSize,
offerCollateral: actualOfferCollateral,
totalCollateral: actualTotalCollateral,
} = getCsoInfoFromOffer(csoOrderOffer, 'v1');
// Fees are very high, so use dust threshold for max gain
const offerFees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const expectedMaxGainForContractSize_ = offerFees.addn(
Value.fromSats(dustThreshold(BigInt(feePerByte))),
);
const expectedMaxGainForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
expectedMaxGainForContractSize_.sats,
BigInt(100),
),
);
const expectedMaxLossForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedNormalizedMaxGain = Value.fromSats(
roundDownToNearestMultiplier(
(expectedMaxGainForContractSize.sats * BigInt(1e8)) /
contractSize.sats,
BigInt(100),
),
);
expect(actualNormalizedMaxGain.sats).to.equal(
expectedNormalizedMaxGain.sats,
);
expect(actualNormalizedMaxLoss.sats).to.equal(normalizedMaxLoss.sats);
expect(actualMaxGainForContractSize.sats).to.equal(
expectedMaxGainForContractSize.sats,
);
expect(actualMaxLossForContractSize.sats).to.equal(
expectedMaxLossForContractSize.sats,
);
expect(minPayout).to.equal(BigInt(121200));
expect(maxPayout).to.equal(collateral.sats);
expect(actualContractSize.sats).to.equal(contractSize.sats);
expect(actualOfferCollateral.sats).to.equal(
collateral.subn(expectedMaxGainForContractSize).sats,
);
expect(actualTotalCollateral.sats).to.equal(collateral.sats);
});
it('should build and decode shift for fees neither', () => {
const shiftForFees: DlcParty = 'neither'; // both parties pay their fair share for network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
const {
normalizedMaxGain: actualNormalizedMaxGain,
normalizedMaxLoss: actualNormalizedMaxLoss,
maxGainForContractSize: actualMaxGainForContractSize,
maxLossForContractSize: actualMaxLossForContractSize,
minPayout,
maxPayout,
contractSize: actualContractSize,
offerCollateral: actualOfferCollateral,
totalCollateral: actualTotalCollateral,
} = getCsoInfoFromOffer(csoOrderOffer, 'v1');
// Fees are very high, so use dust threshold for max gain
const offerFees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const expectedMaxGainForContractSize_ = offerFees.addn(
Value.fromSats(dustThreshold(BigInt(feePerByte))),
);
const expectedMaxGainForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
expectedMaxGainForContractSize_.sats,
BigInt(100),
),
);
const expectedMaxLossForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedNormalizedMaxGain = Value.fromSats(
roundDownToNearestMultiplier(
(expectedMaxGainForContractSize.sats * BigInt(1e8)) /
contractSize.sats,
BigInt(100),
),
);
expect(actualNormalizedMaxGain.sats).to.equal(
expectedNormalizedMaxGain.sats,
);
expect(actualNormalizedMaxLoss.sats).to.equal(normalizedMaxLoss.sats);
expect(actualMaxGainForContractSize.sats).to.equal(
expectedMaxGainForContractSize.sats,
);
expect(actualMaxLossForContractSize.sats).to.equal(
expectedMaxLossForContractSize.sats,
);
expect(minPayout).to.equal(BigInt(121200));
expect(maxPayout).to.equal(collateral.sats);
expect(actualContractSize.sats).to.equal(contractSize.sats);
expect(actualOfferCollateral.sats).to.equal(
collateral.subn(expectedMaxGainForContractSize).sats,
);
expect(actualTotalCollateral.sats).to.equal(collateral.sats);
});
});
describe('high leverage with low fees', () => {
const contractSize = Value.fromBitcoin(0.36);
const collateral = Value.fromBitcoin(0.02);
const numOfferInputs = 5;
const numContracts = 5;
const normalizedMaxGain = Value.fromBitcoin(0.005);
const normalizedMaxLoss = Value.fromBitcoin(0.04);
const feePerByte = BigInt(50);
const skipValidation = false;
it('should build and decode shift for fees offeror', () => {
const shiftForFees: DlcParty = 'offeror'; // 'offeror' pays network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.acceptFees,
);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
const {
normalizedMaxGain: actualNormalizedMaxGain,
normalizedMaxLoss: actualNormalizedMaxLoss,
maxGainForContractSize: actualMaxGainForContractSize,
maxLossForContractSize: actualMaxLossForContractSize,
minPayout,
maxPayout,
contractSize: actualContractSize,
offerCollateral: actualOfferCollateral,
totalCollateral: actualTotalCollateral,
} = getCsoInfoFromOffer(csoOrderOffer, 'v1');
const expectedMaxGainForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxGain.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedMaxLossForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedNormalizedMaxGain = Value.fromSats(
roundUpToNearestMultiplier(
(expectedMaxGainForContractSize.sats * BigInt(1e8)) /
contractSize.sats,
BigInt(100),
),
);
expect(actualNormalizedMaxGain.sats).to.equal(
expectedNormalizedMaxGain.sats,
);
expect(actualNormalizedMaxLoss.sats).to.equal(normalizedMaxLoss.sats);
expect(actualMaxGainForContractSize.sats).to.equal(
expectedMaxGainForContractSize.sats,
);
expect(actualMaxLossForContractSize.sats).to.equal(
expectedMaxLossForContractSize.sats,
);
expect(minPayout).to.equal(BigInt(380000));
expect(maxPayout).to.equal(collateral.sats);
expect(actualContractSize.sats).to.equal(contractSize.sats);
expect(actualOfferCollateral.sats).to.equal(
collateral.subn(expectedMaxGainForContractSize).sats,
);
expect(actualTotalCollateral.sats).to.equal(collateral.sats);
});
it('should build and decode shift for fees acceptor', () => {
const shiftForFees: DlcParty = 'acceptor'; // 'acceptor' pays network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
const {
normalizedMaxGain: actualNormalizedMaxGain,
normalizedMaxLoss: actualNormalizedMaxLoss,
maxGainForContractSize: actualMaxGainForContractSize,
maxLossForContractSize: actualMaxLossForContractSize,
minPayout,
maxPayout,
contractSize: actualContractSize,
offerCollateral: actualOfferCollateral,
totalCollateral: actualTotalCollateral,
} = getCsoInfoFromOffer(csoOrderOffer, 'v1');
const expectedMaxGainForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxGain.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedMaxLossForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedNormalizedMaxGain = Value.fromSats(
roundUpToNearestMultiplier(
(expectedMaxGainForContractSize.sats * BigInt(1e8)) /
contractSize.sats,
BigInt(100),
),
);
expect(actualNormalizedMaxGain.sats).to.equal(
expectedNormalizedMaxGain.sats,
);
expect(actualNormalizedMaxLoss.sats).to.equal(normalizedMaxLoss.sats);
expect(actualMaxGainForContractSize.sats).to.equal(
expectedMaxGainForContractSize.sats,
);
expect(actualMaxLossForContractSize.sats).to.equal(
expectedMaxLossForContractSize.sats,
);
expect(minPayout).to.equal(BigInt(380000));
expect(maxPayout).to.equal(collateral.sats);
expect(actualContractSize.sats).to.equal(contractSize.sats);
expect(actualOfferCollateral.sats).to.equal(
collateral.subn(expectedMaxGainForContractSize).sats,
);
expect(actualTotalCollateral.sats).to.equal(collateral.sats);
});
it('should build and decode shift for fees neither', () => {
const shiftForFees: DlcParty = 'neither'; // both parties pay their fair share for network fees
const fees = Value.fromSats(
getFinalizerByCount(feePerByte, numOfferInputs, 3, numContracts)
.offerFees,
);
const csoOrderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
normalizedMaxLoss,
normalizedMaxGain,
feePerByte,
roundingIntervals,
network,
shiftForFees,
fees,
collateral,
numOfferInputs,
numContracts,
skipValidation,
);
const {
normalizedMaxGain: actualNormalizedMaxGain,
normalizedMaxLoss: actualNormalizedMaxLoss,
maxGainForContractSize: actualMaxGainForContractSize,
maxLossForContractSize: actualMaxLossForContractSize,
minPayout,
maxPayout,
contractSize: actualContractSize,
offerCollateral: actualOfferCollateral,
totalCollateral: actualTotalCollateral,
} = getCsoInfoFromOffer(csoOrderOffer, 'v1');
const expectedMaxGainForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxGain.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedMaxLossForContractSize = Value.fromSats(
roundUpToNearestMultiplier(
(normalizedMaxLoss.sats * contractSize.sats) / BigInt(1e8),
BigInt(100),
),
);
const expectedNormalizedMaxGain = Value.fromSats(
roundUpToNearestMultiplier(
(expectedMaxGainForContractSize.sats * BigInt(1e8)) /
contractSize.sats,
BigInt(100),
),
);
expect(actualNormalizedMaxGain.sats).to.equal(
expectedNormalizedMaxGain.sats,
);
expect(actualNormalizedMaxLoss.sats).to.equal(normalizedMaxLoss.sats);
expect(actualMaxGainForContractSize.sats).to.equal(
expectedMaxGainForContractSize.sats,
);
expect(actualMaxLossForContractSize.sats).to.equal(
expectedMaxLossForContractSize.sats,
);
expect(minPayout).to.equal(BigInt(380000));
expect(maxPayout).to.equal(collateral.sats);
expect(actualContractSize.sats).to.equal(contractSize.sats);
expect(actualOfferCollateral.sats).to.equal(
collateral.subn(expectedMaxGainForContractSize).sats,
);
expect(actualTotalCollateral.sats).to.equal(collateral.sats);
});
});
});
});