Creating your first spore from scratch

This step-by-step guide will help you create your first spore on CKB testnet using the SDK of Spore Protocol. To follow along, you should be familiar with basic typescript and know how to install and configure Node.js dev environment.

Ingredients

Make sure you have the following ingredients ready:

Tools You Need

Project Ingredients

Gather Your Ingredients

1. Create a CKB Address

Method 1

You can use this generator tool to create a CKB testnet address.

Method 2

You can explore your options in the land of CKB Wallets.

Method 3

To create an account via ckb-cli, you need use a testnet RPC node, make sure you have git and cargo installed. Then, in your terminal, run the following commands to install ckb-cli:

git clone <https://github.com/nervosnetwork/ckb-cli.git>
cd ckb-cli
cargo install --path . -f --locked

Create a CKB account

#For testnet
export API_URL=https://testnet.ckb.dev

ckb-cli account new

Get your private key with the follow command by replacing <lock-arg> with your lock_arg ☝ and <extended-privkey-path> with where you want the key to be stored on your device. We’ll need it later when constructing the transaction.

ckb-cli account export --lock-arg <lock-arg> --extended-privkey-path <extended-privkey-path>

The output will give you 2 strings, the top one will be your private key.

For a more detailed guide on ckb-cli setup and interaction, take a peek at the ckb-cli GitHub.

2. Get Some CKBytes

You'll need to reserve some CKBytes to mint your spore on-chain,

  1. Head over to the faucet

  2. Input your CKB Testnet Address for some testnet tokens

3. Select Digital Content

Choose your digital ingredient – an image, video, text or something else. Check and note down the MIME type of your content (e.g., image/png). Just keep it under 500KB for a successful mint.


Now that we've created a testnet wallet, filled it with test CKBytes and a file ready to rock on the blockchain, we are ready to do some action.

Let's craft your first spore by following these steps:

Step 1: Set Up the Project

We'll use yarn as the package manager.

  1. Create a project folder (e.g., my-spore-project) and navigate into it.

mkdir my-spore-project && cd my-spore-project
  1. Initialize your project with yarn:

yarn init -y
  1. Install required packages:

yarn add @types/node --dev
yarn add typescript ts-node
# Install sdk module
yarn add @spore-sdk/core
  1. Create an index.ts file in your project's root folder.

touch index.ts

Step 2: Mix in the Ingredients

Copy and edit the following code into your index.ts file, make sure to add your private key in line 107, specify the content type and filename:

import { SporeConfig, createSpore, updateWitnessArgs, isScriptValueEquals, predefinedSporeConfigs, defaultEmptyWitnessArgs } from '@spore-sdk/core';
import { hd, helpers, HexString, RPC } from '@ckb-lumos/lumos';
import { secp256k1Blake160 } from '@ckb-lumos/common-scripts';
import { Address, Hash, Script } from '@ckb-lumos/base';
import { readFileSync } from 'fs';
import { resolve } from 'path';

interface Wallet {
  lock: Script;
  address: Address;
  signMessage(message: HexString): Hash;
  signTransaction(txSkeleton: helpers.TransactionSkeletonType): helpers.TransactionSkeletonType;
  signAndSendTransaction(txSkeleton: helpers.TransactionSkeletonType): Promise<Hash>;
}

/**
 * Create a Secp256k1Blake160 Sign-all Wallet by a private key and a SporeConfig,
 * providing lock/address, and functions to sign message/transaction and send the transaction on-chain.
 */
