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

src/address.d.ts

Lines changed: 1 addition & 0 deletions
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;

src/address.js

Lines changed: 33 additions & 0 deletions
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;

test/address.spec.ts

Lines changed: 72 additions & 0 deletions
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
});

ts_src/address.ts

Lines changed: 35 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy