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:
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:
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.
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.
Create an index.ts file in your project's root folder.
touchindex.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 inline 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';interfaceWallet { 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. */functioncreateSecp256k1Wallet(privateKey:HexString, config:SporeConfig):Wallet {constSecp256k1Blake160=config.lumos.SCRIPTS.SECP256K1_BLAKE160!;// Generate a lock script from the private keyconstlock:Script= { codeHash:Secp256k1Blake160.CODE_HASH, hashType:Secp256k1Blake160.HASH_TYPE, args:hd.key.privateKeyToBlake160(privateKey), };// Generate address from the lock scriptconstaddress=helpers.encodeToAddress(lock, { config:config.lumos, });// Sign for a messagefunctionsignMessage(message:HexString):Hash {returnhd.key.signRecoverable(message, privateKey); }// Sign prepared signing entries,// and then fill signatures into Transaction.witnessesfunctionsignTransaction(txSkeleton:helpers.TransactionSkeletonType):helpers.TransactionSkeletonType {constsigningEntries=txSkeleton.get('signingEntries');constsignatures=newMap<HexString,Hash>();constinputs=txSkeleton.get('inputs');let witnesses =txSkeleton.get('witnesses');for (let i =0; i <signingEntries.size; i++) {constentry=signingEntries.get(i)!;if (entry.type ==='witness_args_lock') {// Skip if the input's lock does not match to the wallet's lockconstinput=inputs.get(entry.index);if (!input ||!isScriptValueEquals(input.cellOutput.lock, lock)) {continue; }// Sign messageif (!signatures.has(entry.message)) {constsig=signMessage(entry.message);signatures.set(entry.message, sig); }// Update signature to Transaction.witnessesconstsignature=signatures.get(entry.message)!;constwitness=witnesses.get(entry.index, defaultEmptyWitnessArgs); witnesses =witnesses.set(entry.index,updateWitnessArgs(witness,'lock', signature)); } }returntxSkeleton.set('witnesses', witnesses); }// Sign the transaction and send it via RPCasyncfunctionsignAndSendTransaction(txSkeleton:helpers.TransactionSkeletonType):Promise<Hash> {// 1. Sign transaction txSkeleton =secp256k1Blake160.prepareSigningEntries(txSkeleton, { config:config.lumos }); txSkeleton =signTransaction(txSkeleton);// 2. Convert TransactionSkeleton to Transactionconsttx=helpers.createTransactionFromSkeleton(txSkeleton);// 3. Send transactionconstrpc=newRPC(config.ckbNodeUrl);returnawaitrpc.sendTransaction(tx,'passthrough'); }return { lock, address, signMessage, signTransaction, signAndSendTransaction, };}// Get local image file and return an ArrayBufferasyncfunctionfetchLocalFile(src:string) {constbuffer=readFileSync(resolve(__dirname, src));returnnewUint8Array(buffer).buffer;}asyncfunctionmain() {// Use the testnet configurationconstconfig=predefinedSporeConfigs.Aggron4;// NOTE: Be careful to protect this and do not make your private key public except you know what you are doing!constprivateKey='0xc153ee57dc8ae3dac3495c828d6f8c3fef6b1d0c74fc31101c064137b3269d6d';// Create out account/sign helperconstaccount=createSecp256k1Wallet(privateKey, config);let { txSkeleton } =awaitcreateSpore({ 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:awaitfetchLocalFile('./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, });consthash=awaitaccount.signAndSendTransaction(txSkeleton);console.log('createSpore sent, txHash:', hash);}main();