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": [