Skip to content

Commit 799d497

Browse files
committed
Feat: Add dust calculation
1 parent 31b6c27 commit 799d497

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

Diff for: src/address.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ 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+
export declare function dustAmountFromOutputScript(script: Buffer, satPerKvb?: number): number;
1718
export declare function toOutputScript(address: string, network?: Network): Buffer;

Diff for: src/address.js

+33
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,36 @@ 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+
function dustAmountFromOutputScript(script, satPerKvb = 1000) {
128+
// If unspendable, return 0
129+
// https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84
130+
// (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
131+
if (
132+
(script.length > 0 && script[0] == ops_1.OPS.OP_RETURN) ||
133+
script.length > 10000
134+
) {
135+
return 0;
136+
}
137+
const inputBytes = isSegwit(script) ? 67 : 148;
138+
const outputBytes = script.length + 8 + varuint.encodingLength(script.length);
139+
return Math.ceil((inputBytes + outputBytes) * 3 * (satPerKvb / 1000));
140+
}
141+
exports.dustAmountFromOutputScript = dustAmountFromOutputScript;
142+
function isSegwit(script) {
143+
if (script.length < 4 || script.length > 42) return false;
144+
if (
145+
script[0] !== ops_1.OPS.OP_0 &&
146+
(script[0] < ops_1.OPS.OP_1 || script[0] > ops_1.OPS.OP_16)
147+
)
148+
return false;
149+
if (script[1] + 2 !== script.length) return false;
150+
return true;
151+
}
119152
function toOutputScript(address, network) {
120153
network = network || networks.bitcoin;
121154
let decodeBase58;

Diff for: 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'), 1000, 0],
159+
[Buffer.from('6a08deadbeefdeadbeef', 'hex'), 1000, 0],
160+
// 3 byte non-segwit output is 3 + 1 + 8 + 148 = 160 * 3 = 480
161+
[Buffer.from('020102', 'hex'), 1000, 480],
162+
// * 2 the feerate, * 2 the result
163+
[Buffer.from('020102', 'hex'), 2000, 960],
164+
// P2PKH is 546 (well known)
165+
[
166+
Buffer.from(
167+
'76a914b6211d1f14f26ea4aed0e4a55e56e82656c7233d88ac',
168+
'hex',
169+
),
170+
1000,
171+
546,
172+
],
173+
// P2WPKH is 294 (mentioned in Core comments)
174+
[
175+
Buffer.from('00145f72106b919817aa740fc655cce1a59f2d804e16', 'hex'),
176+
1000,
177+
294,
178+
],
179+
// P2TR (and P2WSH) is 330
180+
[
181+
Buffer.from(
182+
'51208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963',
183+
'hex',
184+
),
185+
1000,
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+
1000,
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+
1000,
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+
1000,
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
});

Diff for: ts_src/address.ts

+35
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,39 @@ 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+
export function dustAmountFromOutputScript(
150+
script: Buffer,
151+
satPerKvb: number = 1000,
152+
): number {
153+
// If unspendable, return 0
154+
// https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84
155+
// (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
156+
if (
157+
(script.length > 0 && script[0] == OPS.OP_RETURN) ||
158+
script.length > 10000
159+
) {
160+
return 0;
161+
}
162+
163+
const inputBytes = isSegwit(script) ? 67 : 148;
164+
const outputBytes = script.length + 8 + varuint.encodingLength(script.length);
165+
166+
return Math.ceil((inputBytes + outputBytes) * 3 * (satPerKvb / 1000));
167+
}
168+
169+
function isSegwit(script: Buffer): boolean {
170+
if (script.length < 4 || script.length > 42) return false;
171+
if (script[0] !== OPS.OP_0 && (script[0] < OPS.OP_1 || script[0] > OPS.OP_16))
172+
return false;
173+
if (script[1] + 2 !== script.length) return false;
174+
return true;
175+
}
176+
142177
export function toOutputScript(address: string, network?: Network): Buffer {
143178
network = network || networks.bitcoin;
144179

0 commit comments

Comments
 (0)