UNPKG

atomicals-js

Version:
1,030 lines (1,029 loc) 48.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AtomicalOperationBuilder = exports.REQUEST_NAME_TYPE = exports.REALM_CLAIM_TYPE = exports.MAX_SEQUENCE = exports.EXCESSIVE_FEE_LIMIT = exports.OUTPUT_BYTES_BASE = exports.INPUT_BYTES_BASE = exports.BASE_BYTES = exports.DUST_AMOUNT = exports.DEFAULT_SATS_BYTE = void 0; const address_keypair_path_1 = require("./address-keypair-path"); const os = __importStar(require("os")); const atomical_format_helpers_1 = require("./atomical-format-helpers"); const ecc = __importStar(require("tiny-secp256k1")); const ecpair_1 = require("ecpair"); const tinysecp = ecc; const bitcoin = require("bitcoinjs-lib"); const chalk = __importStar(require("chalk")); bitcoin.initEccLib(ecc); const bitcoinjs_lib_1 = require("bitcoinjs-lib"); (0, bitcoinjs_lib_1.initEccLib)(tinysecp); const command_helpers_1 = require("../commands/command-helpers"); const select_funding_utxo_1 = require("./select-funding-utxo"); const utils_1 = require("./utils"); const witness_stack_to_script_witness_1 = require("../commands/witness_stack_to_script_witness"); const worker_threads_1 = require("worker_threads"); const readline = __importStar(require("readline")); const ECPair = (0, ecpair_1.ECPairFactory)(tinysecp); exports.DEFAULT_SATS_BYTE = 10; const DEFAULT_SATS_ATOMICAL_UTXO = 1000; const SEND_RETRY_SLEEP_SECONDS = 15; const SEND_RETRY_ATTEMPTS = 20; exports.DUST_AMOUNT = 546; exports.BASE_BYTES = 10.5; exports.INPUT_BYTES_BASE = 57.5; exports.OUTPUT_BYTES_BASE = 43; exports.EXCESSIVE_FEE_LIMIT = 1000000; // Limit to 1/100 of a BTC for now exports.MAX_SEQUENCE = 0xffffffff; var REALM_CLAIM_TYPE; (function (REALM_CLAIM_TYPE) { REALM_CLAIM_TYPE["DIRECT"] = "direct"; REALM_CLAIM_TYPE["RULE"] = "rule"; })(REALM_CLAIM_TYPE = exports.REALM_CLAIM_TYPE || (exports.REALM_CLAIM_TYPE = {})); function logMiningProgressToConsole(dowork, disableMiningChalk, txid, nonces) { if (!dowork) { return; } if (disableMiningChalk) { if (nonces % 10000 === 0) { console.log("Generated nonces: ", nonces, ", time: ", Math.floor(Date.now() / 1000)); } return; } process.stdout.clearLine(0); process.stdout.cursorTo(0); process.stdout.write(chalk.red(txid, " nonces: ", nonces)); } function printBitworkLog(bitworkInfo, commit) { if (!bitworkInfo) { return; } console.log(`\nAtomicals Bitwork Mining - Requested bitwork proof-of-work mining for the ${commit ? "commit" : "reveal"} transaction.`); if (commit) { console.log(`bitworkc (input): ${bitworkInfo.input_bitwork}`); console.log(`bitworkc (decoded): ${bitworkInfo.hex_bitwork}`); } else { console.log(`bitworkr (input): ${bitworkInfo.input_bitwork}`); console.log(`bitworkr (decoded): ${bitworkInfo.hex_bitwork}`); } console.log(`---------\nWARNING: This might take a very long time depending on the speed of the CPU.`); console.log(`Time to mine estimates: `); console.log(`- prefix length <= 4: about a minute or two. ~65,536 hashes on average.`); console.log(`- prefix length 5: several minutes. ~1,048,576 hashes on average.`); console.log(`- prefix length 6: up to an hour or more. ~16,777,216 hashes on average.`); console.log(`- prefix length >= 7: a few hours or much longer. >268,435,456 hashes on average.`); console.log(`\nStarting mining now...\n`); } var REQUEST_NAME_TYPE; (function (REQUEST_NAME_TYPE) { REQUEST_NAME_TYPE["NONE"] = "NONE"; REQUEST_NAME_TYPE["CONTAINER"] = "CONTAINER"; REQUEST_NAME_TYPE["TICKER"] = "TICKER"; REQUEST_NAME_TYPE["REALM"] = "REALM"; REQUEST_NAME_TYPE["SUBREALM"] = "SUBREALM"; REQUEST_NAME_TYPE["ITEM"] = "ITEM"; })(REQUEST_NAME_TYPE = exports.REQUEST_NAME_TYPE || (exports.REQUEST_NAME_TYPE = {})); class AtomicalOperationBuilder { constructor(options) { this.options = options; this.userDefinedData = null; this.containerMembership = null; this.bitworkInfoCommit = null; this.bitworkInfoReveal = null; this.requestName = null; this.requestParentId = null; this.requestNameType = REQUEST_NAME_TYPE.NONE; this.meta = {}; this.args = {}; this.init = {}; this.ctx = {}; this.parentInputAtomical = null; this.inputUtxos = []; this.additionalOutputs = []; if (!this.options) { throw new Error("Options required"); } if (!this.options.electrumApi) { throw new Error("electrumApi required"); } if (!this.options.satsbyte) { this.options.satsbyte = exports.DEFAULT_SATS_BYTE; } if (this.options.opType === "nft") { if (!this.options.nftOptions) { throw new Error("nftOptions required for nft type"); } } if (this.options.opType === "ft") { if (!this.options.ftOptions) { throw new Error("ftOptions required for ft type"); } } if (this.options.opType === "dft") { if (!this.options.dftOptions) { throw new Error("dftOptions required for dft type"); } } if (this.options.opType === "dmt") { if (!this.options.dmtOptions) { throw new Error("dmtOptions required for dmt type"); } } } setRBF(value) { this.options.rbf = value; } setRequestContainer(name) { if (this.options.opType !== "nft") { throw new Error("setRequestContainer can only be set for NFT types"); } const trimmed = name.startsWith("#") ? name.substring(1) : name; (0, atomical_format_helpers_1.isValidContainerName)(name); this.requestName = trimmed; this.requestNameType = REQUEST_NAME_TYPE.CONTAINER; this.requestParentId = null; } setRequestRealm(name) { if (this.options.opType !== "nft") { throw new Error("setRequestRealm can only be set for NFT types"); } const trimmed = name.startsWith("+") ? name.substring(1) : name; (0, atomical_format_helpers_1.isValidRealmName)(name); this.requestName = trimmed; this.requestNameType = REQUEST_NAME_TYPE.REALM; this.requestParentId = null; } setRequestSubrealm(name, parentRealmId, realmClaimType) { if (this.options.opType !== "nft") { throw new Error("setRequestSubrealm can only be set for NFT types"); } if (!(0, atomical_format_helpers_1.isAtomicalId)(parentRealmId)) { throw new Error("Invalid parent realm atomical id for subrealm"); } if (name.indexOf(".") === -1) { throw new Error("Cannot request subrealm for a top level realm"); } const trimmed = name.startsWith("+") ? name.substring(1) : name; const splitNames = trimmed.split("."); const subrealmFinalPart = splitNames[splitNames.length - 1]; (0, atomical_format_helpers_1.isValidSubRealmName)(subrealmFinalPart); this.requestName = subrealmFinalPart; this.requestParentId = parentRealmId; this.requestNameType = REQUEST_NAME_TYPE.SUBREALM; if (realmClaimType === REALM_CLAIM_TYPE.DIRECT) { this.setArgs({ claim_type: "direct", }); } else if (realmClaimType === REALM_CLAIM_TYPE.RULE) { this.setArgs({ claim_type: "rule", }); } else { throw new Error("RealmClaimType must be DIRECT or RULE"); } } setRequestItem(itemId, parentContainerId) { if (this.options.opType !== "nft") { throw new Error("setRequestItem can only be set for NFT types"); } if (!(0, atomical_format_helpers_1.isAtomicalId)(parentContainerId)) { throw new Error("Invalid parent container atomical id for item"); } this.requestName = itemId; this.requestParentId = parentContainerId; this.requestNameType = REQUEST_NAME_TYPE.ITEM; } setRequestTicker(name) { if (this.options.opType !== "dft" && this.options.opType !== "ft") { throw new Error("setRequestTicker can only be set for dft or ft types"); } const trimmed = name.startsWith("$") ? name.substring(1) : name; (0, atomical_format_helpers_1.isValidTickerName)(trimmed); this.requestName = trimmed; this.requestNameType = REQUEST_NAME_TYPE.TICKER; this.requestParentId = null; } /** * For each array element do: * * - determine if it's a file, or a file with an alias, or a scalar/json object type * * @param fieldTypeHints The type hint string array */ static getDataObjectFromStringTypeHints(fieldTypeHints) { return __awaiter(this, void 0, void 0, function* () { return (0, command_helpers_1.prepareFilesDataAsObject)(fieldTypeHints); }); } setData(data, log = false) { if (!data) { return; } if (typeof data !== "object") { throw new Error("Data must be an object"); } /*if (data['args']) { throw new Error(`Data cannot have field named 'args' set manually. Use setArgs method.`); }*/ if (data["meta"]) { throw new Error(`Data cannot have field named 'meta' set manually. Use options config.`); } if (data["ctx"]) { throw new Error(`Data cannot have field named 'ctx' set manually. Use options config.`); } if (data["init"]) { throw new Error(`Data cannot have field named 'init' set manually. Use options config.`); } this.userDefinedData = data; if (log) { console.log("setData", this.userDefinedData); } } getData() { return this.userDefinedData; } setArgs(args) { this.args = args; } getArgs() { return this.args; } setInit(init) { this.init = init; } getInit() { return this.init; } setMeta(meta) { this.meta = meta; } getMeta() { return this.meta; } setCtx(ctx) { this.ctx = ctx; } getCtx() { return this.ctx; } setContainerMembership(containerName) { if (!containerName) { throw new Error("Empty container name"); } const trimmedContainerName = containerName.startsWith("#") ? containerName.substring(1) : containerName; if (!(0, atomical_format_helpers_1.isValidContainerName)(trimmedContainerName)) { return; } this.containerMembership = trimmedContainerName; } setBitworkCommit(bitworkString) { if (!bitworkString) { return; } this.bitworkInfoCommit = (0, atomical_format_helpers_1.isValidBitworkString)(bitworkString); } setBitworkReveal(bitworkString) { if (!bitworkString) { return; } this.bitworkInfoReveal = (0, atomical_format_helpers_1.isValidBitworkString)(bitworkString); } /** * * @param utxoPartial The UTXO to spend in the constructed tx * @param wif The signing WIF key for the utxo */ addInputUtxo(utxoPartial, wif) { const keypairInput = ECPair.fromWIF(wif); const keypairInputInfo = (0, address_keypair_path_1.getKeypairInfo)(keypairInput); this.inputUtxos.push({ utxo: utxoPartial, keypairInfo: keypairInputInfo, }); } /** * Set an input parent for linking with $parent reference of the operation to an input spend */ setInputParent(input) { // Validate the parentId is an atomical id in compact form if (!(0, atomical_format_helpers_1.isAtomicalId)(input.parentId)) { throw new Error("Invalid parent atomical id: " + input.parentId); } this.parentInputAtomical = input; } getInputParent() { if (!this.parentInputAtomical) { return null; } return this.parentInputAtomical; } /** * Additional output to add, to be used with addInputUtxo normally * @param output Output to add */ addOutput(output) { this.additionalOutputs.push(output); } isEmpty(obj) { return Object.keys(obj).length === 0; } start(fundingWIF) { var _a, _b, _c, _d, _e; return __awaiter(this, void 0, void 0, function* () { const fundingKeypairRaw = ECPair.fromWIF(fundingWIF); const fundingKeypair = (0, address_keypair_path_1.getKeypairInfo)(fundingKeypairRaw); let performBitworkForRevealTx = !!this.bitworkInfoReveal; let performBitworkForCommitTx = !!this.bitworkInfoCommit; let scriptP2TR = null; let hashLockP2TR = null; if (this.options.meta) { this.setMeta(yield AtomicalOperationBuilder.getDataObjectFromStringTypeHints(this.options.meta)); } if (this.options.init) { this.setInit(yield AtomicalOperationBuilder.getDataObjectFromStringTypeHints(this.options.init)); } if (this.options.ctx) { this.setCtx(yield AtomicalOperationBuilder.getDataObjectFromStringTypeHints(this.options.ctx)); } let copiedData = Object.assign({}, this.userDefinedData); // if (!this.isEmpty(this.getArgs())) { copiedData["args"] = this.getArgs(); } if (!this.isEmpty(this.getCtx())) { copiedData["ctx"] = this.getCtx(); } if (!this.isEmpty(this.getMeta())) { copiedData["meta"] = this.getMeta(); } if (!this.isEmpty(this.getInit())) { copiedData["init"] = this.getInit(); } // If it's a container membership request, add it in if (this.containerMembership) { copiedData["in"] = `["#${this.containerMembership}"]`; } switch (this.requestNameType) { case REQUEST_NAME_TYPE.TICKER: copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["request_ticker"] = this.requestName; break; case REQUEST_NAME_TYPE.REALM: copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["request_realm"] = this.requestName; break; case REQUEST_NAME_TYPE.SUBREALM: copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["request_subrealm"] = this.requestName; copiedData["args"]["parent_realm"] = this.requestParentId; break; case REQUEST_NAME_TYPE.CONTAINER: copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["request_container"] = this.requestName; break; case REQUEST_NAME_TYPE.ITEM: copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["request_dmitem"] = this.requestName; copiedData["args"]["parent_container"] = this.requestParentId; console.log(copiedData); console.log(" this.requestParentId;", this.requestParentId); default: break; } if (performBitworkForCommitTx) { copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["bitworkc"] = (_a = this.bitworkInfoCommit) === null || _a === void 0 ? void 0 : _a.hex_bitwork; } if (performBitworkForRevealTx) { copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["bitworkr"] = (_b = this.bitworkInfoReveal) === null || _b === void 0 ? void 0 : _b.hex_bitwork; } if (this.options.opType === "dmt") { copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["mint_ticker"] = (_c = this.options.dmtOptions) === null || _c === void 0 ? void 0 : _c.ticker; } let parentAtomicalInfo = this.getInputParent(); if (parentAtomicalInfo) { copiedData["args"] = copiedData["args"] || {}; copiedData["args"]["parents"] = { [parentAtomicalInfo.parentId]: 0, }; // Also supports one parent for now } if (this.options.satsbyte == -1) { const response = yield this.options.electrumApi.estimateFee(1); let estimatedSatsByte = Math.ceil((response.result / 1000) * 100000000); if (isNaN(estimatedSatsByte)) { estimatedSatsByte = 200; // Something went wrong, just default to 30 bytes sat estimate console.log('satsbyte fee query failed, defaulted to: ', estimatedSatsByte); } else { this.options.satsbyte = estimatedSatsByte; console.log('satsbyte fee auto-detected to: ', estimatedSatsByte); } } else { console.log('satsbyte fee manually set to: ', this.options.satsbyte); } let unixtime = Math.floor(Date.now() / 1000); let nonce = Math.floor(Math.random() * 10000000); let noncesGenerated = 0; let atomicalId = null; let commitTxid = null; let revealTxid = null; let commitMinedWithBitwork = false; // Placeholder for only estimating tx deposit fee size. if (performBitworkForCommitTx) { // Use zero nonce in order for recoverable real addresses. copiedData["args"]["nonce"] = 0; copiedData["args"]["time"] = unixtime; } console.log("copiedData", copiedData); const mockAtomPayload = new command_helpers_1.AtomicalsPayload(copiedData); if (this.options.verbose) { console.log("copiedData", copiedData); } const payloadSize = mockAtomPayload.cbor().length; console.log("Payload CBOR Size (bytes): ", payloadSize); if (payloadSize <= 1000) { console.log("Payload Encoded: ", copiedData); } const mockBaseCommitForFeeCalculation = (0, command_helpers_1.prepareCommitRevealConfig)(this.options.opType, fundingKeypair, mockAtomPayload); const fees = this.calculateFeesRequiredForAccumulatedCommitAndReveal(mockBaseCommitForFeeCalculation.hashLockP2TR.redeem.output.length, performBitworkForRevealTx); //////////////////////////////////////////////////////////////////////// // Begin Commit Transaction //////////////////////////////////////////////////////////////////////// if (performBitworkForCommitTx) { // Attempt to get funding UTXO information const fundingUtxo = yield (0, select_funding_utxo_1.getFundingUtxo)(this.options.electrumApi, fundingKeypair.address, fees.commitAndRevealFeePlusOutputs); // Log bitwork info if available printBitworkLog(this.bitworkInfoCommit, true); // Close the electrum API connection this.options.electrumApi.close(); // Set the default concurrency level to the number of CPU cores minus 1 const defaultConcurrency = os.cpus().length - 1; // Read the concurrency level from .env file const envConcurrency = process.env.CONCURRENCY ? parseInt(process.env.CONCURRENCY, 10) : -1; // Use envConcurrency if it is a positive number; otherwise, use defaultConcurrency const concurrency = envConcurrency > 0 ? envConcurrency : defaultConcurrency; // Logging the set concurrency level to the console console.log(`Concurrency set to: ${concurrency}`); const workerOptions = this.options; const workerBitworkInfoCommit = this.bitworkInfoCommit; let workers = []; let resolveWorkerPromise; // Create a promise to await the completion of worker tasks const workerPromise = new Promise((resolve) => { resolveWorkerPromise = resolve; }); let isWorkDone = false; // Function to stop all worker threads const stopAllWorkers = () => { workers.forEach((worker) => { worker.terminate(); }); workers = []; }; // Calculate the range of sequences to be assigned to each worker const seqRangePerWorker = Math.floor(exports.MAX_SEQUENCE / concurrency); // Initialize and start worker threads for (let i = 0; i < concurrency; i++) { console.log("Initializing worker: " + i); const worker = new worker_threads_1.Worker("./dist/utils/miner-worker.js"); // Handle messages from workers worker.on("message", (message) => __awaiter(this, void 0, void 0, function* () { console.log("Solution found, try composing the transaction..."); if (!isWorkDone) { isWorkDone = true; stopAllWorkers(); const atomPayload = new command_helpers_1.AtomicalsPayload(message.finalCopyData); const updatedBaseCommit = (0, command_helpers_1.prepareCommitRevealConfig)(workerOptions.opType, fundingKeypair, atomPayload); let psbtStart = new bitcoinjs_lib_1.Psbt({ network: command_helpers_1.NETWORK }); psbtStart.setVersion(1); psbtStart.addInput({ hash: fundingUtxo.txid, index: fundingUtxo.index, sequence: message.finalSequence, tapInternalKey: Buffer.from(fundingKeypair.childNodeXOnlyPubkey), witnessUtxo: { value: fundingUtxo.value, script: Buffer.from(fundingKeypair.output, "hex"), }, }); psbtStart.addOutput({ address: updatedBaseCommit.scriptP2TR.address, value: this.getOutputValueForCommit(fees), }); this.addCommitChangeOutputIfRequired(fundingUtxo.value, fees, psbtStart, fundingKeypair.address); psbtStart.signInput(0, fundingKeypair.tweakedChildNode); psbtStart.finalizeAllInputs(); const interTx = psbtStart.extractTransaction(); const rawtx = interTx.toHex(); yield AtomicalOperationBuilder.finalSafetyCheckForExcessiveFee(psbtStart, interTx); if (!this.broadcastWithRetries(rawtx)) { console.log("Error sending", interTx.getId(), rawtx); throw new Error("Unable to broadcast commit transaction after attempts: " + interTx.getId()); } else { console.log("Success sent tx: ", interTx.getId()); } commitMinedWithBitwork = true; performBitworkForCommitTx = false; // In both scenarios we copy over the args if (!performBitworkForCommitTx) { scriptP2TR = updatedBaseCommit.scriptP2TR; hashLockP2TR = updatedBaseCommit.hashLockP2TR; } // Resolve the worker promise with the received message resolveWorkerPromise(message); } })); worker.on("error", (error) => { console.error("worker error: ", error); if (!isWorkDone) { isWorkDone = true; stopAllWorkers(); } }); worker.on("exit", (code) => { if (code !== 0) { console.error(`Worker stopped with exit code ${code}`); } }); // Calculate sequence range for this worker const seqStart = i * seqRangePerWorker; let seqEnd = seqStart + seqRangePerWorker - 1; // Ensure the last worker covers the remaining range if (i === concurrency - 1) { seqEnd = exports.MAX_SEQUENCE - 1; } // Send necessary data to the worker const messageToWorker = { copiedData, seqStart, seqEnd, workerOptions, fundingWIF, fundingUtxo, fees, performBitworkForCommitTx, workerBitworkInfoCommit, scriptP2TR, hashLockP2TR, }; worker.postMessage(messageToWorker); workers.push(worker); } console.log("Stay calm and grab a drink! Miner workers have started mining... "); // Await results from workers const messageFromWorker = yield workerPromise; console.log("Workers have completed their tasks."); } else { scriptP2TR = mockBaseCommitForFeeCalculation.scriptP2TR; hashLockP2TR = mockBaseCommitForFeeCalculation.hashLockP2TR; } //////////////////////////////////////////////////////////////////////// // Begin Reveal Transaction //////////////////////////////////////////////////////////////////////// // The scriptP2TR and hashLockP2TR will contain the utxo needed for the commit and now can be revealed const utxoOfCommitAddress = yield (0, select_funding_utxo_1.getFundingUtxo)(this.options.electrumApi, scriptP2TR.address, this.getOutputValueForCommit(fees), commitMinedWithBitwork, 5); commitTxid = utxoOfCommitAddress.txid; atomicalId = commitTxid + "i0"; // Atomicals are always minted at the 0'th output const tapLeafScript = { leafVersion: hashLockP2TR.redeem.redeemVersion, script: hashLockP2TR.redeem.output, controlBlock: hashLockP2TR.witness[hashLockP2TR.witness.length - 1], }; if (performBitworkForRevealTx) { printBitworkLog(this.bitworkInfoReveal); } noncesGenerated = 0; do { let totalInputsforReveal = 0; // We calculate the total inputs for the reveal to determine to make change output or not let totalOutputsForReveal = 0; // Calculate total outputs for the reveal and compare to totalInputsforReveal and reveal fee let nonce = Math.floor(Math.random() * 100000000); let unixTime = Math.floor(Date.now() / 1000); let psbt = new bitcoinjs_lib_1.Psbt({ network: command_helpers_1.NETWORK }); psbt.setVersion(1); psbt.addInput({ sequence: this.options.rbf ? command_helpers_1.RBF_INPUT_SEQUENCE : undefined, hash: utxoOfCommitAddress.txid, index: utxoOfCommitAddress.vout, witnessUtxo: { value: utxoOfCommitAddress.value, script: hashLockP2TR.output, }, tapLeafScript: [tapLeafScript], }); totalInputsforReveal += utxoOfCommitAddress.value; // Add any additional inputs that were assigned for (const additionalInput of this.inputUtxos) { psbt.addInput({ sequence: this.options.rbf ? command_helpers_1.RBF_INPUT_SEQUENCE : undefined, hash: additionalInput.utxo.hash, index: additionalInput.utxo.index, witnessUtxo: additionalInput.utxo.witnessUtxo, tapInternalKey: additionalInput.keypairInfo.childNodeXOnlyPubkey, }); totalInputsforReveal += additionalInput.utxo.witnessUtxo.value; } // Note, we do not assign any outputs by default. // The caller must decide how many outputs to assign always // The reason is the caller knows the context to create them in // Add any additional outputs that were assigned for (const additionalOutput of this.additionalOutputs) { psbt.addOutput({ address: additionalOutput.address, value: additionalOutput.value, }); totalOutputsForReveal += additionalOutput.value; } if (parentAtomicalInfo) { psbt.addInput({ sequence: this.options.rbf ? command_helpers_1.RBF_INPUT_SEQUENCE : undefined, hash: parentAtomicalInfo.parentUtxoPartial.hash, index: parentAtomicalInfo.parentUtxoPartial.index, witnessUtxo: parentAtomicalInfo.parentUtxoPartial.witnessUtxo, tapInternalKey: parentAtomicalInfo.parentKeyInfo.childNodeXOnlyPubkey, }); totalInputsforReveal += parentAtomicalInfo.parentUtxoPartial.witnessUtxo.value; psbt.addOutput({ address: parentAtomicalInfo.parentKeyInfo.address, value: parentAtomicalInfo.parentUtxoPartial.witnessUtxo .value, }); totalOutputsForReveal += parentAtomicalInfo.parentUtxoPartial.witnessUtxo.value; } if (noncesGenerated % 10000 == 0) { unixTime = Math.floor(Date.now() / 1000); } const data = Buffer.from(unixTime + ":" + nonce, "utf8"); const embed = bitcoin.payments.embed({ data: [data] }); if (performBitworkForRevealTx) { psbt.addOutput({ script: embed.output, value: 0, }); } this.addRevealOutputIfChangeRequired(totalInputsforReveal, totalOutputsForReveal, fees.revealFeeOnly, fundingKeypair.address); psbt.signInput(0, fundingKeypair.childNode); // Sign all the additional inputs, if there were any let signInputIndex = 1; for (const additionalInput of this.inputUtxos) { psbt.signInput(signInputIndex, additionalInput.keypairInfo.tweakedChildNode); signInputIndex++; } if (parentAtomicalInfo) { console.log("parentAtomicalInfo", parentAtomicalInfo); psbt.signInput(signInputIndex, parentAtomicalInfo.parentKeyInfo.tweakedChildNode); } // We have to construct our witness script in a custom finalizer const customFinalizer = (_inputIndex, input) => { const scriptSolution = [input.tapScriptSig[0].signature]; const witness = scriptSolution .concat(tapLeafScript.script) .concat(tapLeafScript.controlBlock); return { finalScriptWitness: (0, witness_stack_to_script_witness_1.witnessStackToScriptWitness)(witness), }; }; psbt.finalizeInput(0, customFinalizer); // Finalize all the additional inputs, if there were any let finalizeInputIndex = 1; for (; finalizeInputIndex <= this.inputUtxos.length; finalizeInputIndex++) { psbt.finalizeInput(finalizeInputIndex); } if (parentAtomicalInfo) { psbt.finalizeInput(finalizeInputIndex); } const revealTx = psbt.extractTransaction(); const checkTxid = revealTx.getId(); logMiningProgressToConsole(performBitworkForRevealTx, this.options.disableMiningChalk, checkTxid, noncesGenerated); let shouldBroadcast = !performBitworkForRevealTx; if (performBitworkForRevealTx && (0, atomical_format_helpers_1.hasValidBitwork)(checkTxid, (_d = this.bitworkInfoReveal) === null || _d === void 0 ? void 0 : _d.prefix, (_e = this.bitworkInfoReveal) === null || _e === void 0 ? void 0 : _e.ext)) { process.stdout.clearLine(0); process.stdout.cursorTo(0); process.stdout.write(chalk.green(checkTxid, " nonces: " + noncesGenerated)); console.log("\nBitwork matches reveal txid! ", revealTx.getId(), "@ time: " + Math.floor(Date.now() / 1000)); shouldBroadcast = true; } // Broadcast either because there was no bitwork requested, and we are done. OR... // broadcast because we found the bitwork and it is ready to be broadcasted if (shouldBroadcast) { console.log("\nPrint raw tx in case of broadcast failure", revealTx.toHex()); yield AtomicalOperationBuilder.finalSafetyCheckForExcessiveFee(psbt, revealTx); console.log("\nBroadcasting tx...", revealTx.getId()); const interTx = psbt.extractTransaction(); const rawtx = interTx.toHex(); if (!(yield this.broadcastWithRetries(rawtx))) { console.log("Error sending", revealTx.getId(), rawtx); throw new Error("Unable to broadcast reveal transaction after attempts"); } else { console.log("Success sent tx: ", revealTx.getId()); } revealTxid = interTx.getId(); performBitworkForRevealTx = false; // Done } nonce++; noncesGenerated++; } while (performBitworkForRevealTx); const ret = { success: true, data: { commitTxid, revealTxid, }, }; if (this.options.opType === "nft" || this.options.opType === "ft" || this.options.opType === "dft") { ret["data"]["atomicalId"] = atomicalId; } if (this.options.opType === "dat") { ret["data"]["dataId"] = revealTxid + "i0"; ret["data"]["urn"] = "atom:btc:dat:" + revealTxid + "i0"; } return ret; }); } broadcastWithRetries(rawtx) { return __awaiter(this, void 0, void 0, function* () { let attempts = 0; let result = null; do { try { console.log("rawtx", rawtx); result = yield this.options.electrumApi.broadcast(rawtx); if (result) { break; } } catch (err) { console.log("Network error broadcasting (Trying again soon...)", err); yield this.options.electrumApi.resetConnection(); // Put in a sleep to help the connection reset more gracefully in case there is some delay console.log(`Will retry to broadcast transaction again in ${SEND_RETRY_SLEEP_SECONDS} seconds...`); yield (0, utils_1.sleeper)(SEND_RETRY_SLEEP_SECONDS); } attempts++; } while (attempts < SEND_RETRY_ATTEMPTS); return result; }); } static translateFromBase32ToHex(bitwork) { return bitwork; } totalOutputSum() { let sum = 0; for (const additionalOutput of this.additionalOutputs) { sum += additionalOutput.value; } return sum; } getTotalAdditionalInputValues() { let sum = 0; for (const utxo of this.inputUtxos) { sum += utxo.utxo.witnessUtxo.value; } return sum; } getTotalAdditionalOutputValues() { let sum = 0; for (const output of this.additionalOutputs) { sum += output.value; } return sum; } calculateAmountRequiredForReveal(hashLockP2TROutputLen = 0, performBitworkForRevealTx = false) { // <Previous txid> <Output index> <Length of scriptSig> <Sequence number> // 32 + 4 + 1 + 4 = 41 // <Witness stack item length> <Signature> ... <Control block> // (1 + 65 + 34) / 4 = 25 // Total: 41 + 25 = 66 //----------------------------------------- // OP_RETURN size // 8-bytes value, a one-byte script’s size const OP_RETURN_BYTES = 21 + 8 + 1; //----------------------------------------- const REVEAL_INPUT_BYTES_BASE = 66; // OP_RETURN size let hashLockCompactSizeBytes = 9; let op_Return_SizeBytes = 0; if (performBitworkForRevealTx) { op_Return_SizeBytes = OP_RETURN_BYTES; } if (hashLockP2TROutputLen <= 252) { hashLockCompactSizeBytes = 1; } else if (hashLockP2TROutputLen <= 0xffff) { hashLockCompactSizeBytes = 3; } else if (hashLockP2TROutputLen <= 0xffffffff) { hashLockCompactSizeBytes = 5; } return Math.ceil(this.options.satsbyte * (exports.BASE_BYTES + // Reveal input REVEAL_INPUT_BYTES_BASE + (hashLockCompactSizeBytes + hashLockP2TROutputLen) / 4 + // Additional inputs this.inputUtxos.length * exports.INPUT_BYTES_BASE + // Outputs this.additionalOutputs.length * exports.OUTPUT_BYTES_BASE + // Bitwork Output OP_RETURN Size Bytes op_Return_SizeBytes)); } calculateFeesRequiredForCommit() { let fees = Math.ceil(this.options.satsbyte * (exports.BASE_BYTES + 1 * exports.INPUT_BYTES_BASE + 1 * exports.OUTPUT_BYTES_BASE)); return fees; } getOutputValueForCommit(fees) { // Note that `Additional inputs` refers to the additional inputs in a reveal tx. return fees.revealFeePlusOutputs - this.getTotalAdditionalInputValues(); } getAdditionalFundingRequiredForReveal() { return 0; } /** * Get the commit and reveal fee. The commit fee assumes it is chained together * @returns */ calculateFeesRequiredForAccumulatedCommitAndReveal(hashLockP2TROutputLen = 0, performBitworkForRevealTx = false) { const revealFee = this.calculateAmountRequiredForReveal(hashLockP2TROutputLen, performBitworkForRevealTx); const commitFee = this.calculateFeesRequiredForCommit(); const commitAndRevealFee = commitFee + revealFee; const commitAndRevealFeePlusOutputs = commitFee + revealFee + this.totalOutputSum(); const revealFeePlusOutputs = revealFee + this.totalOutputSum(); const ret = { commitAndRevealFee, commitAndRevealFeePlusOutputs, revealFeePlusOutputs, commitFeeOnly: commitFee, revealFeeOnly: revealFee, }; return ret; } /** * Adds an extra output at the end if it was detected there would be excess satoshis for the reveal transaction * @param fee Fee calculations * @returns */ addRevealOutputIfChangeRequired(totalInputsValue, totalOutputsValue, revealFee, address) { const currentSatoshisFeePlanned = totalInputsValue - totalOutputsValue; // It will be invalid, but at least we know we don't need to add change if (currentSatoshisFeePlanned <= 0) { return; } // In order to keep the fee-rate unchanged, we should add extra fee for the new added change output. const excessSatoshisFound = currentSatoshisFeePlanned - revealFee - this.options.satsbyte * exports.OUTPUT_BYTES_BASE; // There were no excess satoshis, therefore no change is due if (excessSatoshisFound <= 0) { return; } // There were some excess satoshis, but let's verify that it meets the dust threshold to make change if (excessSatoshisFound >= exports.DUST_AMOUNT) { this.addOutput({ address: address, value: excessSatoshisFound, }); } } /** * Adds an extra output at the end if it was detected there would be excess satoshis for the reveal transaction * @param fee Fee calculations * @returns */ addCommitChangeOutputIfRequired(extraInputValue, fee, pbst, address) { const totalInputsValue = extraInputValue; const totalOutputsValue = this.getOutputValueForCommit(fee); const calculatedFee = totalInputsValue - totalOutputsValue; // It will be invalid, but at least we know we don't need to add change if (calculatedFee <= 0) { return; } // In order to keep the fee-rate unchanged, we should add extra fee for the new added change output. const expectedFee = fee.commitFeeOnly + this.options.satsbyte * exports.OUTPUT_BYTES_BASE; const differenceBetweenCalculatedAndExpected = calculatedFee - expectedFee; if (differenceBetweenCalculatedAndExpected <= 0) { return; } // There were some excess satoshis, but let's verify that it meets the dust threshold to make change if (differenceBetweenCalculatedAndExpected >= exports.DUST_AMOUNT) { pbst.addOutput({ address: address, value: differenceBetweenCalculatedAndExpected, }); } } /** * a final safety check to ensure we don't accidentally broadcast a tx with too high of a fe * @param psbt Partially signed bitcoin tx coresponding to the tx to calculate the total inputs values provided * @param tx The tx to broadcast, uses the outputs to calculate total outputs */ static finalSafetyCheckForExcessiveFee(psbt, tx) { return __awaiter(this, void 0, void 0, function* () { let sumInputs = 0; psbt.data.inputs.map((inp) => { sumInputs += inp.witnessUtxo.value; }); let sumOutputs = 0; tx.outs.map((out) => { sumOutputs += out.value; }); const fee = sumInputs - sumOutputs; if (fee > exports.EXCESSIVE_FEE_LIMIT) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { let reply = ''; const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); console.log(`Excessive fee ${fee} satoshis detected. Hardcoded to ${exports.EXCESSIVE_FEE_LIMIT} satoshis. Aborting due to protect funds.`); const allowedInput = ['y', 'yes', 'n', 'no']; while (!allowedInput.includes(reply)) { reply = (yield prompt("To ignore and continue type 'y', or 'n' to cancel: ")); if (!allowedInput.includes(reply)) { console.log("Invalid input."); } } if (reply === 'y' || reply === 'yes') { return; } if (reply === 'n' || reply === 'no') { throw 'Aborted for excessive fee. User cancelled'; } throw 'Aborted for excessive fee.'; } finally { rl.close(); } } }); } /** * Helper function to resolve a parent atomical id and the wallet record into a format that's easily processable by setInputParent * @param electrumxApi * @param parentId * @param parentOwner */ static resolveInputParent(electrumxApi, parentId, parentOwner) { return __awaiter(this, void 0, void 0, function* () { const { atomicalInfo, locationInfo, inputUtxoPartial } = yield (0, command_helpers_1.getAndCheckAtomicalInfo)(electrumxApi, parentId, parentOwner.address); const parentKeypairInput = ECPair.fromWIF(parentOwner.WIF); const parentKeypairInputInfo = (0, address_keypair_path_1.getKeypairInfo)(parentKeypairInput); const inp = { parentId, parentUtxoPartial: inputUtxoPartial, parentKeyInfo: parentKeypairInputInfo, }; return inp; }); } } exports.AtomicalOperationBuilder = AtomicalOperationBuilder;