PageSourceSearch

https://mangofarmassets.com/lib/js/blockchain/transaction.js

js mangofarmassets.com collected 2026-10-09 02:54:23 UTC 18,993 bytes, 556 lines download raw bytes

1// transaction.js
2import API from "./api.js";
3
4class Transaction {
5  constructor(token, wallet) {
6    this.token = token;
7    this.wallet = wallet;
8    this.inputs = [];
9    this.outputs = {};
10    this.privkeys = [];
11    this.totals = {};
12    this.threshold = {};
13    this.fee = 0.01; // legacy default; NOT used in fee finalization anymore
14    this.change_i = 0;
15    this.change_path = 1;
16    this.used_change_addresses = [];
17    this.api = new API(this.wallet.mode);
18
19    // ---- New fee-related state ----
20    this.FEE_DUST_RVN = 0.00001;  // conservative dust threshold
21    this.FEE_MAX_RVN = 0.5;       // sanity cap for fees
22    this.fee_rate_kvb = 0.01015194; // default feerate (RVN/kvB) — overwritten by stub
23    this.rvn_change_addr = null;  // track a single RVN change output address
24    this.FEE_BOOST = 1.25;
25
26
27    this.fee_address = {
28      issue: {
29        test: { n1issueAssetXXXXXXXXXXXXXXXXWdnemQ: 500 },
30        main: { RXissueAssetXXXXXXXXXXXXXXXXXhhZGt: 500 },
31      },
32      reissue: {
33        test: { n1ReissueAssetXXXXXXXXXXXXXXWG9NLd: 100 },
34        main: { RXReissueAssetXXXXXXXXXXXXXXVEFAWu: 100 },
35      },
36      issue_unique: {
37        test: { n1issueUniqueAssetXXXXXXXXXXS4695i: 5 },
38        main: { RXissueUniqueAssetXXXXXXXXXXWEAe58: 5 },
39      },
40      issuesub: {
41        test: { n1issueSubAssetXXXXXXXXXXXXXbNiH6v: 100 },
42        main: { RXissueSubAssetXXXXXXXXXXXXXWcwhwL: 100 },
43      },
44    };
45
46    this.used_inputs = {};
47    this.fetch_used();
48  }
49
50  // =======================
51  // Event & cache helpers
52  // =======================
53
54  dispatch_event = async (event_name, detail) => {
55    window.dispatchEvent(new CustomEvent(event_name, { detail }));
56  };
57
58  fetch_used = async () => {
59    const used_inputs = await this.api.cache.get("used_inputs", "json");
60    this.used_inputs = { ...this.used_inputs, ...used_inputs };
61  };
62
63  mark_used = async (utxo) => {
64    const utxo_id = await this.utxo_id(utxo);
65    this.used_inputs[utxo_id] = utxo;
66    this.api.cache.set("used_inputs", this.used_inputs);
67  };
68
69  utxo_id = async (utxo) => {
70    const utxo_id = await this.api.crypto.hash(JSON.stringify(utxo));
71    return utxo_id;
72  };
73
74  // =======================
75  // PREP FLOWS
76  // =======================
77
78  prep = {
79    send: async (send_data) => {
80      console.log("sending prep", send_data);
81      this.dispatch_event("transaction:step", {
82        step: "initializing",
83        progress: 5,
84        message: "Setting up transaction...",
85        transactionType: "send",
86      });
87
88      const assets = Object.keys(send_data);
89      let asset_index = 0;
90      const total_assets = assets.length + (!assets.includes("RVN") ? 1 : 0);
91
92      this.dispatch_event("transaction:step", {
93        step: "selecting_utxos",
94        progress: 10,
95        message: "Finding available coins...",
96        transactionType: "send",
97      });
98
99      // Build thresholds EXCLUDING miner fee; we'll finalize fees after change
100      for (const asset in send_data) {
101        this.threshold[asset] = 0;
102        for (const output in send_data[asset]) {
103          if (asset === "RVN") {
104            const amount = parseFloat(send_data[asset][output]);
105            this.threshold[asset] += amount;
106            this.outputs[output] = amount;
107          } else {
108            const asset_amount = parseFloat(send_data[asset][output]);
109            this.threshold[asset] += asset_amount;
110            this.outputs[output] = { transfer: { [asset]: asset_amount } };
111          }
112        }
113
114        await this.utxo_select(asset);
115        asset_index++;
116        const utxo_progress = 10 + (asset_index / total_assets) * 10;
117        this.dispatch_event("transaction:step", {
118          step: "selecting_utxos",
119          progress: Math.min(utxo_progress, 25),
120          message: `Finding ${asset} coins...`,
121          assetContext: asset,
122          transactionType: "send",
123        });
124      }
125
126      // If we aren't sending RVN but still need to pay fees later, ensure RVN inputs exist
127      if (!assets.includes("RVN")) {
128        this.threshold.RVN = 0; // no explicit RVN outputs in this send
129        await this.utxo_select("RVN");
130        this.dispatch_event("transaction:step", {
131          step: "selecting_utxos",
132          progress: 25,
133          message: "Finding RVN coins for fees...",
134          assetContext: "RVN",
135          transactionType: "send",
136        });
137      }
138
139      this.dispatch_event("transaction:step", {
140        step: "calculating_change",
141        progress: 25,
142        message: "Computing change outputs...",
143        transactionType: "send",
144      });
145
146      // Compute per-asset change (provisional, without fee)
147      asset_index = 0;
148      for (const asset in send_data) {
149        await this.calc_change(asset);
150        asset_index++;
151        const change_progress = 25 + (asset_index / total_assets) * 7;
152        this.dispatch_event("transaction:step", {
153          step: "calculating_change",
154          progress: Math.min(change_progress, 32),
155          message: `Computing ${asset} change...`,
156          assetContext: asset,
157          transactionType: "send",
158        });
159      }
160
161      if (!assets.includes("RVN")) {
162        await this.calc_change("RVN"); // provisional
163        this.dispatch_event("transaction:step", {
164          step: "calculating_change",
165          progress: 35,
166          message: "Computing RVN change...",
167          assetContext: "RVN",
168          transactionType: "send",
169        });
170      }
171
172      // ==== Finalize fees iteratively (measure → adjust) ====
173      this.dispatch_event("transaction:step", {
174        step: "fees",
175        progress: 40,
176        message: "Finalizing network fees...",
177        transactionType: "send",
178      });
179      await this.finalize_fees();
180
181      this.dispatch_event("transaction:step", {
182        step: "preparing",
183        progress: 50,
184        message: "Transaction prepared successfully",
185        transactionType: "send",
186      });
187      console.log(this.inputs, this.outputs);
188      return { inputs: this.inputs, outputs: this.outputs };
189    },
190
191    issue: async (issue_obj, admin_token = null) => {
192      console.log("issue tx", issue_obj, admin_token);
193      this.dispatch_event("transaction:step", {
194        step: "initializing",
195        progress: 5,
196        message: "Setting up asset creation...",
197        transactionType: "mint",
198      });
199
200      let issue_type = "issue";
201      if (admin_token) {
202        switch (Object.keys(issue_obj)[0]) {
203          case "reissue":
204            issue_type = "reissue";
205            break;
206          case "issue":
207            issue_type = "issuesub";
208            break;
209        }
210      } else {
211        switch (Object.keys(issue_obj)[0]) {
212          case "issue_unique":
213            issue_type = "issue_unique";
214            this.dispatch_event("transaction:step", {
215              step: "selecting_utxos",
216              progress: 10,
217              message: "Finding admin token coins...",
218              transactionType: "mint",
219            });
220            const base_asset = issue_obj.issue_unique.root_name + "!";
221            this.threshold[base_asset] = 0;
222            await this.utxo_select(base_asset);
223            this.dispatch_event("transaction:step", {
224              step: "selecting_utxos",
225              progress: 15,
226              message: "Admin token coins selected",
227              assetContext: base_asset,
228              transactionType: "mint",
229            });
230            break;
231        }
232      }
233
234      // Protocol fee output (explicit RVN output to fee address)
235      this.outputs = Object.assign({}, this.fee_address[issue_type][this.wallet.mode]);
236
237      // Threshold includes only explicit outputs (NOT miner fee yet)
238      const explicit_fee_rvn = parseFloat(
239        Object.values(this.fee_address[issue_type][this.wallet.mode])
240      );
241      this.threshold.RVN = explicit_fee_rvn;
242
243      // Get an address for receiving the asset (unchanged logic)
244      const unused_marker = sessionStorage.getItem(`unused_marker_0`);
245      const start_i = unused_marker ? parseInt(unused_marker) : this.change_i;
246      const issue_fetch = await this.wallet.address.fetch_empty(start_i, this.change_path);
247      const issue_address = issue_fetch.address;
248      this.change_i = parseInt(issue_fetch.i) + 1;
249
250      // Select RVN for explicit outputs; miner fee will be handled later
251      this.dispatch_event("transaction:step", {
252        step: "selecting_utxos",
253        progress: 20,
254        message: "Finding RVN coins...",
255        assetContext: "RVN",
256        transactionType: "mint",
257      });
258      await this.utxo_select("RVN");
259
260      this.dispatch_event("transaction:step", {
261        step: "calculating_change",
262        progress: 25,
263        message: "Computing RVN change...",
264        assetContext: "RVN",
265        transactionType: "mint",
266      });
267      await this.calc_change("RVN");
268
269      if (admin_token) {
270        const admin_token_name =
271          typeof admin_token === "string" ? admin_token : admin_token.name;
272        this.dispatch_event("transaction:step", {
273          step: "selecting_utxos",
274          progress: 30,
275          message: "Finding admin token coins...",
276          assetContext: admin_token_name,
277          transactionType: "mint",
278        });
279        this.threshold[admin_token_name] = 0;
280        await this.utxo_select(admin_token_name);
281        this.dispatch_event("transaction:step", {
282          step: "calculating_change",
283          progress: 35,
284          message: "Computing admin token change...",
285          assetContext: admin_token_name,
286          transactionType: "mint",
287        });
288        if (issue_type !== "reissue") await this.calc_change(admin_token_name);
289      }
290
291      // The actual asset issuance payload
292      this.outputs[issue_address] = { ...issue_obj };
293
294      // ==== Finalize fees iteratively (measure → adjust) ====
295      this.dispatch_event("transaction:step", {
296        step: "fees",
297        progress: 40,
298        message: "Finalizing network fees...",
299        transactionType: "mint",
300      });
301      await this.finalize_fees();
302
303      this.dispatch_event("transaction:step", {
304        step: "preparing",
305        progress: 50,
306        message: "Asset creation prepared successfully",
307        transactionType: "mint",
308      });
309      console.log(this.inputs, this.outputs);
310      return { inputs: this.inputs, outputs: this.outputs };
311    },
312  };
313
314  // =======================
315  // UTXO selection & change
316  // =======================
317
318  utxo_select = async (asset) => {
319    if (!this.wallet.balance[asset]) throw new Error(`Asset ${asset} not found`);
320    const utxos = await this.wallet.balance[asset].utxos;
321    let total_amount = 0;
322    for (const utxo of utxos) {
323      const utxo_id = await this.utxo_id(utxo);
324      if (!this.used_inputs[utxo_id]) {
325        const utxo_amount = utxo.satoshis / 1e8;
326        total_amount += utxo_amount;
327        utxo.vout = utxo.vout ?? utxo.outputIndex;
328        this.inputs.push(utxo);
329        if (total_amount >= this.threshold[asset]) break;
330      }
331    }
332    if (total_amount < this.threshold[asset]) throw new Error(`Insufficient ${asset}`);
333    this.totals[asset] = total_amount;
334  };
335
336  calc_change = async (asset) => {
337    const change_amount = parseFloat(
338      (this.totals[asset] - this.threshold[asset]).toFixed(8)
339    );
340    if (change_amount > 0) {
341      const session_marker = `unused_marker_${this.change_path}`;
342      const unused_marker = parseInt(sessionStorage.getItem(session_marker));
343      const start_i = !unused_marker || this.change_i > 0 ? this.change_i : unused_marker;
344      const change_fetch = await this.wallet.address.fetch_empty(start_i, this.change_path);
345      this.change_i = parseInt(change_fetch.i) + 1;
346
347      if (asset === "RVN") {
348        this.rvn_change_addr = change_fetch.address; // track RVN change
349        this.outputs[this.rvn_change_addr] = change_amount;
350      } else {
351        this.outputs[change_fetch.address] = { transfer: { [asset]: change_amount } };
352      }
353    }
354  };
355
356  // =======================
357  // FEE / SIZE HELPERS
358  // =======================
359
360  // Stub feerate provider — do NOT call the node here.
361  // Uses the provided estimatesmartfee value for 6 blocks: 0.01015194 RVN/kvB
362  get_feerate_kvb = async () => {
363    this.fee_rate_kvb = 0.01015194;
364    return this.fee_rate_kvb;
365  };
366
367  sum_outputs_rvn = (exclude_addr = null) => {
368    let total = 0;
369    for (const addr in this.outputs) {
370      if (exclude_addr && addr === exclude_addr) continue;
371      const v = this.outputs[addr];
372      if (typeof v === "number") total += v;
373    }
374    return parseFloat(total.toFixed(8));
375  };
376
377  set_rvn_change = (addr, amount) => {
378    if (amount <= 0) {
379      if (addr && this.outputs[addr] && typeof this.outputs[addr] === "number") {
380        delete this.outputs[addr];
381      }
382      return;
383    }
384    this.outputs[addr] = parseFloat(amount.toFixed(8));
385  };
386
387  ensure_rvn_change_address = async () => {
388    if (this.rvn_change_addr && this.outputs[this.rvn_change_addr] !== undefined) {
389      return this.rvn_change_addr;
390    }
391    const session_marker = `unused_marker_${this.change_path}`;
392    const unused_marker = parseInt(sessionStorage.getItem(session_marker));
393    const start_i = !unused_marker || this.change_i > 0 ? this.change_i : unused_marker;
394    const change_fetch = await this.wallet.address.fetch_empty(start_i, this.change_path);
395    this.change_i = parseInt(change_fetch.i) + 1;
396    this.rvn_change_addr = change_fetch.address;
397    return this.rvn_change_addr;
398  };
399
400  // Build provisional raw tx to get its size in bytes (vsize == bytes on RVN)
401  estimate_vsize_bytes = async () => {
402    const tmp = await this.api.transaction.generate(this.inputs, this.outputs);
403    const hex = tmp.raw_hex || "";
404    return Math.ceil(hex.length / 2);
405  };
406
407  // Try to add one more RVN UTXO when fee is short
408  add_one_more_rvn_input = async () => {
409    if (!this.wallet.balance.RVN) throw new Error("RVN balance not found");
410    const utxos = await this.wallet.balance.RVN.utxos;
411    for (const utxo of utxos) {
412      const utxo_id = await this.utxo_id(utxo);
413      const already_included = this.inputs.find(
414        (i) =>
415          i.txid === utxo.txid &&
416          (i.vout ?? i.outputIndex) === (utxo.vout ?? utxo.outputIndex)
417      );
418      if (!this.used_inputs[utxo_id] && !already_included) {
419        utxo.vout = utxo.vout ?? utxo.outputIndex;
420        this.inputs.push(utxo);
421        const add_amt = utxo.satoshis / 1e8;
422        this.totals.RVN = (this.totals.RVN || 0) + add_amt;
423        return true;
424      }
425    }
426    return false;
427  };
428
429  // Iteratively adjust change/inputs until fee converges with structure
430  finalize_fees = async () => {
431    await this.get_feerate_kvb();
432    if (typeof this.totals.RVN !== "number") this.totals.RVN = 0;
433
434    const change_addr = await this.ensure_rvn_change_address();
435
436    let iterations = 0;
437    let structure_changed = true;
438
439    while (iterations < 10 && structure_changed) {
440      iterations += 1;
441      structure_changed = false;
442
443      // 1) Measure provisional size with current structure
444      const vsize = await this.estimate_vsize_bytes();
445
446      const base_fee_rvn = Math.ceil(vsize / 1000) * this.fee_rate_kvb;
447      const boosted_fee_rvn = base_fee_rvn * this.FEE_BOOST;
448
449      const fee_rvn = Math.min(
450        parseFloat(boosted_fee_rvn.toFixed(8)),
451        this.FEE_MAX_RVN
452      );
453
454      // 2) Available for RVN change after paying explicit RVN outputs + fee
455      const explicit_rvn_out = this.sum_outputs_rvn(change_addr);
456      const available_for_change = parseFloat(
457        (this.totals.RVN - explicit_rvn_out - fee_rvn).toFixed(8)
458      );
459
460      if (available_for_change >= this.FEE_DUST_RVN) {
461        // keep/change output
462        const existed = !!this.outputs[change_addr];
463        this.set_rvn_change(change_addr, available_for_change);
464        if (!existed) structure_changed = true; // creating the change line adds an output
465      } else if (available_for_change > 0 && available_for_change < this.FEE_DUST_RVN) {
466        // too small → drop change (structure change)
467        if (this.outputs[change_addr]) {
468          this.set_rvn_change(change_addr, 0);
469          structure_changed = true;
470        }
471        // fee will effectively absorb that dust on next loop by smaller vsize
472      } else if (available_for_change < 0) {
473        // not enough to pay fee → add one more RVN input
474        const added = await this.add_one_more_rvn_input();
475        if (!added) throw new Error("Insufficient RVN to pay fee");
476        structure_changed = true;
477      } else {
478        // exactly zero change → ensure no dangling change output
479        if (this.outputs[change_addr]) {
480          this.set_rvn_change(change_addr, 0);
481          structure_changed = true;
482        }
483      }
484    }
485
486    if (iterations >= 10) {
487      throw new Error("Fee finalization did not converge");
488    }
489  };
490
491  // =======================
492  // Build / sign / broadcast
493  // =======================
494
495  generate = async () => {
496    this.dispatch_event("transaction:step", {
497      step: "generating",
498      progress: 50,
499      message: "Building raw transaction...",
500    });
501    const api_result = await this.api.transaction.generate(this.inputs, this.outputs);
502    this.raw_hex = api_result.raw_hex;
503    this.raw_data = api_result.raw_data;
504    this.dispatch_event("transaction:step", {
505      step: "generating",
506      progress: 70,
507      message: "Raw transaction built successfully",
508    });
509  };
510
511  sign = async () => {
512    this.dispatch_event("transaction:step", {
513      step: "signing",
514      progress: 70,
515      message: "Signing transaction...",
516    });
517    for (const input of this.inputs) {
518      const privkey = await this.wallet.privkey(input.derivation, input.index);
519      this.privkeys.push(privkey);
520    }
521    var tx = new coinjs.transaction().deserialize(this.raw_hex);
522    this.signed_hex = tx.signAsset(this.privkeys, this.inputs);
523    this.dispatch_event("transaction:step", {
524      step: "signing",
525      progress: 85,
526      message: "Transaction signed successfully",
527    });
528    return this.signed_hex;
529  };
530
531  broadcast = async () => {
532    if (!this.signed_hex) throw new Error("signed hex not generated");
533    this.dispatch_event("transaction:step", {
534      step: "broadcasting",
535      progress: 85,
536      message: "Broadcasting to network...",
537    });
538    const broadcast_result = await this.api.transaction.broadcast(this.signed_hex);
539    if (!broadcast_result.txid.status) {
540      this.dispatch_event("transaction:step", {
541        step: "complete",
542        progress: 100,
543        message: "Transaction complete!",
544      });
545      return { txid: broadcast_result.txid, status: "success" };
546    } else {
547      this.dispatch_event("transaction:error", {
548        error: "Broadcast failed: " + JSON.stringify(broadcast_result),
549        step: "broadcasting",
550      });
551      return { error: JSON.stringify(broadcast_result), status: "failed" };
552    }
553  };
554}
555
556export default Transaction;

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.