diff --git a/src/payments/embed.js b/src/payments/embed.js new file mode 100644 index 0000000..c636c80 --- /dev/null +++ b/src/payments/embed.js @@ -0,0 +1,56 @@ +const lazy = require('./lazy') +const typef = require('typeforce') +const OPS = require('bitcoin-ops') + +const bscript = require('../script') +const BITCOIN_NETWORK = require('../networks').bitcoin + +function stacksEqual (a, b) { + if (a.length !== b.length) return false + + return a.every(function (x, i) { + return x.equals(b[i]) + }) +} + +// output: OP_RETURN ... +function p2data (a, opts) { + if ( + !a.data && + !a.output + ) throw new TypeError('Not enough data') + opts = opts || { validate: true } + + typef({ + network: typef.maybe(typef.Object), + output: typef.maybe(typef.Buffer), + data: typef.maybe(typef.arrayOf(typef.Buffer)) + }, a) + + const network = a.network || BITCOIN_NETWORK + const o = { network } + + lazy.prop(o, 'output', function () { + if (!a.data) return + return bscript.compile([OPS.OP_RETURN].concat(a.data)) + }) + lazy.prop(o, 'data', function () { + if (!a.output) return + return bscript.decompile(a.output).slice(1) + }) + + // extended validation + if (opts.validate) { + if (a.output) { + const chunks = bscript.decompile(a.output) + if (chunks[0] !== OPS.OP_RETURN) throw new TypeError('Output is invalid') + if (!chunks.slice(1).every(typef.Buffer)) throw new TypeError('Output is invalid') + + if (a.data && !stacksEqual(a.data, o.data)) throw new TypeError('Data mismatch') + } + } + + return Object.assign(o, a) +} + +module.exports = p2data diff --git a/src/payments/index.js b/src/payments/index.js index 9e869f5..d445466 100644 --- a/src/payments/index.js +++ b/src/payments/index.js @@ -1,3 +1,4 @@ +const embed = require('./embed') const p2ms = require('./p2ms') const p2pk = require('./p2pk') const p2pkh = require('./p2pkh') @@ -5,15 +6,7 @@ const p2sh = require('./p2sh') const p2wpkh = require('./p2wpkh') const p2wsh = require('./p2wsh') -module.exports = { - p2ms: p2ms, - p2pk: p2pk, - p2pkh: p2pkh, - p2sh: p2sh, - p2wpkh: p2wpkh, - p2wsh: p2wsh -} +module.exports = { embed, p2ms, p2pk, p2pkh, p2sh, p2wpkh, p2wsh } // TODO -// OP_RETURN // witness commitment diff --git a/src/payments/p2data.js b/src/payments/p2data.js new file mode 100644 index 0000000..e69de29 diff --git a/src/templates/nulldata.js b/src/templates/nulldata.js index 2ad09d7..d42fd71 100644 --- a/src/templates/nulldata.js +++ b/src/templates/nulldata.js @@ -1,8 +1,6 @@ // OP_RETURN {data} const bscript = require('../script') -const types = require('../types') -const typeforce = require('typeforce') const OPS = require('bitcoin-ops') function check (script) { @@ -13,22 +11,4 @@ function check (script) { } check.toJSON = function () { return 'null data output' } -function encode (data) { - typeforce([types.Buffer], data) - - return bscript.compile([OPS.OP_RETURN].concat(data)) -} - -function decode (buffer) { - typeforce(check, buffer) - - return bscript.decompile(buffer).slice(1) -} - -module.exports = { - output: { - check: check, - decode: decode, - encode: encode - } -} +module.exports = { output: { check: check } } diff --git a/test/fixtures/embed.json b/test/fixtures/embed.json new file mode 100644 index 0000000..ccc0e70 --- /dev/null +++ b/test/fixtures/embed.json @@ -0,0 +1,63 @@ +{ + "valid": [ + { + "description": "output from output", + "arguments": { + "output": "OP_RETURN a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4" + }, + "expected": { + "data": [ + "a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4" + ], + "output": "OP_RETURN a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4", + "input": null, + "witness": null + } + }, + { + "description": "output from data", + "arguments": { + "data": [ + "a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4" + ] + }, + "expected": { + "data": [ + "a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4" + ], + "output": "OP_RETURN a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4", + "input": null, + "witness": null + } + } + ], + "invalid": [ + { + "exception": "Not enough data", + "arguments": {} + } + ], + "dynamic": { + "depends": { + "data": [ "data", "output" ], + "output": [ "output", "data" ] + }, + "details": [ + { + "description": "embed", + "data": [ + "a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4" + ], + "output": "OP_RETURN a3b147dbe4a85579fc4b5a1811e76620560e07267e62b9a0d6858f9127735cadd82f67e06c24dbc4" + }, + { + "description": "embed", + "data": [ + "a3b147dbe4a85579fc4b5a1811e76620560e0726", + "7e62b9a0d6858f9127735cadd82f67e06c24dbc4" + ], + "output": "OP_RETURN a3b147dbe4a85579fc4b5a1811e76620560e0726 7e62b9a0d6858f9127735cadd82f67e06c24dbc4" + } + ] + } +} diff --git a/test/integration/transactions.js b/test/integration/transactions.js index 5bb1342..5358461 100644 --- a/test/integration/transactions.js +++ b/test/integration/transactions.js @@ -91,10 +91,9 @@ describe('bitcoinjs-lib (transactions)', function () { const txb = new bitcoin.TransactionBuilder(regtest) const data = Buffer.from('bitcoinjs-lib', 'utf8') - const dataScript = require('../../src/templates/nulldata').output.encode([data]) - + const embed = bitcoin.payments.embed({ data: [data] }) txb.addInput(unspent.txId, unspent.vout) - txb.addOutput(dataScript, 1000) + txb.addOutput(embed.output, 1000) txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e5) txb.sign(0, keyPair) diff --git a/test/payments.js b/test/payments.js index 5722d88..3af6699 100644 --- a/test/payments.js +++ b/test/payments.js @@ -3,20 +3,21 @@ const assert = require('assert') const u = require('./payments.utils') -;['p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(function (p) { +;['embed', 'p2ms', 'p2pk', 'p2pkh', 'p2sh', 'p2wpkh', 'p2wsh'].forEach(function (p) { describe(p, function () { const fn = require('../src/payments/' + p) const fixtures = require('./fixtures/' + p) fixtures.valid.forEach(function (f, i) { - const args = u.preform(f.arguments) - it(f.description + ' as expected', function () { + const args = u.preform(f.arguments) const actual = fn(args, f.options) + u.equate(actual, f.expected, f.arguments) }) it(f.description + ' as expected (no validation)', function () { + const args = u.preform(f.arguments) const actual = fn(args, Object.assign({}, f.options, { validate: false })) diff --git a/test/payments.utils.js b/test/payments.utils.js index 22001e9..2dbed3d 100644 --- a/test/payments.utils.js +++ b/test/payments.utils.js @@ -7,6 +7,12 @@ function tryHex (x) { if (Array.isArray(x)) return x.map(tryHex) return x } + +function fromHex (x) { + if (typeof x === 'string') return Buffer.from(x, 'hex') + if (Array.isArray(x)) return x.map(fromHex) + return x +} function tryASM (x) { if (Buffer.isBuffer(x)) return bscript.toASM(x) return x @@ -64,6 +70,7 @@ function equate (a, b, args) { if ('n' in b) t.strictEqual(a.n, b.n, 'Inequal *.n') if ('pubkeys' in b) t.deepEqual(tryHex(a.pubkeys), tryHex(b.pubkeys), 'Inequal *.pubkeys') if ('signatures' in b) t.deepEqual(tryHex(a.signatures), tryHex(b.signatures), 'Inequal *.signatures') + if ('data' in b) t.deepEqual(tryHex(a.data), tryHex(b.data), 'Inequal *.data') } function preform (x) { @@ -80,21 +87,18 @@ function preform (x) { } if (typeof x.output === 'string') x.output = asmToBuffer(x.output) if (typeof x.input === 'string') x.input = asmToBuffer(x.input) - if (Array.isArray(x.witness)) { - x.witness = x.witness.map(function (y) { - return Buffer.from(y, 'hex') - }) - } + if (Array.isArray(x.witness)) x.witness = x.witness.map(fromHex) + if (x.data) x.data = x.data.map(fromHex) if (x.hash) x.hash = Buffer.from(x.hash, 'hex') if (x.pubkey) x.pubkey = Buffer.from(x.pubkey, 'hex') if (x.signature) x.signature = Buffer.from(x.signature, 'hex') - if (x.pubkeys) x.pubkeys = x.pubkeys.map(function (y) { return Buffer.from(y, 'hex') }) + if (x.pubkeys) x.pubkeys = x.pubkeys.map(fromHex) if (x.signatures) x.signatures = x.signatures.map(function (y) { return Number.isFinite(y) ? y : Buffer.from(y, 'hex') }) if (x.redeem) { if (typeof x.redeem.input === 'string') x.redeem.input = asmToBuffer(x.redeem.input) if (typeof x.redeem.output === 'string') x.redeem.output = asmToBuffer(x.redeem.output) - if (Array.isArray(x.redeem.witness)) x.redeem.witness = x.redeem.witness.map(function (y) { return Buffer.from(y, 'hex') }) + if (Array.isArray(x.redeem.witness)) x.redeem.witness = x.redeem.witness.map(fromHex) x.redeem.network = bnetworks[x.redeem.network] || x.network || bnetworks.bitcoin }