UNPKG

bitverse-atomicals-js

Version:

Atomicals Javascript Library and CLI - atomicals.xyz

205 lines (204 loc) 12.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.TransferInteractiveNftCommand = void 0; const command_interface_1 = require("./command.interface"); 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 atomical_format_helpers_1 = require("../utils/atomical-format-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 get_command_1 = require("./get-command"); const get_by_realm_command_1 = require("./get-by-realm-command"); const get_by_container_command_1 = require("./get-by-container-command"); const tinysecp = require('tiny-secp256k1'); (0, bitcoinjs_lib_1.initEccLib)(tinysecp); const ECPair = (0, ecpair_1.ECPairFactory)(tinysecp); class TransferInteractiveNftCommand { constructor(electrumApi, options, atomicalAliasOrId, currentOwnerAtomicalWIF, receiveAddress, fundingWIF, satsbyte, satsoutput) { this.electrumApi = electrumApi; this.options = options; this.atomicalAliasOrId = atomicalAliasOrId; this.currentOwnerAtomicalWIF = currentOwnerAtomicalWIF; this.receiveAddress = receiveAddress; this.fundingWIF = fundingWIF; this.satsbyte = satsbyte; this.satsoutput = satsoutput; } run() { return __awaiter(this, void 0, void 0, function* () { (0, address_helpers_1.detectAddressTypeToScripthash)(this.receiveAddress); const keypairAtomical = ECPair.fromWIF(this.currentOwnerAtomicalWIF); const keypairFunding = ECPair.fromWIF(this.fundingWIF); const p2tr = bitcoin.payments.p2tr({ internalPubkey: (0, create_key_pair_1.toXOnly)(keypairAtomical.publicKey), network: command_helpers_1.NETWORK }); const atomicalType = (0, atomical_format_helpers_1.getAtomicalIdentifierType)(this.atomicalAliasOrId); let cmd; if (atomicalType.type === atomical_format_helpers_1.AtomicalIdentifierType.ATOMICAL_ID || atomicalType.type === atomical_format_helpers_1.AtomicalIdentifierType.ATOMICAL_NUMBER) { cmd = new get_command_1.GetCommand(this.electrumApi, atomicalType.providedIdentifier || '', command_interface_1.AtomicalsGetFetchType.GET); } else if (atomicalType.type === atomical_format_helpers_1.AtomicalIdentifierType.REALM_NAME) { cmd = new get_by_realm_command_1.GetByRealmCommand(this.electrumApi, atomicalType.realmName || '', command_interface_1.AtomicalsGetFetchType.GET); } else if (atomicalType.type === atomical_format_helpers_1.AtomicalIdentifierType.CONTAINER_NAME) { cmd = new get_by_container_command_1.GetByContainerCommand(this.electrumApi, atomicalType.containerName || '', command_interface_1.AtomicalsGetFetchType.GET); } else { throw 'Atomical identifier is invalid. Use a realm name, container name or atomicalId or atomical number'; } const cmdResult = yield cmd.run(); if (!cmdResult.success) { throw 'Unable to resolve Atomical.'; } console.log("===================================================================="); console.log("Transfer Interactive (NFT)"); console.log("===================================================================="); const atomicalId = cmdResult.data.result.atomical_id; const atomicalNumber = cmdResult.data.result.atomical_number; console.log(`Atomical Id: ${atomicalId}`); console.log(`Atomical Number: ${atomicalNumber}`); if (cmdResult.data.result.$container) { console.log('Container Name: ', cmdResult.data.result.$container); } if (cmdResult.data.result.$full_realm_name) { console.log('Full Realm Name: ', cmdResult.data.result.$full_realm_name); } console.log('Expected existing address owner of the private key (WIF) provided: ', p2tr.address); console.log("Satoshis amount to add into the Atomical at transfer:", this.satsoutput); console.log("Requested fee rate satoshis/byte:", this.satsbyte); const atomicalInfo = yield this.electrumApi.atomicalsGetLocation(atomicalId); const atomicalDecorated = (0, atomical_format_helpers_1.decorateAtomical)(atomicalInfo.result); console.log(JSON.stringify(atomicalDecorated, null, 2)); // Check to make sure that the location is controlled by the same address as supplied by the WIF if (!atomicalDecorated.location_info_obj || !atomicalDecorated.location_info_obj.locations || !atomicalDecorated.location_info_obj.locations.length || atomicalDecorated.location_info_obj.locations[0].address !== p2tr.address) { throw `Atomical is controlled by a different address (${atomicalDecorated.location_info_obj.locations[0].address}) than the provided wallet (${p2tr.address})`; } if (atomicalDecorated.location_info_obj.locations[0].atomicals_at_location.length > 1) { throw `Multiple atomicals are located at the same address as the NFT. Use the splat command to separate them first.`; } const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const prompt = (query) => new Promise((resolve) => rl.question(query, resolve)); try { const cont = yield prompt("Proceed with transfer? Type 'y' to continue: "); if (cont.toLowerCase() === 'y') { const txid = yield this.performTransfer(atomicalDecorated, keypairAtomical, keypairFunding, this.satsbyte, this.satsoutput, this.receiveAddress); return { success: true, data: { txid } }; } else { console.log("Cancelled"); return { success: false, message: "Cancelled transfer" }; } } catch (e) { console.log('e', e.toString()); return { success: false, message: e, e: e.toString() }; } finally { rl.close(); } }); } performTransfer(atomical, atomicalKeypair, fundingKeypair, satsbyte, satsoutput, receiveAddress) { return __awaiter(this, void 0, void 0, function* () { const keypairAtomical = (0, address_keypair_path_1.getKeypairInfo)(atomicalKeypair); if (atomical.type !== 'NFT') { throw 'Atomical is not an NFT. It is expected to be an NFT type'; } if (!atomical.location_info_obj || !atomical.location_info_obj.locations || !atomical.location_info_obj.locations.length || atomical.location_info_obj.locations[0].address !== keypairAtomical.address) { throw "Provided atomical WIF does not match the location address of the Atomical"; } 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...'); if (!atomical.location_info_obj || atomical.location_info_obj.locations.length !== 1) { throw 'expected exactly one location_info for NFT Atomical'; } const location = atomical.location_info_obj.locations[0]; const { expectedSatoshisDeposit } = (0, command_helpers_1.calculateFundsRequired)(location.value, satsoutput, satsbyte, 0); const psbt = new bitcoin.Psbt({ network: command_helpers_1.NETWORK }); // Add the atomical input, the value from the input counts towards the total satoshi amount required psbt.addInput({ sequence: this.options.rbf ? command_helpers_1.RBF_INPUT_SEQUENCE : undefined, hash: location.txid, index: location.index, witnessUtxo: { value: location.value, script: Buffer.from(location.script, 'hex') }, tapInternalKey: keypairAtomical.childNodeXOnlyPubkey, }); // There is a funding deficit // Could fund with the atomical input value, but we wont // const requiresDeposit = expectedSatoshisDeposit > 0; (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, false); console.log(`Detected UTXO (${utxo.txid}:${utxo.vout}) with value ${utxo.value} for funding the transfer 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, }); psbt.addOutput({ value: this.satsoutput, address: receiveAddress, }); const isMoreThanDustChangeRemaining = utxo.value - expectedSatoshisDeposit >= 546; if (isMoreThanDustChangeRemaining) { // Add change output console.log(`Adding change output, remaining: ${utxo.value - expectedSatoshisDeposit}`); psbt.addOutput({ value: utxo.value - expectedSatoshisDeposit, address: keypairFundingInfo.address }); } psbt.signInput(0, keypairAtomical.tweakedChildNode); psbt.signInput(1, keypairFundingInfo.tweakedChildNode); psbt.finalizeAllInputs(); const tx = psbt.extractTransaction(); const rawtx = tx.toHex(); console.log(`Constructed Atomicals NFT Transfer, attempting to broadcast: ${tx.getId()}`); let broadcastedTxId = yield this.electrumApi.broadcast(rawtx); console.log(`Success!`); return broadcastedTxId; }); } } exports.TransferInteractiveNftCommand = TransferInteractiveNftCommand;