function createSecp256k1Wallet(privateKey: HexString, config: SporeConfig): Wallet {
  const Secp256k1Blake160 = config.lumos.SCRIPTS.SECP256K1_BLAKE160!;

  // Generate a lock script from the private key
  const lock: Script = {
    codeHash: Secp256k1Blake160.CODE_HASH,
    hashType: Secp256k1Blake160.HASH_TYPE,
    args: hd.key.privateKeyToBlake160(privateKey),
  };

  // Generate address from the lock script
  const address = helpers.encodeToAddress(lock, {
    config: config.lumos,
  });

  // Sign for a message
  function signMessage(message: HexString): Hash {
    return hd.key.signRecoverable(message, privateKey);
  }

  // Sign prepared signing entries,
  // and then fill signatures into Transaction.witnesses
  function signTransaction(txSkeleton: helpers.TransactionSkeletonType): helpers.TransactionSkeletonType {
    const signingEntries = txSkeleton.get('signingEntries');
    const signatures = new Map<HexString, Hash>();
    const inputs = txSkeleton.get('inputs');

    let witnesses = txSkeleton.get('witnesses');
    for (let i = 0; i < signingEntries.size; i++) {
      const entry = signingEntries.get(i)!;
      if (entry.type === 'witness_args_lock') {
        // Skip if the input's lock does not match to the wallet's lock
        const input = inputs.get(entry.index);
        if (!input || !isScriptValueEquals(input.cellOutput.lock, lock)) {
          continue;
        }

        // Sign message
        if (!signatures.has(entry.message)) {
          const sig = signMessage(entry.message);
          signatures.set(entry.message, sig);
        }

        // Update signature to Transaction.witnesses
        const signature = signatures.get(entry.message)!;
        const witness = witnesses.get(entry.index, defaultEmptyWitnessArgs);
        witnesses = witnesses.set(entry.index, updateWitnessArgs(witness, 'lock', signature));
      }
    }

    return txSkeleton.set('witnesses', witnesses);
  }

  // Sign the transaction and send it via RPC
  async function signAndSendTransaction(txSkeleton: helpers.TransactionSkeletonType): Promise<Hash> {
    // 1. Sign transaction
    txSkeleton = secp256k1Blake160.prepareSigningEntries(txSkeleton, { config: config.lumos });
    txSkeleton = signTransaction(txSkeleton);

    // 2. Convert TransactionSkeleton to Transaction
    const tx = helpers.createTransactionFromSkeleton(txSkeleton);

    // 3. Send transaction
    const rpc = new RPC(config.ckbNodeUrl);
    return await rpc.sendTransaction(tx, 'passthrough');
  }

  return {
    lock,
    address,
    signMessage,
    signTransaction,
    signAndSendTransaction,
  };
}

// Get local image file and return an ArrayBuffer
async function fetchLocalFile(src: string) {
  const buffer = readFileSync(resolve(__dirname, src));
  return new Uint8Array(buffer).buffer;
}

async function main() {
  // Use the testnet configuration
  const config = predefinedSporeConfigs.Aggron4;

  // NOTE: Be careful to protect this and do not make your private key public except you know what you are doing!
  const privateKey = '0xc153ee57dc8ae3dac3495c828d6f8c3fef6b1d0c74fc31101c064137b3269d6d';

  // Create out account/sign helper
  const account = createSecp256k1Wallet(privateKey, config);

  let { txSkeleton } = await createSpore({
    data: {
      // Specify the content's MIME type
      contentType: 'image/png',
      // Extra parameters of contentType
      contentTypeParameters: {
        immortal: true,
      },
      // Fill in the spore's content as bytes,
      content: await fetchLocalFile('./image.jpg'),
      // fill in the spores' belonging cluster's id, optional, here we leave it empty
      clusterId: undefined,
    },
    fromInfos: [account.address],
    toLock: account.lock,
    config,
  });

  const hash = await account.signAndSendTransaction(txSkeleton);
  console.log('createSpore sent, txHash:', hash);
}

main();

Step 3: Send Your Spore On-chain!

  1. Save the changes to your index.ts file.

  2. Run the Script

yarn run ts-node ./index.ts
  1. Check your transaction on https://pudge.explorer.nervos.org by searching for your transaction hash to see the transaction details and your Spore cell.

Congratulations! You've created your very first spore and preserved it for eternity on the blockchain.

In this tutorial, you learned how to:

  1. Create a CKB address and obtain CKBytes tokens.

  2. Set up your project with Spore Protocol SDK.

  3. Mint your spore on-chain and verify the transaction.

Last updated