UNPKG

myathhhh

Version:

Atomicals Javascript Library and CLI - atomicals.xyz

459 lines (458 loc) 22.4 kB
"use strict"; 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.TransferInteractiveBuilderCommand = void 0; const ecc = require("tiny-secp256k1"); const ecpair_1 = require("ecpair"); const readline = require("readline"); const bitcoin = require('bitcoinjs-lib'); bitcoin.initEccLib(ecc); const qrcode = require("qrcode-terminal"); const bitcoinjs_lib_1 = require("bitcoinjs-lib"); const address_helpers_1 = require("../utils/address-helpers"); const create_key_pair_1 = require("../utils/create-key-pair"); const address_keypair_path_1 = require("../utils/address-keypair-path"); const command_helpers_1 = require("./command-helpers"); const utils_1 = require("../utils/utils"); const atomical_format_helpers_1 = require("../utils/atomical-format-helpers"); const protocol_tags_1 = require("../types/protocol-tags"); const tinysecp = require('tiny-secp256k1'); (0, bitcoinjs_lib_1.initEccLib)(tinysecp); const ECPair = (0, ecpair_1.ECPairFactory)(tinysecp); class TransferInteractiveBuilderCommand { constructor(electrumApi, options, currentOwnerAtomicalWIF, fundingWIF, validatedWalletInfo, satsbyte, nofunding, atomicalIdReceipt, atomicalIdReceiptType, forceSkipValidation) { this.electrumApi = electrumApi; this.options = options; this.currentOwnerAtomicalWIF = currentOwnerAtomicalWIF; this.fundingWIF = fundingWIF; this.validatedWalletInfo = validatedWalletInfo; this.satsbyte = satsbyte; this.nofunding = nofunding; this.atomicalIdReceipt = atomicalIdReceipt; this.atomicalIdReceiptType = atomicalIdReceiptType; this.forceSkipValidation = forceSkipValidation; console.log(this.atomicalIdReceipt); } run() { return __awaiter(this, void 0, void 0, function* () { if (this.atomicalIdReceipt && !(0, atomical_format_helpers_1.isAtomicalId)(this.atomicalIdReceipt)) { throw new Error('AtomicalId receipt is not a valid atomical id'); } const keypairAtomical = ECPair.fromWIF(this.currentOwnerAtomicalWIF); const keypairFunding = ECPair.fromWIF(this.fundingWIF); const keypairFundingInfo = (0, address_keypair_path_1.getKeypairInfo)(keypairFunding); const keypairAtomicalInfo = (0, address_keypair_path_1.getKeypairInfo)(keypairAtomical); const p2tr = bitcoin.payments.p2tr({ internalPubkey: (0, create_key_pair_1.toXOnly)(keypairAtomical.publicKey), network: command_helpers_1.NETWORK }); console.log("===================================================================="); console.log("Transfer Interactive Builder (UTXOs and FTs)"); console.log("===================================================================="); const transferOptions = yield this.promptTransferOptions(keypairAtomicalInfo.address); const tx = yield this.buildAndSendTransaction(transferOptions, keypairAtomicalInfo, keypairFundingInfo, this.satsbyte); return { tx }; }); } promptTransferOptions(address) { return __awaiter(this, void 0, void 0, function* () { const balanceInfo = yield this.getUtxoBalanceSummary(address); const sumValues = balanceInfo.utxos.reduce((accum, item) => accum + item.value, 0); console.log(`Current Owner Address: ${address}`); console.log(`Confirmed Balance: `, sumValues); if (balanceInfo.utxos.length === 0) { throw `No UTXOs available for address ${address}`; } console.log(`---------------------------------------------------------------------`); console.log(`Step 1. Select UTXOs to send`); console.log(`---`); console.log(`UTXOs Count: `, balanceInfo.utxos.length); console.log(`UTXOs: `); let i = 0; balanceInfo.utxos.map((utxo) => { console.log(`${i}.`); console.log(JSON.stringify(utxo, null, 2)); i++; }); const selectedUtxos = yield this.promptUtxoSelection(balanceInfo); yield this.promptIfDetectedSomeAtomicalsAtSameUtxos(selectedUtxos); console.log('Selected UTXOs For Sending: ', JSON.stringify(selectedUtxos, null, 2)); console.log(`---------------------------------------------------------------------`); console.log(`Step 2. Enter receive amounts`); console.log(`UTXOs Chosen Count: `, selectedUtxos.length); const chosenSum = selectedUtxos.reduce((accum, item) => accum + item.value, 0); console.log(`UTXOs Chosen Balance: `, chosenSum); console.log(`---`); const outputs = yield this.promptAmountsToSend(this.validatedWalletInfo, chosenSum); console.log('Selected UTXOs: ', JSON.stringify(selectedUtxos, null, 2)); console.log('Recipients: ', JSON.stringify(outputs, null, 2)); console.log(`---------------------------------------------------------------------`); console.log(`Step 3. Confirm and send`); yield this.promptContinue(balanceInfo, selectedUtxos); return { balanceInfo, selectedUtxos, outputs }; }); } promptIfDetectedSomeAtomicalsAtSameUtxos(selectedUtxos) { return __awaiter(this, void 0, void 0, function* () { let isOtherAtomicalsFound = false; const indexesOfSelectedUtxosWithMultipleAtomicals = []; for (const utxo of selectedUtxos) { if (!utxo.atomicals) { continue; } if (utxo.atomicals.length) { isOtherAtomicalsFound = true; } } if (!isOtherAtomicalsFound) { return; } 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(`WARNING! There are some chosen UTXOs which contain Atomicals which would be transferred at the same time.`); console.log(`It is recommended to use the "extract" (NFT) or "skip" (FT) operations to seperate them first.`); let i = 0; for (const item of indexesOfSelectedUtxosWithMultipleAtomicals) { console.log(`${i}.`); console.log(JSON.stringify(item, null, 2)); i++; } reply = (yield prompt("To ignore and continue type 'y' or 'n' to cancel: ")); if (reply === 'y' || reply === 'yes') { return; } if (reply === 'n' || reply === 'no') { throw 'Aborted. User cancelled'; } throw 'Aborted'; } finally { rl.close(); } }); } promptUtxoSelection(info) { return __awaiter(this, void 0, void 0, function* () { let selectedUtxos = []; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { let reply = ''; const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); while (reply !== 'f') { const currentBalance = selectedUtxos.reduce((accum, item) => accum + item.value, 0); console.log(`Selected amount: ${currentBalance}`); console.log(`Options: '*' for all, or enter specific UTXO number or 'f' for Finished selecting`); console.log('-'); reply = (yield prompt("Select which UTXOs to transfer: ")); switch (reply) { case '*': return info.utxos; case 'f': return selectedUtxos; default: const parsedNum = parseInt(reply, 10); if (parsedNum >= info.utxos.length || parsedNum < 0) { console.log('Invalid selection. Maximum: ' + (info.utxos.length - 1)); continue; } selectedUtxos.push(info.utxos[parsedNum]); // Filter out dups selectedUtxos = selectedUtxos.filter(utils_1.onlyUnique); break; } } return selectedUtxos; } finally { rl.close(); } }); } promptContinue(info, selectedUtxos) { return __awaiter(this, void 0, void 0, function* () { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { let reply = ''; const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); reply = (yield prompt("Does everything look good above? To continue funding the transfer type 'y' or 'yes': ")); if (reply === 'y' || reply === 'yes') { return; } throw 'Aborted'; } finally { rl.close(); } }); } getUtxoBalanceSummary(address) { return __awaiter(this, void 0, void 0, function* () { const res = yield this.electrumApi.atomicalsByAddress(address); const utxosFiltered = []; for (const utxo of res.utxos) { // DO NOT Ignore the utxos which have atomicals in them // This builder is meant to be flexible. if (utxo.atomicals && utxo.atomicals.length) { // continue; } utxosFiltered.push({ txid: utxo.txid, index: utxo.index, value: utxo.value, height: utxo.height, atomicals: utxo.atomicals }); } return { utxos: utxosFiltered }; }); } promptAmountsToSend(validatedWalletInfo, availableBalance) { return __awaiter(this, void 0, void 0, function* () { let remainingBalance = availableBalance; const amountsToSend = []; const min = 1000; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); try { const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); while (remainingBalance > 0) { console.log(`Recipients: `); let accumulatd = 0; amountsToSend.map((item) => { var _a; console.log(`${item.address ? item.address : (_a = item.opReturn) === null || _a === void 0 ? void 0 : _a.toString()}: ${item.value}`); accumulatd += item.value; }); if (!amountsToSend.length) { console.log('No recipients yet...'); } console.log('-'); console.log(`Accumulated amount: ${accumulatd}`); console.log(`Remaining amount: ${remainingBalance}`); console.log(`'f' for Finished adding recipients`); console.log('-'); let reply = yield prompt("Enter address and amount seperated by a space: "); if (reply === 'f') { break; } const splitted = reply.split(/[ ,]+/); if (splitted[0] === 'op_return') { const generalData = Buffer.from(splitted[1], 'utf8'); const embed = bitcoin.payments.embed({ data: [generalData] }); const paymentRecieptOpReturn = embed.output; amountsToSend.push({ opReturn: paymentRecieptOpReturn, value: 0 }); } else { let addressPart = (0, address_helpers_1.performAddressAliasReplacement)(validatedWalletInfo, splitted[0]); const valuePart = parseInt(splitted[1], 10); if (valuePart < 546 || !valuePart) { console.log('Invalid value, minimum: 546'); continue; } if (remainingBalance - valuePart < 0) { console.log('Invalid value, maximum remaining: ' + remainingBalance); continue; } try { (0, address_helpers_1.detectAddressTypeToScripthash)(addressPart.address); } catch (err) { console.log('Invalid address'); continue; } amountsToSend.push({ address: addressPart.address, value: valuePart }); remainingBalance -= valuePart; } } if (!this.nofunding) { if (remainingBalance > 0) { throw new Error('Remaining balance was not 0'); } } console.log('Successfully allocated entire available amounts to recipients...'); return amountsToSend; } finally { rl.close(); } }); } buildAndSendTransaction(transferOptions, keyPairAtomical, keyPairFunding, satsbyte) { return __awaiter(this, void 0, void 0, function* () { const psbt = new bitcoin.Psbt({ network: command_helpers_1.NETWORK }); let tokenBalanceIn = 0; let tokenBalanceOut = 0; let tokenInputsLength = 0; let tokenOutputsLength = 0; for (const utxo of transferOptions.selectedUtxos) { // Add the atomical input, the value from the input counts towards the total satoshi amount required const { output } = (0, address_helpers_1.detectAddressTypeToScripthash)(keyPairAtomical.address); psbt.addInput({ sequence: this.options.rbf ? command_helpers_1.RBF_INPUT_SEQUENCE : undefined, hash: utxo.txid, index: utxo.index, witnessUtxo: { value: utxo.value, script: Buffer.from(output, 'hex') }, tapInternalKey: keyPairAtomical.childNodeXOnlyPubkey, }); tokenBalanceIn += utxo.value; tokenInputsLength++; } for (const output of transferOptions.outputs) { if (output.opReturn) { psbt.addOutput({ value: output.value, script: output.opReturn }); } else { psbt.addOutput({ value: output.value, address: output.address, }); } tokenBalanceOut += output.value; tokenOutputsLength++; } if (this.atomicalIdReceipt) { const outpoint = (0, atomical_format_helpers_1.compactIdToOutpoint)(this.atomicalIdReceipt); const atomEnvBuf = Buffer.from(protocol_tags_1.ATOMICALS_PROTOCOL_ENVELOPE_ID, 'utf8'); const payOpBuf = Buffer.from(this.atomicalIdReceiptType || 'p', 'utf8'); const outpointBuf = Buffer.from(outpoint, 'hex'); const embed = bitcoin.payments.embed({ data: [atomEnvBuf, payOpBuf, outpointBuf] }); const paymentRecieptOpReturn = embed.output; psbt.addOutput({ script: paymentRecieptOpReturn, value: 0, }); } if (!this.nofunding) { // TODO DETECT THAT THERE NEEDS TO BE CHANGE ADDED AND THEN if (tokenBalanceIn !== tokenBalanceOut) { throw 'Invalid input and output does not match for token. Developer Error.'; } } const { expectedSatoshisDeposit } = (0, command_helpers_1.calculateUtxoFundsRequired)(transferOptions.selectedUtxos.length, transferOptions.outputs.length, satsbyte, 0); if (expectedSatoshisDeposit < 546) { throw 'Invalid expectedSatoshisDeposit. Developer Error.'; } (0, command_helpers_1.logBanner)(`DEPOSIT ${expectedSatoshisDeposit / 100000000} BTC to ${keyPairFunding.address}`); qrcode.generate(keyPairFunding.address, { small: false }); console.log(`...`); console.log(`...`); console.log(`WAITING UNTIL ${expectedSatoshisDeposit / 100000000} BTC RECEIVED AT ${keyPairFunding.address}`); console.log(`...`); console.log(`...`); let utxo = yield this.electrumApi.waitUntilUTXO(keyPairFunding.address, expectedSatoshisDeposit, 5, false); console.log(`Detected UTXO (${utxo.txid}:${utxo.vout}) with value ${utxo.value} for funding the transfer operation...`); let basisValue = 0; if (!this.nofunding) { // Add the funding input psbt.addInput({ sequence: this.options.rbf ? command_helpers_1.RBF_INPUT_SEQUENCE : undefined, hash: utxo.txid, index: utxo.outputIndex, witnessUtxo: { value: utxo.value, script: keyPairFunding.output }, tapInternalKey: keyPairFunding.childNodeXOnlyPubkey, }); basisValue = utxo.value; } const isMoreThanDustChangeRemaining = basisValue - expectedSatoshisDeposit >= 546; if (isMoreThanDustChangeRemaining) { // Add change output console.log(`Adding change output, remaining: ${basisValue - expectedSatoshisDeposit}`); psbt.addOutput({ value: basisValue - expectedSatoshisDeposit, address: keyPairFunding.address, }); } let i = 0; for (i = 0; i < tokenInputsLength; i++) { console.log(`Signing Atomical input ${i}...`); psbt.signInput(i, keyPairAtomical.tweakedChildNode); } // Sign the final funding input if (!this.nofunding) { console.log('Signing funding input...'); psbt.signInput(i, keyPairFunding.tweakedChildNode); } psbt.finalizeAllInputs(); const tx = psbt.extractTransaction(); const rawtx = tx.toHex(); console.log(`Constructed Atomicals FT Transfer, attempting to broadcast: ${tx.getId()}`); let broadcastedTxId = yield this.electrumApi.broadcast(rawtx, this.forceSkipValidation); console.log(`Success!`); return { success: true, data: { txid: broadcastedTxId } }; }); } accumulateAsc(amount, utxos) { const cloned = [...utxos]; cloned.sort(function (a, b) { return a.value - b.value; }); const selectedUtxos = []; let remainingAmount = amount; for (const utxo of cloned) { selectedUtxos.push(utxo); remainingAmount -= amount; if (remainingAmount <= 0) { break; } } return selectedUtxos; } accumulateDesc(amount, utxos) { const cloned = [...utxos]; cloned.sort(function (a, b) { return b.value - a.value; }); const selectedUtxos = []; let remainingAmount = amount; for (const utxo of cloned) { selectedUtxos.push(utxo); remainingAmount -= amount; if (remainingAmount <= 0) { break; } } return selectedUtxos; } } exports.TransferInteractiveBuilderCommand = TransferInteractiveBuilderCommand;