Skip to content

Commit

Permalink
Merge pull request #59 from bob-collective/feat/ordinals
Browse files Browse the repository at this point in the history
feat: add code to inscribe ordinals
  • Loading branch information
gregdhill authored Oct 24, 2023
2 parents 3a84e31 + 2267c79 commit fa64969
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 3 deletions.
2 changes: 2 additions & 0 deletions sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"@types/mocha": "^10.0.2",
"@types/node": "^20.8.3",
"chai": "^4.3.10",
"ecpair": "^2.1.0",
"mocha": "^10.2.0",
"tiny-secp256k1": "^2.2.3",
"ts-mocha": "^10.0.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.2.2"
Expand Down
97 changes: 97 additions & 0 deletions sdk/src/ordinals/commit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as bitcoinjsLib from "bitcoinjs-lib";

const encoder = new TextEncoder();

function toXOnly(pubkey: Buffer) {
return pubkey.subarray(1, 33);
}

interface Inscription {
contentType: Buffer;
content: Buffer;
}

/**
* Create a basic text inscription.
*/
export function createTextInscription(text: string): Inscription {
const contentType = Buffer.from(encoder.encode("text/plain;charset=utf-8"));
const content = Buffer.from(encoder.encode(text));
return { contentType, content };
}

function createInscriptionScript(xOnlyPublicKey: Buffer, inscription: Inscription) {
const protocolId = Buffer.from(encoder.encode("ord"));
return [
xOnlyPublicKey,
bitcoinjsLib.opcodes.OP_CHECKSIG,
bitcoinjsLib.opcodes.OP_0,
bitcoinjsLib.opcodes.OP_IF,
protocolId,
1,
1,
inscription.contentType,
bitcoinjsLib.opcodes.OP_0,
inscription.content,
bitcoinjsLib.opcodes.OP_ENDIF,
];
}

export interface CommitTxData {
script: (number | Buffer)[];
scriptTaproot: bitcoinjsLib.payments.Payment;
outputScript: Buffer;
tapLeafScript: {
leafVersion: number;
script: Buffer;
controlBlock: Buffer;
}
}

/**
* Create the commit tx of the input public key and inscription data.
* @dev Requires caller to initialize ECC lib.
*/
export function createCommitTxData(
network: bitcoinjsLib.Network,
publicKey: Buffer,
inscription: Inscription
): CommitTxData {
const xOnlyPublicKey = toXOnly(publicKey);
const script = createInscriptionScript(xOnlyPublicKey, inscription);

const outputScript = bitcoinjsLib.script.compile(script);

const scriptTree = {
output: outputScript,
redeemVersion: 192, // 0xc0
};

const scriptTaproot = bitcoinjsLib.payments.p2tr({
internalPubkey: xOnlyPublicKey,
scriptTree,
redeem: scriptTree,
network,
});

const cblock = scriptTaproot.witness?.[scriptTaproot.witness.length - 1];

const tapLeafScript = {
leafVersion: scriptTaproot.redeemVersion!,
script: outputScript,
controlBlock: cblock,
};

return {
script,
scriptTaproot,
outputScript,
tapLeafScript,
};
}

export interface CommitTxResult {
txId: string;
sendUtxoIndex: number;
sendAmount: number;
}
99 changes: 99 additions & 0 deletions sdk/src/ordinals/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import * as bitcoinjsLib from "bitcoinjs-lib";

import { DummySigner, RemoteSigner } from "./signer";
import { CommitTxData, createCommitTxData, createTextInscription } from "./commit";
import { createRevealTx, customFinalizer, signRevealTx } from "./reveal";

export { RemoteSigner };

/**
* Estimate the virtual size of a 1 input 1 output reveal tx.
*/
function estimateTxSize(
network: bitcoinjsLib.Network,
publicKey: Buffer,
commitTxData: CommitTxData,
toAddress: string,
amount: number,
) {
const psbt = new bitcoinjsLib.Psbt({ network });

const { scriptTaproot, tapLeafScript } = commitTxData;
psbt.addInput({
hash: Buffer.alloc(32, 0),
index: 0,
witnessUtxo: {
value: amount,
script: scriptTaproot.output!,
},
tapLeafScript: [tapLeafScript],
});

psbt.addOutput({
value: amount,
address: toAddress,
});

psbt.signInput(0, new DummySigner(publicKey));
psbt.finalizeInput(0, customFinalizer(commitTxData));

const tx = psbt.extractTransaction();
return tx.virtualSize();
}

/**
* Inscribe some text data on Bitcoin using the remote signer.
*
* @param signer - Implementation to interact with Bitcoin and sign the PSBT.
* @param toAddress - The address to receive the inscription.
* @param feeRate - Fee rate of the Bitcoin network (satoshi / byte).
* @param text - Data to inscribe in the witness of the reveal transaction.
* @param postage - Amount of postage to include in the inscription.
* @returns Promise which resolves to the reveal transaction.
*/
export async function inscribeText(
signer: RemoteSigner,
toAddress: string,
feeRate: number,
text: string,
postage = 10000,
) {
const bitcoinNetwork = await signer.getNetwork();
const publicKey = Buffer.from(await signer.getPublicKey(), "hex");

const inscription = createTextInscription(text);
const commitTxData = createCommitTxData(bitcoinNetwork, publicKey, inscription);

const revealTxSize = estimateTxSize(bitcoinNetwork, publicKey, commitTxData, toAddress, postage);

// https://github.com/ordinals/ord/blob/ea1c7c8f73e1c30df547000ac7ccd82051cb60af/src/subcommand/wallet/inscribe/batch.rs#L501
const revealFee = revealTxSize * feeRate;
// https://github.com/ordinals/ord/blob/ea1c7c8f73e1c30df547000ac7ccd82051cb60af/src/subcommand/wallet/inscribe/batch.rs#L327
const commitTxAmount = revealFee + postage;

const commitAddress = commitTxData.scriptTaproot.address!;
const commitTxId = await signer.sendToAddress(commitAddress, commitTxAmount);
const commitUtxoIndex = await signer.getUtxoIndex(commitAddress, commitTxId);

const commitTxResult = {
txId: commitTxId,
sendUtxoIndex: commitUtxoIndex,
sendAmount: commitTxAmount,
};

const revealPsbt = createRevealTx(
bitcoinNetwork,
commitTxData,
commitTxResult,
toAddress,
postage,
);

const revealTx = await signRevealTx(
signer,
commitTxData,
revealPsbt
);

return revealTx;
}
67 changes: 67 additions & 0 deletions sdk/src/ordinals/reveal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as bitcoinjsLib from "bitcoinjs-lib";
import * as psbtUtils from "bitcoinjs-lib/src/psbt/psbtutils";

