ecash-lib
Version:
Library for eCash transaction building
251 lines • 8.7 kB
JavaScript
;
// Copyright (c) 2025 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseSlp = void 0;
const str_js_1 = require("../io/str.js");
const hex_js_1 = require("../io/hex.js");
const opcode_js_1 = require("../opcode.js");
const op_js_1 = require("../op.js");
const common_js_1 = require("./common.js");
const slp_js_1 = require("./slp.js");
/**
* Parse the given SLP OP_RETURN Script.
*
* For data that's clearly not SLP it will return `undefined`.
* For example, if the OP_RETURN or LOKAD ID is missing.
*
* For an unknown token type, it'll return SlpUnknown.
*
* For a known token type, it'll parse the remaining data, or throw an error if
* the format is invalid or if there's an unknown tx type.
*
* This behavior mirrors that of Chronik for consistency.
**/
function parseSlp(opreturnScript) {
const ops = opreturnScript.ops();
const opreturnOp = ops.next();
// Return undefined if not OP_RETURN
if (opreturnOp === undefined ||
(0, op_js_1.isPushOp)(opreturnOp) ||
opreturnOp !== opcode_js_1.OP_RETURN) {
return undefined;
}
// Return undefined if LOKAD ID is not "SLP\0"
const lokadId = ops.next();
if (lokadId === undefined || !(0, op_js_1.isPushOp)(lokadId)) {
return undefined;
}
if ((0, str_js_1.bytesToStr)(lokadId.data) !== slp_js_1.SLP_LOKAD_ID_STR) {
return undefined;
}
// Parse token type
const tokenTypeBytes = nextBytes(ops);
if (tokenTypeBytes === undefined) {
throw new Error('Missing tokenType');
}
if (tokenTypeBytes.length !== 1) {
throw new Error('tokenType must be exactly 1 byte');
}
const tokenType = tokenTypeBytes[0];
if (tokenType !== slp_js_1.SLP_FUNGIBLE &&
tokenType !== slp_js_1.SLP_MINT_VAULT &&
tokenType !== slp_js_1.SLP_NFT1_GROUP &&
tokenType !== slp_js_1.SLP_NFT1_CHILD) {
return {
txType: common_js_1.UNKNOWN_STR,
tokenType,
};
}
// Parse tx type (GENESIS, MINT, SEND, BURN)
const txTypeBytes = nextBytes(ops);
if (txTypeBytes === undefined) {
throw new Error('Missing txType');
}
const txType = (0, str_js_1.bytesToStr)(txTypeBytes);
// Handle tx type specific parsing.
// Advances the `ops` Script iterator
switch (txType) {
case common_js_1.GENESIS_STR:
return nextGenesis(ops, tokenType);
case common_js_1.MINT_STR:
return nextMint(ops, tokenType);
case common_js_1.SEND_STR:
return nextSend(ops, tokenType);
case common_js_1.BURN_STR:
return nextBurn(ops, tokenType);
default:
throw new Error('Unknown txType');
}
}
exports.parseSlp = parseSlp;
function nextGenesis(ops, tokenType) {
// Parse genesis info
const tokenTicker = (0, str_js_1.bytesToStr)(nextBytesRequired(ops, 'tokenTicker'));
const tokenName = (0, str_js_1.bytesToStr)(nextBytesRequired(ops, 'tokenName'));
const url = (0, str_js_1.bytesToStr)(nextBytesRequired(ops, 'url'));
const hash = nextBytesRequired(ops, 'hash');
if (hash.length !== 0 && hash.length !== slp_js_1.SLP_GENESIS_HASH_NUM_BYTES) {
throw new Error(`hash must be either 0 or ${slp_js_1.SLP_GENESIS_HASH_NUM_BYTES} bytes`);
}
const decimalsBytes = nextBytesRequired(ops, 'decimals');
if (decimalsBytes.length !== 1) {
throw new Error('decimals must be exactly 1 byte');
}
const decimals = decimalsBytes[0];
if (decimals > common_js_1.MAX_DECIMALS) {
throw new Error(`decimals must be at most ${common_js_1.MAX_DECIMALS}`);
}
// Parse mint data
let mintVaultScripthash = undefined;
let mintBatonOutIdx = undefined;
if (tokenType === slp_js_1.SLP_MINT_VAULT) {
const scripthashBytes = nextBytesRequired(ops, 'mintVaultScripthash');
if (scripthashBytes.length !== slp_js_1.SLP_MINT_VAULT_SCRIPTHASH_NUM_BYTES) {
throw new Error(`mintVaultScripthash must be exactly ${slp_js_1.SLP_MINT_VAULT_SCRIPTHASH_NUM_BYTES} ` +
'bytes long');
}
mintVaultScripthash = (0, hex_js_1.toHex)(scripthashBytes);
}
else {
mintBatonOutIdx = nextMintOutIdx(ops, tokenType);
}
const initialAtoms = parseSlpAtoms(nextBytesRequired(ops, 'initialAtoms'));
nextEnd(ops, 'GENESIS');
return {
txType: common_js_1.GENESIS_STR,
tokenType,
genesisInfo: {
tokenTicker,
tokenName,
url,
hash: hash.length !== 0 ? (0, hex_js_1.toHex)(hash) : undefined,
mintVaultScripthash,
decimals,
},
initialAtoms,
mintBatonOutIdx,
};
}
function nextMint(ops, tokenType) {
const tokenId = nextTokenId(ops);
if (tokenType === slp_js_1.SLP_MINT_VAULT) {
const additionalAtomsArray = nextSlpAtomsArray(ops);
return {
txType: common_js_1.MINT_STR,
tokenType,
tokenId,
additionalAtomsArray,
};
}
else if (tokenType === slp_js_1.SLP_NFT1_CHILD) {
throw new Error('SLP_NFT1_CHILD cannot have MINT transactions');
}
else {
const mintBatonOutIdx = nextMintOutIdx(ops, tokenType);
const additionalAtoms = parseSlpAtoms(nextBytesRequired(ops, 'additionalAtoms'));
nextEnd(ops, 'MINT');
return {
txType: common_js_1.MINT_STR,
tokenType,
tokenId,
additionalAtoms,
mintBatonOutIdx,
};
}
}
function nextSend(ops, tokenType) {
const tokenId = nextTokenId(ops);
const sendAtomsArray = nextSlpAtomsArray(ops);
return {
txType: common_js_1.SEND_STR,
tokenType,
tokenId,
sendAtomsArray,
};
}
function nextBurn(ops, tokenType) {
const tokenId = nextTokenId(ops);
const burnAtoms = parseSlpAtoms(nextBytesRequired(ops, 'burnAtoms'));
nextEnd(ops, 'BURN');
return {
txType: common_js_1.BURN_STR,
tokenType,
tokenId,
burnAtoms,
};
}
function nextBytes(iter) {
const op = iter.next();
if (op === undefined) {
return undefined;
}
if (!(0, op_js_1.isPushOp)(op)) {
throw new Error('SLP only supports push-ops');
}
return op.data;
}
function nextBytesRequired(iter, name) {
const bytes = nextBytes(iter);
if (bytes === undefined) {
throw new Error('Missing ' + name);
}
return bytes;
}
function nextMintOutIdx(iter, tokenType) {
const outIdxBytes = nextBytesRequired(iter, 'mintBatonOutIdx');
if (outIdxBytes.length > 1) {
throw new Error('mintBatonOutIdx must be at most 1 byte long');
}
if (outIdxBytes.length === 1) {
if (tokenType === slp_js_1.SLP_NFT1_CHILD) {
throw new Error('SLP_NFT1_CHILD cannot have a mint baton');
}
const mintBatonOutIdx = outIdxBytes[0];
if (mintBatonOutIdx < 2) {
throw new Error('mintBatonOutIdx must be at least 2');
}
return mintBatonOutIdx;
}
return undefined;
}
function nextTokenId(iter) {
const tokenIdBytes = nextBytesRequired(iter, 'tokenId');
if (tokenIdBytes.length !== common_js_1.TOKEN_ID_NUM_BYTES) {
throw new Error(`tokenId must be exactly ${common_js_1.TOKEN_ID_NUM_BYTES} bytes long`);
}
// Note: SLP token ID endianness is big-endian
return (0, hex_js_1.toHex)(tokenIdBytes);
}
function nextSlpAtomsArray(iter) {
const atomsArray = [];
let bytes = undefined;
while ((bytes = nextBytes(iter)) !== undefined) {
atomsArray.push(parseSlpAtoms(bytes));
}
if (atomsArray.length === 0) {
throw new Error('atomsArray cannot be empty');
}
if (atomsArray.length > slp_js_1.SLP_MAX_SEND_OUTPUTS) {
throw new Error(`atomsArray can at most be ${slp_js_1.SLP_MAX_SEND_OUTPUTS} items long`);
}
return atomsArray;
}
function nextEnd(iter, txType) {
if (iter.next() !== undefined) {
throw new Error(`Superfluous ${txType} bytes`);
}
}
function parseSlpAtoms(bytes) {
if (bytes.length !== slp_js_1.SLP_ATOMS_NUM_BYTES) {
throw new Error(`SLP atoms must be exactly ${slp_js_1.SLP_ATOMS_NUM_BYTES} bytes long`);
}
let number = 0n;
for (let i = 0; i < slp_js_1.SLP_ATOMS_NUM_BYTES; ++i) {
number <<= 8n;
number |= BigInt(bytes[i]);
}
return number;
}
//# sourceMappingURL=slp.parse.js.map