Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Owen/add inscription fields #111

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"**/Thumbs.db": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"search.exclude": {
"**/dist": true,
Expand Down
5 changes: 5 additions & 0 deletions examples/node/inscribe.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ async function main() {
postage: 1500 // base value of the inscription in sats
})

transaction.withParent({
txid: '17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526ad',
index: 0
})

// generate deposit address and fee for inscription
const revealed = await transaction.generateCommit();
console.log(revealed) // deposit revealFee to address
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"bugs": "https://github.com/sadoprotocol/ordit-sdk/issues",
"license": "MIT",
"scripts": {
"prepare": "husky install && chmod ug+x .husky/*",
"prepare": "husky && chmod ug+x .husky/*",
"publish": "node ./scripts/publish.js",
"local": "turbo run local",
"build": "turbo run build",
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
}
7 changes: 6 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"scripts": {
"build": "tsc -b ./tsconfig.build.json",
"build:watch": "tsc -b ./tsconfig.build.json --watch",
"test": "jest",
"flush": "npm run clean && rm -rf ./node_modules",
"clean": "rm -rf ./.turbo ./dist",
"lint": "eslint ."
Expand All @@ -30,6 +31,7 @@
"bitcoinjs-lib": "^6.1.5",
"bitcoinjs-message": "^2.2.0",
"buffer-reverse": "^1.0.1",
"cbor": "^9.0.2",
"cross-fetch": "^3.1.8",
"ecpair": "^2.1.0",
"ethers": "^6.10.0",
Expand All @@ -38,7 +40,10 @@
"devDependencies": {
"@sadoprotocol/eslint-config": "^0.0.2",
"@sadoprotocol/typescript": "^0.0.2",
"@types/node": "^20.3.1"
"@types/jest": "^29.5.12",
"@types/node": "^20.3.1",
"jest": "^29.7.0",
"ts-jest": "^29.1.2"
},
"eslintConfig": {
"root": true,
Expand Down
115 changes: 115 additions & 0 deletions packages/sdk/src/inscription/encode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as cbor from 'cbor';
import { InscriptionID } from "./types";
import { encodeNumber, encodeInscriptionID, encodeJSONAsCBORBuffer } from './encode';

describe('encodeNumber', () => {
it('should encode a positive number correctly', () => {
const num = 123456;
const encoded = encodeNumber(num);
expect(encoded).toBeInstanceOf(Buffer);
expect(encoded.readInt32BE()).toBe(num);
});

it('should encode a negative number correctly', () => {
const num = -123456;
const encoded = encodeNumber(num);
expect(encoded).toBeInstanceOf(Buffer);
expect(encoded.readInt32BE()).toBe(num);
});

it('should encode zero correctly', () => {
const num = 0;
const encoded = encodeNumber(num);
expect(encoded).toBeInstanceOf(Buffer);
expect(encoded.readInt32BE()).toBe(num);
});

it('should throw an error if the number is too large', () => {
const num = 2 ** 31; // One more than the max 32-bit integer
expect(() => encodeNumber(num)).toThrow();
});

it('should throw an error if the number is too small', () => {
const num = -(2 ** 31) - 1; // One less than the min 32-bit integer
expect(() => encodeNumber(num)).toThrow();
});
});

describe('encodeInscriptionID', () => {
it('should correctly encode a given InscriptionID into a Buffer', () => {
// Arrange
const inscriptionID: InscriptionID = {
txid: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
index: 0
};

// Act
const resultBuffer = encodeInscriptionID(inscriptionID);

// Assert
expect(resultBuffer).toBeInstanceOf(Buffer);
expect(resultBuffer.toString('hex')).toBe('1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100');
});

it('should correctly encode a given InscriptionID into a Buffer with index 255', () => {
// Arrange
const inscriptionID: InscriptionID = {
txid: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
index: 255
};

// Act
const resultBuffer = encodeInscriptionID(inscriptionID);

// Assert
expect(resultBuffer).toBeInstanceOf(Buffer);
expect(resultBuffer.toString('hex')).toBe('1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100ff');
});

it('should correctly encode a given InscriptionID into a Buffer with index 256', () => {
// Arrange
const inscriptionID: InscriptionID = {
txid: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
index: 256
};

// Act
const resultBuffer = encodeInscriptionID(inscriptionID);

// Assert
expect(resultBuffer).toBeInstanceOf(Buffer);
expect(resultBuffer.toString('hex')).toBe('1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201000001');
});
});

describe('encodeJSONAsCBORBuffer', () => {
test('encodes a JSON object into a single CBOR buffer if under 520 bytes', () => {
const json = { key: 'value' };
const encodedBuffers = encodeJSONAsCBORBuffer(json);
expect(encodedBuffers).toHaveLength(1);
expect(Buffer.isBuffer(encodedBuffers[0])).toBe(true);
expect(encodedBuffers[0].length).toBeLessThanOrEqual(520);
expect(cbor.decode(encodedBuffers[0])).toEqual(json);
});

test('splits CBOR buffer into multiple parts if over 520 bytes', () => {
const largeJson = { longKey: 'a'.repeat(1000) }; // Adjust size to ensure it's over 520 bytes when encoded
const encodedBuffers = encodeJSONAsCBORBuffer(largeJson);
expect(encodedBuffers.length).toBeGreaterThan(1);
encodedBuffers.forEach(buffer => {
expect(Buffer.isBuffer(buffer)).toBe(true);
expect(buffer.length).toBeLessThanOrEqual(520);
});
const combinedBuffer = Buffer.concat(encodedBuffers);
expect(cbor.decode(combinedBuffer)).toEqual(largeJson);
});

test('handles edge case where CBOR encoding is exactly 520 bytes', () => {
const edgeCaseJson = { exactSizeKey: 'b'.repeat(512) }; // Adjust the repeat count to get an exact 520-byte output
const encodedBuffers = encodeJSONAsCBORBuffer(edgeCaseJson);
expect(encodedBuffers).toHaveLength(2);
expect(encodedBuffers[0].length).toEqual(520);
const combinedBuffer = Buffer.concat(encodedBuffers);
expect(cbor.decode(combinedBuffer)).toEqual(edgeCaseJson);
});
});
77 changes: 77 additions & 0 deletions packages/sdk/src/inscription/encode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@

import cbor from 'cbor';
import { InscriptionID } from "./types";

export function encodeNumber(num: number): Buffer {
const buffer = Buffer.alloc(4)
buffer.writeInt32BE(num);
return buffer;
}

export function encodeTag(tag: number): Buffer {
const buffer = Buffer.alloc(1)
buffer.writeInt8(tag);
return buffer;
}

function reverseBufferByteChunks(src: Buffer): Buffer {
const buffer = Buffer.alloc(src.length);
for (let i = 0, j = src.length - 1; i <= j; i++, j--) {
buffer[i] = src[j];
buffer[j] = src[i];
}
return buffer;
}


export function encodeInscriptionID(inscriptionID: InscriptionID): Buffer {
// Convert the txid string to a Buffer
const txidBuffer = Buffer.from(inscriptionID.txid, 'hex');
const reversedTxidBuffer = reverseBufferByteChunks(txidBuffer);

// Convert the index to a 4-byte little-endian Buffer
const indexBuffer = Buffer.alloc(4);
indexBuffer.writeUInt32LE(inscriptionID.index);

// Trim trailing zero bytes
let trimmedIndexBuffer = indexBuffer;
for (let i = indexBuffer.length - 1; i >= 0; i--) {
if (indexBuffer[i] === 0) {
trimmedIndexBuffer = indexBuffer.slice(0, i);
} else {
break;
}
}

return Buffer.concat([reversedTxidBuffer, trimmedIndexBuffer]);
}

export function encodeJSONAsCBORBuffer(json: any): Buffer[] {
// Encode the JSON object into CBOR format
const cborEncoded = cbor.encode(json);

// Check if the encoded CBOR data exceeds 520 bytes
if (cborEncoded.length <= 520) {
// If it does not exceed 520 bytes, directly return an array containing one Buffer
return [cborEncoded];
} else {
// If it exceeds 520 bytes, the Buffer needs to be split
const parts: Buffer[] = [];
let startIndex = 0;

while (startIndex < cborEncoded.length) {
// Calculate the length of the split, up to 520 bytes
const length = Math.min(520, cborEncoded.length - startIndex);
// Create a new Buffer to store the split part
const part = Buffer.alloc(length);
// Copy a portion of the CBOR data into the new Buffer
cborEncoded.copy(part, 0, startIndex, startIndex + length);
// Add the split part to the array
parts.push(part);
// Update the starting index
startIndex += length;
}

return parts;
}
}
15 changes: 15 additions & 0 deletions packages/sdk/src/inscription/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ export interface Inscription {
value?: number // postage
}

export interface InscriptionID {
txid: string,
index: number
}

export enum InscriptionFieldTag {
ContentType = 1,
Pointer = 2,
Parent = 3,
Metadata = 5,
Metaprotocol = 7,
ContentEncoding = 9,
Delegate = 11
}

export interface InputsToSign {
address: string
signingIndexes: number[]
Expand Down
74 changes: 74 additions & 0 deletions packages/sdk/src/inscription/witness.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as bitcoin from "bitcoinjs-lib";
import { buildWitnessScript, WitnessScriptOptions } from './witness';

describe('buildWitnessScript', () => {
test('throws error when required options are missing', () => {
const options: Partial<WitnessScriptOptions> = {};

expect(() => buildWitnessScript(options as WitnessScriptOptions)).toThrow();
});

test('includes empty fields in the script', () => {
const options: WitnessScriptOptions = {
xkey: 'somehexkey',
};

const script = buildWitnessScript(options);
const decompiled = bitcoin.script.decompile(script);
expect(decompiled).toBeTruthy()
});

test('includes delegate field in the script', () => {
const options: WitnessScriptOptions = {
xkey: 'somehexkey',
delegate: {
txid: '17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526ad',
index: 0
},
};

const script = buildWitnessScript(options);
const decompiled = bitcoin.script.decompile(script);
expect(decompiled).toBeTruthy()
});

test('returns a script with OP_CHECKSIG for recovery', () => {
const options: WitnessScriptOptions = {
xkey: 'somehexkey',
mediaType: 'text/plain',
mediaContent: 'Hello, world!',
recover: true,
};

const script = buildWitnessScript(options);
expect(script).toEqual(bitcoin.script.compile([
Buffer.from(options.xkey, "hex"),
bitcoin.opcodes.OP_CHECKSIG
]));
});

test('includes all provided fields in the script', () => {
const options: WitnessScriptOptions = {
xkey: 'somehexkey',
mediaType: 'text/plain',
mediaContent: 'Hello, world!',
meta: { some: 'metadata' },
pointer: 123,
parent: {
txid: '3bd72a7ef68776c9429961e43043ff65efa7fb2d8bb407386a9e3b19f149bc36',
index: 0
},
metaprotocol: 'metaprotocol-data',
contentEncoding: 'utf-8',
delegate: {
txid: '17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526ad',
index: 0
},
};

const script = buildWitnessScript(options);
const decompiled = bitcoin.script.decompile(script);
expect(decompiled).toBeTruthy()
});

});
Loading