import { RemoteSigner } from "./signer";
import { CommitTxData, CommitTxResult } from "./commit";

const { witnessStackToScriptWitness } = psbtUtils;

/**
* Create the reveal tx which spends the commit tx.
*/
export function createRevealTx(
network: bitcoinjsLib.Network,
commitTxData: CommitTxData,
commitTxResult: CommitTxResult,
toAddress: string,
amount: number,
) {
const { scriptTaproot, tapLeafScript } = commitTxData;

const psbt = new bitcoinjsLib.Psbt({ network });

psbt.addInput({
hash: commitTxResult.txId,
index: commitTxResult.sendUtxoIndex,
witnessUtxo: {
value: commitTxResult.sendAmount,
script: scriptTaproot.output!,
},
tapLeafScript: [tapLeafScript],
});

psbt.addOutput({
value: amount,
address: toAddress,
});

return psbt;
}

export const customFinalizer = (commitTxData: CommitTxData) => {
const { outputScript, tapLeafScript } = commitTxData;

return (inputIndex: number, input: any) => {
const witness = [input.tapScriptSig[inputIndex].signature]
.concat(outputScript)
.concat(tapLeafScript.controlBlock);

return {
finalScriptWitness: witnessStackToScriptWitness(witness),
};
};
}

export async function signRevealTx(
signer: RemoteSigner,
commitTxData: CommitTxData,
psbt: bitcoinjsLib.Psbt
) {
// reveal should only have one input
psbt = await signer.signPsbt(0, psbt);

// we have to construct our witness script in a custom finalizer
psbt.finalizeInput(0, customFinalizer(commitTxData));

return psbt.extractTransaction();
}
58 changes: 58 additions & 0 deletions sdk/src/ordinals/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Network, Psbt, Signer } from "bitcoinjs-lib";

/**
* Dummy signer implementation used to estimate tx fees.
*/
export class DummySigner implements Signer {
publicKey: Buffer;
constructor(publicKey: Buffer) {
this.publicKey = publicKey;
}
sign(_hash: Buffer, _lowR?: boolean | undefined): Buffer {
// https://github.com/bitcoin/bitcoin/blob/607d5a46aa0f5053d8643a3e2c31a69bfdeb6e9f/src/script/sign.cpp#L611
return Buffer.from("304502210100000000000000000000000000000000000000000000000000000000000000000220010000000000000000000000000000000000000000000000000000000000000001", "hex");
}
signSchnorr(hash: Buffer): Buffer {
// https://github.com/bitcoin/bitcoin/blob/607d5a46aa0f5053d8643a3e2c31a69bfdeb6e9f/src/script/sign.cpp#L626
return Buffer.alloc(64, 0);
}
}

export interface RemoteSigner {
/**
* Get the configured Bitcoin network.
*
* @returns {Promise<string>} A promise that resolves to the current network.
*/
getNetwork(): Promise<Network>;
/**
* Get the configured public key of the signer.
*
* @returns {Promise<string>} A promise that resolves to the hex encoded public key.
*/
getPublicKey(): Promise<string>;
/**
* Send an amount of Satoshis to the recipient.
*
* @param {string} toAddress - The address of the recipient.
* @param {number} amount - The Satoshis the recipient should receive.
* @returns {Promise<string>} A promise that resolves to the transaction ID.
*/
sendToAddress(toAddress: string, amount: number): Promise<string>;
/**
* Get the index of a UTXO in a transaction based on the recipient address.
*
* @param {string} toAddress - The address of the recipient.
* @param {string} txId - The transaction ID to check.
* @returns {Promise<number>} A promise that resolves to the UTXO index.
*/
getUtxoIndex(toAddress: string, txId: string): Promise<number>;
/**
* Sign the PSBT at the specified input index.
*
* @param {number} inputIndex - The input index to sign for.
* @param {Psbt} psbt - The PSBT containing that input.
* @returns {Promise<Psbt>} A promise that resolves to the signed PSBT.
*/
signPsbt(inputIndex: number, psbt: Psbt): Promise<Psbt>;
};
3 changes: 1 addition & 2 deletions sdk/src/relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export async function getBitcoinTxInfo(
electrsClient: ElectrsClient,
txId: string,
forWitness?: boolean,
forWitness?: boolean,
): Promise<BitcoinTxInfo> {
const txHex = await electrsClient.getTransactionHex(txId);
const tx = Transaction.fromHex(txHex);
Expand Down Expand Up @@ -125,7 +124,7 @@ export async function getBitcoinTxProof(
bitcoinHeaders: bitcoinHeaders,
}
}

/**
* Retrieves Bitcoin block headers using an Electrs client.
*
Expand Down
Loading

0 comments on commit fa64969

Please sign in to comment.