ecash-lib
Version:
Library for eCash transaction building
259 lines (233 loc) • 7.37 kB
text/typescript
// 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.
import { toHex, toHexRev } from '../io/hex.js';
import { bytesToStr } from '../io/str.js';
import { Bytes } from '../io/bytes.js';
import {
ALP_LOKAD_ID,
ALP_MAX_SIZE,
ALP_STANDARD,
AlpTokenType_Number,
MintData,
} from './alp.js';
import {
BURN_STR,
GENESIS_STR,
GenesisInfo,
MINT_STR,
SEND_STR,
TOKEN_ID_NUM_BYTES,
UNKNOWN_STR,
} from './common.js';
/**
* Parsed ALP GENESIS pushdata.
* Note: This must always be the first eMPP pushdata.
**/
export interface AlpGenesis {
/** "GENESIS" */
txType: typeof GENESIS_STR;
/** Token type of the token to create */
tokenType: AlpTokenType_Number;
/** Info about the token */
genesisInfo: GenesisInfo;
/** Which tokens/mint baton to create initially */
mintData: MintData;
}
/** Parsed ALP MINT pushdata. */
export interface AlpMint {
/** "MINT" */
txType: typeof MINT_STR;
/** Token type of the token to mint */
tokenType: AlpTokenType_Number;
/** Token ID of the token to mint */
tokenId: string;
/** Which tokens/mint baton to create */
mintData: MintData;
}
/** Parsed ALP SEND pushdata. */
export interface AlpSend {
/** "SEND" */
txType: typeof SEND_STR;
/** Token type of the token to send */
tokenType: AlpTokenType_Number;
/** Token ID of the token to send */
tokenId: string;
/** Array of the number of token atoms to send to the outputs at 1 to N */
sendAtomsArray: bigint[];
}
/** Parsed ALP BURN pushdata */
export interface AlpBurn {
/** "BURN" */
txType: typeof BURN_STR;
/** Token type of the token to burn */
tokenId: string;
/** Token ID of the token to burn */
tokenType: AlpTokenType_Number;
/** How many tokens should be burned */
burnAtoms: bigint;
}
/** New unknown ALP token type */
export interface AlpUnknown {
/** Placeholder for unknown token type or tx type */
txType: typeof UNKNOWN_STR;
/** Token type number */
tokenType: number;
}
/** Parsed ALP pushdata */
export type AlpData = AlpGenesis | AlpMint | AlpSend | AlpBurn | AlpUnknown;
/**
* Parse the given ALP pushdata. eMPP allows multiple pushdata per OP_RETURN.
*
* For data that's clearly not ALP (i.e. doesn't start with LOKAD ID "SLP2"),
* it will return `undefined`.
*
* For an unknown token type, it'll return AlpUnknown.
*
* 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.
**/
export function parseAlp(pushdata: Uint8Array): AlpData | undefined {
// Must have at least 4 bytes for the LOKAD ID
if (pushdata.length < ALP_LOKAD_ID.length) {
return undefined;
}
const bytes = new Bytes(pushdata);
// If the pushdata doesn't start with "SLP2" (ALP's LOKAD ID), return undefined
const lokadId = bytesToStr(bytes.readBytes(ALP_LOKAD_ID.length));
if (lokadId != bytesToStr(ALP_LOKAD_ID)) {
return undefined;
}
// Return UNKNOWN for unknown token types (only "STANDARD" known so far)
const tokenType = readU8(bytes, 'tokenType');
if (tokenType != ALP_STANDARD) {
return {
txType: 'UNKNOWN',
tokenType,
};
}
// Parse tx type (GENESIS, MINT, SEND, BURN)
const txType = bytesToStr(readVarBytes(bytes, 'txType'));
// Handle tx type specific parsing
switch (txType) {
case GENESIS_STR:
return readGenesis(bytes, tokenType);
case MINT_STR:
return readMint(bytes, tokenType);
case SEND_STR:
return readSend(bytes, tokenType);
case BURN_STR:
return readBurn(bytes, tokenType);
default:
throw new Error('Unknown txType');
}
}
function readGenesis(bytes: Bytes, tokenType: AlpTokenType_Number): AlpGenesis {
const tokenTicker = bytesToStr(readVarBytes(bytes, 'tokenTicker'));
const tokenName = bytesToStr(readVarBytes(bytes, 'tokenName'));
const url = bytesToStr(readVarBytes(bytes, 'url'));
const data = readVarBytes(bytes, 'data');
const authPubkey = readVarBytes(bytes, 'authPubkey');
const decimals = readU8(bytes, 'decimals');
const mintData = readMintData(bytes);
ensureEnd(bytes, 'GENESIS');
return {
txType: GENESIS_STR,
tokenType,
genesisInfo: {
tokenTicker,
tokenName,
url,
data: toHex(data),
authPubkey: toHex(authPubkey),
decimals,
},
mintData,
};
}
function readMint(bytes: Bytes, tokenType: AlpTokenType_Number): AlpMint {
const tokenId = readTokenId(bytes);
const mintData = readMintData(bytes);
ensureEnd(bytes, 'MINT');
return {
txType: MINT_STR,
tokenType,
tokenId,
mintData,
};
}
function readSend(bytes: Bytes, tokenType: AlpTokenType_Number): AlpSend {
const tokenId = readTokenId(bytes);
const sendAtomsArray = readAtomsArray(bytes, 'sendAtomsArray');
ensureEnd(bytes, 'SEND');
return {
txType: SEND_STR,
tokenType,
tokenId,
sendAtomsArray,
};
}
function readBurn(bytes: Bytes, tokenType: AlpTokenType_Number): AlpBurn {
const tokenId = readTokenId(bytes);
const burnAtoms = readU48(bytes, 'burnAtoms');
ensureEnd(bytes, 'BURN');
return {
txType: BURN_STR,
tokenType,
tokenId,
burnAtoms,
};
}
function readU8(bytes: Bytes, name: string): number {
if (bytes.idx >= bytes.data.length) {
throw new Error(`Missing ${name}`);
}
return bytes.readU8();
}
function readU48(bytes: Bytes, name: string): bigint {
if (bytes.idx >= bytes.data.length) {
throw new Error(`Missing ${name}`);
}
return bytes.readU48();
}
function readTokenId(bytes: Bytes): string {
// Note: ALP token ID endianness is little-endian (like in prevOut)
return toHexRev(bytes.readBytes(TOKEN_ID_NUM_BYTES));
}
function readSize(bytes: Bytes, name: string): number {
const size = readU8(bytes, name);
if (size > ALP_MAX_SIZE) {
throw new Error(`Size must be between 0 and ${ALP_MAX_SIZE}`);
}
return size;
}
function readVarBytes(bytes: Bytes, name: string): Uint8Array {
const numBytes = readSize(bytes, name);
return bytes.readBytes(numBytes);
}
function readAtomsArray(bytes: Bytes, name: string): bigint[] {
const numAtoms = readSize(bytes, name);
const atomsArray = [];
for (let i = 0; i < numAtoms; ++i) {
atomsArray.push(bytes.readU48());
}
return atomsArray;
}
function readMintData(bytes: Bytes): MintData {
const atomsArray = readAtomsArray(bytes, 'atomsArray');
const numBatons = readU8(bytes, 'numBatons');
if (numBatons > ALP_MAX_SIZE) {
throw new Error(`numBatons must be between 0 and ${ALP_MAX_SIZE}`);
}
return {
atomsArray,
numBatons,
};
}
function ensureEnd(bytes: Bytes, txType: string) {
if (bytes.idx < bytes.data.length) {
throw new Error(`Superfluous ${txType} bytes`);
}
}