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.