Skip to content

Commit

Permalink
add write to eth cookbook
Browse files Browse the repository at this point in the history
  • Loading branch information
barraguda committed May 2, 2024
1 parent 1da39f5 commit 8208a82
Show file tree
Hide file tree
Showing 2 changed files with 311 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
- [Intro to Web UI with File Transfer](./cookbook/file_transfer_ui.md)
- [Writing and Running Scripts](./cookbook/writing_scripts.md)
- [Reading Data from ETH](./cookbook/reading_data_from_eth.md)
- [Writing Data to ETH](./cookbook/writing_data_to_eth.md)
- [Use ZK proofs with SP1](./cookbook/zk_with_sp1.md)
- [API Reference](./api_reference.md)
- [ETH Provider API](./apis/eth_provider.md)
Expand Down
310 changes: 310 additions & 0 deletions src/cookbook/writing_data_to_eth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# Writing Data to ETH

For this cookbook entry, we will 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
```

We can now create a `contracts` directory within our 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`:

```solidity
// 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++;
}
}
```

We can write a simple script to deploy it at a predictable address, create the file `scripts/Deploy.s.sol`:

```solidity
// 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 we boot a fakechain, either with `kit f` which boots one at port 8545 in the background, or manually with `anvil --load-state kinostate.json`.

Then we can run `forge script --rpc-url http://localhost:8545 script/Deploy.s.sol --broadcast` we'll see a printout that looks something like this:

```
== Logs ==
Counter deployed at address: 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
```

Great! Now let's write the kinode app to interact with it!

We're going to use some functions from the `eth` library in `kinode_process_lib`:

```rust
use kinode_process_lib::eth;
```

Also we'll need to request the capability to message `eth:distro:sys`, so we can add it to the `request_capabilities` field in `pkg/manifest.json`.

Next, we'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. We'll add it to `counter/Cargo.toml`:

```
alloy-sol-types = "0.7.0"
```

Now, importing the following types from the crate:

```rust
use alloy_sol_types::{sol, SolCall, SolValue};
```

We can do the following:

```rust
sol! {
contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}
}
```

Pretty cool huh? We can now do things like define a setNumber() call just like this:

```rust
let contract_call = setNumberCall { newNumber: U256::from(58)};
```

We'll start with a simple setup to read the current count, and print it out!

```rust
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: "wit",
world: "process",
});

sol! {
contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}
}

pub const COUNTER_ADDRESS: &str = "0x610178dA211FEF7D417bC0e6FeD39F05609AD788";

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(TransactionInput::new(count.into()));
let x = provider.call(tx, None);

match x {
Ok(b) => {
let number = U256::abi_decode(&b, false).unwrap();
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, we'll add the 2 writes that are possible: increment() and setNumber(newNumber). To do this, we'll need to define a wallet, and import a few new crates:

```
alloy-primitives = "0.7.0"
alloy-rlp = "0.3.4"
alloy-signer-wallet = { git = "https://github.com/alloy-rs/alloy", rev = "cad7935" }
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "cad7935" }
alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "cad7935" }
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "cad7935" }
```

We'll also define a simple enum so we can call the program with each of the 3 actions:

```rust
#[derive(Debug, Deserialize, Serialize)]
pub enum CounterAction {
Increment,
Read,
SetNumber(u64),
}
```

When creating a wallet, we can use one of the funded addresses on the anvil fakechain, like so:

```rust
use alloy_consensus::{SignableTransaction, TxEnvelope, TxLegacy};
use alloy_network::TxSignerSync;
use alloy_primitives::TxKind;
use alloy_rlp::Encodable;
use alloy_rpc_types::TransactionRequest;
use alloy_signer_wallet::LocalWallet;
use alloy_sol_types::{sol, SolCall, SolValue};
use kinode_process_lib::{
await_message, call_init,
eth::{Address as EthAddress, Provider, TransactionInput, U256},
println, Address, Response,
};

let wallet =
LocalWallet::from_str("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80")
.unwrap();
```

First, branching on the enum type `Increment`, let's call the increment() function with no arguments:

```rust
CounterAction::Increment => {
let increment = Counter::incrementCall {}.abi_encode();
let nonce = provider
.get_transaction_count(wallet.address(), None)
.unwrap()
.to::<u64>();

let mut tx = TxLegacy {
chain_id: Some(31337),
nonce: nonce,
to: TxKind::Call(EthAddress::from_str(COUNTER_ADDRESS).unwrap()),
gas_limit: 100000,
gas_price: 100000000,
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(&mut buf);

let tx_hash = provider.send_raw_transaction(buf.into());
println!("tx_hash: {:?}", tx_hash);
}
```

Note how we do provider.get_transaction_count() to get our current nonce!

Next, let's do the same for setNumber!

```rust
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 = TxLegacy {
chain_id: Some(31337),
nonce: nonce,
to: TxKind::Call(EthAddress::from_str(COUNTER_ADDRESS).unwrap()),
gas_limit: 100000,
gas_price: 100000000,
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, we 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!](https://github.com/bitful-pannul/counterexample)

0 comments on commit 8208a82

Please sign in to comment.