UNPKG

bitverse-atomicals-js

Version:

Atomicals Javascript Library and CLI - atomicals.xyz

598 lines (597 loc) 26.3 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.getAndCheckAtomicalInfo = exports.normalizeIdentifier = exports.guessPrefixType = exports.appendMintUpdateRevealScript = exports.AtomicalsPayload = exports.encodeFiles = exports.prepareArgsMetaCtx = exports.prepareObjectfield = exports.prepareFilesDataBackup = exports.readJsonFileAsCompleteDataObjectEncodeHash = exports.readJsonFileAsCompleteDataObjectEncodeAtomicalIds = exports.prepareFilesDataAsObject = exports.readFileAsCompleteDataObject = exports.prepareFilesData = exports.readAsAtomicalFileData = exports.prepareCommitRevealConfig = exports.prepareCommitRevealConfig2 = exports.appendMintUpdateRevealScript2 = exports.calculateUtxoFundsRequired = exports.calculateFTFundsRequired = exports.calculateFundsRequired = exports.logBanner = exports.NETWORK = exports.RBF_INPUT_SEQUENCE = void 0; const path_1 = require("path"); const mime = require("mime-types"); const file_utils_1 = require("../utils/file-utils"); const cbor = require("borc"); const bitcoinjs_lib_1 = require("bitcoinjs-lib"); const protocol_tags_1 = require("../types/protocol-tags"); const resolve_command_1 = require("./resolve-command"); const command_interface_1 = require("./command.interface"); const atomical_format_helpers_1 = require("../utils/atomical-format-helpers"); const address_helpers_1 = require("../utils/address-helpers"); const dotenv = require("dotenv"); dotenv.config(); exports.RBF_INPUT_SEQUENCE = 0xfffffffd; exports.NETWORK = process.env.NETWORK === 'testnet' ? bitcoinjs_lib_1.networks.testnet : process.env.NETWORK == "regtest" ? bitcoinjs_lib_1.networks.regtest : bitcoinjs_lib_1.networks.bitcoin; function logBanner(text) { console.log("===================================================================="); console.log(text); console.log("===================================================================="); } exports.logBanner = logBanner; const calculateFundsRequired = (additionalInputValue, atomicalSats, satsByte, mintDataLength = 0, baseTxByteLength = 300) => { // The default base includes assumes 1 input and 1 output with room to spare const estimatedTxSizeBytes = baseTxByteLength + mintDataLength; const expectedFee = estimatedTxSizeBytes * satsByte; let expectedSatoshisDeposit = expectedFee + atomicalSats - additionalInputValue; if (expectedSatoshisDeposit > 0 && expectedSatoshisDeposit < 546) { expectedSatoshisDeposit = 546; } return { expectedSatoshisDeposit, expectedFee }; }; exports.calculateFundsRequired = calculateFundsRequired; const calculateFTFundsRequired = (numberOfInputs, numberOfOutputs, satsByte, mintDataLength = 0, baseTxByteLength = 300) => { // The default base includes assumes 1 input and 1 output with room to spare const estimatedTxSizeBytes = baseTxByteLength + mintDataLength; const baseInputSize = 36 + 4 + 64; const baseOutputSize = 8 + 20 + 4; let expectedSatoshisDeposit = (estimatedTxSizeBytes + (numberOfInputs * baseInputSize) + (numberOfOutputs * baseOutputSize)) * satsByte; if (expectedSatoshisDeposit > 0 && expectedSatoshisDeposit < 546) { expectedSatoshisDeposit = 546; } return { expectedSatoshisDeposit }; }; exports.calculateFTFundsRequired = calculateFTFundsRequired; const calculateUtxoFundsRequired = (numberOfInputs, numberOfOutputs, satsByte, mintDataLength = 0, baseTxByteLength = 300) => { // The default base includes assumes 1 input and 1 output with room to spare const estimatedTxSizeBytes = baseTxByteLength + mintDataLength; const baseInputSize = 36 + 4 + 64; const baseOutputSize = 8 + 20 + 4; let expectedSatoshisDeposit = (estimatedTxSizeBytes + (numberOfInputs * baseInputSize) + (numberOfOutputs * baseOutputSize)) * satsByte; if (expectedSatoshisDeposit > 0 && expectedSatoshisDeposit < 546) { expectedSatoshisDeposit = 546; } return { expectedSatoshisDeposit }; }; exports.calculateUtxoFundsRequired = calculateUtxoFundsRequired; const appendMintUpdateRevealScript2 = (opType, keypair, files, log = true) => { let ops = `${keypair.childNodeXOnlyPubkey.toString('hex')} OP_CHECKSIG OP_0 OP_IF `; ops += `${Buffer.from(protocol_tags_1.ATOMICALS_PROTOCOL_ENVELOPE_ID, 'utf8').toString('hex')}`; ops += ` ${Buffer.from(opType, 'utf8').toString('hex')}`; const payload = {}; for (const file of files) { if (file.contentType !== 'object') { payload[file.name] = { '$ct': file.contentType, '$b': file.data }; } else if (file.contentType === 'object') { payload[file.name] = file.data; } } function deepEqual(x, y) { const ok = Object.keys, tx = typeof x, ty = typeof y; return x && y && tx === 'object' && tx === ty ? (ok(x).length === ok(y).length && ok(x).every(key => deepEqual(x[key], y[key]))) : (x === y); } const cborEncoded = cbor.encode(payload); // Decode to do sanity check const cborDecoded = cbor.decode(cborEncoded); if (log) { console.log('CBOR Encoded', JSON.stringify(cborDecoded, null, 2)); } if (!deepEqual(cborDecoded, payload)) { throw 'CBOR Decode error objects are not the same. Developer error'; } const chunks = (0, file_utils_1.chunkBuffer)(cborEncoded, 520); for (let chunk of chunks) { ops += ` ${chunk.toString('hex')}`; } ops += ` OP_ENDIF`; return ops; }; exports.appendMintUpdateRevealScript2 = appendMintUpdateRevealScript2; const prepareCommitRevealConfig2 = (opType, keypair, filesData, log = true) => { const revealScript = (0, exports.appendMintUpdateRevealScript2)(opType, keypair, filesData, log); const hashscript = bitcoinjs_lib_1.script.fromASM(revealScript); const scriptTree = { output: hashscript, }; const hash_lock_script = hashscript; const hashLockRedeem = { output: hash_lock_script, redeemVersion: 192, }; const scriptP2TR = bitcoinjs_lib_1.payments.p2tr({ internalPubkey: keypair.childNodeXOnlyPubkey, scriptTree, network: exports.NETWORK }); const hashLockP2TR = bitcoinjs_lib_1.payments.p2tr({ internalPubkey: keypair.childNodeXOnlyPubkey, scriptTree, redeem: hashLockRedeem, network: exports.NETWORK }); return { scriptP2TR, hashLockP2TR }; }; exports.prepareCommitRevealConfig2 = prepareCommitRevealConfig2; const prepareCommitRevealConfig = (opType, keypair, atomicalsPayload, log = true) => { const revealScript = (0, exports.appendMintUpdateRevealScript)(opType, keypair, atomicalsPayload, log); const hashscript = bitcoinjs_lib_1.script.fromASM(revealScript); const scriptTree = { output: hashscript, }; const hash_lock_script = hashscript; const hashLockRedeem = { output: hash_lock_script, redeemVersion: 192, }; const scriptP2TR = bitcoinjs_lib_1.payments.p2tr({ internalPubkey: keypair.childNodeXOnlyPubkey, scriptTree, network: exports.NETWORK }); const hashLockP2TR = bitcoinjs_lib_1.payments.p2tr({ internalPubkey: keypair.childNodeXOnlyPubkey, scriptTree, redeem: hashLockRedeem, network: exports.NETWORK }); return { scriptP2TR, hashLockP2TR, hashscript }; }; exports.prepareCommitRevealConfig = prepareCommitRevealConfig; const readAsAtomicalFileData = (file, alternateName) => __awaiter(void 0, void 0, void 0, function* () { let expectedName = file; const rawbytes = yield (0, file_utils_1.fileReader)(file); let fileMintData = { name: alternateName ? alternateName : expectedName, contentType: mime.contentType((0, path_1.basename)(file)) || 'application/octet-stream', data: Buffer.from(rawbytes, 'utf8') }; return fileMintData; }); exports.readAsAtomicalFileData = readAsAtomicalFileData; /** * * Prepare file data from a file on disk, with an optional renaming of the file * OR... * field data (ie: JSON value or object) * * Syntax: * * Case 1: Store raw file, using the filename on disk as the field name: file.txt * Result: file.txt: { ... file data embedded } * * Case 2: Store raw file, but use an alternate field name: filerenamed.to.anything:file.txt * Result: filerenamed.to.anything: { ... file data embedded } * * Case 3: Store scalar value or object, using a specified field name: "meta={\"hello"\:\"world\"}" or meta=123 or "meta=this is a text string" * * @param files Key value array of files and names OR the field name and field data * @returns */ const prepareFilesData = (fields) => __awaiter(void 0, void 0, void 0, function* () { const filesData = []; for (const entry of fields) { if (entry.indexOf(',') === -1 && entry.indexOf('=') === -1) { filesData.push(yield (0, exports.readAsAtomicalFileData)(entry)); } else if (entry.indexOf(',') !== -1) { const entrySplit = entry.split(','); const alternateName = entrySplit[0]; filesData.push(yield (0, exports.readAsAtomicalFileData)(entrySplit[1], alternateName)); } else if (entry.indexOf('=') !== -1) { const fieldName = entry.substring(0, entry.indexOf('=')); const fieldValue = entry.substring(entry.indexOf('=') + 1); const parsedJson = JSON.parse(fieldValue); const fieldData = { name: fieldName, contentType: 'object', data: parsedJson }; filesData.push(fieldData); } else { throw new Error('Invalid field(s) specifications. Aborting...'); } } return filesData; }); exports.prepareFilesData = prepareFilesData; const readFileAsCompleteDataObject = (filePath, givenFileName) => __awaiter(void 0, void 0, void 0, function* () { const fileContents = yield (0, file_utils_1.fileReader)(filePath); return { [givenFileName]: fileContents }; }); exports.readFileAsCompleteDataObject = readFileAsCompleteDataObject; const prepareFilesDataAsObject = (fields, disableAutoncode = false) => __awaiter(void 0, void 0, void 0, function* () { let fieldDataObject = {}; for (const entry of fields) { if (entry.indexOf(',') === -1 && entry.indexOf('=') === -1) { let filename = entry; if (filename.charAt(0) === '@') { if (!filename.endsWith('.json')) { throw new Error('Use of @ for direct embeds must only be used with .json file types'); } filename = entry.substring(1); const jsonFileContents = yield (0, file_utils_1.jsonFileReader)(filename); fieldDataObject = Object.assign({}, fieldDataObject, Object.assign({}, jsonFileContents)); } else { const fileInfo = yield (0, exports.readAsAtomicalFileData)(filename); fieldDataObject[(0, path_1.basename)(fileInfo.name)] = fileInfo.data; } } else if (entry.indexOf(',') !== -1 && entry.indexOf('=') === -1) { const entrySplit = entry.split(','); const filePath = entrySplit[1]; const alternateName = entrySplit[0]; const isInlineJson = filePath.endsWith('.json') ? true : false; if (isInlineJson) { const jsonFileContents = yield (0, file_utils_1.jsonFileReader)(filePath); fieldDataObject[alternateName] = jsonFileContents; } else { const fileInfo = yield (0, exports.readAsAtomicalFileData)(entrySplit[1], alternateName); fieldDataObject[(fileInfo.name)] = { '$ct': fileInfo.contentType, '$b': fileInfo.data }; } } else if (entry.indexOf('=') !== -1) { const fieldName = entry.substring(0, entry.indexOf('=')); const fieldValue = entry.substring(entry.indexOf('=') + 1); try { const parsedJson = JSON.parse(fieldValue); fieldDataObject[fieldName] = parsedJson; } catch (err) { if (typeof fieldValue === 'string') { try { const num = Number(fieldValue); if (!isNaN(num)) { fieldDataObject[fieldName] = Number(fieldValue); } else { fieldDataObject[fieldName] = fieldValue; } } catch (ex) { fieldDataObject[fieldName] = fieldValue; } } } } else { throw new Error('Invalid field(s) specifications. Aborting...'); } } return fieldDataObject; }); exports.prepareFilesDataAsObject = prepareFilesDataAsObject; const readJsonFileAsCompleteDataObjectEncodeAtomicalIds = (jsonFile, autoEncode = false, autoEncodePattern) => __awaiter(void 0, void 0, void 0, function* () { if (!jsonFile.endsWith('.json')) { throw new Error('Filename must end in json'); } const jsonFileContents = yield (0, file_utils_1.jsonFileReader)(jsonFile); if (autoEncode) { const updatedObject = {}; (0, atomical_format_helpers_1.encodeIds)(jsonFileContents, updatedObject, atomical_format_helpers_1.encodeAtomicalIdToBuffer, atomical_format_helpers_1.encodeHashToBuffer, autoEncodePattern); return updatedObject; } return jsonFileContents; }); exports.readJsonFileAsCompleteDataObjectEncodeAtomicalIds = readJsonFileAsCompleteDataObjectEncodeAtomicalIds; const readJsonFileAsCompleteDataObjectEncodeHash = (jsonFile, autoEncode = false, autoEncodePattern) => __awaiter(void 0, void 0, void 0, function* () { if (!jsonFile.endsWith('.json')) { throw new Error('Filename must end in json'); } const jsonFileContents = yield (0, file_utils_1.jsonFileReader)(jsonFile); if (autoEncode) { const updatedObject = {}; (0, atomical_format_helpers_1.encodeIds)(jsonFileContents, updatedObject, atomical_format_helpers_1.encodeAtomicalIdToBuffer, atomical_format_helpers_1.encodeHashToBuffer, autoEncodePattern); return updatedObject; } return jsonFileContents; }); exports.readJsonFileAsCompleteDataObjectEncodeHash = readJsonFileAsCompleteDataObjectEncodeHash; const prepareFilesDataBackup = (files, names) => __awaiter(void 0, void 0, void 0, function* () { let fileCount = 0; const nameMap = {}; const filesData = []; for (const file of files) { let expectedName = file; let mimeTypeHint; if (names.length) { if (names.length !== files.length) { throw 'Error names argument length must match the number of files provided'; } const splitted = names[fileCount].split(','); expectedName = splitted[0]; mimeTypeHint = splitted[1] && splitted[1] === 'object' ? 'object' : null; } if (nameMap[expectedName]) { throw `Error invalid name ${expectedName} for --names. Check there are no duplicates and that '_' is also not used`; } nameMap[expectedName] = true; const fileIndex = fileCount + 1; const rawbytes = yield (0, file_utils_1.fileReader)(file); let fileMintData = { name: expectedName, contentType: mime.contentType(file) || 'application/octet-stream', data: Buffer.from(rawbytes, 'utf8') }; if (mimeTypeHint === 'object') { const rawbytes = yield (0, file_utils_1.fileReader)(file); const parsedJson = JSON.parse(rawbytes); fileMintData = { name: expectedName, contentType: 'object', data: parsedJson }; } filesData.push(fileMintData); console.log(`File #${fileIndex} name locally`, file); console.log(`File #${fileIndex} field name:`, expectedName); console.log(`File #${fileIndex} size:`, rawbytes.length); console.log(`File #${fileIndex} content type:`, fileMintData.contentType); console.log('-------'); fileCount++; } console.log("Total number of files to be added in transaction:", files.length); return filesData; }); exports.prepareFilesDataBackup = prepareFilesDataBackup; const prepareObjectfield = (filesData, objectToAdd) => __awaiter(void 0, void 0, void 0, function* () { for (const prop in objectToAdd) { if (!objectToAdd.hasOwnProperty(prop)) { continue; } filesData.push({ name: prop, contentType: 'object', data: isNaN(objectToAdd[prop]) ? objectToAdd[prop] : Number(objectToAdd[prop]) }); } return filesData; }); exports.prepareObjectfield = prepareObjectfield; const prepareArgsMetaCtx = (args = undefined, meta = undefined, ctx = undefined, log = true) => __awaiter(void 0, void 0, void 0, function* () { if (log) { console.log('Args', args); console.log('Meta', meta); console.log('Ctx', ctx); } const filesData = []; if (args) { filesData.push({ name: 'args', contentType: 'object', data: args }); } if (meta) { filesData.push({ name: 'meta', contentType: 'object', data: meta }); } if (ctx) { filesData.push({ name: 'ctx', contentType: 'object', data: ctx }); } return filesData; }); exports.prepareArgsMetaCtx = prepareArgsMetaCtx; const encodeFiles = (files) => { const payload = {}; for (const file of files) { if (file.contentType !== 'object') { payload[file.name] = { '$ct': file.contentType, '$d': file.data }; } else if (file.contentType === 'object') { payload[file.name] = file.data; } } return payload; }; exports.encodeFiles = encodeFiles; /** * Ensure provided object is restricted to the set of allowable datatypes to be CBOR atomicals friendly. * */ class AtomicalsPayload { constructor(originalData) { this.originalData = originalData; if (!originalData) { this.originalData = {}; return; } function deepEqual(x, y) { const ok = Object.keys, tx = typeof x, ty = typeof y; return x && y && tx === 'object' && tx === ty ? (ok(x).length === ok(y).length && ok(x).every(key => deepEqual(x[key], y[key]))) : (x === y); } function isAllowedtype(tc, allowBuffer = true) { if (tc === 'object' || tc === 'Number' || tc === 'number' || tc === 'null' || tc === 'string' || tc == 'boolean') { return true; } if (allowBuffer && tc === 'buffer') { return true; } return false; } function validateWhitelistedDatatypes(x, allowBuffer = true) { const ok = Object.keys; const tx = typeof x; const isAllowed = isAllowedtype(tx, allowBuffer); if (!isAllowed) { return false; } if (tx === 'object') { return ok(x).every(key => validateWhitelistedDatatypes(x[key], allowBuffer)); } return true; } if (!validateWhitelistedDatatypes(originalData)) { throw new Error('Invalid payload contains disallowed data types. Use only number, string, null, or buffer'); } // Also make sure that if either args, ctx, init, or meta are provided, then we never allow buffer. if (originalData['args']) { if (!validateWhitelistedDatatypes(originalData['args'], false)) { throw 'args field invalid due to presence of buffer type'; } } if (originalData['ctx']) { if (!validateWhitelistedDatatypes(originalData['ctx'], false)) { throw 'ctx field invalid due to presence of buffer type'; } } if (originalData['meta']) { if (!validateWhitelistedDatatypes(originalData['meta'], false)) { throw 'meta field invalid due to presence of buffer type'; } } const payload = Object.assign({}, originalData); const cborEncoded = cbor.encode(payload); // Decode to do sanity check const cborDecoded = cbor.decode(cborEncoded); if (!deepEqual(cborDecoded, payload)) { throw 'CBOR Decode error objects are not the same. Developer error'; } if (!deepEqual(originalData, payload)) { throw 'CBOR Payload Decode error objects are not the same. Developer error'; } this.cborEncoded = cborEncoded; } get() { return this.originalData; } cbor() { return this.cborEncoded; } } exports.AtomicalsPayload = AtomicalsPayload; const appendMintUpdateRevealScript = (opType, keypair, payload, log = true) => { let ops = `${keypair.childNodeXOnlyPubkey.toString('hex')} OP_CHECKSIG OP_0 OP_IF `; ops += `${Buffer.from(protocol_tags_1.ATOMICALS_PROTOCOL_ENVELOPE_ID, 'utf8').toString('hex')}`; ops += ` ${Buffer.from(opType, 'utf8').toString('hex')}`; const chunks = (0, file_utils_1.chunkBuffer)(payload.cbor(), 520); for (let chunk of chunks) { ops += ` ${chunk.toString('hex')}`; } ops += ` OP_ENDIF`; return ops; }; exports.appendMintUpdateRevealScript = appendMintUpdateRevealScript; const guessPrefixType = (id) => { if (id.startsWith('#')) { return id; } if (id.startsWith('+')) { return id; } if (id.startsWith('$')) { return id; } if (id.indexOf('.') !== -1) { return id; } return id; }; exports.guessPrefixType = guessPrefixType; const normalizeIdentifier = (id, expectedType) => { switch (expectedType) { case null: return (0, exports.guessPrefixType)(id); case atomical_format_helpers_1.AtomicalIdentifierType.CONTAINER_NAME: return id.startsWith('#') ? id : '#' + id; case atomical_format_helpers_1.AtomicalIdentifierType.REALM_NAME: return id.startsWith('+') ? id : '+' + id; case atomical_format_helpers_1.AtomicalIdentifierType.TICKER_NAME: return id.startsWith('$') ? id : '$' + id; default: } return id; }; exports.normalizeIdentifier = normalizeIdentifier; const getAndCheckAtomicalInfo = (electrumApi, atomicalAliasOrId, expectedOwnerAddress, expectedType = 'NFT', expectedSubType = null) => __awaiter(void 0, void 0, void 0, function* () { var _a; const getLocationCommand = new resolve_command_1.ResolveCommand(electrumApi, atomicalAliasOrId, command_interface_1.AtomicalsGetFetchType.LOCATION); const getLocationResponse = yield getLocationCommand.run(); if (!getLocationResponse.success) { console.log(JSON.stringify(getLocationResponse, null, 2)); throw new Error(`Error: Unable to get location.`); } const atomicalInfo = getLocationResponse.data.result; if (expectedType === 'NFT' && atomicalInfo.type !== expectedType) { console.log('atomicalInfo', atomicalInfo); throw `Atomical is not an type ${expectedType}. It is expected to be an ${expectedType} type. atomicalAliasOrId=${atomicalAliasOrId}`; } if (expectedType === 'FT' && atomicalInfo.type !== expectedType) { console.log('atomicalInfo', atomicalInfo); throw `Atomical is not an type ${expectedType}. It is expected to be an ${expectedType} type. atomicalAliasOrId=${atomicalAliasOrId}`; } if (expectedSubType && atomicalInfo.subtype !== expectedSubType) { console.log('atomicalInfo', atomicalInfo); throw `Atomical is not subtype ${expectedSubType}. It is expected to be an ${expectedSubType} type. atomicalAliasOrId=${atomicalAliasOrId}`; } const atomicalDecorated = (0, atomical_format_helpers_1.decorateAtomical)(atomicalInfo); let locationInfoObj = atomicalDecorated.location_info_obj; let locationInfo = locationInfoObj.locations; // Check to make sure that the location is controlled by the same address as supplied by the WIF if (!locationInfo || !locationInfo.length || locationInfo[0].address !== expectedOwnerAddress) { const address = (_a = locationInfo === null || locationInfo === void 0 ? void 0 : locationInfo[0]) === null || _a === void 0 ? void 0 : _a.address; if (address) { throw `Atomical is controlled by a different address (${address}) than the provided wallet (${expectedOwnerAddress})`; } else { throw 'Atomical is no longer controlled.'; } } locationInfo = locationInfo[0]; const inputUtxoPartial = (0, address_helpers_1.IsAtomicalOwnedByWalletRecord)(expectedOwnerAddress, atomicalDecorated); return { atomicalInfo, locationInfo, inputUtxoPartial }; }); exports.getAndCheckAtomicalInfo = getAndCheckAtomicalInfo;