-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #59 from bob-collective/feat/ordinals
feat: add code to inscribe ordinals
- Loading branch information
Showing
8 changed files
with
376 additions
and
3 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
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; | ||
} |
This file contains 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
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; | ||
} |
This file contains 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
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(); | ||
} |
This file contains 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
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>; | ||
}; |
This file contains 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
Oops, something went wrong.