Writing Data to ETH
For this cookbook entry, you'll create and deploy a simple Counter
contract onto a fake local chain, and write a kinode app to interact with it.
Using kit
, create a new project with the echo
template:
kit new counter --template echo
cd counter
Now you can create a contracts
directory within counter
using forge init contracts
.
If foundry is not installed, it can be installed with:
curl -L https://foundry.paradigm.xyz | bash
You can see the simple Counter.sol
contract in contracts/src/Counter.sol
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
You can write a simple script to deploy it at a predictable address, create the file scripts/Deploy.s.sol
:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console, VmSafe} from "forge-std/Script.sol";
import {Counter} from "../src/Counter.sol";
contract DeployScript is Script {
function setUp() public {}
function run() public {
VmSafe.Wallet memory wallet = vm.createWallet(
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
);
vm.startBroadcast(wallet.privateKey);
Counter counter = new Counter();
console.log("Counter deployed at address: ", address(counter));
vm.stopBroadcast();
}
}
Now boot a fakechain, either with kit f
which boots one at port 8545 in the background, or with kit c
.
Then you can run:
forge script --rpc-url http://localhost:8545 script/Deploy.s.sol --broadcast
You'll see a printout that looks something like this:
== Logs ==
Counter deployed at address: 0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82
Great! Now you'll write the kinode app to interact with it.
You're going to use some functions from the eth
library in kinode_process_lib
:
#![allow(unused)] fn main() { use kinode_process_lib::eth; }
Also you'll need to request the capability to message eth:distro:sys
, so you can add it to the request_capabilities
field in pkg/manifest.json
.
Next, you'll need some sort of ABI in order to interact with the contracts.
The crate alloy-sol-types
gives us a solidity macro to either define contracts from JSON, or directly in the rust code.
You'll add it to counter/Cargo.toml
:
alloy-sol-types = "0.7.6"
Now, importing the following types from the crate:
#![allow(unused)] fn main() { use alloy_sol_types::{sol, SolCall, SolValue}; }
You can do the following:
#![allow(unused)] fn main() { sol! { contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } } }
Pretty cool, you can now do things like define a setNumber() call just like this:
#![allow(unused)] fn main() { let contract_call = setNumberCall { newNumber: U256::from(58)}; }
Start with a simple setup to read the current count, and print it out!
#![allow(unused)] fn main() { use kinode_process_lib::{await_message, call_init, eth::{Address as EthAddress, Provider, TransactionInput, TransactionRequest, U256}, println, Address, Response}; use alloy_sol_types::{sol, SolCall, SolValue}; use std::str::FromStr; wit_bindgen::generate!({ path: "target/wit", world: "process-v0", }); sol! { contract Counter { uint256 public number; function setNumber(uint256 newNumber) public { number = newNumber; } function increment() public { number++; } } } pub const COUNTER_ADDRESS: &str = "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82"; fn read(provider: &Provider) -> anyhow::Result<U256> { let counter_address = EthAddress::from_str(COUNTER_ADDRESS).unwrap(); let count = Counter::numberCall {}.abi_encode(); let tx = TransactionRequest::default() .to(counter_address) .input(count.into()); let x = provider.call(tx, None); match x { Ok(b) => { let number = U256::abi_decode(&b, false)?; println!("current count: {:?}", number.to::<u64>()); Ok(number) } Err(e) => { println!("error getting current count: {:?}", e); Err(anyhow::anyhow!("error getting current count: {:?}", e)) } } } call_init!(init); fn init(our: Address) { println!("begin"); let provider = Provider::new(31337, 5); let _count = read(&provider); loop { match handle_message(&our, &provider) { Ok(()) => {} Err(e) => { println!("error: {:?}", e); } }; } } }
Now add the 2 writes that are possible: increment() and setNumber(newNumber). To do this, you'll need to define a wallet, and import a few new crates:
alloy-primitives = "0.7.6"
alloy-rlp = "0.3.5"
alloy = { version = "0.1.2", features = [
"network",
"signers",
"signer-local",
"consensus",
"rpc-types"
]}
You'll also define a simple enum so you can call the program with each of the 3 actions:
#![allow(unused)] fn main() { #[derive(Debug, Deserialize, Serialize)] pub enum CounterAction { Increment, Read, SetNumber(u64), } }
When creating a wallet, you can use one of the funded addresses on the anvil fakechain, like so:
#![allow(unused)] fn main() { use alloy::{ consensus::{SignableTransaction, TxEip1559, TxEnvelope}, network::{eip2718::Encodable2718, TxSignerSync}, primitives::TxKind, rpc::types::eth::TransactionRequest, signers::local::PrivateKeySigner, }; use alloy_rlp::Encodable; use alloy_sol_types::{sol, SolCall, SolValue}; use kinode_process_lib::{ await_message, call_init, eth::{Address as EthAddress, Provider, U256}, println, Address, Response, }; use serde::{Deserialize, Serialize}; use std::str::FromStr; let wallet = PrivateKeySigner::from_str("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") .unwrap(); }
First, branching on the enum type Increment
, call the increment() function with no arguments:
#![allow(unused)] fn main() { CounterAction::Increment => { let increment = Counter::incrementCall {}.abi_encode(); let nonce = provider .get_transaction_count(wallet.address(), None) .unwrap() .to::<u64>(); let mut tx = TxEip1559 { chain_id: 31337, nonce: nonce, to: TxKind::Call(EthAddress::from_str(COUNTER_ADDRESS).unwrap()), gas_limit: 15000000, max_fee_per_gas: 10000000000, max_priority_fee_per_gas: 300000000, input: increment.into(), ..Default::default() }; let sig = wallet.sign_transaction_sync(&mut tx)?; let signed = TxEnvelope::from(tx.into_signed(sig)); let mut buf = vec![]; signed.encode_2718(&mut buf); let tx_hash = provider.send_raw_transaction(buf.into()); println!("tx_hash: {:?}", tx_hash); } }
Note how you can do provider.get_transaction_count()
to get the current nonce of the account!
Next, do the same for setNumber
!
#![allow(unused)] fn main() { CounterAction::SetNumber(n) => { let set_number = Counter::setNumberCall { newNumber: U256::from(n), } .abi_encode(); let nonce = provider .get_transaction_count(wallet.address(), None) .unwrap() .to::<u64>(); let mut tx = TxEip1559 { chain_id: 31337, nonce: nonce, to: TxKind::Call(EthAddress::from_str(COUNTER_ADDRESS).unwrap()), gas_limit: 15000000, max_fee_per_gas: 10000000000, max_priority_fee_per_gas: 300000000, input: set_number.into(), ..Default::default() }; let sig = wallet.sign_transaction_sync(&mut tx)?; let signed = TxEnvelope::from(tx.into_signed(sig)); let mut buf = vec![]; signed.encode(&mut buf); let tx_hash = provider.send_raw_transaction(buf.into()); println!("tx_hash: {:?}", tx_hash); } }
Nice!
Putting it all together, you can build and start the package on a fake node (kit f
if you don't have one running), kit bs
.
fake.dev > m our@counter:counter:template.os '{"SetNumber": 55}'
counter:template.os: tx_hash: Ok(0x5dba574f2a9a2c095cee960868433e23c64b685966fba57568c4d6a0fd99ef6c)
fake.dev > m our@counter:counter:template.os "Read"
counter:template.os: current count: 55
fake.dev > m our@counter:counter:template.os "Increment"
counter:template.os: tx_hash: Ok(0xc38ee230c2605c294a37794244334c0d20a5b5e090704b34f4a7998021418d7b)
fake.dev > m our@counter:counter:template.os "Read"
counter:template.os: current count: 56
You can find these steps outlined by commit in the counter example repo!