UNPKG

bitverse-atomicals-js

Version:

Atomicals Javascript Library and CLI - atomicals.xyz

357 lines (356 loc) 20.2 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.PendingSubrealmsCommand = void 0; const address_helpers_1 = require("../utils/address-helpers"); const readline = require("readline"); const chalk = require("chalk"); const address_keypair_path_1 = require("../utils/address-keypair-path"); const command_helpers_1 = require("./command-helpers"); const ecc = require("tiny-secp256k1"); const bitcoin = require('bitcoinjs-lib'); bitcoin.initEccLib(ecc); const qrcode = require("qrcode-terminal"); const bitcoinjs_lib_1 = require("bitcoinjs-lib"); ; const ecpair_1 = require("ecpair"); const tinysecp = require('tiny-secp256k1'); (0, bitcoinjs_lib_1.initEccLib)(tinysecp); const ECPair = (0, ecpair_1.ECPairFactory)(tinysecp); const atomical_format_helpers_1 = require("../utils/atomical-format-helpers"); const protocol_tags_1 = require("../types/protocol-tags"); class PendingSubrealmsCommand { constructor(electrumApi, options, address, fundingWIF, satsbyte, display) { this.electrumApi = electrumApi; this.options = options; this.address = address; this.fundingWIF = fundingWIF; this.satsbyte = satsbyte; this.display = display; } static isCurrentAtomicalPendingCandidate(entry) { return entry['request_subrealm_status']['pending_candidate_atomical_id'] == entry['atomical_id']; } static isPendingCandidate(entry) { switch (entry['request_subrealm_status']['status']) { case 'pending_awaiting_confirmations_payment_received_prematurely': case 'pending_awaiting_confirmations_for_payment_window': case 'pending_awaiting_confirmations': case 'pending_awaiting_payment': if (PendingSubrealmsCommand.isCurrentAtomicalPendingCandidate(entry)) return true; default: } return false; } run() { return __awaiter(this, void 0, void 0, function* () { const keypairFunding = ECPair.fromWIF(this.fundingWIF); const { scripthash } = (0, address_helpers_1.detectAddressTypeToScripthash)(this.address); let res = yield this.electrumApi.atomicalsByScripthash(scripthash, true); const statusMap = {}; const current_block_height = res.global.height; for (const prop in res.atomicals) { if (!res.atomicals.hasOwnProperty(prop)) { continue; } const entry = res.atomicals[prop]; if (entry.type !== 'NFT') { continue; } if (!entry.subtype || entry.subtype !== 'request_subrealm') { continue; } const entryStatus = entry['request_subrealm_status']['status']; if (!PendingSubrealmsCommand.isPendingCandidate(entry)) { continue; } let candidateInfo = null; for (const candidate of entry['subrealm_candidates']) { if (candidate['atomical_id'] == entry['atomical_id']) { candidateInfo = candidate; break; } } // It is a pending for the current atomical statusMap[entry.subtype] = statusMap[entry.subtype] || {}; statusMap[entry.subtype][entryStatus] = statusMap[entry.subtype][entryStatus] || []; const obj = Object.assign({ atomical_id: entry['atomical_id'], atomical_number: entry['atomical_number'], request_full_realm_name: entry['request_full_realm_name'], status: entry['request_subrealm_status'], payment_type: entry['payment_type'] }, entry); if (candidateInfo.payment_type == 'applicable_rule') { obj['make_payment_from_height'] = candidateInfo['make_payment_from_height']; obj['payment_due_no_later_than_height'] = candidateInfo['payment_due_no_later_than_height']; obj['applicable_rule'] = candidateInfo['applicable_rule']; } statusMap[entry.subtype][entryStatus].push(obj); } const statusReturn = Object.assign({ current_block_height }, statusMap); this.makePrettyMenu(statusReturn); if (this.display) { console.log('display on'); return { success: true, data: statusReturn }; } let selection = yield this.promptSubrealmSelection(statusReturn['request_subrealm']['pending_awaiting_payment']); if (!selection) { return { success: false, data: statusReturn }; } // overide to test sending premature payments // selection = statusReturn['request_subrealm']['pending_awaiting_confirmations_for_payment_window'][0]; const expectedPaymentOutputsMap = selection['applicable_rule']['matched_rule']['o']; const paymentOutputs = []; console.log('Sats per byte', this.satsbyte); let num = 0; for (const propScript in expectedPaymentOutputsMap) { if (!expectedPaymentOutputsMap.hasOwnProperty(propScript)) { continue; } const outputValue = expectedPaymentOutputsMap[propScript]['v']; const outputArc20 = expectedPaymentOutputsMap[propScript]['id']; const expectedAddress = (0, address_helpers_1.detectScriptToAddressType)(propScript); paymentOutputs.push({ address: expectedAddress, value: outputValue }); console.log('Output #' + num); if (outputArc20) { console.log('Price: ', outputValue / 100000000, `ARC20: (${outputArc20})`); } else { console.log('Price: ', outputValue / 100000000); } if (outputArc20) { console.log(`WARNING: You must send ARC20: (${outputArc20}) for this output`); } console.log('Payment Address:', expectedAddress); num++; } const paymentResult = yield this.makePayment(selection['atomical_id'], paymentOutputs, keypairFunding, this.satsbyte); return { success: true, data: paymentResult }; }); } hasSubrealmsAwaitingPayment(statusReturn) { return statusReturn['request_subrealm'] && statusReturn['request_subrealm']['pending_awaiting_payment']; } hasSubrealmsAwaitingPaymentWindow(statusReturn) { return statusReturn['request_subrealm'] && statusReturn['request_subrealm']['pending_awaiting_confirmations_for_payment_window']; } calculateFundsRequired(price, satsbyte) { const base = 300 * (satsbyte ? satsbyte : 1); return base + price; } makePayment(atomicalId, paymentOutputs, fundingKeypair, satsbyte) { return __awaiter(this, void 0, void 0, function* () { const keypairFundingInfo = (0, address_keypair_path_1.getKeypairInfo)(fundingKeypair); console.log('Funding address of the funding private key (WIF) provided: ', keypairFundingInfo.address); (0, command_helpers_1.logBanner)('Preparing Funding Fees...'); let price = 0; paymentOutputs.map((e) => { price += e.value; }); console.log('paymentOutputs', paymentOutputs); const expectedSatoshisDeposit = this.calculateFundsRequired(price, satsbyte); const psbt = new bitcoin.Psbt({ network: command_helpers_1.NETWORK }); (0, command_helpers_1.logBanner)(`DEPOSIT ${expectedSatoshisDeposit / 100000000} BTC to ${keypairFundingInfo.address}`); qrcode.generate(keypairFundingInfo.address, { small: false }); console.log(`...`); console.log(`...`); console.log(`WAITING UNTIL ${expectedSatoshisDeposit / 100000000} BTC RECEIVED AT ${keypairFundingInfo.address}`); console.log(`...`); console.log(`...`); let utxo = yield this.electrumApi.waitUntilUTXO(keypairFundingInfo.address, expectedSatoshisDeposit, 5, true); console.log(`Detected UTXO (${utxo.txid}:${utxo.vout}) with value ${utxo.value} for funding the operation...`); // 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: keypairFundingInfo.output }, tapInternalKey: keypairFundingInfo.childNodeXOnlyPubkey, }); for (const paymentOutput of paymentOutputs) { psbt.addOutput({ value: paymentOutput.value, address: paymentOutput.address, }); } const outpoint = (0, atomical_format_helpers_1.compactIdToOutpoint)(atomicalId); const atomEnvBuf = Buffer.from(protocol_tags_1.ATOMICALS_PROTOCOL_ENVELOPE_ID, 'utf8'); const payOpBuf = Buffer.from('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, }); // Add op return here psbt.signInput(0, keypairFundingInfo.tweakedChildNode); psbt.finalizeAllInputs(); const tx = psbt.extractTransaction(); const rawtx = tx.toHex(); console.log('rawtx', rawtx); console.log(`Constructed Atomicals Payment, attempting to broadcast: ${tx.getId()}`); console.log(`About to broadcast`); let broadcastedTxId = yield this.electrumApi.broadcast(rawtx); console.log(`Success!`); return broadcastedTxId; }); } makePrettyMenu(statusReturn) { console.log(chalk.blue.bold('GENERAL INFORMATION')); console.log(chalk.blue.bold('------------------------------------------------------')); console.log('Current Block Height: ', chalk.bold(statusReturn.current_block_height)); console.log('\n'); console.log(chalk.blue.bold('PENDING AWAITING PAYMENT')); console.log(chalk.blue.bold('------------------------------------------------------')); let counter = 0; if (!this.hasSubrealmsAwaitingPayment(statusReturn)) { console.log('There are no subrealms awaiting payment at the moment. Any pending awaiting payments will appear here during their payment windows.'); console.log('\n'); } else { for (const subrealm_pending of statusReturn['request_subrealm']['pending_awaiting_payment']) { console.log(chalk.bold(counter + '. Subrealm Request: +' + subrealm_pending['request_full_realm_name'])); console.log('Atomical Id: ' + subrealm_pending['atomical_id']); console.log('Atomical Number: ' + subrealm_pending['atomical_number']); console.log('Status: ', subrealm_pending['status']['status']); const make_payment_from_height = subrealm_pending['make_payment_from_height']; const payment_due_no_later_than_height = subrealm_pending['payment_due_no_later_than_height']; console.log(`make_payment_from_height: ` + chalk.bold(`${make_payment_from_height}`)); console.log(`payment_due_no_later_than_height: ` + chalk.bold(`${payment_due_no_later_than_height}`)); const expectedPaymentOutputs = subrealm_pending['applicable_rule']['matched_rule']['o']; let i = 0; for (const propScript in expectedPaymentOutputs) { if (!expectedPaymentOutputs.hasOwnProperty(propScript)) { continue; } const expectedOutputScript = propScript; const expectedAddress = (0, address_helpers_1.detectScriptToAddressType)(expectedOutputScript); console.log('Expected Payment Outputs For Rule: '); //const applicableRulePrice = expectedPaymentOutputs[propScript]; const outputValue = expectedPaymentOutputs[propScript]['v']; const outputArc20 = expectedPaymentOutputs[propScript]['id']; const price = outputValue / 100000000; if (outputArc20) { console.log(`Payment output #${i} to address ${expectedAddress} for amount ${price} in ARC20: ${outputArc20}`); } else { console.log(`Payment output #${i} to address ${expectedAddress} for amount ${price}`); } i++; } const outpoint = (0, atomical_format_helpers_1.compactIdToOutpoint)(subrealm_pending['atomical_id']); const atomEnvBuf = Buffer.from(protocol_tags_1.ATOMICALS_PROTOCOL_ENVELOPE_ID, 'utf8'); const payOpBuf = Buffer.from('p', 'utf8'); const outpointBuf = Buffer.from(outpoint, 'hex'); const embed = bitcoin.payments.embed({ data: [atomEnvBuf, payOpBuf, outpointBuf] }); const paymentReceipt = embed.output; console.log(`Payment receipt (OP_RETURN): ` + chalk.bold(`${paymentReceipt.toString('hex')}`)); console.log(chalk.red.bold(`ACTION REQUIRED: Make payment outputs before block height ${payment_due_no_later_than_height}`)); console.log(chalk.red.bold(`WARNING: If the payment is not made by block ${payment_due_no_later_than_height} then someone else could claim this subrealm and your request will expire.`)); console.log('\n'); counter++; } } console.log(chalk.blue.bold('PENDING AWAITING CONFIRMATIONS FOR PAYMENT WINDOW')); console.log(chalk.blue.bold('------------------------------------------------------')); if (!this.hasSubrealmsAwaitingPaymentWindow(statusReturn)) { console.log('There are no subrealms awaiting confirmations for the payment window. Go ahead and mint a subrealm first.'); console.log('\n'); } else { for (const subrealm_pending of statusReturn['request_subrealm']['pending_awaiting_confirmations_for_payment_window']) { console.log(chalk.bold('Pending Subrealm Request: +' + subrealm_pending['request_full_realm_name'])); console.log('Atomical Id: ' + subrealm_pending['atomical_id']); console.log('Atomical Number: ' + subrealm_pending['atomical_number']); console.log('Status: ', subrealm_pending['status']['status']); const make_payment_from_height = subrealm_pending['make_payment_from_height']; const payment_due_no_later_than_height = subrealm_pending['payment_due_no_later_than_height']; console.log(`make_payment_from_height: ` + chalk.bold(`${make_payment_from_height}`)); console.log(`payment_due_no_later_than_height: ` + chalk.bold(`${payment_due_no_later_than_height}`)); const applicableRulePrice = subrealm_pending['applicable_rule']['matched_rule']['satoshis']; const price = applicableRulePrice / 100000000; console.log(chalk.green.bold(`NO ACTION REQUIRED YET. Wait until block height ${make_payment_from_height} for the payment window to open. The price will be ${price} if your request is the leading candidate.`)); console.log(`WARNING: If a payment is made prematurely ${payment_due_no_later_than_height} then your funds would be lost if someone had an earlier commit and reveals it before block height ${make_payment_from_height}`); console.log('------------------------------------------------------'); } } console.log(chalk.blue.bold('OTHER PENDING STATES')); console.log(chalk.blue.bold('------------------------------------------------------')); let foundOther = false; for (const prop in statusReturn['request_subrealm']) { if (!statusReturn['request_subrealm'].hasOwnProperty(prop)) { continue; } if (prop === 'pending_awaiting_payment' || prop === 'pending_awaiting_confirmations_for_payment_window') { continue; } foundOther = true; const subrealm_pendings_list = statusReturn['request_subrealm'][prop]; for (const subrealm_pending of subrealm_pendings_list) { console.log(chalk.bold('Pending: +' + subrealm_pending['request_full_realm_name'])); console.log('Atomical Id: ' + subrealm_pending['atomical_id']); console.log('Atomical Number: ' + subrealm_pending['atomical_number']); console.log('Status: ', subrealm_pending['status']['status']); console.log(`NO ACTION REQUIRED`); console.log('------------------------------------------------------'); } } if (!foundOther) { console.log('There are no subrealms in other pending states'); console.log('\n'); } } promptSubrealmSelection(pendingSubrealms) { return __awaiter(this, void 0, void 0, function* () { if (!pendingSubrealms || !pendingSubrealms.length) { return null; } 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 !== 'q') { console.log(`Specify the number of the subrealm above to make a payment for or 'q' to quit.`); console.log('-'); reply = (yield prompt("Enter your selection: ")); switch (reply) { case 'q': throw new Error('User cancelled'); default: const parsedNum = parseInt(reply, 10); if (parsedNum >= pendingSubrealms.length || parsedNum < 0) { console.log('Invalid selection.'); continue; } return pendingSubrealms[parsedNum]; } } return null; } finally { rl.close(); } }); } } exports.PendingSubrealmsCommand = PendingSubrealmsCommand;