From 786715c172910cd6490ee09c22e76df02c69ff42 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 22 Aug 2024 20:51:38 -0600 Subject: [PATCH 1/5] feat(platform): support special txes (version, type, extra payload) --- dashtx.js | 109 ++++++++++++++++++++++++++++++++++++++---------- tests/parser.js | 48 +++++++++++++++++---- 2 files changed, 127 insertions(+), 30 deletions(-) diff --git a/dashtx.js b/dashtx.js index ef4b5a7..260ae43 100644 --- a/dashtx.js +++ b/dashtx.js @@ -89,6 +89,7 @@ var DashTx = ("object" === typeof module && exports) || {}; let TxUtils = {}; const CURRENT_VERSION = 3; + const TYPE_VERSION = 0; const SATOSHIS = 100000000; const MAX_U8 = Math.pow(2, 8) - 1; @@ -143,11 +144,13 @@ var DashTx = ("object" === typeof module && exports) || {}; Tx.LEGACY_DUST = 2000; Tx._HEADER_ONLY_SIZE = - 4 + // version + 2 + // version + 2 + // type 4; // locktime Tx.HEADER_SIZE = - 4 + // version + 2 + // version + 2 + // type 1 + // input count 1 + // output count 4; // locktime @@ -307,11 +310,13 @@ var DashTx = ("object" === typeof module && exports) || {}; /** @type {TxInfoSigned} */ let txInfoSigned = { + version: txInfo.version || CURRENT_VERSION, + type: txInfo.type || TYPE_VERSION, /** @type {Array} */ inputs: [], outputs: txInfo.outputs, - version: txInfo.version || CURRENT_VERSION, locktime: txInfo.locktime || 0x00, + extraPayload: txInfo.extraPayload || "", transaction: "", }; @@ -694,9 +699,10 @@ var DashTx = ("object" === typeof module && exports) || {}; * or the largest available coins until that total is met. */ //@ts-ignore - TODO update typedefs - Tx.createLegacyTx = function (coins, outputs, changeOutput) { + Tx.createLegacyTx = function (coins, outputs, changeOutput, extraPayload) { // TODO bump to 4 for DIP: enforce tx hygiene let version = CURRENT_VERSION; + let type = TYPE_VERSION; coins = coins.slice(0); outputs = outputs.slice(0); @@ -780,10 +786,12 @@ var DashTx = ("object" === typeof module && exports) || {}; let txInfo = { version, + type, inputs, outputs, changeIndex, locktime, + extraPayload, }; // let change = txInfo.outputs[txInfo.changeIndex]; @@ -1127,9 +1135,11 @@ var DashTx = ("object" === typeof module && exports) || {}; Tx.serialize = function ( { version = CURRENT_VERSION, + type = TYPE_VERSION, inputs, - locktime = 0x0, outputs, + locktime = 0x0, + extraPayload = "", /* maxFee = 10000, */ _debug = false, }, @@ -1142,8 +1152,10 @@ var DashTx = ("object" === typeof module && exports) || {}; /** @type Array */ let tx = []; - let v = TxUtils._toUint32LE(version); + let v = TxUtils._toUint16LE(version); tx.push(v); + let t = TxUtils._toUint16LE(type); + tx.push(t); void Tx.serializeInputs(inputs, { _tx: tx, _sep: _sep }); void Tx.serializeOutputs(outputs, { _tx: tx, _sep: _sep }); @@ -1151,13 +1163,18 @@ var DashTx = ("object" === typeof module && exports) || {}; let locktimeHex = TxUtils._toUint32LE(locktime); tx.push(locktimeHex); - let txHex = tx.join(_sep); + if (extraPayload) { + let nExtraPayload = Tx.utils.toVarInt(extraPayload.length / 2); + tx.push(nExtraPayload); + tx.push(extraPayload); + } if (sigHashType) { let sigHashTypeHex = TxUtils._toUint32LE(sigHashType); - txHex = `${txHex}${sigHashTypeHex}`; + tx.push(sigHashTypeHex); } + let txHex = tx.join(_sep); return txHex; }; //@ts-ignore - same function, but typed and documented separately for clarity @@ -1466,10 +1483,15 @@ var DashTx = ("object" === typeof module && exports) || {}; tx.offset = 0; - tx.versionHex = hex.substr(tx.offset, 8); + tx.versionHex = hex.substr(tx.offset, 4); let versionHexRev = Tx.utils.reverseHex(tx.versionHex); tx.version = parseInt(versionHexRev, 16); - tx.offset += 8; + tx.offset += 4; + + tx.typeHex = hex.substr(tx.offset, 4); + let typeHexRev = Tx.utils.reverseHex(tx.typeHex); + tx.type = parseInt(typeHexRev, 16); + tx.offset += 4; let [numInputs, numInputsSize] = TxUtils._parseVarIntHex(hex, tx.offset); tx.offset += numInputsSize; @@ -1638,10 +1660,26 @@ var DashTx = ("object" === typeof module && exports) || {}; tx.locktime = parseInt(locktimeHexRev, 16); tx.offset += 8; + tx.extraPayloadSizeHex = ""; + /** @type {Uint8?} */ + tx.extraPayloadSize = null; + tx.extraPayloadHex = ""; + if (tx.type > 0) { + // TODO varint + tx.extraPayloadSizeHex = hex.substr(tx.offset, 2); + tx.extraPayloadSize = parseInt(tx.extraPayloadSizeHex, 16); + tx.offset += 2; + + tx.extraPayloadHex = hex.substr(tx.offset, 2 * tx.extraPayloadSize); + tx.offset += 2 * tx.extraPayloadSize; + } + tx.sigHashTypeHex = hex.substr(tx.offset); if (tx.sigHashTypeHex) { - tx.sigHashType = parseInt(tx.sigHashTypeHex.slice(0, 2)); - hex = hex.slice(0, -8); + let firstLEIntByte = tx.sigHashTypeHex.slice(0, 2); + tx.sigHashType = parseInt(firstLEIntByte); + let fullLEIntHexSize = 8; + hex = hex.slice(0, -fullLEIntHexSize); // but the size is actually 4 bytes } tx.size = hex.length / 2; @@ -1806,6 +1844,18 @@ var DashTx = ("object" === typeof module && exports) || {}; throw err; }; + /** + * Just assumes that all target CPUs are Little-Endian, + * which is true in practice, and much simpler. + * @param {BigInt|Number} n - 16-bit positive int to encode + */ + TxUtils._toUint16LE = function (n) { + let hexLE = TxUtils._toUint32LE(n); + // ex: 03000800 => 0300 + hexLE = hexLE.slice(0, 4); + return hexLE; + }; + /** * Just assumes that all target CPUs are Little-Endian, * which is true in practice, and much simpler. @@ -1916,6 +1966,7 @@ if ("object" === typeof module) { /** @typedef {Number} Float64 */ /** @typedef {Number} Uint8 */ +/** @typedef {Number} Uint16 */ /** @typedef {Number} Uint32 */ /** @typedef {Number} Uint53 */ /** @typedef {String} Hex */ @@ -1953,11 +2004,13 @@ if ("object" === typeof module) { /** * @typedef TxInfo + * @prop {Uint16} [version] + * @prop {Uint16} [type] * @prop {Array} inputs - * @prop {Uint32} [locktime] - 0 by default * @prop {Array} outputs - * @prop {Uint32} [version] - * @prop {String} [transaction] - signed transaction hex + * @prop {Uint32} [locktime] - 0 by default + * @prop {Hex} [extraPayload] - extra payload bytes + * @prop {Hex} [transaction] - signed transaction hex * @prop {Boolean} [_debug] - bespoke debug output */ @@ -1971,10 +2024,12 @@ if ("object" === typeof module) { /** * @typedef TxInfoSigned + * @prop {Uint16} version + * @prop {Uint16} type * @prop {Array} inputs - * @prop {Uint32} locktime - 0 by default * @prop {Array} outputs - * @prop {Uint32} version + * @prop {Uint32} locktime - 0 by default + * @prop {Hex} extraPayload - extra payload bytes * @prop {String} transaction - signed transaction hex * @prop {Boolean} [_debug] - bespoke debug output */ @@ -2127,9 +2182,12 @@ if ("object" === typeof module) { /** * @callback TxCreateRaw * @param {Object} opts + * @param {Uint16} [opts.version] + * @param {Uint16} [opts.type] * @param {Array} opts.inputs * @param {Array} opts.outputs - * @param {Uint32} [opts.version] + * @param {Uint32} [opts.locktime] + * @param {Hex} [opts.extraPayload] * @param {Boolean} [opts._debug] - bespoke debug output */ @@ -2142,9 +2200,12 @@ if ("object" === typeof module) { /** * @callback TxCreateSigned * @param {Object} opts + * @param {Uint16} [opts.version] + * @param {Uint16} [opts.type] * @param {Array} opts.inputs * @param {Array} opts.outputs - * @param {Uint32} [opts.version] + * @param {Uint32} [opts.locktime] + * @param {Hex} [opts.extraPayload] * @param {Boolean} [opts._debug] - bespoke debug output * xparam {String} [opts.sigHashType] - hex, typically 01 (ALL) */ @@ -2252,10 +2313,12 @@ if ("object" === typeof module) { /** * @callback TxSerialize * @param {Object} txInfo + * @param {Uint16} [txInfo.version] + * @param {Uint16} [txInfo.type] * @param {Array} txInfo.inputs - * @param {Uint32} [txInfo.locktime] * @param {Array} txInfo.outputs - * @param {Uint32} [txInfo.version] + * @param {Uint32} [txInfo.locktime] + * @param {Hex?} [txInfo.extraPayload] - extra payload * @param {Boolean} [txInfo._debug] - bespoke debug output * @param {Uint32} [sigHashType] */ @@ -2263,10 +2326,12 @@ if ("object" === typeof module) { /** * @callback TxSerializeForSig * @param {Object} txInfo + * @param {Uint16} [txInfo.version] + * @param {Uint16} [txInfo.type] * @param {Array} txInfo.inputs * @param {Uint32} [txInfo.locktime] * @param {Array} txInfo.outputs - * @param {Uint32} [txInfo.version] + * @param {Hex?} [txInfo.extraPayload] - extra payload * @param {Boolean} [txInfo._debug] - bespoke debug output * @param {Uint32} sigHashType */ diff --git a/tests/parser.js b/tests/parser.js index 349d5c6..51606be 100644 --- a/tests/parser.js +++ b/tests/parser.js @@ -11,12 +11,18 @@ async function test() { let filename = "dsf.tx-request.hex"; let txInfo = await parseHexFile(filename); - if (txInfo.versionHex !== "02000000") { - throw new Error(`${filename} versionHex is not 02000000`); + if (txInfo.versionHex !== "0200") { + throw new Error(`${filename} versionHex is not 0200`); } if (txInfo.version !== 2) { throw new Error(`${filename} version is not 2`); } + if (txInfo.typeHex !== "0000") { + throw new Error(`${filename} typeHex is not 0000`); + } + if (txInfo.type !== 0) { + throw new Error(`${filename} type is not 0`); + } if (txInfo.inputs.length !== 18) { throw new Error( @@ -36,6 +42,10 @@ async function test() { throw new Error(`${filename} locktime is not 0`); } + if (txInfo.extraPayloadHex !== "") { + throw new Error(`${filename} extraPayloadHex is not '' (empty string)`); + } + if (txInfo.sigHashTypeHex) { throw new Error(`${filename} should not have sigHashTypeHex`); } @@ -48,12 +58,18 @@ async function test() { let filename = "dss.tx-response.hex"; let txInfo = await parseHexFile(filename); - if (txInfo.versionHex !== "02000000") { - throw new Error(`${filename} versionHex is not 02000000`); + if (txInfo.versionHex !== "0200") { + throw new Error(`${filename} versionHex is not 0200`); } if (txInfo.version !== 2) { throw new Error(`${filename} version is not 2`); } + if (txInfo.typeHex !== "0000") { + throw new Error(`${filename} typeHex is not 0000`); + } + if (txInfo.type !== 0) { + throw new Error(`${filename} type is not 0`); + } if (txInfo.inputs.length !== 2) { throw new Error(`${filename} # inputs is not 2: ${txInfo.inputs.length}`); @@ -72,6 +88,10 @@ async function test() { `${filename} locktime is not 2004296226: ${txInfo.locktime}`, ); } + + if (txInfo.extraPayloadHex !== "") { + throw new Error(`${filename} extraPayload is not '' (empty string)`); + } } { @@ -107,12 +127,18 @@ async function test() { let filename = "sighash-all.tx.hex"; let txInfo = await parseHexFile(filename); - if (txInfo.versionHex !== "02000000") { - throw new Error(`${filename} versionHex is not 02000000`); + if (txInfo.versionHex !== "0200") { + throw new Error(`${filename} versionHex is not 0200`); } if (txInfo.version !== 2) { throw new Error(`${filename} version is not 2`); } + if (txInfo.typeHex !== "0000") { + throw new Error(`${filename} typeHex is not 0000`); + } + if (txInfo.type !== 0) { + throw new Error(`${filename} type is not 0`); + } let scriptIndex = -1; let nulls = 0; @@ -158,12 +184,18 @@ async function test() { let filename = "sighash-any.tx.hex"; let txInfo = await parseHexFile(filename); - if (txInfo.versionHex !== "02000000") { - throw new Error(`${filename} versionHex is not 02000000`); + if (txInfo.versionHex !== "0200") { + throw new Error(`${filename} versionHex is not 0200`); } if (txInfo.version !== 2) { throw new Error(`${filename} version is not 2`); } + if (txInfo.typeHex !== "0000") { + throw new Error(`${filename} typeHex is not 0000`); + } + if (txInfo.type !== 0) { + throw new Error(`${filename} type is not 0`); + } let scriptIndex = -1; let nulls = 0; From 7bd96be9729cb53e920999cdec87b07f05a29b05 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 22 Aug 2024 21:33:13 -0600 Subject: [PATCH 2/5] ref!: make .toUint32LE(n) and .toUint64LE(n) public --- dashtx.js | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/dashtx.js b/dashtx.js index 260ae43..b756a14 100644 --- a/dashtx.js +++ b/dashtx.js @@ -63,6 +63,8 @@ * @prop {TxHexToBytes} hexToBytes * @prop {TxBytesToHex} bytesToHex * @prop {TxStringToHex} strToHex + * @prop {TxToUint32LE} toUint32LE + * @prop {TxToUint64LE} toUint64LE */ /** @@ -1160,7 +1162,7 @@ var DashTx = ("object" === typeof module && exports) || {}; void Tx.serializeInputs(inputs, { _tx: tx, _sep: _sep }); void Tx.serializeOutputs(outputs, { _tx: tx, _sep: _sep }); - let locktimeHex = TxUtils._toUint32LE(locktime); + let locktimeHex = TxUtils.toUint32LE(locktime); tx.push(locktimeHex); if (extraPayload) { @@ -1170,7 +1172,7 @@ var DashTx = ("object" === typeof module && exports) || {}; } if (sigHashType) { - let sigHashTypeHex = TxUtils._toUint32LE(sigHashType); + let sigHashTypeHex = TxUtils.toUint32LE(sigHashType); tx.push(sigHashTypeHex); } @@ -1221,7 +1223,7 @@ var DashTx = ("object" === typeof module && exports) || {}; `expected utxo property 'input[${i}]outputIndex' to be an integer representing this input's previous output index`, ); } - let reverseVout = TxUtils._toUint32LE(voutIndex); + let reverseVout = TxUtils.toUint32LE(voutIndex); tx.push(reverseVout); //@ts-ignore - enum types not handled properly here @@ -1309,7 +1311,7 @@ var DashTx = ("object" === typeof module && exports) || {}; if (!output.satoshis) { throw new Error(`every output must have 'satoshis'`); } - let satoshis = TxUtils._toUint64LE(output.satoshis); + let satoshis = TxUtils.toUint64LE(output.satoshis); tx.push(satoshis); if (!output.pubKeyHash) { @@ -1348,7 +1350,7 @@ var DashTx = ("object" === typeof module && exports) || {}; */ Tx._createMemoScript = function (memoHex, sats, i = 0) { let outputHex = []; - let satoshis = TxUtils._toUint64LE(sats); + let satoshis = TxUtils.toUint64LE(sats); outputHex.push(satoshis); assertHex(memoHex, `output[${i}].memo`); @@ -1816,17 +1818,17 @@ var DashTx = ("object" === typeof module && exports) || {}; //@ts-ignore if (n <= MAX_U16) { - return "fd" + TxUtils._toUint32LE(n).slice(0, 4); + return "fd" + TxUtils.toUint32LE(n).slice(0, 4); } //@ts-ignore if (n <= MAX_U32) { - return "fe" + TxUtils._toUint32LE(n); + return "fe" + TxUtils.toUint32LE(n); } //@ts-ignore if (n <= MAX_U53) { - return "ff" + TxUtils._toUint64LE(n); + return "ff" + TxUtils.toUint64LE(n); } if ("bigint" !== typeof n) { @@ -1836,7 +1838,7 @@ var DashTx = ("object" === typeof module && exports) || {}; } if (n <= MAX_U64) { - return "ff" + TxUtils._toUint64LE(n); + return "ff" + TxUtils.toUint64LE(n); } let err = new Error(E_TOO_BIG_INT); @@ -1850,7 +1852,7 @@ var DashTx = ("object" === typeof module && exports) || {}; * @param {BigInt|Number} n - 16-bit positive int to encode */ TxUtils._toUint16LE = function (n) { - let hexLE = TxUtils._toUint32LE(n); + let hexLE = TxUtils.toUint32LE(n); // ex: 03000800 => 0300 hexLE = hexLE.slice(0, 4); return hexLE; @@ -1861,7 +1863,7 @@ var DashTx = ("object" === typeof module && exports) || {}; * which is true in practice, and much simpler. * @param {BigInt|Number} n - 32-bit positive int to encode */ - TxUtils._toUint32LE = function (n) { + TxUtils.toUint32LE = function (n) { // make sure n is uint32/int53, not int32 //n = n >>> 0; @@ -1871,6 +1873,13 @@ var DashTx = ("object" === typeof module && exports) || {}; let hexLE = Tx.utils.reverseHex(hex); return hexLE; }; + //@ts-ignore + TxUtils._toUint32LE = function (n) { + console.warn( + "warn: use public TxUtils.toUint32LE() instead of internal TxUtils._toUint32LE()", + ); + return TxUtils.toUint32LE(n); + }; /** * This can handle Big-Endian CPUs, which don't exist, @@ -1878,7 +1887,7 @@ var DashTx = ("object" === typeof module && exports) || {}; * @param {BigInt|Number} n - 64-bit BigInt or <= 53-bit Number to encode * @returns {String} - 8 Little-Endian bytes */ - TxUtils._toUint64LE = function (n) { + TxUtils.toUint64LE = function (n) { let bn; if ("bigint" === typeof n) { bn = n; @@ -1903,6 +1912,13 @@ var DashTx = ("object" === typeof module && exports) || {}; return hex; }; + //@ts-ignore + TxUtils._toUint64LE = function (n) { + console.warn( + "warn: use public TxUtils.toUint64LE() instead of internal TxUtils._toUint64LE()", + ); + return TxUtils.toUint64LE(n); + }; /** @type TxToVarIntSize */ TxUtils.toVarIntSize = function (n) { @@ -2456,3 +2472,15 @@ if ("object" === typeof module) { * @param {String} utf8 * @returns {String} - encoded bytes as hex */ + +/** + * @callback TxToUint32LE + * @param {Uint32} n + * @returns {Hex} + */ + +/** + * @callback TxToUint64LE + * @param {Uint32} n + * @returns {Hex} + */ From 25c2d57e9c236255a0bf8adf1e4d5e1ab9ceb5d8 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 22 Aug 2024 23:17:44 -0600 Subject: [PATCH 3/5] ref: move Tx.createPkhScript(pubKeyHash) to public export --- dashtx.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/dashtx.js b/dashtx.js index b756a14..68be543 100644 --- a/dashtx.js +++ b/dashtx.js @@ -24,6 +24,7 @@ * @prop {TxCreateInputRaw} createInputRaw * @prop {TxCreateForSig} createForSig * @prop {TxCreateInputForSig} createInputForSig + * @prop {TxCreatePkhScript} createPkhScript * @prop {TxCreateSigned} createSigned * @prop {TxGetId} getId - only useful for fully signed tx * @prop {TxCreateLegacyTx} createLegacyTx @@ -1122,7 +1123,7 @@ var DashTx = ("object" === typeof module && exports) || {}; `signable input must have either 'pubKeyHash' or 'script'`, ); } - lockScript = `${OP_DUP}${OP_HASH160}${PKH_SIZE}${input.pubKeyHash}${OP_EQUALVERIFY}${OP_CHECKSIG}`; + lockScript = Tx.createPkhScript(input.pubKeyHash); } return { txId: input.txId || input.txid, @@ -1134,6 +1135,15 @@ var DashTx = ("object" === typeof module && exports) || {}; }; }; + /** + * @param {Hex} pubKeyHash + * @returns {Hex} + */ + Tx.createPkhScript = function (pubKeyHash) { + let lockScript = `${OP_DUP}${OP_HASH160}${PKH_SIZE}${pubKeyHash}${OP_EQUALVERIFY}${OP_CHECKSIG}`; + return lockScript; + }; + Tx.serialize = function ( { version = CURRENT_VERSION, @@ -2195,6 +2205,12 @@ if ("object" === typeof module) { * @param {Uint32} inputIndex - create hashable tx for this input */ +/** + * @callback TxCreatePkhScript + * @param {Hex} pubKeyHash + * @returns {Hex} - ${OP_DUP}${OP_HASH160}${PKH_SIZE}${pubKeyHash}${OP_EQUALVERIFY}${OP_CHECKSIG} + */ + /** * @callback TxCreateRaw * @param {Object} opts From 8beb69807028f7dc84688cbe85965273c6af6d76 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 22 Aug 2024 21:03:35 -0600 Subject: [PATCH 4/5] chore(release): bump to v0.20.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6380477..78511d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dashtx", - "version": "0.19.1", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dashtx", - "version": "0.19.1", + "version": "0.20.0", "license": "SEE LICENSE IN LICENSE", "bin": { "dashtx-inspect": "bin/inspect.js" diff --git a/package.json b/package.json index 55f5713..918a9c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashtx", - "version": "0.19.1", + "version": "0.20.0", "description": "Create DASH Transactions with Vanilla JS (0 deps, cross-platform)", "main": "dashtx.js", "module": "dashtx.mjs", From 8dd017c6dd0e5ee09f62a9b248389ed1801b5b28 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 27 Aug 2024 03:25:36 -0600 Subject: [PATCH 5/5] chore(release): bump to v0.20.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78511d5..a448b58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dashtx", - "version": "0.20.0", + "version": "0.20.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dashtx", - "version": "0.20.0", + "version": "0.20.1", "license": "SEE LICENSE IN LICENSE", "bin": { "dashtx-inspect": "bin/inspect.js" diff --git a/package.json b/package.json index 918a9c3..fa1f82e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashtx", - "version": "0.20.0", + "version": "0.20.1", "description": "Create DASH Transactions with Vanilla JS (0 deps, cross-platform)", "main": "dashtx.js", "module": "dashtx.mjs",