diff --git a/src/address.d.ts b/src/address.d.ts index be0e00a61..47a76b9a7 100644 --- a/src/address.d.ts +++ b/src/address.d.ts @@ -14,4 +14,5 @@ export declare function fromBech32(address: string): Bech32Result; export declare function toBase58Check(hash: Buffer, version: number): string; export declare function toBech32(data: Buffer, version: number, prefix: string): string; export declare function fromOutputScript(output: Buffer, network?: Network): string; +export declare function dustAmountFromOutputScript(script: Buffer, satPerKvb?: number): number; export declare function toOutputScript(address: string, network?: Network): Buffer; diff --git a/src/address.js b/src/address.js index ada8042af..456d2561d 100644 --- a/src/address.js +++ b/src/address.js @@ -1,6 +1,7 @@ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.toOutputScript = + exports.dustAmountFromOutputScript = exports.fromOutputScript = exports.toBech32 = exports.toBase58Check = @@ -8,9 +9,11 @@ exports.toOutputScript = exports.fromBase58Check = void 0; const networks = require('./networks'); +const ops_1 = require('./ops'); const payments = require('./payments'); const bscript = require('./script'); const types_1 = require('./types'); +const varuint = require('bip174/src/lib/converter/varint'); const bech32_1 = require('bech32'); const bs58check = require('bs58check'); const FUTURE_SEGWIT_MAX_SIZE = 40; @@ -116,6 +119,36 @@ function fromOutputScript(output, network) { throw new Error(bscript.toASM(output) + ' has no matching Address'); } exports.fromOutputScript = fromOutputScript; +/* + * This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script. + * + * Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63 + */ +function dustAmountFromOutputScript(script, satPerKvb = 1000) { + // If unspendable, return 0 + // https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84 + // (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE); + if ( + (script.length > 0 && script[0] == ops_1.OPS.OP_RETURN) || + script.length > 10000 + ) { + return 0; + } + const inputBytes = isSegwit(script) ? 67 : 148; + const outputBytes = script.length + 8 + varuint.encodingLength(script.length); + return Math.ceil((inputBytes + outputBytes) * 3 * (satPerKvb / 1000)); +} +exports.dustAmountFromOutputScript = dustAmountFromOutputScript; +function isSegwit(script) { + if (script.length < 4 || script.length > 42) return false; + if ( + script[0] !== ops_1.OPS.OP_0 && + (script[0] < ops_1.OPS.OP_1 || script[0] > ops_1.OPS.OP_16) + ) + return false; + if (script[1] + 2 !== script.length) return false; + return true; +} function toOutputScript(address, network) { network = network || networks.bitcoin; let decodeBase58; diff --git a/test/address.spec.ts b/test/address.spec.ts index 23c18b9f6..d5050d982 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -150,4 +150,76 @@ describe('address', () => { }); }); }); + + describe('dustAmountFromOutputScript', () => { + it('gets correct values', () => { + const vectors = [ + // OP_RETURN is always 0 regardless of size + [Buffer.from('6a04deadbeef', 'hex'), 1000, 0], + [Buffer.from('6a08deadbeefdeadbeef', 'hex'), 1000, 0], + // 3 byte non-segwit output is 3 + 1 + 8 + 148 = 160 * 3 = 480 + [Buffer.from('020102', 'hex'), 1000, 480], + // * 2 the feerate, * 2 the result + [Buffer.from('020102', 'hex'), 2000, 960], + // P2PKH is 546 (well known) + [ + Buffer.from( + '76a914b6211d1f14f26ea4aed0e4a55e56e82656c7233d88ac', + 'hex', + ), + 1000, + 546, + ], + // P2WPKH is 294 (mentioned in Core comments) + [ + Buffer.from('00145f72106b919817aa740fc655cce1a59f2d804e16', 'hex'), + 1000, + 294, + ], + // P2TR (and P2WSH) is 330 + [ + Buffer.from( + '51208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963', + 'hex', + ), + 1000, + 330, + ], + // P2TR (and P2WSH) with OP_16 for some reason is still 330 + [ + Buffer.from( + '60208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963', + 'hex', + ), + 1000, + 330, + ], + // P2TR (and P2WSH) with 0x61 instead of OP number for some reason is now 573 + [ + Buffer.from( + '61208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963', + 'hex', + ), + 1000, + 573, + ], + // P2TR (and P2WSH) with 0x50 instead of OP 1-16 for some reason is now 573 + [ + Buffer.from( + '50208215bbb39e58fc799515d72a76a29400c146f7044dcf44925877ed3219782963', + 'hex', + ), + 1000, + 573, + ], + ] as const; + + for (const [script, feeRatekvB, expected] of vectors) { + assert.strictEqual( + baddress.dustAmountFromOutputScript(script, feeRatekvB), + expected, + ); + } + }); + }); }); diff --git a/ts_src/address.ts b/ts_src/address.ts index ce224fd11..fe36fb5d0 100644 --- a/ts_src/address.ts +++ b/ts_src/address.ts @@ -1,8 +1,10 @@ import { Network } from './networks'; import * as networks from './networks'; +import { OPS } from './ops'; import * as payments from './payments'; import * as bscript from './script'; import { typeforce, tuple, Hash160bit, UInt8 } from './types'; +import * as varuint from 'bip174/src/lib/converter/varint'; import { bech32, bech32m } from 'bech32'; import * as bs58check from 'bs58check'; export interface Base58CheckResult { @@ -139,6 +141,39 @@ export function fromOutputScript(output: Buffer, network?: Network): string { throw new Error(bscript.toASM(output) + ' has no matching Address'); } +/* + * This uses the logic from Bitcoin Core to decide what is the dust threshold for a given script. + * + * Ref: https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/policy/policy.cpp#L26-L63 + */ +export function dustAmountFromOutputScript( + script: Buffer, + satPerKvb: number = 1000, +): number { + // If unspendable, return 0 + // https://github.com/bitcoin/bitcoin/blob/160d23677ad799cf9b493eaa923b2ac080c3fb8e/src/script/script.h#L554C16-L554C84 + // (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE); + if ( + (script.length > 0 && script[0] == OPS.OP_RETURN) || + script.length > 10000 + ) { + return 0; + } + + const inputBytes = isSegwit(script) ? 67 : 148; + const outputBytes = script.length + 8 + varuint.encodingLength(script.length); + + return Math.ceil((inputBytes + outputBytes) * 3 * (satPerKvb / 1000)); +} + +function isSegwit(script: Buffer): boolean { + if (script.length < 4 || script.length > 42) return false; + if (script[0] !== OPS.OP_0 && (script[0] < OPS.OP_1 || script[0] > OPS.OP_16)) + return false; + if (script[1] + 2 !== script.length) return false; + return true; +} + export function toOutputScript(address: string, network?: Network): Buffer { network = network || networks.bitcoin;