Skip to content

Commit

Permalink
feat: add batch functionality to core (#206)
Browse files Browse the repository at this point in the history
Add batch dlc funding txs util functions to core including:
- CoinSelect dualFundingCoinSelect
- Builder buildCustomStrategyOrderOffer with multi contract support
- CsoInfo getCsoInfoFromOffer decode bug fix (increment to V1 CsoInfo)

Add tests for changes
  • Loading branch information
matthewjablack authored Mar 14, 2024
1 parent 9ac2278 commit bb6b4ab
Show file tree
Hide file tree
Showing 14 changed files with 1,485 additions and 183 deletions.
1 change: 0 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@ test:

lint:
yarn lint

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"publish:all": "lerna publish from-package",
"prepublishOnly": "npm run build"
},
"pre-commit": [ "lint" ],
"pre-commit": [
"lint"
],
"publishConfig": {
"access": "public"
},
Expand All @@ -27,6 +29,7 @@
"author": "Atomic Finance <info@atomic.finance>",
"license": "MIT",
"devDependencies": {
"@babel/runtime": "^7.24.0",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/bn.js": "^4.11.6",
"@types/chai": "^4.2.11",
Expand Down
9 changes: 9 additions & 0 deletions packages/bitcoin/lib/Value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ export class Value implements ICloneable<Value> {
return this.bitcoin.toFixed(8);
}

/**
* Converts the value to a standard JavaScript number. This is safe
* for values that are within the Number.MAX_SAFE_INTEGER range.
* @returns {number} The value as a number.
*/
public toNumber(): number {
return Number(this._picoSats / BigInt(1e12)); // Convert picosatoshis to satoshis and then to a number
}

/**
* Returns true if the current value is equal to the other value
* @param other
Expand Down
179 changes: 179 additions & 0 deletions packages/core/__tests__/dlc/CoinSelect.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { Value } from '@node-dlc/bitcoin';
import { expect } from 'chai';

import { dualFees, dualFundingCoinSelect, UTXO } from '../../lib';

const getUtxos = (totalCollateral: bigint, numUtxos = 1) => {
const utxos: UTXO[] = [];

for (let i = 0; i < numUtxos; i++) {
utxos.push({
address: 'bcrt1qjzut0906d9sk4hml4k6sz6cssljktf4c7yl80f',
txid: 'c7bf12ac16aba1cf6c7769117294853453f7da3006363dfe4e8979847e32f7e1',
value: Math.ceil(Number(totalCollateral) / numUtxos),
vout: Math.floor(Math.random() * 11), // random integer between 0 and 10
});
}

return utxos;
};

describe('CoinSelect', () => {
describe('dualFundingCoinSelect', () => {
it('should select coins correctly for 5 UTXOs and feeRate 450', () => {
const feeRate = BigInt(450);
const numUtxos = 5;
const totalCollateral = Value.fromBitcoin(0.01);
const offerCollateral = Value.fromBitcoin(0.0075);

const utxos = getUtxos(totalCollateral.sats, numUtxos);

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
feeRate,
);

expect(fee).to.equal(BigInt(217350));
expect(inputs.length).to.equal(5);
});

it('should fail to select coins if fee is greater than sum of utxos', () => {
const feeRate = BigInt(450);
const numUtxos = 5;
const totalCollateral = Value.fromBitcoin(0.01);
const offerCollateral = Value.fromBitcoin(0.096);

const utxos = getUtxos(totalCollateral.sats, numUtxos);

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
feeRate,
);

expect(fee).to.equal(BigInt(94950));
expect(inputs.length).to.equal(0);
});

it('should fail to select coins if detrimental input', () => {
const feeRate = BigInt(450);
const numUtxos = 1;
const totalCollateral = Value.fromSats(30000);
const offerCollateral = Value.fromSats(20000);

const utxos = getUtxos(totalCollateral.sats, numUtxos);

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
feeRate,
);

expect(fee).to.equal(BigInt(94950));
expect(inputs.length).to.equal(0);
});

it('should prioritize utxo selection', () => {
const feeRate = BigInt(450);
const numUtxos = 5;
const totalCollateral = Value.fromBitcoin(0.01);
const offerCollateral = Value.fromBitcoin(0.0075);

const utxos = getUtxos(totalCollateral.sats, numUtxos);

utxos.push({
address: 'bcrt1qjzut0906d9sk4hml4k6sz6cssljktf4c7yl80f',
txid:
'c7bf12ac16aba1cf6c7769117294853453f7da3006363dfe4e8979847e32f7e1',
value: 10000,
vout: Math.floor(Math.random() * 11), // random integer between 0 and 10
});

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
feeRate,
);

expect(fee).to.equal(BigInt(217350));
expect(inputs.length).to.equal(5);
});
});

describe('Additional CoinSelect Tests', () => {
const feeRate = BigInt(450);
const totalCollateral = Value.fromBitcoin(0.01);
const offerCollateral = Value.fromBitcoin(0.0075);

it('should fail to select coins when all UTXOs are detrimental due to high fee rate', () => {
const highFeeRate = BigInt(10000); // An exaggerated fee rate to make the point
const utxos = getUtxos(totalCollateral.sats, 5);

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
highFeeRate,
);

expect(inputs.length).to.equal(0);
expect(fee).to.equal(BigInt(2110000));
});

it('should select all UTXOs when they are just enough to cover collateral and fees', () => {
const utxos = getUtxos(totalCollateral.sats, 5);

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
feeRate,
);

expect(inputs.length).to.equal(5);
expect(fee).to.equal(BigInt(217350));
});

it('should select UTXOs optimally from a mixed set', () => {
const mixedUtxos = [
...getUtxos(Value.fromBitcoin(0.005).sats, 2), // smaller UTXOs
...getUtxos(Value.fromBitcoin(0.02).sats, 3), // larger UTXOs
];

const { fee, inputs } = dualFundingCoinSelect(
mixedUtxos,
[offerCollateral.sats],
feeRate,
);

// Expecting it to select fewer, larger UTXOs over many small ones
expect(inputs.length).to.be.lessThan(5);
expect(fee).to.equal(BigInt(125550));
});

it('should handle an empty array of UTXOs correctly', () => {
const { fee, inputs } = dualFundingCoinSelect(
[],
[offerCollateral.sats],
feeRate,
);

expect(inputs.length).to.equal(0);
expect(fee).to.equal(dualFees(feeRate, 1, 1)); // The fee for an attempt with no inputs
});

it('should select fewer UTXOs with a very low fee rate', () => {
const lowFeeRate = BigInt(1); // An extremely low fee rate
const utxos = getUtxos(totalCollateral.sats, 10); // More UTXOs than needed

const { fee, inputs } = dualFundingCoinSelect(
utxos,
[offerCollateral.sats],
lowFeeRate,
);

// Expecting it to select fewer UTXOs due to the low cost of adding an input
expect(inputs.length).to.be.lessThan(10);
expect(fee).to.equal(BigInt(687));
});
});
});
46 changes: 31 additions & 15 deletions packages/core/__tests__/dlc/finance/Builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ describe('OrderOffer Builder', () => {
expect(payoutCurvePieces[1].endpointPayout).to.equal(contractSize.sats);
});

it('should build a CSO OrderOffer with contractSize 0', () => {
it('should fail to build a CSO OrderOffer with contractSize 0', () => {
const contractSize = Value.zero();

const roundingIntervals = buildRoundingIntervalsFromIntervals(
Expand All @@ -319,19 +319,24 @@ describe('OrderOffer Builder', () => {
],
);

const orderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
contractSize,
maxLoss,
maxGain,
feeRate,
roundingIntervals,
network,
);

expect(orderOffer.contractInfo.totalCollateral).to.equal(
contractSize.sats,
);
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', () => {
Expand All @@ -347,7 +352,12 @@ describe('OrderOffer Builder', () => {
],
);

oracleAnnouncement.announcementSig = Buffer.from('deadbeef', 'hex');
oracleAnnouncement.announcementSig = Buffer.from(
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
'hex',
);

const skipValidation = true;

const orderOffer = buildCustomStrategyOrderOffer(
oracleAnnouncement,
Expand All @@ -357,6 +367,12 @@ describe('OrderOffer Builder', () => {
feeRate,
roundingIntervals,
network,
undefined,
undefined,
undefined,
undefined,
undefined,
skipValidation,
);

expect(() => orderOffer.validate()).to.throw(Error);
Expand Down
Loading

0 comments on commit bb6b4ab

Please sign in to comment.