Skip to content

Commit

Permalink
add Bech32 support to toOutputScript/fromOutputScript
Browse files Browse the repository at this point in the history
  • Loading branch information
dcousens committed Aug 21, 2017
1 parent d1052e4 commit b1272a1
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 113 deletions.
65 changes: 34 additions & 31 deletions src/address.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,26 @@ var types = require('./types')

function fromBase58Check (address) {
var payload = bs58check.decode(address)

// TODO: 4.0.0, move to "toOutputScript"
if (payload.length < 21) throw new TypeError(address + ' is too short')
if (payload.length > 21) throw new TypeError(address + ' is too long')

var version = payload.readUInt8(0)
var hash = payload.slice(1)

return { hash: hash, version: version }
return { version: version, hash: hash }
}

function fromBech32 (address, expectedPrefix) {
function fromBech32 (address) {
var result = bech32.decode(address)
var prefix = result.prefix
var words = result.words
if (expectedPrefix !== undefined) {
if (prefix !== expectedPrefix) throw new Error('Expected ' + expectedPrefix + ', got ' + prefix)
}

var version = words[0]
if (version > 16) throw new Error('Invalid version (' + version + ')')
var program = bech32.fromWords(words.slice(1))
var data = bech32.fromWords(result.words.slice(1))

if (version === 0) {
if (program.length !== 20 && program.length !== 32) throw new Error('Unknown program')
} else {
if (program.length < 2) throw new Error('Program too short')
if (program.length > 40) throw new Error('Program too long')
return {
version: result.words[0],
prefix: result.prefix,
data: Buffer.from(data)
}

return { version, prefix, program: Buffer.from(program) }
}

function toBase58Check (hash, version) {
Expand All @@ -49,16 +40,8 @@ function toBase58Check (hash, version) {
return bs58check.encode(payload)
}

function toBech32 (prefix, version, program) {
if (version > 16) throw new Error('Invalid version (' + version + ')')
if (version === 0) {
if (program.length !== 20 && program.length !== 32) throw new Error('Unknown program')
} else {
if (program.length < 2) throw new Error('Program too short')
if (program.length > 40) throw new Error('Program too long')
}

var words = bech32.toWords(program)
function toBech32 (data, version, prefix) {
var words = bech32.toWords(data)
words.unshift(version)

return bech32.encode(prefix, words)
Expand All @@ -69,16 +52,36 @@ function fromOutputScript (outputScript, network) {

if (bscript.pubKeyHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(3, 23), network.pubKeyHash)
if (bscript.scriptHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(2, 22), network.scriptHash)
if (bscript.witnessPubKeyHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 22), 0, network.bech32)
if (bscript.witnessScriptHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 34), 0, network.bech32)

throw new Error(bscript.toASM(outputScript) + ' has no matching Address')
}

function toOutputScript (address, network) {
network = network || networks.bitcoin

var decode = fromBase58Check(address)
if (decode.version === network.pubKeyHash) return bscript.pubKeyHash.output.encode(decode.hash)
if (decode.version === network.scriptHash) return bscript.scriptHash.output.encode(decode.hash)
var decode
try {
decode = fromBase58Check(address)
} catch (e) {}

if (decode) {
if (decode.version === network.pubKeyHash) return bscript.pubKeyHash.output.encode(decode.hash)
if (decode.version === network.scriptHash) return bscript.scriptHash.output.encode(decode.hash)
} else {
try {
decode = fromBech32(address)
} catch (e) {}

if (decode) {
if (decode.prefix !== network.bech32) throw new Error(address + ' has an invalid prefix')
if (decode.version === 0) {
if (decode.data.length === 20) return bscript.witnessPubKeyHash.output.encode(decode.data)
if (decode.data.length === 32) return bscript.witnessScriptHash.output.encode(decode.data)
}
}
}

throw new Error(address + ' has no matching Script')
}
Expand Down
2 changes: 2 additions & 0 deletions src/networks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module.exports = {
bitcoin: {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'bc',
bip32: {
public: 0x0488b21e,
private: 0x0488ade4
Expand All @@ -14,6 +15,7 @@ module.exports = {
},
testnet: {
messagePrefix: '\x18Bitcoin Signed Message:\n',
bech32: 'tb',
bip32: {
public: 0x043587cf,
private: 0x04358394
Expand Down
49 changes: 25 additions & 24 deletions test/address.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var fixtures = require('./fixtures/address.json')
describe('address', function () {
describe('fromBase58Check', function () {
fixtures.standard.forEach(function (f) {
if (!f.base58check) return

it('decodes ' + f.base58check, function () {
var decode = baddress.fromBase58Check(f.base58check)

Expand All @@ -27,34 +29,34 @@ describe('address', function () {
})

describe('fromBech32', function () {
fixtures.bech32.forEach((f) => {
it('encodes ' + f.address, function () {
var actual = baddress.fromBech32(f.address)
fixtures.standard.forEach((f) => {
if (!f.bech32) return

it('decodes ' + f.bech32, function () {
var actual = baddress.fromBech32(f.bech32)

assert.strictEqual(actual.prefix, f.prefix)
assert.strictEqual(actual.program.toString('hex'), f.program)
assert.strictEqual(actual.version, f.version)
assert.strictEqual(actual.prefix, networks[f.network].bech32)
assert.strictEqual(actual.data.toString('hex'), f.data)
})
})

fixtures.invalid.bech32.forEach((f, i) => {
if (f.address === undefined) return

it('decode fails for ' + f.address + '(' + f.exception + ')', function () {
it('decode fails for ' + f.bech32 + '(' + f.exception + ')', function () {
assert.throws(function () {
baddress.fromBech32(f.address, f.prefix)
baddress.fromBech32(f.address)
}, new RegExp(f.exception))
})
})
})

describe('fromOutputScript', function () {
fixtures.standard.forEach(function (f) {
it('parses ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () {
it('encodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () {
var script = bscript.fromASM(f.script)
var address = baddress.fromOutputScript(script, networks[f.network])

assert.strictEqual(address, f.base58check)
assert.strictEqual(address, f.base58check || f.bech32.toLowerCase())
})
})

Expand All @@ -71,7 +73,9 @@ describe('address', function () {

describe('toBase58Check', function () {
fixtures.standard.forEach(function (f) {
it('formats ' + f.hash + ' (' + f.network + ')', function () {
if (!f.base58check) return

it('encodes ' + f.hash + ' (' + f.network + ')', function () {
var address = baddress.toBase58Check(Buffer.from(f.hash, 'hex'), f.version)

assert.strictEqual(address, f.base58check)
Expand All @@ -81,32 +85,29 @@ describe('address', function () {

describe('toBech32', function () {
fixtures.bech32.forEach((f, i) => {
// unlike the reference impl., we don't support mixed/uppercase
var string = f.address.toLowerCase()
var program = Buffer.from(f.program, 'hex')
if (!f.bech32) return
var data = Buffer.from(f.data, 'hex')

it('encode ' + string, function () {
assert.deepEqual(baddress.toBech32(f.prefix, f.version, program), string)
it('encode ' + f.address, function () {
assert.deepEqual(baddress.toBech32(data, f.version, f.prefix), f.address)
})
})

fixtures.invalid.bech32.forEach((f, i) => {
if (!f.prefix || f.version === undefined || f.program === undefined) return
if (!f.prefix || f.version === undefined || f.data === undefined) return

it('encode fails (' + f.exception, function () {
assert.throws(function () {
baddress.toBech32(f.prefix, f.version, Buffer.from(f.program, 'hex'))
baddress.toBech32(Buffer.from(f.data, 'hex'), f.version, f.prefix)
}, new RegExp(f.exception))
})
})
})

describe('toOutputScript', function () {
fixtures.standard.forEach(function (f) {
var network = networks[f.network]

it('exports ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () {
var script = baddress.toOutputScript(f.base58check, network)
it('decodes ' + f.script.slice(0, 30) + '... (' + f.network + ')', function () {
var script = baddress.toOutputScript(f.base58check || f.bech32, networks[f.network])

assert.strictEqual(bscript.toASM(script), f.script)
})
Expand All @@ -115,7 +116,7 @@ describe('address', function () {
fixtures.invalid.toOutputScript.forEach(function (f) {
it('throws when ' + f.exception, function () {
assert.throws(function () {
baddress.toOutputScript(f.address)
baddress.toOutputScript(f.address, f.network)
}, new RegExp(f.address + ' ' + f.exception))
})
})
Expand Down
Loading

0 comments on commit b1272a1

Please sign in to comment.