State
Coin Set Refresher
Before we jump in to the bulk of this lesson I wanted to remind you of how the Chia blockchain works. Chia uses a coin set model very similar to that of Bitcoin's UTXO model. This is very different than the account model used in other major chains. Instead of having an account with a balance, you just have a collection of unspent coins that you are able to spend.
For example:
- Coin 1: 0.25 XCH
- Coin 2: 1.75 XCH
- Coin 3: 0.25 XCH
- Coin 4: 1.75 XCH
Balance: 4 XCH
You will often hear "everything is a coin" being said. This is true!
This introduces some unique approaches to creating software on the Chia blockchain.
To use Chialisp on the Chia blockchain, we must create a coin. As part of the coin set model, spending a coin results in a collection of removals and additions. The removals are your existing coins used for the spend being destroyed, and the additions are new coins (including any change) being created.
Here is an example
- Say you have 1.75 XCH
- You send .75 XCH to some address (remember, an address is an encoded form of puzzle hash)
- This results in all 1.75 of your XCH being spent
- a new coin worth 1 XCH is created to be returned back to you (this is called change)
- a new coin worth .75 XCH is created for the destination puzzle hash
Another example:
- Say you have 2 XCH made up of multiple small coins
- You send 1 XCH to some address
- Coins of .1 XCH, .5 XCH, and .45 XCH are being spent to sum up to 1.05 XCH (greater than or equal to 1 XCH spend)
- a new coin worth .05 XCH is created to be returned back to you
- a new coin worth 1 XCH is created for the destination puzzle hash
What's Next?
We will be building a simple example of a coin that stores a message that anyone can change by spending the coin. This will be built using the previous lesson's project setup, but you can find all of index.ts
here:
index.ts
import { mnemonicToSeedSync } from "bip39";
import { fromHex, PrivateKey, toHex } from "chia-bls";
import { Coin, formatHex, FullNode, sanitizeHex, toCoinId } from "chia-rpc";
import { KeyStore, StandardWallet } from "chia-wallet-lib";
import { Program } from "clvm-lib";
import dotenv from "dotenv";
import fs from "fs";
import os from "os";
import path from "path";
dotenv.config();
const mnemonic = process.env.MNEMONIC!;
const privateKey = PrivateKey.fromSeed(mnemonicToSeedSync(mnemonic));
const dir = path.join(__dirname, "..");
const messagePuzzle = Program.deserializeHex(
fs.readFileSync(path.join(dir, "message.clsp.hex"), "utf-8"),
);
const node = new FullNode(os.homedir() + "/.chia/mainnet");
const keyStore = new KeyStore(privateKey);
const wallet = new StandardWallet(node, keyStore);
const genesis = fromHex(process.env.GENESIS!);
const amount = 1;
const fee = 0.00005e12;
async function newInstance(initialMessage: Program) {
await wallet.sync();
const spend = wallet.createSpend();
// Curry the puzzle
const puzzle = messagePuzzle.curry([
// Mod hash
Program.fromBytes(messagePuzzle.hash()),
// Message is empty until the eve is spent
Program.nil,
]);
// Create the eve coin
const send = await wallet.send(puzzle.hash(), amount, fee);
spend.coin_spends.push(...send);
// Calculate the root coin id
const eveCoin: Coin = {
parent_coin_info: formatHex(toHex(toCoinId(send[0].coin))),
puzzle_hash: formatHex(puzzle.hashHex()),
amount,
};
// Create the eve solution
const solution = Program.fromList([
// Message
initialMessage,
// Amount
Program.fromInt(amount),
]);
// Spend the eve coin
spend.coin_spends.push({
coin: eveCoin,
puzzle_reveal: puzzle.serializeHex(),
solution: solution.serializeHex(),
});
// Sign the wallet spend
wallet.signSpend(spend, genesis);
// Complete the transaction
console.log("Eve coin id:", toHex(toCoinId(eveCoin)));
console.log(await node.pushTx(spend));
}
interface SyncInfo {
parent: string;
current: string;
}
async function sync(): Promise<SyncInfo> {
const eveCoinId = process.env.EVE_COIN_ID!;
let current = eveCoinId;
let parent = current;
while (true) {
// Fetch coins created by the current coin
const coinRecords = await node.getCoinRecordsByParentIds(
[current],
undefined,
undefined,
true,
);
if (!coinRecords.success) throw new Error(coinRecords.error);
// If there are none, we are already synced
if (!coinRecords.coin_records.length) break;
// Update the parent
parent = current;
// Continue with the child coin as the new singleton
const coinRecord = coinRecords.coin_records[0];
current = toHex(toCoinId(coinRecord.coin));
}
return {
parent,
current,
};
}
async function getMessage(syncInfo: SyncInfo): Promise<Program> {
const coinRecord = await node.getCoinRecordByName(syncInfo.parent);
if (!coinRecord.success) throw new Error(coinRecord.error);
const puzzleAndSolution = await node.getPuzzleAndSolution(
syncInfo.parent,
coinRecord.coin_record.spent_block_index,
);
if (!puzzleAndSolution.success) throw new Error(puzzleAndSolution.error);
const spend = puzzleAndSolution.coin_solution;
const solution = Program.deserializeHex(sanitizeHex(spend.solution)).toList();
return solution[0];
}
async function printMessage() {
const syncInfo = await sync();
const message = await getMessage(syncInfo);
console.log("Message:", message.toString());
}
async function setMessage(newMessage: Program) {
await wallet.sync();
const syncInfo = await sync();
const message = await getMessage(syncInfo);
// Fetch the coin record
const coinRecord = await node.getCoinRecordByName(syncInfo.current);
if (!coinRecord.success) throw new Error(coinRecord.error);
const coin = coinRecord.coin_record.coin;
const spend = wallet.createSpend();
// Create the current puzzle
const puzzle = messagePuzzle.curry([
Program.fromBytes(messagePuzzle.hash()),
message,
]);
// Create the solution
const solution = Program.fromList([newMessage, Program.fromInt(coin.amount)]);
spend.coin_spends.push({
// Spend the current singleton
coin,
// The puzzle reveal contains the old message
puzzle_reveal: puzzle.serializeHex(),
// Spend it with the new message
solution: solution.serializeHex(),
});
const send = await wallet.sendFee(fee);
spend.coin_spends.push(...send);
wallet.signSpend(spend, genesis);
console.log(await node.pushTx(spend));
}
// newInstance(Program.fromText('Hello, world!'));
// printMessage();
// setMessage(Program.fromText('Goodbye, world!'));
State in Chialisp
We are now going to be discussing the idea of state. State is used to maintain some value on-chain. This can be done with a coin that recreates itself currying in a new value.
The example we will be using today stores a message that can be changed by anyone. Essentially, the first coin we create will store the initial state, which is the message. Then, every time the coin is spent, we will create a new coin that stores the new message inside of it.
The first coin is called the eve, and every coin that follows it forms a complete lineage of the history.
First, we will install the needed Chialisp dependencies:
cdv clsp retrieve condition_codes curry_and_treehash