Skip to content

Commit

Permalink
feat(sdk)!: introduce royalty payments in instant-trade flow (#66)
Browse files Browse the repository at this point in the history
* feat: add creator royalty as per OIP-6

ref: https://www.oips.io/oip-06-ordinals-royalty-standard

* refactor: set max decimals to 5

* refactor: make royalty optional

* fix: update property name

* feat: bind royalty payment to seller PSBT

* feat: support multiple injectable outputs

* fix: check if pct is number

* refactor: update publish-collection example

* feat: add royalty class member

* refactor: rename inscriberMode to autoAdjustment

* fix: bind index to collection txid

* fix: prevent injectable output from replacing existing output

* fix: retrieve more UTXOs only when pre-fetched UTXOs are not enough

* refactor!: cleanup & class member visibility change

also, replace direct assignment w/ passing param in constructor

* refactor: update publish-collection example

* refactor: update decimals for royalty pct to 8

* refactor: pay no royalty if amount below dust
  • Loading branch information
iamcrazycoder authored Sep 22, 2023
1 parent ee32996 commit 32770c2
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 113 deletions.
17 changes: 9 additions & 8 deletions examples/node/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,20 @@ const publisherWallet = new Ordit({

// set default address types for both wallets
userWallet.setDefaultAddress("taproot");
publisherWallet.setDefaultAddress("nested-segwit");
publisherWallet.setDefaultAddress("taproot");

async function publish() {
const getPublisherLegacyAddress = () => {
publisherWallet.setDefaultAddress("legacy")
const legacyAddress = publisherWallet.selectedAddress
publisherWallet.setDefaultAddress("nested-segwit") // switch back to default
publisherWallet.setDefaultAddress("taproot") // switch back to default

return legacyAddress
}

//publish
const transaction = await publishCollection({
address: publisherWallet.selectedAddress,
network,
feeRate: 2,
title: "Collection Name",
Expand All @@ -40,25 +41,24 @@ async function publish() {
email: "[email protected]",
name: "Your Name"
},
royalty: {
address: publisherWallet.selectedAddress,
pct: 0.05
},
publishers: [getPublisherLegacyAddress()],
inscriptions: [
{
iid: "el-01",
lim: 10,
sri: "sha256-Ujac9y464/qlFmtfLDxORaUtIDH8wrHgv8L9bpPeb28="
},
{
iid: "el-02",
lim: 2,
sri: "sha256-zjQXDuk++5sICrObmfWqAM5EibidXd2emZoUcU2l5Pg="
}
],
url: "https://example.com",
publicKey: publisherWallet.publicKey,
destination: publisherWallet.selectedAddress,
changeAddress: publisherWallet.selectedAddress,
postage: 1000,
mediaContent: 'Collection Name', // this will be inscribed on-chain as primary content
mediaContent: '5% Royalty Collection', // this will be inscribed on-chain as primary content
mediaType: "text/plain"
});

Expand Down Expand Up @@ -88,6 +88,7 @@ async function mint() {

// publish
const transaction = await mintFromCollection({
address: userWallet.selectedAddress,
network,
collectionOutpoint: collectionId,
inscriptionIid: "el-01",
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk/src/fee/FeeEstimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { MAXIMUM_FEE } from "../constants"
import { FeeEstimatorOptions } from "./types"

export default class FeeEstimator {
feeRate: number
network: Network
psbt!: Psbt
witness?: Buffer[] = []
fee = 0
protected feeRate: number
protected network: Network
protected psbt: Psbt
protected witness?: Buffer[] = []
protected fee = 0

private virtualSize = 0
private weight = 0
Expand Down
34 changes: 28 additions & 6 deletions packages/sdk/src/inscription/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export async function publishCollection({
url,
slug,
creator,
royalty,
publishers,
inscriptions,
...options
Expand All @@ -15,15 +16,30 @@ export async function publishCollection({
throw new Error("Invalid inscriptions supplied.")
}

if (royalty) {
// 0 = 0%, 10 = 1000%
if (isNaN(royalty.pct) || royalty.pct < 0 || royalty.pct > 10) {
throw new Error("Invalid royalty %")
}

royalty.pct = +new Intl.NumberFormat("en", {
maximumFractionDigits: 8,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
roundingMode: "trunc"
}).format(royalty.pct)
}

const collectionMeta = {
p: "vord", // protocol
v: 1, // version
ty: "col",
title: title,
title,
desc: description,
url: url,
slug: slug,
creator: creator,
url,
slug,
creator,
royalty,
publ: publishers,
insc: inscriptions
}
Expand Down Expand Up @@ -110,6 +126,7 @@ function validateInscriptions(inscriptions: CollectionInscription[] = []) {
}

export type PublishCollectionOptions = Pick<GetWalletOptions, "safeMode"> & {
address: string
feeRate: number
postage: number
mediaType: string
Expand All @@ -127,9 +144,13 @@ export type PublishCollectionOptions = Pick<GetWalletOptions, "safeMode"> & {
email?: string
address: string
}
royalty?: {
address: string
pct: number
}
network: Network
publicKey: string
outs?: Outputs
outputs?: Outputs
encodeMetadata?: boolean
enableRBF?: boolean
}
Expand All @@ -141,6 +162,7 @@ export type CollectionInscription = {
}

export type MintFromCollectionOptions = Pick<GetWalletOptions, "safeMode"> & {
address: string
feeRate: number
postage: number
mediaType: string
Expand All @@ -154,7 +176,7 @@ export type MintFromCollectionOptions = Pick<GetWalletOptions, "safeMode"> & {
signature: string
network: Network
publicKey: string
outs?: Outputs
outputs?: Outputs
traits?: any
encodeMetadata?: boolean
enableRBF?: boolean
Expand Down
11 changes: 9 additions & 2 deletions packages/sdk/src/instant-trade/InstantTradeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ import { OrditApi } from ".."
import { MINIMUM_AMOUNT_IN_SATS } from "../constants"
import { PSBTBuilder, PSBTBuilderOptions } from "../transactions/PSBTBuilder"

export interface InstantTradeBuilderArgOptions extends Pick<PSBTBuilderOptions, "publicKey" | "network" | "address"> {
export interface InstantTradeBuilderArgOptions
extends Pick<PSBTBuilderOptions, "publicKey" | "network" | "address" | "autoAdjustment" | "feeRate"> {
inscriptionOutpoint?: string
}

export default class InstantTradeBuilder extends PSBTBuilder {
protected inscriptionOutpoint?: string
protected price = 0
protected postage = 0
protected royalty = 0

constructor({ address, network, publicKey, inscriptionOutpoint }: InstantTradeBuilderArgOptions) {
constructor({ address, network, publicKey, inscriptionOutpoint, autoAdjustment }: InstantTradeBuilderArgOptions) {
super({
address,
feeRate: 0,
network,
publicKey,
outputs: [],
autoAdjustment,
instantTradeMode: true
})

Expand All @@ -30,6 +33,10 @@ export default class InstantTradeBuilder extends PSBTBuilder {
this.price = parseInt(value.toString())
}

setRoyalty(value: number) {
this.royalty = value
}

protected async verifyAndFindInscriptionUTXO(address?: string) {
if (!this.inscriptionOutpoint) {
throw new Error("set inscription outpoint to the class")
Expand Down
33 changes: 19 additions & 14 deletions packages/sdk/src/instant-trade/InstantTradeBuyerTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { Output } from "../transactions/types"
import InstantTradeBuilder, { InstantTradeBuilderArgOptions } from "./InstantTradeBuilder"

interface InstantTradeBuyerTxBuilderArgOptions extends InstantTradeBuilderArgOptions {
feeRate: number
sellerPSBT: string
receiveAddress?: string
}
Expand All @@ -36,10 +35,10 @@ export default class InstantTradeBuyerTxBuilder extends InstantTradeBuilder {
super({
address,
network,
publicKey
publicKey,
feeRate
})

this.feeRate = feeRate
this.receiveAddress = receiveAddress
this.decodeSellerPSBT(sellerPSBT)
}
Expand Down Expand Up @@ -68,6 +67,11 @@ export default class InstantTradeBuyerTxBuilder extends InstantTradeBuilder {
this.setPrice((this.sellerPSBT.data.globalMap.unsignedTx as any).tx.outs[0].value - this.postage)
}

private decodeRoyalty() {
const royaltyOutput = (this.sellerPSBT.data.globalMap.unsignedTx as any).tx.outs[1]
royaltyOutput && this.setRoyalty(royaltyOutput.value)
}

private bindRefundableOutput() {
this.outputs = [
{
Expand Down Expand Up @@ -98,17 +102,17 @@ export default class InstantTradeBuyerTxBuilder extends InstantTradeBuilder {
sats: this.sellerPSBT.data.inputs[0].witnessUtxo?.value,
injectionIndex: INSTANT_BUY_SELLER_INPUT_INDEX
}
] as unknown as InjectableInput[]

//outputs
this.injectableOutputs = [
{
standardOutput: this.sellerPSBT.data.outputs[0],
txOutput: (this.sellerPSBT.data.globalMap.unsignedTx as any).tx.outs[0],
sats: (this.sellerPSBT.data.globalMap.unsignedTx as any).tx.outs[0].value,
injectionIndex: INSTANT_BUY_SELLER_INPUT_INDEX
}
] as InjectableOutput[]
] as InjectableInput[]

// outputs
this.injectableOutputs = this.sellerPSBT.data.outputs.map((standardOutput, index) => {
return {
standardOutput,
txOutput: (this.sellerPSBT.data.globalMap.unsignedTx as any).tx.outs[index],
sats: (this.sellerPSBT.data.globalMap.unsignedTx as any).tx.outs[index].value,
injectionIndex: INSTANT_BUY_SELLER_INPUT_INDEX + index
} as InjectableOutput
})
}

private async findUTXOs() {
Expand Down Expand Up @@ -155,6 +159,7 @@ export default class InstantTradeBuyerTxBuilder extends InstantTradeBuilder {
}

this.decodePrice()
this.decodeRoyalty()
this.bindRefundableOutput()
this.bindInscriptionOutput()
this.mergePSBTs()
Expand Down
46 changes: 40 additions & 6 deletions packages/sdk/src/instant-trade/InstantTradeSellerTxBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as bitcoin from "bitcoinjs-lib"

import { processInput } from ".."
import { OrditApi, processInput } from ".."
import { MINIMUM_AMOUNT_IN_SATS } from "../constants"
import { UTXO } from "../transactions/types"
import InstantTradeBuilder, { InstantTradeBuilderArgOptions } from "./InstantTradeBuilder"

Expand All @@ -23,11 +24,12 @@ export default class InstantTradeSellerTxBuilder extends InstantTradeBuilder {
address,
network,
publicKey,
inscriptionOutpoint
inscriptionOutpoint,
autoAdjustment: false, // Prevents PSBTBuilder from adding additional input and change output
feeRate: 0 // seller in instant-trade does not pay network fee
})

this.receiveAddress = receiveAddress
this.feeRate = 0 // seller in instant-trade does not pay network fee
}

private async generatSellerInputs() {
Expand All @@ -45,15 +47,47 @@ export default class InstantTradeSellerTxBuilder extends InstantTradeBuilder {
this.inputs = [input]
}

private generateSellerOutputs() {
private async generateSellerOutputs() {
const royalty = await this.calculateRoyalty()
this.outputs = [{ address: this.receiveAddress || this.address, value: this.price + this.postage }]

if (royalty && royalty.amount >= MINIMUM_AMOUNT_IN_SATS) {
this.outputs.push({
address: royalty.address, // creator address
value: royalty.amount // royalty in sats to be paid to original creator
})
}
}

private async calculateRoyalty() {
if (!this.utxo?.inscriptions?.length || !this.utxo?.inscriptions[0]?.meta?.col) {
return
}

const collection = await OrditApi.fetchInscription({
id: `${this.utxo.inscriptions[0].meta.col}i0`,
network: this.network
})
const royalty = collection.meta?.royalty
if (!royalty || !royalty.address || !royalty.pct) {
return
}
const amount = Math.ceil(royalty.pct * this.price)

return {
address: royalty.address as string,
amount: amount >= MINIMUM_AMOUNT_IN_SATS ? amount : 0
}
}

async build() {
this.utxo = await this.verifyAndFindInscriptionUTXO()
if (isNaN(this.price) || this.price < MINIMUM_AMOUNT_IN_SATS) {
throw new Error("Invalid price")
}

this.utxo = await this.verifyAndFindInscriptionUTXO()
await this.generatSellerInputs()
this.generateSellerOutputs()
await this.generateSellerOutputs()

await this.prepare()
}
Expand Down
Loading

0 comments on commit 32770c2

Please sign in to comment.