Create a Digital Object Using Spore Protocol
Tutorial Overview
- An IDE/Editor that supports TypeScript
- CKB dev environment: OffCKB
Spore Protocol on CKB
Spore is an on-chain digital object (DOB)
data:
content-type: Bytes # String Bytes
content: Bytes
# OPTIONAL
cluster_id: Bytes
type:
hash_type: "data1"
code_hash: SPORE_TYPE_DATA_HASH
args: SPORE_ID
lock:
<user_defined>
Notice that the data field of the Spore Cell contains content-type
and content
, which allow users to turn any content form into a digital object. All the fields in a Spore Cell are immutable once created.
In this tutorial, we will build a simple dApp to turn a picture on your computer into a digital object on the blockchain using the Spore SDK.
Setup Devnet & Run Example
Step 1: Clone the Repository
To get started with the tutorial dApp, clone the repository and navigate to the appropriate directory using the following commands:
git clone https://github.com/nervosnetwork/docs.nervos.org.git
cd docs.nervos.org/examples/create-dob
Step 2: Start the Devnet
To interact with the dApp, ensure that your Devnet is up and running. After installing @offckb/cli, open a terminal and start the Devnet with the following command:
- Command
- Response
offckb node
/bin/sh: /Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/ckb/ckb: No such file or directory
/Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/ckb/ckb not found, download and install the new version 0.113.1..
CKB installed successfully.
init Devnet config folder: /Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/devnet
modified /Users/nervosDocs/.nvm/versions/node/v18.12.1/lib/node_modules/@offckb/cli/target/devnet/ckb-miner.toml
CKB output: 2024-03-20 07:56:44.765 +00:00 main INFO sentry sentry is disabled
CKB output: 2024-03-20 07:56:44.766 +00:00 main INFO ckb_bin::helper raise_fd_limit newly-increased limit: 61440
CKB output: 2024-03-20 07:56:44.854 +00:00 main INFO ckb_bin::subcommand::run ckb version: 0.113.1 (95ad24b 2024-01-31)
CKB output: 2024-03-20 07:56:45.320 +00:00 main INFO ckb_db_migration Init database version 20230206163640
CKB output: 2024-03-20 07:56:45.329 +00:00 main INFO ckb_launcher Touch chain spec hash: Byte32(0x3036c73473a371f3aa61c588c38924a93fb8513e481fa7c8d884fc4cf5fd368a)
You might want to check pre-funded accounts and copy private keys for later use. Open another terminal and execute:
- Command
- Response
offckb accounts
Print account list, each account is funded with 42_000_000_00000000 capacity in the genesis block.
[
{
privkey: '0x6109170b275a09ad54877b82f7d9930f88cab5717d484fb4741ae9d1dd078cd6',
pubkey: '0x02025fa7b61b2365aa459807b84df065f1949d58c0ae590ff22dd2595157bffefa',
lockScript: {
codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8',
hashType: 'type',
args: '0x8e42b1999f265a0078503c4acec4d5e134534297'
},
address: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqvwg2cen8extgq8s5puft8vf40px3f599cytcyd8',
args: '0x8e42b1999f265a0078503c4acec4d5e134534297'
},
{
privkey: '0x9f315d5a9618a39fdc487c7a67a8581d40b045bd7a42d83648ca80ef3b2cb4a1',
pubkey: '0x026efa0579f09cc7c1129b78544f70098c90b2ab155c10746316f945829c034a2d',
lockScript: {
codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8',
hashType: 'type',
args: '0x758d311c8483e0602dfad7b69d9053e3f917457d'
},
address: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqt435c3epyrupszm7khk6weq5lrlyt52lg48ucew',
args: '0x758d311c8483e0602dfad7b69d9053e3f917457d'
},
#...
]
Step 3: Run the Example
Navigate to your project, install the node dependencies, and start running the example:
- Command
- Response
yarn && NETWORK=devnet yarn start
$ parcel index.html
Server running at http://localhost:1234
✨ Built in 66ms
Now, the app is running in http://localhost:1234
Behind the Scene
Open the lib.ts
file in your project, it lists all the important functions that do the most of work for the project.
Create Digital Object
Check out the createSporeDOB
function:
export async function createSporeDOB(
privkey: string,
content: Uint8Array
): Promise<{ txHash: string; outputIndex: number }>;
It accepts two parameters,
- the private key that is used to sign and create the digital object
- the content to be stored in the digital object.
The content can be any type of data that is serialized into a Uint8Array
. Here we are dealing with images, so the content is the result of FileReader.readAsArrayBuffer
. You can check out the following code recipe in handleFileChange
function from the react frontend index.tsx
:
const reader = new FileReader();
reader.onload = () => {
// Access the file content here
const content = reader.result;
if (content && content instanceof ArrayBuffer) {
const uint8Array = new Uint8Array(content);
setFileContent(uint8Array);
}
};
// Read the file as ArrayBuffer
reader.readAsArrayBuffer(files[0]);
Once we have the picture content and the private key, we will build a transaction that produces a Spore output Cell, aka the digital object Cell. We can handle all the logic with Lumos.js, but with the help of Spore-SDK, it becomes very simple to do:
export async function createSporeDOB(
privkey: string,
content: Uint8Array
): Promise<{ txHash: string; outputIndex: number }> {
const wallet = createDefaultLockWallet(privkey);
const { txSkeleton, outputIndex } = await createSpore({
data: {
contentType: "image/jpeg",
content,
},
toLock: wallet.lock,
fromInfos: [wallet.address],
config: SPORE_CONFIG,
});
const txHash = await wallet.signAndSendTransaction(txSkeleton);
console.log(`Spore created at transaction: ${txHash}`);
console.log(
`Spore ID: ${
txSkeleton.get("outputs").get(outputIndex)!.cellOutput.type!.args
}`
);
return { txHash, outputIndex };
}
Notice that the createDefaultLockWallet
and const txHash = await wallet.signAndSendTransaction(txSkeleton);
are just some methods that helps us to keep the code clean, all it does is the same as the previous tutorials involving signing and sending transactions.
Render Content from Digital Object
Once we created our digital object on-chain, what we love to do is to render and show this digital object. To do this, we need to first find the Spore Cell of our digital object and extract the data from the Spore Cell and decode the content from the data to render it in the browser.
Check out the showSporeContent
function:
export async function showSporeContent(txHash: string, index = 0) {
const indexHex = "0x" + index.toString(16);
const { cell } = await rpc.getLiveCell({ txHash, index: indexHex }, true);
if (cell == null) {
return alert("Cell not found, please retry later");
}
const data = cell.data.content;
const msg = unpackToRawSporeData(data);
console.log("Spore data: ", msg);
return msg;
}
We locate the Spore Cell by accepting a outpoint parameter(txHash
and outputIndex
), and use rpc.getLiveCell
to get the Live Cell. Then we unpack the data content from this Cell:
const data = cell.data.content;
const msg = unpackToRawSporeData(data);
To render the image from this raw data, check out the renderSpore
function in the index.tsx
:
const renderSpore = async () => {
const res = await showSporeContent(txHash, outputIndex);
if (!res) return;
setRawSporeData(res);
// Create Blob from binary data
const buffer = hexStringToUint8Array(res.content.toString().slice(2));
const blob = new Blob([buffer], { type: res.contentType });
const url = URL.createObjectURL(blob);
setImageURL(url);
};
...
{imageURL && <img src={imageURL} />}
Congratulations!
By following this tutorial this far, you have mastered how digital-object works on CKB. Here's a quick recap:
- How Spore protocol works on CKB
- Create an on-chain digital object with a picture via Spore-SDK
- Render the picture in the browser from your digital object
Next Step
So now your dApp works great on the local blockchain, you might want to switch it to different environments like Testnet and Mainnet.
To do that, just change the environment variable NETWORK
to testnet
:
export NETWORK=testnet
For more details, check out the README.md.
Additional Resources
- Spore protocol: docs.spore.pro
- CKB transaction structure: RFC-0022-transaction-structure
- CKB data structure basics: RFC-0019-data-structure