From cb3b09095d98fc757f2d4c4133926fa9b09fe220 Mon Sep 17 00:00:00 2001 From: AJ ONeal <aj@therootcompany.com> Date: Sun, 14 Apr 2024 00:11:50 -0600 Subject: [PATCH 1/6] feat: private export of "script" _*_HEX constants --- dashtx.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dashtx.js b/dashtx.js index 56fba18..b8bed76 100644 --- a/dashtx.js +++ b/dashtx.js @@ -101,6 +101,19 @@ var DashTx = ("object" === typeof module && exports) || {}; const PKH_SIZE = (20).toString(16); // 0x14 const PKH_SCRIPT_SIZE = (25).toString(16); // 0x19 + //@ts-ignore - for debug only + Tx._OP_DUP_HEX = OP_DUP; + //@ts-ignore - for debug only + Tx._OP_HASH160_HEX = OP_HASH160; + //@ts-ignore - for debug only + Tx._OP_EQUALVERIFY_HEX = OP_EQUALVERIFY; + //@ts-ignore - for debug only + Tx._OP_CHECKSIG_HEX = OP_CHECKSIG; + //@ts-ignore - for debug only + Tx._PKH_SIZE_HEX = PKH_SIZE; + //@ts-ignore - for debug only + Tx._PKH_SCRIPT_SIZE_HEX = PKH_SCRIPT_SIZE; + const E_LITTLE_INT = "JavaScript 'Number's only go up to uint53, you must use 'BigInt' (ex: `let amount = 18014398509481984n`) for larger values"; const E_TOO_BIG_INT = From 2ab4ea98ca3276e85409e9158ed47785669b5a0d Mon Sep 17 00:00:00 2001 From: AJ ONeal <aj@therootcompany.com> Date: Wed, 17 Apr 2024 12:37:38 -0600 Subject: [PATCH 2/6] feat: private export of Tx.utils._toUint64LE for monkey-patching --- dashtx.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dashtx.js b/dashtx.js index b8bed76..8223df2 100644 --- a/dashtx.js +++ b/dashtx.js @@ -1236,7 +1236,7 @@ var DashTx = ("object" === typeof module && exports) || {}; if (!output.satoshis) { throw new Error(`every output must have 'satoshis'`); } - let satoshis = toUint64LE(output.satoshis); + let satoshis = TxUtils._toUint64LE(output.satoshis); outputHex.push(satoshis); if (!output.pubKeyHash) { @@ -1276,7 +1276,7 @@ var DashTx = ("object" === typeof module && exports) || {}; */ Tx._createMemoScript = function (memoHex, i = 0) { let outputHex = []; - let satoshis = toUint64LE(0); + let satoshis = TxUtils._toUint64LE(0); outputHex.push(satoshis); assertHex(memoHex, `output[${i}].memo`); @@ -1518,7 +1518,7 @@ var DashTx = ("object" === typeof module && exports) || {}; //@ts-ignore if (n <= MAX_U53) { - return "ff" + toUint64LE(n); + return "ff" + TxUtils._toUint64LE(n); } if ("bigint" !== typeof n) { @@ -1528,7 +1528,7 @@ var DashTx = ("object" === typeof module && exports) || {}; } if (n <= MAX_U64) { - return "ff" + toUint64LE(n); + return "ff" + TxUtils._toUint64LE(n); } let err = new Error(E_TOO_BIG_INT); @@ -1558,7 +1558,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 */ - function toUint64LE(n) { + TxUtils._toUint64LE = function (n) { let bn; if ("bigint" === typeof n) { bn = n; @@ -1582,7 +1582,7 @@ var DashTx = ("object" === typeof module && exports) || {}; let hex = hexArr.join(""); return hex; - } + }; /** @type TxToVarIntSize */ TxUtils.toVarIntSize = function (n) { From abb15a5b7b575a70747411ee188d5ee5365bfa23 Mon Sep 17 00:00:00 2001 From: AJ ONeal <aj@therootcompany.com> Date: Wed, 17 Apr 2024 12:55:18 -0600 Subject: [PATCH 3/6] feat: private export of Tx.utils._toUint32LE for monkey-patching --- dashtx.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dashtx.js b/dashtx.js index 8223df2..e095c16 100644 --- a/dashtx.js +++ b/dashtx.js @@ -1105,7 +1105,7 @@ var DashTx = ("object" === typeof module && exports) || {}; /** @type Array<String> */ let tx = []; - let v = toUint32LE(version); + let v = TxUtils._toUint32LE(version); tx.push(v); // txMap.version = v; @@ -1140,7 +1140,7 @@ var DashTx = ("object" === typeof module && exports) || {}; "expected utxo property 'outputIndex' to be an integer representing this input's previous output index", ); } - let reverseVout = toUint32LE(voutIndex); + let reverseVout = TxUtils._toUint32LE(voutIndex); inputHex.push(reverseVout); //@ts-ignore - enum types not handled properly here @@ -1260,7 +1260,7 @@ var DashTx = ("object" === typeof module && exports) || {}; tx.push(txOut); } - let locktimeHex = toUint32LE(locktime); + let locktimeHex = TxUtils._toUint32LE(locktime); tx.push(locktimeHex); // txMap.locktime = locktimeHex; @@ -1359,7 +1359,7 @@ var DashTx = ("object" === typeof module && exports) || {}; Tx.hashPartial = async function (txHex, sigHashType = Tx.SIGHASH_ALL) { let txSignable = txHex; if (sigHashType) { - let sigHashTypeHex = toUint32LE(sigHashType); + let sigHashTypeHex = TxUtils._toUint32LE(sigHashType); txSignable = `${txSignable}${sigHashTypeHex}`; } //console.log("Signable Tx Hex"); @@ -1508,12 +1508,12 @@ var DashTx = ("object" === typeof module && exports) || {}; //@ts-ignore if (n <= MAX_U16) { - return "fd" + toUint32LE(n).slice(0, 4); + return "fd" + TxUtils._toUint32LE(n).slice(0, 4); } //@ts-ignore if (n <= MAX_U32) { - return "fe" + toUint32LE(n); + return "fe" + TxUtils._toUint32LE(n); } //@ts-ignore @@ -1541,7 +1541,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 */ - function toUint32LE(n) { + TxUtils._toUint32LE = function (n) { // make sure n is uint32/int53, not int32 //n = n >>> 0; @@ -1550,7 +1550,7 @@ var DashTx = ("object" === typeof module && exports) || {}; let hexLE = Tx.utils.reverseHex(hex); return hexLE; - } + }; /** * This can handle Big-Endian CPUs, which don't exist, From e4f7e0caffa8fb69f7fe5c5b2f31ca053169b3bf Mon Sep 17 00:00:00 2001 From: AJ ONeal <aj@therootcompany.com> Date: Wed, 17 Apr 2024 13:36:22 -0600 Subject: [PATCH 4/6] ref: move .createDonationOutput(), ._packInputs() and ._packOutputs() --- README.md | 2 + dashtx.js | 120 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 15efa3e..d71f77d 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,8 @@ Tx.create({ sign, getPrivateKey }); tx.legacy.draftSingleOutput({ utxos, inputs, output }); tx.legacy.finalizePresorted(txDraft, keys); +Tx.createDonationOutput(); + Tx.appraise({ inputs, outputs }); Tx.getId(txHex); diff --git a/dashtx.js b/dashtx.js index e095c16..0629047 100644 --- a/dashtx.js +++ b/dashtx.js @@ -15,6 +15,7 @@ * @prop {TxToDash} toDash * @prop {TxToSats} toSats * @prop {TxCreate} create + * @prop {TxCreateDonationOutput} createDonationOutput * @prop {TxCreateRaw} createRaw * @prop {TxCreateHashable} createHashable * @prop {TxCreateSigned} createSigned @@ -35,6 +36,8 @@ * @prop {Function} _hashAndSignAll * @prop {Function} _legacyMustSelectInputs * @prop {Function} _legacySelectOptimalUtxos + * @prop {Function} _packInputs + * @prop {Function} _packOutputs */ /** @@ -118,6 +121,7 @@ var DashTx = ("object" === typeof module && exports) || {}; "JavaScript 'Number's only go up to uint53, you must use 'BigInt' (ex: `let amount = 18014398509481984n`) for larger values"; const E_TOO_BIG_INT = "JavaScript 'BigInt's are arbitrarily large, but you may only use up to UINT64 for transactions"; + const E_NO_OUTPUTS = `'outputs' list must not be empty - use the developer debug option '_DANGER_donate: true' to bypass`; Tx.SATOSHIS = SATOSHIS; Tx.LEGACY_DUST = 2000; @@ -190,6 +194,11 @@ var DashTx = ("object" === typeof module && exports) || {}; let size = 0; for (let output of outputs) { + if (output.message) { + if (!output.memo) { + output.memo = TxUtils.strToHex(output.message); + } + } if (output.memo) { let memoSize = output.memo.length / 2; if (memoSize > MAX_U8) { @@ -1096,27 +1105,45 @@ var DashTx = ("object" === typeof module && exports) || {}; //@ts-ignore _donation_memo, }) { - let sep = ""; + let _sep = ""; if (_debug) { - sep = "\n"; + _sep = "\n"; } - // let txMap = {}; - /** @type Array<String> */ let tx = []; let v = TxUtils._toUint32LE(version); tx.push(v); - // txMap.version = v; + void Tx._packInputs({ tx, inputs, _sep }); + + if (_DANGER_donate === true) { + let output = Tx.createDonationOutput({ memo: _donation_memo }); + outputs.push(output); + } + void Tx._packOutputs({ tx, outputs, _sep }); + + let locktimeHex = TxUtils._toUint32LE(locktime); + tx.push(locktimeHex); + + let txHex = tx.join(_sep); + return txHex; + }; + + /** + * Privately exported for use by DashJoin.js + * @param {Object} opts + * @param {Array<String>} opts.tx + * @param {Array<TxInputRaw|TxInputHashable|TxInputSigned>} opts.inputs + * @param {String} [opts._sep] - string to join hex segements ('' or '\n') + * @returns {Array<String>} - tx (original is modified if provided) + */ + Tx._packInputs = function ({ tx = [], inputs, _sep = "" }) { let nInputs = Tx.utils.toVarInt(inputs.length); tx.push(nInputs); - // txMap.input_count = nInputs; - // txMap.inputs = []; for (let input of inputs) { let inputHex = []; - // txMap.inputs.push(inputHex); let txId = input.txId; if (!txId) { @@ -1178,41 +1205,37 @@ var DashTx = ("object" === typeof module && exports) || {}; let sequence = "ffffffff"; inputHex.push(sequence); - let txIn = inputHex.join(sep); + let txIn = inputHex.join(_sep); tx.push(txIn); } - if (!outputs.length) { - if (!_DANGER_donate) { - throw new Error( - `'outputs' list must not be empty - use the developer debug option '_DANGER_donate: true' to bypass`, - ); - } - - let memo = _donation_memo; - if (!memo) { - let encoder = new TextEncoder(); - let gifts = ["💸", "🎁", "🧧"]; - let indexIsh = Math.random() * 3; - let index = Math.floor(indexIsh); - let gift = encoder.encode(gifts[index]); - memo = TxUtils.bytesToHex(gift); - } + return tx; + }; - outputs.push({ - satoshis: 0, - memo: memo, - }); + /** + * Privately exported for use by DashJoin.js + * @param {Object} opts + * @param {Array<String>} opts.tx + * @param {Array<TxOutput>} opts.outputs + * @param {String} [opts._sep] - string to join hex segements ('' or '\n') + * @returns {Array<String>} - tx (original is modified if provided) + */ + Tx._packOutputs = function ({ tx = [], outputs, _sep = "" }) { + if (!outputs.length) { + throw new Error(E_NO_OUTPUTS); } let nOutputs = Tx.utils.toVarInt(outputs.length); tx.push(nOutputs); - // txMap.output_count = nOutputs; - // txMap.outputs = []; for (let i = 0; i < outputs.length; i += 1) { let output = outputs[i]; + if (output.message) { + if (!output.memo) { + output.memo = TxUtils.strToHex(output.message); + } + } if (output.memo) { let invalid = output.satoshis || output.address || output.pubKeyHash; if (invalid) { @@ -1222,16 +1245,14 @@ var DashTx = ("object" === typeof module && exports) || {}; } let outputHex = Tx._createMemoScript(output.memo, i); - // txMap.outputs.push(outputHex); - let txOut = outputHex.join(sep); + let txOut = outputHex.join(_sep); tx.push(txOut); continue; } /** @type {Array<String>} */ let outputHex = []; - // txMap.outputs.push(outputHex); if (!output.satoshis) { throw new Error(`every output must have 'satoshis'`); @@ -1256,18 +1277,25 @@ var DashTx = ("object" === typeof module && exports) || {}; outputHex.push(output.pubKeyHash); outputHex.push(`${OP_EQUALVERIFY}${OP_CHECKSIG}`); - let txOut = outputHex.join(sep); + let txOut = outputHex.join(_sep); tx.push(txOut); } - let locktimeHex = TxUtils._toUint32LE(locktime); - tx.push(locktimeHex); - // txMap.locktime = locktimeHex; + return tx; + }; - // console.log("DEBUG txMap", txMap); + Tx.createDonationOutput = function (opts) { + let satoshis = 0; + let message = opts?.message; + if (!message) { + let gifts = ["💸", "🎁", "🧧"]; + let indexIsh = Math.random() * 3; + let index = Math.floor(indexIsh); + message = gifts[index]; + } - let txHex = tx.join(sep); - return txHex; + let output = { satoshis, message }; + return output; }; /** @@ -1779,6 +1807,7 @@ if ("object" === typeof module) { /** * @typedef TxOutput * @prop {String} [memo] - hex bytes of a memo (incompatible with pubKeyHash / address) + * @prop {String} [message] - memo, but as a UTF-8 string * @prop {String} [address] - payAddr as Base58Check (human-friendly) * @prop {String} [pubKeyHash] - payAddr's raw hex value (decoded, not Base58Check) * @prop {Uint53} satoshis - the number of smallest units of the currency @@ -1831,6 +1860,13 @@ if ("object" === typeof module) { * @returns {Uint32} */ +/** + * Create a donation output with a nice message. + * @callback TxCreateDonationOutput + * @param {Object} [opts] + * @param {String} [opts.message] - UTF-8 Memo String + */ + /** * @callback TxCreateHashable * @param {TxInfo} txInfo @@ -1986,5 +2022,5 @@ if ("object" === typeof module) { /** * @callback TxStringToHex * @param {String} utf8 - * @returns {String} + * @returns {String} - encoded bytes as hex */ From cb2cfbe3b47d0010899008393d9e8d1bbe5d134e Mon Sep 17 00:00:00 2001 From: AJ ONeal <aj@therootcompany.com> Date: Wed, 17 Apr 2024 13:40:01 -0600 Subject: [PATCH 5/6] ref!: remove debug option for donating / burning inputs --- dashtx.js | 15 +-------------- tests/memo.js | 13 +++---------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/dashtx.js b/dashtx.js index 0629047..2594705 100644 --- a/dashtx.js +++ b/dashtx.js @@ -121,7 +121,7 @@ var DashTx = ("object" === typeof module && exports) || {}; "JavaScript 'Number's only go up to uint53, you must use 'BigInt' (ex: `let amount = 18014398509481984n`) for larger values"; const E_TOO_BIG_INT = "JavaScript 'BigInt's are arbitrarily large, but you may only use up to UINT64 for transactions"; - const E_NO_OUTPUTS = `'outputs' list must not be empty - use the developer debug option '_DANGER_donate: true' to bypass`; + const E_NO_OUTPUTS = `'outputs' list must not be empty; use .createDonationOutput({ message }) to burn inputs`; Tx.SATOSHIS = SATOSHIS; Tx.LEGACY_DUST = 2000; @@ -937,10 +937,6 @@ var DashTx = ("object" === typeof module && exports) || {}; inputs: [], outputs: txInfo.outputs, version: txInfo.version || CURRENT_VERSION, - //@ts-ignore - debug only - _DANGER_donate: txInfo._DANGER_donate, - //@ts-ignore - _donation_memo: txInfo._donation_memo, }; // temp shim @@ -1100,10 +1096,6 @@ var DashTx = ("object" === typeof module && exports) || {}; outputs, /* maxFee = 10000, */ _debug = false, - //@ts-ignore - debug only - _DANGER_donate = false, - //@ts-ignore - _donation_memo, }) { let _sep = ""; if (_debug) { @@ -1116,11 +1108,6 @@ var DashTx = ("object" === typeof module && exports) || {}; tx.push(v); void Tx._packInputs({ tx, inputs, _sep }); - - if (_DANGER_donate === true) { - let output = Tx.createDonationOutput({ memo: _donation_memo }); - outputs.push(output); - } void Tx._packOutputs({ tx, outputs, _sep }); let locktimeHex = TxUtils._toUint32LE(locktime); diff --git a/tests/memo.js b/tests/memo.js index 5f6c979..ad8aa09 100644 --- a/tests/memo.js +++ b/tests/memo.js @@ -114,20 +114,13 @@ Zora.test("can create donation tx via memo", async function (t) { { pubKeyHash: pkh, satoshis: 20000, txId: txId, outputIndex: 0 }, ]; - /** @type {Array<DashTx.TxOutput>} */ - let outputs = []; - - let encoder = new TextEncoder(); - //let memoBytes = encoder.encode("💸"); - //let memoBytes = encoder.encode("🎁"); - let memoBytes = encoder.encode("🧧"); - let memo = DashTx.utils.bytesToHex(memoBytes); + //let donationOutput = Tx.createDonationOutput(); + let memoOutput = { satoshis: 0, message: "🧧" }; + let outputs = [memoOutput]; let txInfo = { inputs: inputs, outputs: outputs, - _DANGER_donate: true, - _donation_memo: memo, }; let privKey = DashTx.utils.hexToBytes(privKeyHex); From 4420cfecfaefa5b03e09fd6797e902250734b506 Mon Sep 17 00:00:00 2001 From: AJ ONeal <aj@therootcompany.com> Date: Fri, 19 Apr 2024 02:02:34 -0600 Subject: [PATCH 6/6] chore(release): bump to v0.14.4 --- 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 258959e..5dc6338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dashtx", - "version": "0.14.3", + "version": "0.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dashtx", - "version": "0.14.3", + "version": "0.14.4", "license": "SEE LICENSE IN LICENSE", "bin": { "dashtx-inspect": "bin/inspect.js" diff --git a/package.json b/package.json index 0b10706..02755cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dashtx", - "version": "0.14.3", + "version": "0.14.4", "description": "Create DASH Transactions with Vanilla JS (0 deps, cross-platform)", "main": "index.js", "files": [