Skip to content
This repository has been archived by the owner on Sep 14, 2023. It is now read-only.

Commit

Permalink
docs: NFTs pallet example (#815)
Browse files Browse the repository at this point in the history
Co-authored-by: Harry Solovay <harrysolovay@gmail.com>
  • Loading branch information
nythrox and harrysolovay authored Apr 16, 2023
1 parent 5b4f75b commit 280d4cc
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .trunignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
examples/xcm/asset_teleportation.eg.ts
examples/ink/*.eg.ts
examples/nfts
examples/xcm/asset_teleportation.eg.ts
10 changes: 5 additions & 5 deletions capi.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ export const config: CapiConfig = {
binary: polkadot,
chain: "rococo-local",
parachains: {
statemine: {
id: 1000,
binary: polkadotParachain,
chain: "statemine-local",
},
contracts: {
id: 2000,
binary: polkadotParachain,
chain: "contracts-rococo-local",
},
westmint: {
id: 3000,
binary: polkadotParachain,
chain: "westmint-local",
},
},
},
rococoWestmint: {
Expand Down
130 changes: 130 additions & 0 deletions examples/nfts.eg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* @title NFTs
* @stability unstable
* @description An example using the upcoming NFTs pallet to create an NFT collection,
* mint, list and purchase an NFT, as well as lock the collection and NFT as to prevent.
*/
import {
CollectionConfig,
createUsers,
MintSettings,
MintType,
Nfts,
PalletNftsEvent,
RuntimeEvent,
Utility,
} from "@capi/rococo-westmint/westmint"
import { assertEquals } from "asserts"
import { $, Rune } from "capi"
import { DefaultCollectionSetting, DefaultItemSetting } from "capi/patterns/nfts.ts"
import { signature } from "capi/patterns/signature/westmint.ts"

// Create two test users. Alexa will mint and list the NFT. Billy will purchase it.
const { alexa, billy } = await createUsers()

// Create a collection and get the resulting events.
const createEvents = await Nfts
.create({
config: CollectionConfig({
settings: DefaultCollectionSetting.AllOff,
mintSettings: MintSettings({
mintType: MintType.Issuer(),
defaultItemSettings: DefaultItemSetting.AllOff,
}),
}),
admin: alexa.address,
})
.signed(signature({ sender: alexa }))
.sent()
.dbgStatus("Create collection:")
.finalizedEvents()
.run()

// Extract the collection's id from emitted events.
const collection = (() => {
for (const { event } of createEvents) {
if (RuntimeEvent.isNfts(event) && PalletNftsEvent.isCreated(event.value)) {
return event.value.collection
}
}
return
})()

// Ensure the collection id is a number.
$.assert($.u32, collection)
console.log("Collection id:", collection)

// We'll create a single NFT with the id of 46
const item = 46

// Mint an item to the collection.
await Nfts
.mint({
collection,
item,
mintTo: alexa.address,
})
.signed(signature({ sender: alexa }))
.sent()
.dbgStatus("Mint the NFT:")
.finalized()
.run()

const owner = Nfts.Item
.value([collection, item])
.unhandle(undefined)
.access("owner")

// Retrieve the final owner.
const initialOwner = await owner.run()

// Ensure Alexa is the initial owner.
console.log("Initial owner:", initialOwner)
assertEquals(initialOwner, alexa.publicKey)

// Submit a batch, which reverts if any calls fail. The contained calls do the following:
//
// 1. Set the price.
// 2. Prevent further minting.
// 3. Lock the collection to prevent changes.
const price = 1000000n
await Utility
.batchAll({
calls: Rune.array([
Nfts.setPrice({ collection, item, price }),
Nfts.setCollectionMaxSupply({ collection, maxSupply: 1 }),
Nfts.lockCollection({ collection, lockSettings: 8n }), // TODO: enum helper
]),
})
.signed(signature({ sender: alexa }))
.sent()
.dbgStatus("Sale prep:")
.finalized()
.run()

// Retrieve the price of the NFT.
const bidPrice = await Nfts.ItemPriceOf
.value([collection, item])
.unhandle(undefined)
.access(0)
.run()

// Ensure the `bidPrice` is the expected value.
console.log(bidPrice)
assertEquals(price, bidPrice)

// Buy the NFT as Billy.
await Nfts
.buyItem({ collection, item, bidPrice })
.signed(signature({ sender: billy }))
.sent()
.dbgStatus("Purchase:")
.finalized()
.run()

// Retrieve the final owner.
const finalOwner = await owner.run()

// Ensure Billy is the final owner.
console.log("Final owner:", finalOwner)
assertEquals(finalOwner, billy.publicKey)
2 changes: 1 addition & 1 deletion examples/xcm/asset_teleportation.eg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
CumulusPalletParachainSystemEvent,
RuntimeEvent,
System,
} from "@capi/rococo-dev/statemine"
} from "@capi/rococo-westmint/westmint"
import { assert } from "asserts"
import { alice, Rune } from "capi"
import { signature } from "capi/patterns/signature/polkadot.ts"
Expand Down
2 changes: 1 addition & 1 deletion import_map.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"imports": {
"@capi/": "http://localhost:4646/713cee0c730a507e/"
"@capi/": "http://localhost:4646/15e2f7fb3ffb651e/"
},
"scopes": {
"examples/": {
Expand Down
19 changes: 19 additions & 0 deletions patterns/nfts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// The nfts pallet uses inverted bitflags; on means exclude and off means include.

export const DefaultCollectionSetting = {
TransferableItems: 1n << 0n,
UnlockedMetadata: 1n << 1n,
UnlockedAttributes: 1n << 2n,
UnlockedMaxSupply: 1n << 3n,
DepositRequired: 1n << 4n,
AllOff: 0n,
AllOn: 0b11111n,
}

export const DefaultItemSetting = {
Transferable: 1n << 0n,
UnlockedMetadata: 1n << 1n,
UnlockedAttributes: 1n << 2n,
AllOff: 0n,
AllOn: 0b111n,
}
6 changes: 3 additions & 3 deletions patterns/signature/polkadot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { $, hex, ss58, ValueRune } from "../../mod.ts"
import { Rune, RunicArgs } from "../../rune/Rune.ts"
import { Era } from "../../scale_info/overrides/Era.ts"

export interface SignatureProps {
sender: ExtrinsicSender<PolkadotSignatureChain>
export interface SignatureProps<T extends Chain> {
sender: ExtrinsicSender<T>
checkpoint?: string
mortality?: Era
nonce?: number
Expand All @@ -28,7 +28,7 @@ export interface PolkadotSignatureChain extends AddressPrefixChain {
}
}

export function signature<X>(_props: RunicArgs<X, SignatureProps>) {
export function signature<X>(_props: RunicArgs<X, SignatureProps<PolkadotSignatureChain>>) {
return <CU>(chain: ChainRune<PolkadotSignatureChain, CU>) => {
const props = RunicArgs.resolve(_props)
const addrPrefix = chain.addressPrefix()
Expand Down
58 changes: 58 additions & 0 deletions patterns/signature/westmint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Westmint } from "@capi/rococo-westmint/westmint"
import { ChainRune, Era, hex, Rune, RunicArgs, SignatureData, ss58, ValueRune } from "../../mod.ts"
import { SignatureProps } from "../signature/polkadot.ts"

type NftSigProps = SignatureProps<Westmint> & {
assetId?: number
}

export function signature<X>(_props: RunicArgs<X, NftSigProps>) {
return <CU>(chain: ChainRune<Westmint, CU>) => {
const props = RunicArgs.resolve(_props)
const addrPrefix = chain.addressPrefix()
const versions = chain.pallet("System").constant("Version").decoded
const specVersion = versions.access("specVersion")
const transactionVersion = versions.access("transactionVersion")
// TODO: create union rune (with `matchTag` method) and utilize here
// TODO: MultiAddress conversion utils
const senderSs58 = Rune
.tuple([addrPrefix, props.sender])
.map(([addrPrefix, sender]) => {
switch (sender.address.type) {
case "Id":
return ss58.encode(addrPrefix, sender.address.value)
default:
throw new Error("unimplemented")
}
})
.throws(ss58.InvalidPayloadLengthError)
const nonce = Rune.resolve(props.nonce)
.unhandle(undefined)
.rehandle(undefined, () => chain.connection.call("system_accountNextIndex", senderSs58))
const genesisHashHex = chain.connection.call("chain_getBlockHash", 0).unsafeAs<string>()
.into(ValueRune)
const genesisHash = genesisHashHex.map(hex.decode)
const checkpointHash = Rune.tuple([props.checkpoint, genesisHashHex]).map(([a, b]) => a ?? b)
.map(hex.decode)
const mortality = Rune.resolve(props.mortality).map((x) => x ?? Era.Immortal)
const tip = Rune.resolve(props.tip).map((x) => x ?? 0n)
return Rune.object({
sender: props.sender,
extra: Rune.object({
CheckMortality: mortality,
CheckNonce: nonce,
ChargeTransactionPayment: tip,
ChargeAssetTxPayment: Rune.object({
assetId: props.assetId,
tip: tip,
}),
}),
additional: Rune.object({
CheckSpecVersion: specVersion,
CheckTxVersion: transactionVersion,
CheckGenesis: genesisHash,
CheckMortality: checkpointHash,
}),
}) satisfies Rune<SignatureData<Westmint>, unknown>
}
}

0 comments on commit 280d4cc

Please sign in to comment.