Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/inscription/inscription-parser.service.bip110.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { readTransaction } from '../../testdata/test.helper';
import { InscriptionParserService } from './inscription-parser.service';

/**
* BIP-110 compatible envelope (ordinals/ord#4545). Payload identical to
* the classic ord envelope; only the outer wrapper differs.
*
* SYNTHETIC DATA: no BIP-110 shape inscription exists on chain yet.
* The fixture uses a "deadbeef"-repeat txid. GOLDEN RULE waived so the
* wire-format parsing is pinned ahead of the first real tx.
*/
describe('BIP-110 compatible inscription envelope', () => {

it('parses a "Hello, world!" inscription wrapped in the BIP-110 envelope', async () => {

const txid = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
const inscription = InscriptionParserService.parse(readTransaction(txid))[0];

expect(inscription.inscriptionId).toBe(`${txid}i0`);
expect(inscription.contentType).toBe('text/plain');
expect(await inscription.getContent()).toBe('Hello, world!');
// 4-byte "ord" marker + 5 pushes (32 bytes) + 3 drops.
expect(inscription.envelopeSize).toBe(35);
});
});
4 changes: 1 addition & 3 deletions src/inscription/inscription-parser.service.envelope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,6 @@ describe('Inscription parser', () => {
expect(txWitness).toEqual('0000000000000000000000000000000000000000000000000000000000000000');

const raw = hexToBytes(txWitness);
const position = getNextInscriptionMark(raw, 0);

expect(position).toEqual(-1);
expect(getNextInscriptionMark(raw, 0)).toBeNull();
});
});
34 changes: 15 additions & 19 deletions src/inscription/inscription-parser.service.helper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,37 +46,33 @@ describe('getKnownFieldValues', () => {

describe('getNextInscriptionMark', () => {

it('should find the inscription mark (00 63 03 6f 72 64) and return the position after it', () => {
it('finds the classic inscription mark and returns the position after it', () => {
const raw = new Uint8Array([0, 1, 2, 0x00, 0x63, 0x03, 0x6f, 0x72, 0x64, 10, 20]);
const startPosition = 0;
const expectedPosition = 9;
expect(getNextInscriptionMark(raw, startPosition)).toEqual(expectedPosition);
expect(getNextInscriptionMark(raw, 0)).toEqual({ pointer: 9, isClassic: true });
});

it('should return -1 if the inscription mark is not found', () => {
it('returns null when the mark is not found', () => {
const raw = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const startPosition = 0;
expect(getNextInscriptionMark(raw, startPosition)).toEqual(-1);
expect(getNextInscriptionMark(raw, 0)).toBeNull();
});

it('should correctly handle an empty array', () => {
const raw = new Uint8Array([]);
const startPosition = 0;
expect(getNextInscriptionMark(raw, startPosition)).toEqual(-1);
it('handles an empty array', () => {
expect(getNextInscriptionMark(new Uint8Array([]), 0)).toBeNull();
});

it('should find the inscription mark even if it starts next to the end of the array', () => {
it('finds the mark near the end of the array', () => {
const raw = new Uint8Array([0, 1, 2, 0x00, 0x63, 0x03, 0x6f, 0x72, 0x64]);
const startPosition = 3;
const expectedPosition = 9;
expect(getNextInscriptionMark(raw, startPosition)).toEqual(expectedPosition);
expect(getNextInscriptionMark(raw, 3)).toEqual({ pointer: 9, isClassic: true });
});

it('should find the inscription mark starting exactly at the startPosition', () => {
it('finds the mark at startPosition', () => {
const raw = new Uint8Array([0x00, 0x63, 0x03, 0x6f, 0x72, 0x64, 10, 20, 30]);
const startPosition = 0;
const expectedPosition = 6;
expect(getNextInscriptionMark(raw, startPosition)).toEqual(expectedPosition);
expect(getNextInscriptionMark(raw, 0)).toEqual({ pointer: 6, isClassic: true });
});

it('classifies a bare "ord" push (no OP_FALSE OP_IF prefix) as BIP-110', () => {
const raw = new Uint8Array([0x03, 0x6f, 0x72, 0x64, 10, 20]);
expect(getNextInscriptionMark(raw, 0)).toEqual({ pointer: 4, isClassic: false });
});
});

Expand Down
88 changes: 38 additions & 50 deletions src/inscription/inscription-parser.service.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,54 +105,47 @@ export function getKnownFieldValues(fields: { tag: number; value: Uint8Array }[]
}

/**
* Searches for the next position of the ordinal inscription mark (0063036f7264)
* within the raw transaction data, starting from a given position.
*
* This function looks for a specific sequence of 6 bytes that represents the start of an ordinal inscription.
* If the sequence is found, the function returns the index immediately following the inscription mark.
* If the sequence is not found, the function returns -1, indicating no inscription mark was found.
*
* Note: This function uses a simple hardcoded approach based on the fixed length of the inscription mark.
* Envelope marker hex patterns emitted by ord.
* INSCRIPTION_MARK_HEX = OP_FALSE OP_IF OP_PUSHBYTES_3 'ord'
* BIP110_INSCRIPTION_MARK_HEX = OP_PUSHBYTES_3 'ord' (ord#4545)
*/
export const INSCRIPTION_MARK_HEX = '0063036f7264';
export const BIP110_INSCRIPTION_MARK_HEX = '036f7264';

/**
* Find the next "ord" push in the raw witness. Returns the position
* immediately after the marker plus which envelope shape carries it
* (classic if the marker is preceded by OP_FALSE OP_IF, BIP-110
* otherwise).
*
* @returns The position immediately after the inscription mark, or -1 if not found.
* @returns { pointer, isClassic } or null when no more markers.
*/
export function getNextInscriptionMark(raw: Uint8Array, startPosition: number): number {

// OP_FALSE
// OP_IF
// OP_PUSHBYTES_3: This pushes the next 3 bytes onto the stack.
// 0x6f, 0x72, 0x64: These bytes translate to the ASCII string "ord"
const inscriptionMark = new Uint8Array([OP_FALSE, OP_IF, OP_PUSHBYTES_3, 0x6f, 0x72, 0x64]);

for (let index = startPosition; index <= raw.length - 6; index++) {
if (raw[index] === inscriptionMark[0] &&
raw[index + 1] === inscriptionMark[1] &&
raw[index + 2] === inscriptionMark[2] &&
raw[index + 3] === inscriptionMark[3] &&
raw[index + 4] === inscriptionMark[4] &&
raw[index + 5] === inscriptionMark[5]) {
return index + 6;
export function getNextInscriptionMark(raw: Uint8Array, startPosition: number): { pointer: number; isClassic: boolean } | null {

for (let i = startPosition; i <= raw.length - 4; i++) {
if (raw[i] === OP_PUSHBYTES_3 &&
raw[i + 1] === 0x6f &&
raw[i + 2] === 0x72 &&
raw[i + 3] === 0x64) {
const isClassic = i >= 2 && raw[i - 2] === OP_FALSE && raw[i - 1] === OP_IF;
return { pointer: i + 4, isClassic };
}
}

return -1;
return null;
}

/**
* Checks if an inscription mark is found within a witness array.
* The Inscription mark hex corresponds to OP_FALSE, OP_IF, OP_PUSHBYTES_3, 'o', 'r', 'd'.

* This code can potentially return false positive matches!
* Matches EITHER envelope shape via its exact hex pattern:
* - classic (OP_FALSE OP_IF OP_PUSHBYTES_3 'ord')
* - BIP-110 (OP_PUSHBYTES_3 'ord', ord#4545)
*
* @param witness - Array of strings, each representing a hexadecimal encoded witness element.
* @returns True if an inscription mark is found, false otherwise.
* This code can potentially return false positive matches!
*/
export function hasInscription(witness: string[]): boolean {

// OP_FALSE (0x00), OP_IF (0x63), OP_PUSHBYTES_3 (0x03), 'o', 'r', 'd' (0x6f, 0x72, 0x64)
const inscriptionMarkHex = '0063036f7264';

return isStringInArrayOfStrings(inscriptionMarkHex, witness);
return isStringInArrayOfStrings(INSCRIPTION_MARK_HEX, witness)
|| isStringInArrayOfStrings(BIP110_INSCRIPTION_MARK_HEX, witness);
}

/**
Expand Down Expand Up @@ -354,21 +347,17 @@ export function measureInscriptionSize(witness: string[]): number | null {
return null;
}

// Find the witness element that contains the inscription (the tapscript)
// OP_FALSE (0x00), OP_IF (0x63), OP_PUSHBYTES_3 (0x03), 'o', 'r', 'd' (0x6f, 0x72, 0x64)
const inscriptionMarkHex = '0063036f7264';
const element = witness.find(e => e.includes(inscriptionMarkHex));
// Find the classic-envelope witness element (the tapscript).
const element = witness.find(e => e.includes(INSCRIPTION_MARK_HEX));
if (!element) {
return null;
}

const raw = hexToBytes(element);

// Find the start of the inscription using the inscription mark
const startPosition = getNextInscriptionMark(raw, 0);

if (startPosition === -1) {
return null; // Inscription mark not found
const mark = getNextInscriptionMark(raw, 0);
if (!mark || !mark.isClassic) {
return null;
}

// Find the position of last OP_ENDIF (0x68)
Expand All @@ -378,11 +367,10 @@ export function measureInscriptionSize(witness: string[]): number | null {
return null; // OP_ENDIF not found
}

// The size of the inscription is from the start position to the last OP_ENDIF
const inscriptionSize = opEndIfIndex - startPosition;

// Add the size of the inscription mark (6 bytes) + OP_ENDIF (1 byte)
return inscriptionSize + 7;
// From the start of the classic wrapper (OP_FALSE OP_IF = 2 bytes
// before the 4-byte "ord" push, so mark.pointer - 6) to one past
// OP_ENDIF.
return opEndIfIndex + 1 - (mark.pointer - 6);
}

/**
Expand Down
Loading
Loading