Skip to content

Commit 074720d

Browse files
committed
Feat: Add dust calculation
1 parent 31b6c27 commit 074720d

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

src/address.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,15 @@ export declare function fromBech32(address: string): Bech32Result;
1414
export declare function toBase58Check(hash: Buffer, version: number): string;
1515
export declare function toBech32(data: Buffer, version: number, prefix: string): string;
1616
export declare function fromOutputScript(output: Buffer, network?: Network): string;
17+
/**
18+
* This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script.
19+
*
20+
* Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63
21+
*
22+
* @param {Buffer} script - This is the script to evaluate a dust limit for.
23+
* @param {number} [satPerVb=1] - This is to account for different MIN_RELAY_TX_FEE amounts. Bitcoin Core does not calculate
24+
* dust based on the mempool ejection cutoff, but always by the MIN_RELAY_TX_FEE.
25+
* This argument should be passed in as satoshi per vByte. Not satoshi per kvByte like Core.
26+
*/
27+
export declare function dustAmountFromOutputScript(script: Buffer, satPerVb?: number): number;
1728
export declare function toOutputScript(address: string, network?: Network): Buffer;

src/address.js

+42
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
'use strict';
22
Object.defineProperty(exports, '__esModule', { value: true });
33
exports.toOutputScript =
4+
exports.dustAmountFromOutputScript =
45
exports.fromOutputScript =
56
exports.toBech32 =
67
exports.toBase58Check =
78
exports.fromBech32 =
89
exports.fromBase58Check =
910
void 0;
1011
const networks = require('./networks');
12+
const ops_1 = require('./ops');
1113
const payments = require('./payments');
1214
const bscript = require('./script');
1315
const types_1 = require('./types');
16+
const varuint = require('bip174/src/lib/converter/varint');
1417
const bech32_1 = require('bech32');
1518
const bs58check = require('bs58check');
1619
const FUTURE_SEGWIT_MAX_SIZE = 40;
@@ -116,6 +119,45 @@ function fromOutputScript(output, network) {
116119
throw new Error(bscript.toASM(output) + ' has no matching Address');
117120
}
118121
exports.fromOutputScript = fromOutputScript;
122+
/**
123+
* This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script.
124+
*
125+
* Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63
126+
*
127+
* @param {Buffer} script - This is the script to evaluate a dust limit for.
128+
* @param {number} [satPerVb=1] - This is to account for different MIN_RELAY_TX_FEE amounts. Bitcoin Core does not calculate
129+
* dust based on the mempool ejection cutoff, but always by the MIN_RELAY_TX_FEE.
130+
* This argument should be passed in as satoshi per vByte. Not satoshi per kvByte like Core.
131+
*/
132+
function dustAmountFromOutputScript(script, satPerVb = 1) {
133+
if (isUnspendableCore(script)) {
134+
return 0;
135+
}
136+
const inputBytes = isSegwit(script) ? 67 : 148;
137+
const outputBytes = script.length + 8 + varuint.encodingLength(script.length);
138+
return Math.ceil((inputBytes + outputBytes) * 3 * satPerVb);
139+
}
140+
exports.dustAmountFromOutputScript = dustAmountFromOutputScript;
141+
function isUnspendableCore(script) {
142+
const startsWithOpReturn =
143+
script.length > 0 && script[0] == ops_1.OPS.OP_RETURN;
144+
const MAX_SCRIPT_SIZE = 10000;
145+
const greaterThanScriptSize = script.length > MAX_SCRIPT_SIZE;
146+
// If unspendable, return 0
147+
// https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84
148+
// (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
149+
return startsWithOpReturn || greaterThanScriptSize;
150+
}
151+
function isSegwit(script) {
152+
if (script.length < 4 || script.length > 42) return false;
153+
if (
154+
script[0] !== ops_1.OPS.OP_0 &&
155+
(script[0] < ops_1.OPS.OP_1 || script[0] > ops_1.OPS.OP_16)
156+
)
157+
return false;
158+
if (script[1] + 2 !== script.length) return false;
159+
return true;
160+
}
119161
function toOutputScript(address, network) {
120162
network = network || networks.bitcoin;
121163
let decodeBase58;

test/address.spec.ts

+72
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,76 @@ describe('address', () => {
150150
});
151151
});
152152
});
153+
154+
describe('dustAmountFromOutputScript', () => {
155+
it('gets correct values', () => {
156+
const vectors = [
157+
// OP_RETURN is always 0 regardless of size
158+
[Buffer.from('6a04deadbeef', 'hex'), 1, 0],
159+
[Buffer.from('6a08deadbeefdeadbeef', 'hex'), 1, 0],
160+
// 3 byte non-segwit output is 3 + 1 + 8 + 148 = 160 * 3 = 480
161+
[Buffer.from('020102', 'hex'), 1, 480],
162+
// * 2 the feerate, * 2 the result
163+
[Buffer.from('020102', 'hex'), 2, 960],
164+
// P2PKH is 546 (well known)
165+
[
166+
Buffer.from(
167+
'76a914b6211d1f14f26ea4aed0e4a55e56e82656c7233d88ac',
168+
'hex',
169+
),
170+
1,
171+
546,
172+
],
173+
// P2WPKH is 294 (mentioned in Core comments)
174+
[
175+
Buffer.from('00145f72106b919817aa740fc655cce1a59f2d804e16', 'hex'),
176+
1,
177+
294,
178+
],
179+
// P2TR (and P2WSH) is 330
180+
[
181+
Buffer.from(
182+
'51208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
183+
'hex',
184+
),
185+
1,
186+
330,
187+
],
188+
// P2TR (and P2WSH) with OP_16 for some reason is still 330
189+
[
190+
Buffer.from(
191+
'60208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
192+
'hex',
193+
),
194+
1,
195+
330,
196+
],
197+
// P2TR (and P2WSH) with 0x61 instead of OP number for some reason is now 573
198+
[
199+
Buffer.from(
200+
'61208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
201+
'hex',
202+
),
203+
1,
204+
573,
205+
],
206+
// P2TR (and P2WSH) with 0x50 instead of OP 1-16 for some reason is now 573
207+
[
208+
Buffer.from(
209+
'50208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
210+
'hex',
211+
),
212+
1,
213+
573,
214+
],
215+
] as const;
216+
217+
for (const [script, feeRatekvB, expected] of vectors) {
218+
assert.strictEqual(
219+
baddress.dustAmountFromOutputScript(script, feeRatekvB),
220+
expected,
221+
);
222+
}
223+
});
224+
});
153225
});

ts_src/address.ts

+44
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Network } from './networks';
22
import * as networks from './networks';
3+
import { OPS } from './ops';
34
import * as payments from './payments';
45
import * as bscript from './script';
56
import { typeforce, tuple, Hash160bit, UInt8 } from './types';
7+
import * as varuint from 'bip174/src/lib/converter/varint';
68
import { bech32, bech32m } from 'bech32';
79
import * as bs58check from 'bs58check';
810
export interface Base58CheckResult {
@@ -139,6 +141,48 @@ export function fromOutputScript(output: Buffer, network?: Network): string {
139141
throw new Error(bscript.toASM(output) + ' has no matching Address');
140142
}
141143

144+
/**
145+
* This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script.
146+
*
147+
* Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63
148+
*
149+
* @param {Buffer} script - This is the script to evaluate a dust limit for.
150+
* @param {number} [satPerVb=1] - This is to account for different MIN_RELAY_TX_FEE amounts. Bitcoin Core does not calculate
151+
* dust based on the mempool ejection cutoff, but always by the MIN_RELAY_TX_FEE.
152+
* This argument should be passed in as satoshi per vByte. Not satoshi per kvByte like Core.
153+
*/
154+
export function dustAmountFromOutputScript(
155+
script: Buffer,
156+
satPerVb: number = 1,
157+
): number {
158+
if (isUnspendableCore(script)) {
159+
return 0;
160+
}
161+
162+
const inputBytes = isSegwit(script) ? 67 : 148;
163+
const outputBytes = script.length + 8 + varuint.encodingLength(script.length);
164+
165+
return Math.ceil((inputBytes + outputBytes) * 3 * satPerVb);
166+
}
167+
168+
function isUnspendableCore(script: Buffer): boolean {
169+
const startsWithOpReturn = script.length > 0 && script[0] == OPS.OP_RETURN;
170+
const MAX_SCRIPT_SIZE = 10000;
171+
const greaterThanScriptSize = script.length > MAX_SCRIPT_SIZE;
172+
// If unspendable, return 0
173+
// https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84
174+
// (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
175+
return startsWithOpReturn || greaterThanScriptSize;
176+
}
177+
178+
function isSegwit(script: Buffer): boolean {
179+
if (script.length < 4 || script.length > 42) return false;
180+
if (script[0] !== OPS.OP_0 && (script[0] < OPS.OP_1 || script[0] > OPS.OP_16))
181+
return false;
182+
if (script[1] + 2 !== script.length) return false;
183+
return true;
184+
}
185+
142186
export function toOutputScript(address: string, network?: Network): Buffer {
143187
network = network || networks.bitcoin;
144188

0 commit comments

Comments
 (0)