Skip to content

Commit

Permalink
feat(sdk): support bulk minting with pointers (#121)
Browse files Browse the repository at this point in the history
* feat(sdk): support bulk minting with pointers

* export types

* add example to inscribeV2
  • Loading branch information
kevzzsk authored Jul 3, 2024
1 parent c5b54f8 commit d1c706c
Show file tree
Hide file tree
Showing 12 changed files with 721 additions and 3 deletions.
65 changes: 65 additions & 0 deletions examples/node/inscribeV2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { bulkMintFromCollection, InscriberV2, JsonRpcDatasource } from "@sadoprotocol/ordit-sdk"
import { Inscriber, Ordit } from "@sadoprotocol/ordit-sdk"

const MNEMONIC = "<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 = "<address>"
const paymentRefundAddress = "<address>"

// 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()
1 change: 1 addition & 0 deletions examples/node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"type": "module",
"scripts": {
"inscribe": "node inscribe",
"inscribeV2": "node inscribeV2",
"read": "node read",
"send": "node send",
"create-psbt": "node create-psbt",
Expand Down
107 changes: 106 additions & 1 deletion packages/sdk/src/inscription/collection.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -174,4 +254,29 @@ export type MintFromCollectionOptions = Pick<GetWalletOptions, "safeMode"> & {
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 }>
50 changes: 50 additions & 0 deletions packages/sdk/src/inscription/encoding.ts
Original file line number Diff line number Diff line change
@@ -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])
}
22 changes: 22 additions & 0 deletions packages/sdk/src/inscription/meta.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 9 additions & 0 deletions packages/sdk/src/inscription/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
85 changes: 85 additions & 0 deletions packages/sdk/src/inscription/witness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -81,10 +117,59 @@ 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
mediaType: string
meta: any
recover?: boolean
}

export type WitnessScriptV2Options = {
xkey: string
envelopes: EnvelopeOpts[]
}
Loading

0 comments on commit d1c706c

Please sign in to comment.