You will have a setup with an implementation contract and a proxy contract. Proxy contract is called and delegates function calls to implementation contract. This allows the implementation contract to be easily upgraded.
Install Open Zeppelin deps used for upgradable contracts.
cd sol-contracts
forge install foundry-rs/forge-std
forge install OpenZeppelin/openzeppelin-foundry-upgrades
forge install OpenZeppelin/openzeppelin-contracts-upgradeableBuild contracts (original and upgraded versions) and copy the abi-s so that Rust and JS can read them.
cd sol-contracts
sh build.shInstall eth_template package on your node.
cd eth_template
kit bsRun a dev server.
cd eth_template/ui
npm run devRun anvil.
anvilIn .env you specify the addresses of the contracts, rpc urls, and the current chain id you want the app to work with.
The variables are prefixed with VITE_ so they can also be used by the UI.
After modifying .env, to make the changes propagate,
- restart the dev server
- make a meaningless change in eth_template/src and run
kit bs.
lazy_statics at the top of the lib.rs file are where .env file is being read on the backend.
Top of the main.jsx file is where .env is being read on the frontend.
Go to your node's home folder, and open the .eth_providers file.
Whichever chain you want to use, will need to have a rpc url set.
For anvil, add the following into the list:
{
"chain_id": 31337,
"trusted": true,
"provider": {
"RpcUrl": "ws://localhost:8545"
}
}TODO - figure out why this code isn't able to set eth providers programmatically.
Set up your foundry wallet.
In place of wallet-name, use anvil, optimism, mainnet, or sepolia, to insert the private key for each of these, respectively.
These names are hardcoded into script.sh, which is used for running Solidity scripts.
When running sh script.sh, you will be asked for the password you input here.
cast wallet import <wallet-name> --interactiveDepending on the current chain_id the process is compiled with (as specified in .env), the terminal commands shown below will store the key specifically for that chain_id.
To store the key encrypted in state, use:
m our@eth_template:eth_template:astronaut.os '{"EncryptWallet": {"private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "password": "some-password"}}'
To be able to interact with the contract, you need to decrypt the key. Be careful, it will be stored in your kinode state unencrypted.
m our@eth_template:eth_template:astronaut.os '{"DecryptWallet": "some-password"}'
After you're done with using it, re-encrypt the key.
m our@eth_template:eth_template:astronaut.os '{"EncryptWallet": {"password": "some-password"}}'
Add Anvil network to Metamask. Use http://localhost:8545 as the RPC URL and 31337 as the chain ID.
Sometimes, the transactions from Metamask on Anvil will stay pending indefinitely. In that case, do the following:
- Delete Anvil network from Metamask and re-add it.
- Clear activity tab data in Metamask (Settings -> Advanced).
Specify the current chain id and its rpc url in the .env file.
Before running a script, run forge clean as part of the workflow.
Specify which script you want to run as the first argument in script.sh.
forge clean
sh script.sh Deploy.s.sol Find proxy address from the output of the deploy script and paste it into the VITE_ANVIL_CONTRACT_ADDRESS field in the .env file.
Recompile the process and restart the server.
From the UI, you can interact with the counter contract in 2 ways.
Send an action to the backend from the UI via WS, which will then make a call to the chain.
Make sure that your connected Metamask account is your Anvil account on the Anvil network. Click "Connect Metamask". Then you can talk to Anvil directly from Metamask.
There are a few other actions for demo purposes which can be accessed from the terminal.
Get all logs of events of type "NumberIncremented" and store them to local index. Starting from block 0. (This is fine if you are using anvil).
m our@eth_template:eth_template:astronaut.os '{"GetIncrementLogs": 0}'
Subscribe to logs of events of type "NumberIncremented" and store them to local index. After subscribing, when you make an increment, the index will be updated.
m our@eth_template:eth_template:astronaut.os "SubscribeIncrementLogs"
Unsubscribe from logs of events of type "NumberIncremented".
m our@eth_template:eth_template:astronaut.os "UnsubscribeIncrementLogs"
Make many increments; a convenience command for testing.
m our@eth_template:eth_template:astronaut.os '{"ManyIncrements": 5}'
To demonstrate getting a large amount of logs safely, we get logs from USDC contract on OP Mainnet.
In .env, change VITE_CURRENT_CHAIN_ID to 10 and run recompile the package.
m our@eth_template:eth_template:astronaut.os '{"GetUsdcLogs": {"from_block": 123865000, "to_block": 123865806}}'
Try running the following in node terminal.
m our eth_template:eth_template:astronaut.os "Decrement"It shouldn't work, since decrement() is not implemented in the initial version of the Counter contract.
You should be able to verify that it is not working correctly by checking the counter value in the UI.
The following steps will show you how to upgrade the contract which will enable the usage of decrement() function.
Assuming you ran sh build.sh which copied the abi of the upgraded contract into the crate, you can modify the sol! macro in contract_caller.rs to use the "abi/CounterV2.json" instead of "abi/Counter.json".
sol!(
#[allow(missing_docs)]
#[sol(rpc)]
#[derive(Debug, Deserialize, Serialize)]
COUNTER,
"abi/CounterV2.json"
);Next, run the upgrade script.
forge clean
sh script.sh UpgradeToV2.s.sol This upgrades the contract, adding a decrement() function.
The app will still call the same proxy address as before, which will delegate the call to new version of implementation contract.
Now run the following in node terminal:
m our eth_template:eth_template:astronaut.os "Decrement"You should be able to verify that it works correctly by checking the counter value in the UI.
Follow along by looking at the code as you're reading this.
sol-contracts contains all the usual foundry code for deployment, testing, etc., but also, the code for integration with the Kinode package is included.
build.sh copies the abi-s of specified contracts into the ui and into the rust backend, which they both use to interact with the contract.
script.sh is the script which is used to run the Deploy.s.sol and UpgradeToV2.s.sol scripts.
Deploy.s.sol deploys the contract to the chain specified in .env.
UpgradeToV2.s.sol upgrades the contract to the upgraded version.
A generalized struct containing methods for interacting with the chain.
Used by ContractCaller struct.
Contains Caller struct.
Implements methods for interacting with multiple contracts, using the primitives from the Caller struct.
To interact with each contract, it imports the contract ABI using sol! macro from alloy.
Filter struct is used to specify which logs to subscribe to, see subscribe_increment_logs in contract_caller.rs.
Subscription updates are handled with handle_eth_message function.
Filter is used whenever getting or subscribing to logs.
Example filter from GetUsdcLogs action:
let address = EthAddress::from_str(
ð_caller.contract_addresses.get(&ContractName::Usdc).unwrap()
).unwrap();
let sender_address = EthAddress::from_str("0xC8373EDFaD6d5C5f600b6b2507F78431C5271fF5").unwrap();
let mut sender_topic_bytes = [0u8; 32];
sender_topic_bytes[12..].copy_from_slice(&sender_address.to_vec());
let sender_topic: FixedBytes<32> = FixedBytes::from_slice(&sender_topic_bytes);
let filter: Filter = Filter::new()
.address(address)
.from_block(from_block)
.to_block(to_block)
.event("Transfer(address,address,uint256)")
.topic1(sender_topic);address specifies the address of the contract from which we are fetching logs.
from_block and to_block specify the desired range.
event specifies what type of event we are fetching, as defined in the ABI.
topic1, topic2, topic3 would in this example refer to address (from), address (to), and uint256 (value) in the event.
topic1, as shown in the code, is used to filter for events where address (from) is equal to sender_address.
All arguments in the filter are optional, but it is recommended to always use address, from_block, and to_block.
get_logs_safely functions allow for getting an arbitrary amount of logs.
If the chunk size is too large for any subset of the block range, they will retry with a halved chunk size, thus making getting logs safe.
It is recommended to use get_logs_safely_binary_search() for most cases.
caller.get_logs_safely_binary_search()
It approximates the largest amount that can be fetched at once by trial and error, and then recursively fetches logs in the requested range until the entire range has been fetched.
caller.get_logs_safely_linear()
It takes a chunk size, and then recursively fetches chunks of logs in the requested range until the entire range has been fetched.
To specify a range, use a Filter.
get_logs_safely_linear only supports a Filter which has:
from_blockas BlockNumberOrTag::Numberto_blockas BlockNumberOrTag::Number or BlockNumberOrTag::Latest
For an example, try Getting Usdc Logs Safely.
An approximate benchmark demonstrated that cca 30 days of logs of all transfers on Usdc contract on Optimism Mainnet was fetched in 5.5 minutes.
Using "Get Number" from the UI as an example.
The chain of messages is as follows: UI -ws-> BE --> chain --> BE -ws-> UI