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:
Copy git clone <https://github.com/nervosnetwork/ckb-cli.git>
cd ckb-cli
cargo install --path . -f --locked
Create a CKB account
Copy #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.
Copy 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,
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.
Create a project folder (e.g., my-spore-project
) and navigate into it.
Copy mkdir my-spore-project && cd my-spore-project
Initialize your project with yarn:
Install required packages:
Copy yarn add @types/node --dev
yarn add typescript ts-node
# Install sdk module
yarn add @spore-sdk/core
Create an index.ts
file in your project's root folder.
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:
Copy 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!
Save the changes to your index.ts
file.
Run Output
Copy yarn run ts-node ./index.ts
Copy createSpore sent, txHash: 0xfd5be439b84ef0e8d1917e2db9370bb99283ce30c953a10c9797ee7464077687
Congratulations! You've created your very first spore and preserved it for eternity on the blockchain.
In this tutorial, you learned how to:
Create a CKB address and obtain CKBytes tokens.
Set up your project with Spore Protocol SDK.
Mint your spore on-chain and verify the transaction.