diff --git a/examples/node/inscribeV2.js b/examples/node/inscribeV2.js new file mode 100644 index 00000000..e36d07e8 --- /dev/null +++ b/examples/node/inscribeV2.js @@ -0,0 +1,65 @@ +import { bulkMintFromCollection, InscriberV2, JsonRpcDatasource } from "@sadoprotocol/ordit-sdk" +import { Inscriber, Ordit } from "@sadoprotocol/ordit-sdk" + +const MNEMONIC = "" +const network = "testnet" +const datasource = new JsonRpcDatasource({ network }) + +async function main() { + // init wallet + const serverWallet = new Ordit({ + bip39: MNEMONIC, + network + }) + + serverWallet.setDefaultAddress("taproot") + + const ordinalReceiverAddress = "
" + const paymentRefundAddress = "
" + + // new inscription tx + const transaction = await bulkMintFromCollection({ + address: serverWallet.selectedAddress, + publicKey: serverWallet.publicKey, + publisherAddress: serverWallet.selectedAddress, + collectionGenesis: "df91a6386fb9b55bd754d6ec49e97e1be4c80ac49e4242ff773634e4c23cc427", + changeAddress: paymentRefundAddress, + feeRate: 10, + outputs: [{ address: ordinalReceiverAddress, value: 999 }], + network, + datasource, + taptreeVersion: "3", + inscriptions: [ + { + mediaContent: "Hello World", + mediaType: "text/plain", + postage: 1000, + nonce: 0, + receiverAddress: ordinalReceiverAddress, + iid: "testhello", + signature: "sig" + } + ] + }) + + // generate deposit address and fee for inscription + const revealed = await transaction.generateCommit() + console.log(revealed) // deposit revealFee to address + + // confirm if deposit address has been funded + const ready = await transaction.isReady() + + if (ready || transaction.ready) { + // build transaction + await transaction.build() + + // sign transaction + const signedTxHex = serverWallet.signPsbt(transaction.toHex(), { isRevealTx: true }) + + // Broadcast transaction + const tx = await datasource.relay({ hex: signedTxHex }) + console.log(tx) + } +} + +main() diff --git a/examples/node/package.json b/examples/node/package.json index 20dd0919..a02e85ff 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "inscribe": "node inscribe", + "inscribeV2": "node inscribeV2", "read": "node read", "send": "node send", "create-psbt": "node create-psbt", diff --git a/packages/sdk/src/inscription/collection.ts b/packages/sdk/src/inscription/collection.ts index 5d350c2a..030a83de 100644 --- a/packages/sdk/src/inscription/collection.ts +++ b/packages/sdk/src/inscription/collection.ts @@ -1,7 +1,17 @@ -import { BaseDatasource, GetWalletOptions, Inscriber, JsonRpcDatasource, TaptreeVersion, verifyMessage } from ".." +import { + BaseDatasource, + EnvelopeOpts, + GetWalletOptions, + Inscriber, + InscriberV2, + JsonRpcDatasource, + TaptreeVersion, + verifyMessage +} from ".." import { Network } from "../config/types" import { MAXIMUM_ROYALTY_PERCENTAGE } from "../constants" import { OrditSDKError } from "../utils/errors" +import { buildMeta } from "./meta" export async function publishCollection({ title, @@ -101,6 +111,76 @@ export async function mintFromCollection(options: MintFromCollectionOptions) { return new Inscriber({ ...options, meta }) } +export async function bulkMintFromCollection({ + inscriptions, + collectionGenesis, + publisherAddress, + address, + publicKey, + feeRate, + datasource, + network, + outputs, + changeAddress, + taptreeVersion +}: BulkMintFromCollectionOptions) { + let currentPointer = 0 + + const { metaList, inscriptionList } = inscriptions.reduce<{ + metaList: EnvelopeOpts[] + inscriptionList: EnvelopeOpts[] + }>( + (acc, insc) => { + const { nonce, mediaContent, mediaType, receiverAddress, postage, iid,signature } = insc + + const meta = buildMeta({ + collectionGenesis, + iid, + publisher: publisherAddress, + nonce, + receiverAddress, + signature + }) + + const metaEnvelope: EnvelopeOpts = { + mediaContent: JSON.stringify(meta), + mediaType: "application/json;charset=utf-8", + receiverAddress, + postage + } + + const inscriptionEnvelope: EnvelopeOpts = { + mediaContent, + mediaType, + pointer: currentPointer === 0 ? undefined : currentPointer.toString(), + receiverAddress, + postage + } + + currentPointer += postage + + return { + metaList: [...acc.metaList, metaEnvelope], + inscriptionList: [...acc.inscriptionList, inscriptionEnvelope] + } + }, + { metaList: [], inscriptionList: [] } + ) + + return new InscriberV2({ + address, + publicKey, + feeRate, + datasource, + network, + outputs, + changeAddress, + taptreeVersion, + metaInscriptions: metaList, + inscriptions: inscriptionList + }) +} + function validateInscriptions(inscriptions: CollectionInscription[] = []) { if (!inscriptions.length) return false @@ -174,4 +254,29 @@ export type MintFromCollectionOptions = Pick & { taptreeVersion?: TaptreeVersion } +export type BulkMintFromCollectionOptions = { + feeRate: number + changeAddress: string + address: string + publicKey: string + network: Network + inscriptions: InscriptionsToMint[] + outputs?: Outputs + enableRBF?: boolean + datasource?: BaseDatasource + taptreeVersion?: TaptreeVersion + collectionGenesis: string + publisherAddress: string +} + +export type InscriptionsToMint = { + nonce: number + mediaContent: string + mediaType: string + receiverAddress: string + postage: number + iid: string + signature?: string // deprecated only used for backward compatibility +} + type Outputs = Array<{ address: string; value: number }> diff --git a/packages/sdk/src/inscription/encoding.ts b/packages/sdk/src/inscription/encoding.ts new file mode 100644 index 00000000..6a8c4cec --- /dev/null +++ b/packages/sdk/src/inscription/encoding.ts @@ -0,0 +1,50 @@ +import { splitInscriptionId } from "../utils" + +export function trimTrailingZeroBytes(buffer: Buffer): Buffer { + let trimmedBuffer = buffer + for (let i = buffer.length - 1; i >= 0; i--) { + // find the first non-zero byte + if (buffer[i] !== 0) { + trimmedBuffer = buffer.subarray(0, i + 1) + break + } + } + return trimmedBuffer +} + +export function encodePointer(num: number | string | bigint): Buffer { + const buffer = Buffer.allocUnsafe(8) + buffer.writeBigUInt64LE(BigInt(num)) + return trimTrailingZeroBytes(buffer) +} + +export function encodeTag(tag: number): Buffer { + let tagInHex = tag.toString(16) + // ensure even length or Buffer.from will remove odd length bytes + if (tagInHex.length % 2 !== 0) { + tagInHex = "0" + tagInHex + } + return Buffer.from(tagInHex, "hex") +} + +function reverseBufferByteChunks(src: Buffer): Buffer { + const buffer = Buffer.from(src) + return buffer.reverse() +} + +export function encodeInscriptionId(inscriptionId: string): Buffer { + const { txId, index } = splitInscriptionId(inscriptionId) + + // reverse txId byte + const txidBuffer = Buffer.from(txId, "hex") + const reversedTxIdBuffer = reverseBufferByteChunks(txidBuffer) + + // Convert index to little-endian, max 4 bytes + const indexBuffer = Buffer.alloc(4) + indexBuffer.writeUInt32LE(index) + + // Trim trailing zero bytes + const trimmedIndexBuffer = trimTrailingZeroBytes(indexBuffer) + + return Buffer.concat([reversedTxIdBuffer, trimmedIndexBuffer]) +} diff --git a/packages/sdk/src/inscription/meta.ts b/packages/sdk/src/inscription/meta.ts new file mode 100644 index 00000000..b4c1b06d --- /dev/null +++ b/packages/sdk/src/inscription/meta.ts @@ -0,0 +1,22 @@ +export interface MetaParams { + collectionGenesis: string + iid: string + publisher: string + nonce: number + receiverAddress: string + signature?: string +} + +export function buildMeta({ collectionGenesis, iid, publisher, nonce, receiverAddress, signature }: MetaParams) { + return { + p: "vord", + v: 1, + ty: "insc", + col: collectionGenesis, + iid, + publ: publisher, + nonce: nonce, + minter: receiverAddress, + sig: signature + } +} diff --git a/packages/sdk/src/inscription/types.ts b/packages/sdk/src/inscription/types.ts index 8b271575..a0977786 100644 --- a/packages/sdk/src/inscription/types.ts +++ b/packages/sdk/src/inscription/types.ts @@ -47,3 +47,12 @@ export interface InputsToSign { signingIndexes: number[] sigHash?: number } + +export interface EnvelopeOpts { + mediaContent?: string + mediaType?: string + pointer?: string + delegateInscriptionId?: string + receiverAddress: string + postage: number +} \ No newline at end of file diff --git a/packages/sdk/src/inscription/witness.ts b/packages/sdk/src/inscription/witness.ts index 3cce6eef..0c0d3673 100644 --- a/packages/sdk/src/inscription/witness.ts +++ b/packages/sdk/src/inscription/witness.ts @@ -3,6 +3,18 @@ import * as bitcoin from "bitcoinjs-lib" import { MAXIMUM_SCRIPT_ELEMENT_SIZE } from "../constants" import { OrditSDKError } from "../utils/errors" +import { encodeInscriptionId, encodePointer, encodeTag } from "./encoding" +import { EnvelopeOpts } from "./types" + +export const INSCRIPTION_FIELD_TAG = { + ContentType: encodeTag(1), + Pointer: encodeTag(2), + Parent: encodeTag(3), + Metadata: encodeTag(5), + Metaprotocol: encodeTag(7), + ContentEncoding: encodeTag(9), + Delegate: encodeTag(11) +} export function buildWitnessScript({ recover = false, ...options }: WitnessScriptOptions) { bitcoin.initEccLib(ecc) @@ -59,6 +71,30 @@ export function buildWitnessScript({ recover = false, ...options }: WitnessScrip ]) } +export function buildWitnessScriptV2({ xkey, envelopes }: WitnessScriptV2Options) { + bitcoin.initEccLib(ecc) + if (!xkey) { + throw new OrditSDKError("xkey is required to build witness script") + } + + const envelopesStackElements:(number | Buffer)[] = [] + // build all envelopes + for (const envelopeOpt of envelopes) { + const envelope = buildEnvelope(envelopeOpt) + envelopesStackElements.push(...envelope) + } + + return bitcoin.script.compile([ + Buffer.from(xkey, "hex"), + bitcoin.opcodes.OP_CHECKSIG, + ...envelopesStackElements + ]) +} + +export function buildRecoverWitnessScript(xkey: string) { + return bitcoin.script.compile([Buffer.from(xkey, "hex"), bitcoin.opcodes.OP_CHECKSIG]) +} + function opPush(data: string | Buffer) { const buff = Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8") if (buff.byteLength > MAXIMUM_SCRIPT_ELEMENT_SIZE) @@ -81,6 +117,50 @@ export const chunkContent = function (str: string, encoding: BufferEncoding = "u return chunks } +export const buildEnvelope = function ({delegateInscriptionId,mediaContent,mediaType,pointer}: EnvelopeOpts) { + if (!delegateInscriptionId && !mediaContent && !mediaType) { + throw new OrditSDKError("mediaContent and mediaType are required to build an envelope") + } + if (!!delegateInscriptionId && !!mediaContent && !!mediaType) { + throw new OrditSDKError("Cannot build an envelope with both media content and a delegate inscription id") + } + + const baseStackElements = [ + bitcoin.opcodes.OP_FALSE, + bitcoin.opcodes.OP_IF, + opPush("ord"), + ] + + if (pointer) { + baseStackElements.push( + INSCRIPTION_FIELD_TAG.Pointer, + encodePointer(pointer) + ) + } + + if (delegateInscriptionId) { + baseStackElements.push( + INSCRIPTION_FIELD_TAG.Delegate, + encodeInscriptionId(delegateInscriptionId) + ) + } + + // TODO: support other tags (Parent, Metadata, Metaprotocol, ContentEncoding) + + if (mediaContent && mediaType) { + baseStackElements.push( + INSCRIPTION_FIELD_TAG.ContentType, + opPush(mediaType), + bitcoin.opcodes.OP_0, + ...chunkContent(mediaContent, !mediaType.includes("text") ? "base64" : "utf8") + ) + } + + // END + baseStackElements.push(bitcoin.opcodes.OP_ENDIF) + return baseStackElements +} + export type WitnessScriptOptions = { xkey: string mediaContent: string @@ -88,3 +168,8 @@ export type WitnessScriptOptions = { meta: any recover?: boolean } + +export type WitnessScriptV2Options = { + xkey: string + envelopes: EnvelopeOpts[] +} \ No newline at end of file diff --git a/packages/sdk/src/transactions/Inscriber.ts b/packages/sdk/src/transactions/Inscriber.ts index 07e27b0c..8efa6e73 100644 --- a/packages/sdk/src/transactions/Inscriber.ts +++ b/packages/sdk/src/transactions/Inscriber.ts @@ -22,6 +22,9 @@ import { SkipStrictSatsCheckOptions, UTXOLimited } from "./types" bitcoin.initEccLib(ecc) +/** + * @deprecated please use InscriberV2 + */ export class Inscriber extends PSBTBuilder { protected mediaType: string protected mediaContent: string @@ -169,7 +172,7 @@ export class Inscriber extends PSBTBuilder { inscriptionOnly: buildWitnessScript({ mediaContent: this.mediaContent, mediaType: this.mediaType, - meta: false, // do not pass in metadata for v2 + meta: false, // do not pass in metadata for taptreeVersion v2 xkey: this.xKey }), recovery: buildWitnessScript({ @@ -185,6 +188,8 @@ export class Inscriber extends PSBTBuilder { buildTaprootTree() { this.buildWitness() switch (this.taptreeVersion) { + case "3": + throw new OrditSDKError("taptreeVersion 3 is not supported for Inscriber. Please use InscriberV2.") case "2": // v2 allows for inscription only minting (without meta) and remains unique based on the meta (OIP-2 specs) this.taprootTree = [ @@ -202,6 +207,8 @@ export class Inscriber extends PSBTBuilder { getReedemScript(): bitcoin.payments.Payment["redeem"] { switch (this.taptreeVersion) { + case "3": + throw new OrditSDKError("taptreeVersion 3 is not supported for Inscriber. Please use InscriberV2.") case "2": return this.getInscriptionOnlyRedeemScript() case "1": diff --git a/packages/sdk/src/transactions/InscriberV2.ts b/packages/sdk/src/transactions/InscriberV2.ts new file mode 100644 index 00000000..6bd60209 --- /dev/null +++ b/packages/sdk/src/transactions/InscriberV2.ts @@ -0,0 +1,360 @@ +import * as ecc from "@bitcoinerlab/secp256k1" +import * as bitcoin from "bitcoinjs-lib" +import { Taptree } from "bitcoinjs-lib/src/types" + +import { + BaseDatasource, + buildRecoverWitnessScript, + buildWitnessScript, + buildWitnessScriptV2, + createTransaction, + EnvelopeOpts, + getDummyP2TRInput, + getNetwork, + TaptreeVersion +} from ".." +import { Network } from "../config/types" +import { MINIMUM_AMOUNT_IN_SATS } from "../constants" +import { OrditSDKError } from "../utils/errors" +import { PSBTBuilder } from "./PSBTBuilder" +import { SkipStrictSatsCheckOptions, UTXOLimited } from "./types" + +bitcoin.initEccLib(ecc) + +export class InscriberV2 extends PSBTBuilder { + protected taptreeVersion?: TaptreeVersion = "3" + + private ready = false + private commitAddress: string | null = null + private payment: bitcoin.payments.Payment | null = null + private suitableUnspent: UTXOLimited | null = null + private recovery = false + private recoverAmount = 0 + private previewMode = false + readonly metaInscriptions: EnvelopeOpts[] + readonly inscriptions: EnvelopeOpts[] + + private witnessScripts: Record< + "recovery" | "inscriptions" | "metaInscriptions" | "inscriptionLegacy" | "metaInscriptionLegacy", + Buffer | null + > = { + inscriptions: null, + metaInscriptions: null, + inscriptionLegacy: null, + metaInscriptionLegacy: null, + recovery: null + } + private taprootTree!: Taptree + + constructor({ + network, + changeAddress, + address, + publicKey, + feeRate, + outputs = [], + taptreeVersion, + datasource, + metaInscriptions, + inscriptions + }: InscriberV2ArgOptions) { + super({ + address, + changeAddress, + feeRate, + network, + publicKey, + outputs, + autoAdjustment: false, + datasource + }) + if (!publicKey || !changeAddress || !feeRate || !network) { + throw new OrditSDKError("Invalid options provided") + } + this.taptreeVersion = taptreeVersion + this.metaInscriptions = metaInscriptions ?? [] + this.inscriptions = inscriptions ?? [] + } + + get data() { + return { + fee: this.fee, + virtualSize: this.virtualSize, + weight: this.weight, + changeAmount: this.changeAmount, + inputAmount: this.inputAmount, + outputAmount: this.outputAmount + } + } + + async build() { + if (!this.suitableUnspent || !this.payment) { + throw new OrditSDKError("Failed to build PSBT. Transaction not ready") + } + + if ((this.taptreeVersion === "2" || this.taptreeVersion === "1") && this.inscriptions.length > 1) { + throw new OrditSDKError("Only 1 inscription is allowed for taptree version 1 and 2") + } + + this.inputs = [ + { + type: "taproot", + hash: this.suitableUnspent.txid, + index: this.suitableUnspent.n, + tapInternalKey: Buffer.from(this.xKey, "hex"), + witnessUtxo: { + script: this.payment.output!, + value: this.suitableUnspent.sats + }, + tapLeafScript: [ + { + leafVersion: this.payment.redeemVersion!, + script: this.payment.redeem!.output!, + controlBlock: this.payment.witness![this.payment.witness!.length - 1] + } + ] + } + ] + + if (!this.recovery) { + this.outputs = [ + ...this.inscriptions.map((inscription) => ({ + address: inscription.receiverAddress, + value: inscription.postage + })) + ].concat(this.outputs) + } + + if (this.recovery) { + this.recoverAmount = this.suitableUnspent.sats - this.fee + // when in recovery mode, there will only be 1 output + this.outputs = [ + { + address: this.changeAddress || this.address, + value: this.recoverAmount + } + ] + } + + await this.prepare() // prepare PSBT using PSBTBuilder + } + + private isBuilt() { + if (!this.commitAddress || !this.fee) { + throw new OrditSDKError("Invalid tx! Make sure you generate commit address or recover and finally build") + } + } + + buildWitness() { + this.witnessScripts = { + inscriptions: buildWitnessScriptV2({ + xkey: this.xKey, + envelopes: this.inscriptions + }), + metaInscriptions: buildWitnessScriptV2({ + xkey: this.xKey, + envelopes: this.metaInscriptions + }), + inscriptionLegacy: buildWitnessScript({ + mediaContent: this.inscriptions[0].mediaContent!, + mediaType: this.inscriptions[0].mediaType!, + meta: false, + xkey: this.xKey + }), + metaInscriptionLegacy: buildWitnessScript({ + mediaContent: this.inscriptions[0].mediaContent!, + mediaType: this.inscriptions[0].mediaType!, + meta: JSON.parse(this.metaInscriptions[0].mediaContent!), + xkey: this.xKey + }), + recovery: buildRecoverWitnessScript(this.xKey) + } + } + + buildTaprootTree() { + this.buildWitness() + switch (this.taptreeVersion) { + case "3": + // v3 allows for multiple/single inscription minting (without meta) and remains unique based on the meta (OIP-2 specs) + this.taprootTree = [ + [{ output: this.witnessScripts.recovery! }, { output: this.witnessScripts.metaInscriptions! }], + { output: this.witnessScripts.inscriptions! } + ] + break + case "2": + // v2 allows for inscription only minting (without meta) and remains unique based on the meta (OIP-2 specs) + this.taprootTree = [ + [{ output: this.witnessScripts.recovery! }, { output: this.witnessScripts.metaInscriptionLegacy! }], + { output: this.witnessScripts.inscriptionLegacy! } + ] + break + case "1": + // v1 allows for inscription (with meta) and recovery minting (OIP-2 specs) + this.taprootTree = [ + { output: this.witnessScripts.metaInscriptionLegacy! }, + { output: this.witnessScripts.recovery! } + ] + break + default: + throw new OrditSDKError("Invalid taptreeVersion provided") + } + } + + getReedemScript(): bitcoin.payments.Payment["redeem"] { + switch (this.taptreeVersion) { + case "3": + return { + output: this.witnessScripts.inscriptions!, + redeemVersion: 192 + } + case "2": + return { + output: this.witnessScripts.inscriptionLegacy!, + redeemVersion: 192 + } + case "1": + return { + output: this.witnessScripts.metaInscriptionLegacy!, + redeemVersion: 192 + } + default: + throw new OrditSDKError("Invalid taptreeVersion provided") + } + } + + getRecoveryRedeemScript(): bitcoin.payments.Payment["redeem"] { + return { + output: this.witnessScripts.recovery!, + redeemVersion: 192 + } + } + + private async preview({ activate }: Record<"activate", boolean> = { activate: true }) { + if (activate) { + this.previewMode = true + this.suitableUnspent = this.recovery + ? (await this.datasource.getUnspents({ address: this.commitAddress! })).spendableUTXOs[0] + : getDummyP2TRInput() + + if (this.recovery && !this.suitableUnspent) { + throw new OrditSDKError("No UTXO found to recover") + } + + this.ready = true + await this.build() + } else { + this.initPSBT() + this.suitableUnspent = null + this.ready = false + // revert inscription outputs + this.outputs = this.outputs.slice(this.inscriptions.length) + this.previewMode = false + } + } + + private restrictUsageInPreviewMode() { + if (this.previewMode) { + throw new OrditSDKError("Unable to process request in preview mode") + } + } + + private async calculateNetworkFeeUsingPreviewMode() { + await this.preview() + this.calculateNetworkFee() + await this.preview({ activate: false }) + } + + async generateCommit() { + this.buildTaprootTree() + this.payment = bitcoin.payments.p2tr({ + internalPubkey: Buffer.from(this.xKey, "hex"), + network: getNetwork(this.network), + scriptTree: this.taprootTree, + redeem: this.getReedemScript() + }) + this.witness = this.payment.witness + + await this.calculateNetworkFeeUsingPreviewMode() + + this.commitAddress = this.payment.address! + return { + address: this.payment.address!, + revealFee: this.fee + this.outputAmount + } + } + + async recover() { + this.recovery = true + this.buildTaprootTree() + + this.payment = createTransaction(Buffer.from(this.xKey, "hex"), "p2tr", this.network, { + scriptTree: this.taprootTree, + redeem: this.getRecoveryRedeemScript() + }) + this.commitAddress = this.payment.address! + + await this.calculateNetworkFeeUsingPreviewMode() + + return { + recoverAddress: this.changeAddress || this.address, + recoverAmount: this.recoverAmount - this.fee, // need to minus this.fee again because the first time, fee is 0 + recoverFee: this.fee + } + } + + async isReady({ skipStrictSatsCheck, customAmount }: SkipStrictSatsCheckOptions = {}) { + this.isBuilt() + + if (!this.ready) { + try { + await this.fetchAndSelectSuitableUnspent({ skipStrictSatsCheck, customAmount }) + } catch (error) { + return false + } + } + + return this.ready + } + + async fetchAndSelectSuitableUnspent({ skipStrictSatsCheck, customAmount }: SkipStrictSatsCheckOptions = {}) { + this.restrictUsageInPreviewMode() + this.isBuilt() + + const amount = this.recovery + ? this.outputAmount - this.fee + : skipStrictSatsCheck && customAmount && !isNaN(customAmount) + ? customAmount + : this.outputAmount + this.fee + + // Output to be paid to user + if (amount < MINIMUM_AMOUNT_IN_SATS) { + throw new OrditSDKError("Requested output amount is lower than minimum dust amount") + } + + const utxos = await this.retrieveSelectedUTXOs(this.commitAddress!, amount) + + if (utxos.length === 0) { + throw new OrditSDKError("No selected utxos retrieved") + } + + this.suitableUnspent = utxos[0] + this.ready = true + + return this.suitableUnspent + } +} + +export type InscriberV2ArgOptions = { + network: Network + address: string + publicKey: string + feeRate: number + changeAddress: string + metaInscriptions: EnvelopeOpts[] + inscriptions: EnvelopeOpts[] + taptreeVersion?: TaptreeVersion + outputs?: Outputs + datasource?: BaseDatasource +} + +type Outputs = Array<{ address: string; value: number }> diff --git a/packages/sdk/src/transactions/index.ts b/packages/sdk/src/transactions/index.ts index ffd98016..e40a2814 100644 --- a/packages/sdk/src/transactions/index.ts +++ b/packages/sdk/src/transactions/index.ts @@ -1,3 +1,4 @@ export * from "./Inscriber" +export * from "./InscriberV2" export * from "./psbt" export * from "./types" diff --git a/packages/sdk/src/transactions/types.ts b/packages/sdk/src/transactions/types.ts index b10bf889..451ee406 100644 --- a/packages/sdk/src/transactions/types.ts +++ b/packages/sdk/src/transactions/types.ts @@ -96,4 +96,4 @@ export interface SkipStrictSatsCheckOptions { customAmount?: number } -export type TaptreeVersion = "1" | "2" +export type TaptreeVersion = "1" | "2" | "3" diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts index df14648d..6a8a2eef 100644 --- a/packages/sdk/src/utils/index.ts +++ b/packages/sdk/src/utils/index.ts @@ -259,3 +259,16 @@ export function getDummyP2TRInput(): UTXO { confirmation: 10 } } + +export const splitInscriptionId = (inscriptionId: string) => { + const [txId, index] = inscriptionId.split(":") + if (txId.length !== 64) { + throw new OrditSDKError(`Invalid inscriptionId: ${inscriptionId}`) + } + const indexNum = parseInt(index, 10) + if (Number.isNaN(indexNum) || indexNum < 0) { + throw new OrditSDKError(`Invalid inscriptionId: ${inscriptionId}`) + } + + return { txId, index: parseInt(index, 10) } +}