Created
June 20, 2023 15:42
-
-
Save owenkellogg/37acdb2d744293af8f106fa3e203ccd7 to your computer and use it in GitHub Desktop.
parse_ordinals.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as fs from 'fs'; | |
import * as https from 'https'; | |
import * as base64 from 'base64-js'; | |
import { ArgumentParser } from 'argparse'; | |
interface CLIArgs { | |
dataUri: boolean; | |
output?: string; | |
txId: string; | |
} | |
interface TransactionResponse { | |
vin: Array<{ witness: string[] }>; | |
} | |
let args: CLIArgs; | |
let raw: Buffer; | |
let pointer: number; | |
function getCLIArgs(): CLIArgs { | |
const ap = new ArgumentParser({ | |
description: 'Parse and output the ordinal inscription inside transaction', | |
}); | |
ap.addArgument('txId', { help: 'transaction ID to retrieve from the API' }); | |
ap.addArgument('-du', '--data-uri', { | |
action: 'storeTrue', | |
help: 'print inscription as data-uri instead of writing to a file', | |
}); | |
ap.addArgument('-o', '--output', { help: 'write inscription to OUTPUT file' }); | |
return ap.parseArgs(); | |
} | |
function getRawData(txId: string): Buffer { | |
const url = `https://mempool.space/api/tx/${txId}`; | |
return new Promise<Buffer>((resolve, reject) => { | |
https.get(url, (response) => { | |
let data = ''; | |
response.on('data', (chunk) => { | |
data += chunk; | |
}); | |
response.on('end', () => { | |
if (response.statusCode !== 200) { | |
console.error(`Failed to retrieve transaction data for ${txId} from the API`); | |
process.exit(1); | |
} | |
const txResponse: TransactionResponse = JSON.parse(data); | |
const txWitness = txResponse.vin[0].witness; | |
const txWitnessString = txWitness.join(''); | |
resolve(Buffer.from(txWitnessString, 'hex')); | |
}); | |
}).on('error', (error) => { | |
console.error(`Failed to retrieve transaction data for ${txId} from the API`); | |
process.exit(1); | |
}); | |
}); | |
} | |
function readBytes(n: number = 1): Buffer { | |
const value = raw.slice(pointer, pointer + n); | |
pointer += n; | |
return value; | |
} | |
function getInitialPosition(): number { | |
const inscriptionMark = Buffer.from('0063036f7264', 'hex'); | |
try { | |
return raw.indexOf(inscriptionMark) + inscriptionMark.length; | |
} catch (error) { | |
console.error('No ordinal inscription found in transaction'); | |
process.exit(1); | |
} | |
} | |
function readContentType(): string { | |
const OP_1 = Buffer.from('51', 'hex'); | |
const byte = readBytes(); | |
if (!byte.equals(OP_1)) { | |
if (!byte.equals(Buffer.from('01', 'hex'))) { | |
console.assert(readBytes().equals(Buffer.from('01', 'hex'))); | |
} | |
} | |
const size = readBytes().readUIntBE(0, byte.length); | |
const contentType = readBytes(size).toString('utf8'); | |
return contentType; | |
} | |
function readPushData(opcode: Buffer): Buffer { | |
const intOpcode = opcode.readUIntBE(0, opcode.length); | |
if (intOpcode >= 0x01 && intOpcode <= 0x4b) { | |
return readBytes(intOpcode); | |
} | |
let numBytes = 0; | |
switch (intOpcode) { | |
case 0x4c: | |
numBytes = 1; | |
break; | |
case 0x4d: | |
numBytes = 2; | |
break; | |
case 0x4e: | |
numBytes = 4; | |
break; | |
default: | |
console.error(`Invalid push opcode ${opcode.toString('hex')} at position ${pointer}`); | |
process.exit(1); | |
} | |
const size = readBytes(numBytes).readUIntLE(0, numBytes); | |
return readBytes(size); | |
} | |
function writeDataUri(data: Buffer, contentType: string): void { | |
const dataBase64 = base64.fromByteArray(data).replace(/\n/g, ''); | |
console.log(`data:${contentType};base64,${dataBase64}`); | |
} | |
function writeFile(data: Buffer, ext: string): void { | |
let filename = args.output; | |
if (filename === undefined) { | |
filename = 'out'; | |
} | |
let baseFilename = filename; | |
let i = 1; | |
while (fs.existsSync(filename)) { | |
i++; | |
filename = `${baseFilename}${i}`; | |
} | |
console.log(`Writing contents to file "${filename}"`); | |
fs.writeFileSync(`${filename}.${ext}`, data); | |
} | |
function getFileExtension(contentType: string): string { | |
const switcher: Record<string, string> = { | |
'text/plain;charset=utf-8': 'txt', | |
'text/html;charset=utf-8': 'html', | |
}; | |
const fileExtension = switcher[contentType] || contentType.split('/')[1]; | |
return fileExtension; | |
} | |
async function main() { | |
args = getCLIArgs(); | |
raw = await getRawData(args.txId); | |
pointer = getInitialPosition(); | |
const contentType = readContentType(); | |
console.log(`Content type: ${contentType}`); | |
const fileExtension = getFileExtension(contentType); | |
console.assert(readBytes().equals(Buffer.from('00', 'hex'))); | |
const data: Buffer[] = []; | |
const OP_ENDIF = Buffer.from('68', 'hex'); | |
let opcode = readBytes(); | |
while (!opcode.equals(OP_ENDIF)) { | |
const chunk = readPushData(opcode); | |
data.push(chunk); | |
opcode = readBytes(); | |
} | |
const finalData = Buffer.concat(data); | |
console.log(`Total size: ${finalData.length} bytes`); | |
if (args.dataUri) { | |
writeDataUri(finalData, contentType); | |
} else { | |
writeFile(finalData, fileExtension); | |
} | |
console.log('\nDone'); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment