Skip to main content

Tutorial 4: Ordinal Lock

Overview

In this tutorial, we will go over how to use sCrypt to build a full-stack dApp on Bitcoin to sell 1Sat Ordinals, including the smart contract and an interactive front-end.

Contract

The contract OrdinalLock allows an ordinal to be offered up for sale on a decentralized marketplace. These listings can be purchased by anyone who is able to pay the requested price. Listings can also be cancelled by the person who listed them.

To record the seller and price, we need to add two properties to the contract.

export class OrdinalLock extends OrdinalNFT {
@prop()
seller: PubKey

@prop()
amount: bigint

...
}

Constructor

Initialize all the @prop properties in the constructor.

constructor(seller: PubKey, amount: bigint) {
super()
this.init(...arguments)
this.seller = seller
this.amount = amount
}

Methods

The public method purchase only needs to confine the transaction's outputs to contain:

  • transfer ordinal to the buyer
  • payment to the seller
@method()
public purchase(receiver: Addr) {
const outputs =
Utils.buildAddressOutput(receiver, 1n) + // ordinal to the buyer
Utils.buildAddressOutput(hash160(this.seller), this.amount) + // fund to the seller
this.buildChangeOutput()
assert(this.ctx.hashOutputs == hash256(outputs), 'hashOutputs check failed')
}

The final complete code is as follows:

import { Addr, prop, method, Utils, hash256, assert, ContractTransaction, bsv, PubKey, hash160, Sig, SigHash } from 'scrypt-ts'
import { OrdiMethodCallOptions, OrdinalNFT } from '../scrypt-ord'

export class OrdinalLock extends OrdinalNFT {
@prop()
seller: PubKey

@prop()
amount: bigint

constructor(seller: PubKey, amount: bigint) {
super()
this.init(...arguments)
this.seller = seller
this.amount = amount
}

@method()
public purchase(receiver: Addr) {
const outputs =
Utils.buildAddressOutput(receiver, 1n) + // ordinal to the buyer
Utils.buildAddressOutput(hash160(this.seller), this.amount) + // fund to the seller
this.buildChangeOutput()
assert(
this.ctx.hashOutputs == hash256(outputs),
'hashOutputs check failed'
)
}

@method(SigHash.ANYONECANPAY_SINGLE)
public cancel(sig: Sig) {
assert(this.checkSig(sig, this.seller), 'seller signature check failed')
const outputs = Utils.buildAddressOutput(hash160(this.seller), 1n) // ordinal back to the seller
assert(
this.ctx.hashOutputs == hash256(outputs),
'hashOutputs check failed'
)
}

static async buildTxForPurchase(
current: OrdinalLock,
options: OrdiMethodCallOptions<OrdinalLock>,
receiver: Addr
): Promise<ContractTransaction> {
const defaultAddress = await current.signer.getDefaultAddress()
const tx = new bsv.Transaction()
.addInput(current.buildContractInput())
.addOutput(
new bsv.Transaction.Output({
script: bsv.Script.fromHex(
Utils.buildAddressScript(receiver)
),
satoshis: 1,
})
)
.addOutput(
new bsv.Transaction.Output({
script: bsv.Script.fromHex(
Utils.buildAddressScript(hash160(current.seller))
),
satoshis: Number(current.amount),
})
)
.change(options.changeAddress || defaultAddress)
return {
tx,
atInputIndex: 0,
nexts: [],
}
}

static async buildTxForCancel(
current: OrdinalLock,
options: OrdiMethodCallOptions<OrdinalLock>
): Promise<ContractTransaction> {
const defaultAddress = await current.signer.getDefaultAddress()
const tx = new bsv.Transaction()
.addInput(current.buildContractInput())
.addOutput(
new bsv.Transaction.Output({
script: bsv.Script.fromHex(
Utils.buildAddressScript(hash160(current.seller))
),
satoshis: 1,
})
)
.change(options.changeAddress || defaultAddress)
return {
tx,
atInputIndex: 0,
nexts: [],
}
}
}

Note the customized calling method buildTxForPurchase and buildTxForCancel ensure the ordinal is in the first input and goes to the first output, which is also a 1sat output.

Frontend

We will add a front-end to the OrdinalLock smart contract according to this guide.

Setup Project

The front-end will be created using Create React App.

npx create-react-app ordinal-lock-demo --template typescript

Install the sCrypt SDK

The sCrypt SDK enables you to easily compile, test, deploy, and call contracts.

Use the scrypt-cli command line to install the SDK.

