Simple elixir client to interact with the ethereum blockchain, deploying and calling smart contracts.
- Erlang/OTP 24
- Elixir 1.13.0
- Rust 1.58.0
- Geth
"I just want to try it right now!!"
Run the following commands to get started. This will configure and start both Ethereum nodes, the Livebook server and the EthClient Elixir application.
git clone git@github.com:lambdaclass/ethereum_war_game_tooling.git
cd ethereum_war_game_tooling
tilt up
Now that you have everything ready you can start playing with the tool in one of two ways.
Accesing to the Application running at http://localhost:8080 and creating a new notebook.
cd eth_client
make tilt.up
All global configuration is kept in the EthClient.Context
module, which holds the following values:
rpc_host
: The hostname needed to interact with the ethereum Json RPC API. This can be an infura-type url or just the hostname of an actual node.chain_id
: The id of the chain currently in use (1
for mainnet,4
for Rinkeby, etc).user_account
: A value of typeEthClient.Account
, which holds two fields,address
andprivate_key
. This is the account used to sign and send all transactions.contract
: A value of typeEthClient.Contract
, which holds two fields,address
andfunctions
.
You can change any of these config values at runtime by using the functions exposed by the EthClient.Context
module.
The default config assumes you're running the local ethereum network this repo provides.
If you want to use infura-type host this are the setps you must follow:
First set your infura api key
Context.set_infura_api_key("your_infura_api_key")
Then set the your etherscan api key
Context.set_etherscan_api_key("your_eth_scan_api_key")
And finally set the name of the chain you want to use, for now this are the supported chains: eht mainnet, rinkeby and ropsten.
EthClient.set_chain("chain_name")
Currently there are three functions in the EthClient
module that form the main API:
-
EthClient.deploy(bin_path)
deploys a compiled smart contract given a path to its.bin
file, generated by compiling said contract (i.e. by running something likesolc --bin contract.sol
). After a successful deployment, the context will be updated to use it. -
EthClient.call(method, arguments)
calls any read-only public method of a contract. -
EthClient.invoke(method, arguments, amount)
calls any public method of a contract that requires a transaction (usually to write stuff to the blockchain). Theamount
parameter controls how mucheth
is sent to the contract.
When there is an .abi
file with the ABI of the contract, user can interact with API in a different way:
-
EthClient.deploy(bin_path, abi_path)
deploys a compiled smart contract (as it is explained in Without an ABI file section) but it also add contract functions to the context when deploying it. -
Then, user can interact with contract functions by calling
Context.contract.functions.contract_function_name.(parameters)
, either if it is a read-only method or not.
When running iex -S mix
, there will be a default bin_path
variable loaded with the path to a compiled Storage
contract and a default abi_path
variable with the path to contract's ABI. You can then immediately deploy it with
EthClient.deploy(bin_path)
and then call each of its functions. If you're running the local ethereum network, you should see something like the following:
iex(1)> EthClient.deploy(bin_path)
19:53:26.918 [info] Deployment transaction accepted by the network, tx_hash: 0x14765466533a85c90ce45dd966854dc5fb95543ba97f343f389872c64ed9597b
19:53:26.922 [info] Waiting for confirmation...
19:53:28.931 [info] Contract deployed, address: 0x69148897094941cfad7fd3d52c5e1a810ba4e123 Current contract updated
:ok
iex(2)> EthClient.call("test_function()", [])
{:ok, "0x0000000000000000000000000000000000000000000000000000000000000001"}
iex(3)> EthClient.invoke("store(uint256)", [20], 0)
19:55:28.127 [info] Transaction accepted by the network, tx_hash: 0x137320dcfb61055313f73aafa799670a4d172936bc91200ebf7a95092f77c297
19:55:28.127 [info] Waiting for confirmation...
19:55:41.177 [info] Transaction confirmed!
{:ok, "0x137320dcfb61055313f73aafa799670a4d172936bc91200ebf7a95092f77c297"}
iex(4)> EthClient.call("retrieve()", [])
{:ok, "0x0000000000000000000000000000000000000000000000000000000000000014"}
Install dependencies with
make init
then source your .bashrc
, .zshrc
or similar files to add foundry to your PATH
, then run foundryup
to install forge
and cast
.
To run two local Ethereum nodes:
cd geth_nodes
make setup
make up
You can use the miner accounts to pay for transactions.
Node 1 miner account
- Address:
0xafb72ccaeb7e22c8a7640f605824b0898424b3da
- Private key:
e90d75baafee04b3d9941bd8d76abe799b391aec596515dee11a9bd55f05709c
Node 2 miner account:
- Address:
0x77b648683cde1d69544ed6f4f7204e8d51c324db
- Private key:
f71d3dd32649f9bdfc8e4a5232d3f245860243756f96fbe070c31fc44c9293f4
You can then deploy the test contract with
make deploy_test_contract
under the root directory.
The code for this contract is in contracts/src/Storage.sol
; it has 3 functions: test_function
will always return 1
, store(uint256)
stores the given number and retrieve()
returns said number.
The output should look like this
forge create --rpc-url http://127.0.0.1:8545 Storage --root contracts/ --private-key df57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e
compiling...
Compiling 1 files with 0.8.10
Compilation finished successfully
Compiler run successful
success.
Deployer: 0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199
Deployed to: 0xb581c9264f59bf0289fa76d61b2d0746dce3c30d
Transaction hash: 0xe2c19a2f0766c0e00c416433a8cf53c9993fc918e2b321f348d8de421c195416
Take note of the address you are given after Deployed to:
, as that is the contract's address in the local chain.
With it, you can now call its test function using cast
like this:
cast call <contract_address> "test_function()(uint256)" --rpc-url http://localhost:8545
which should return 1
.
You can also send a transaction to call the store
function
cast send <contract_address> --private-key <private_key> "store(uint256)" 5 --rpc-url http://localhost:8545
where the private key needs to have some funds to pay for the transaction (for this you can use one of the miner accounts). Output should look like this
blockHash "0xd2f9afae4ef28c63ceccd7575c4370c17ead74448567ca651ec82a7051434e01"
blockNumber "0x5"
contractAddress null
cumulativeGasUsed "0x6746"
effectiveGasPrice "0xd1790ced"
gasUsed "0x6746"
logs []
logsBloom "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
root null
status "0x1"
transactionHash "0x0bc2448d1f7ee4be24bae1201e687d937d5f094af5e64f09ca0b279d62bf0b81"
transactionIndex "0x0"
type "0x2"
After storing a number, you can retrieve it with
cast call <contract_address> "retrieve()(uint256)" --rpc-url http://localhost:8545
Apart from EthClient calls, other modules can be called.
- RPC module handles RPC calls to the node.
- ABI module is a helper used to get the ABI of the desired contract, either calling to etherscan or locally.
- The Contract module uses the ABI module to generate elixir functions that invoke/call the said methods in the contract.
- The Account module defines a struct for accounts
- The Context module saves the current context, ie. contract ABI and address, and is updated as used. It is maintained with a Supervisor process.
- The RawTransaction module handles encoding of transactions for sending. (via RPC module)
- The Application module handles environment