cd ordinal-lock-demo
npm i scrypt-ord
npx scrypt-cli init

This command will create a contract under src/contracts. Replace the file with the contract written above.

Compile Contract

Compile the contract with the following command:

npx scrypt-cli compile

This command will generate a contract artifact file under artifacts.

Compile using the watch option

Monitoring for Real-Time Error Detection

npx scrypt-cli compile --watch

this command will display sCrypt level errors during the compilation process.

Load Contract Artifact

Before writing the front-end code, we need to load the contract artifact in src/index.tsx.

import { OrdinalLock } from './contracts/ordinalLock'
import artifact from '../artifacts/ordinalLock.json'
OrdinalLock.loadArtifact(artifact)

Connect Signer to OrdiProvider

const provider = new OrdiProvider();
const signer = new PandaSigner(provider);

Integrate Wallet

Use requestAuth method of signer to request access to the wallet.

// request authentication
const { isAuthenticated, error } = await signer.requestAuth();
if (!isAuthenticated) {
// something went wrong, throw an Error with `error` message
throw new Error(error);
}

// authenticated
// ...

Load Ordinals

After a user connect wallet, we can get the his address. Call the 1Sat Ordinals API to retrieve ordinals on this address.

useEffect(() => {
loadCollections()
}, [connectedAddress])

function loadCollections() {
if (connectedAddress) {
const url = `https://v3.ordinals.gorillapool.io/api/txos/address/${connectedAddress.toString()}/unspent?bsv20=false`
fetch(url).then(r => r.json()).then(r => r.filter(e => e.origin.data.insc.file.type !== 'application/bsv-20')).then(r => setCollections(r)) }
}

List an Ordinal

For each ordinal in the collection list, we can click the Sell button to list it after filling in the selling price, in satoshis. Sell an ordinal means we need to create a contract instance, and then transfer the ordinal into it. Afterwards, the ordinal is under the control of the contract, meaning it can be bought by anyone paying the price to the seller.

async function sell() {
const signer = new PandaSigner(new OrdiProvider())
const publicKey = await signer.getDefaultPubKey()

const instance = new OrdinalLock(PubKey(toHex(publicKey)), amount)
await instance.connect(signer)

const inscriptionUtxo = await parseUtxo(txid, vout)
const inscriptionP2PKH = OrdiNFTP2PKH.fromUTXO(inscriptionUtxo)
await inscriptionP2PKH.connect(signer)

const { tx } = await inscriptionP2PKH.methods.unlock(
(sigResps) => findSig(sigResps, publicKey),
PubKey(toHex(publicKey)),
{
transfer: instance, // <----
pubKeyOrAddrToSign: publicKey,
} as OrdiMethodCallOptions<OrdiNFTP2PKH>
)
}

Buy an Ordinal

To buy an ordinal that is on sale, we only need to call the contract public method purchase.

async function buy() {
const signer = new PandaSigner(new OrdiProvider())
const address = await signer.getDefaultAddress()
const { tx } = await instance.methods.purchase(Addr(address.toByteString()))
}

Use Yours Wallet

In March 2024, Panda Wallet was rebranded to Yours Wallet.

Yours Wallet is an open-source and non-custodial web3 wallet for BSV and 1Sat Ordinals. This wallet allows users to have full control over their funds, providing security and independence in managing their assets.

To support Yours Wallet in the dApp, we simply replace all the PandaSigner with PandaSigner, that's all.

import { PandaSigner } from "scrypt-ts/dist/bsv/signers/panda-signer"

Different from other signers, we can get two addresses from PandaSigner after the user authorizes the connect action:

  • getDefaultAddress(), the address for sending and receiving BSV, paying transaction fees, etc. The same as other signers.
  • getOrdAddress(), the address for receiving Ordinals only.
const [connectedPayAddress, setConnectedPayAddress] = useState(undefined)
const [connectedOrdiAddress, setConnectedOrdiAddress] = useState(undefined)
...
async function connect() {
const signer = new PandaSigner(new OrdiProvider()) // <---- use `PandaSigner`
const { isAuthenticated, error } = await signer.requestAuth()
if (!isAuthenticated) {
throw new Error(`Unauthenticated: ${error}`)
}
setConnectedPayAddress(await signer.getDefaultAddress()) // <----
setConnectedOrdiAddress(await signer.getOrdAddress()) // <----
}

Load Ordinals

List an Ordinal

Buy an Ordinal

Conclusion

Congratulations! You have successfully completed a full-stack dApp that can sell 1Sat Ordinals on Bitcoin.

The full example repo can be found here.