-
Notifications
You must be signed in to change notification settings - Fork 92
Smart Contracts
The eth
gem allows for creating smart contracts from a file, from address and ABI, as well as from binary payload and ABI.
- From file: provide a Solidity code file and the library will compile it and provide an object with a name and ABI that can be easily deployed using a client.
- From payload and ABI: alternatively, if you have the binary payload and the ABI, a contract can be created that can be deployed too. Once deployed, it's possible to interact with the contract.
- From address and ABI: to interact with a public contract, e.g., the ENS registry, you will need the address and the ABI. It's possible to read the contract using a call or to write the contract by using transact.
Create, compile, and deploy smart contracts.
contract = Eth::Contract.from_file(file: 'spec/fixtures/contracts/dummy.sol')
# => #<Eth::Contract::Dummy:0x00007fbeee936598>
cli = Eth::Client.create "/tmp/geth.ipc"
# => #<Eth::Client::Ipc:0x00007fbeee946128 @gas_limit=21000, @id=0, @max_fee_per_gas=0.2e11, @max_priority_fee_per_gas=0, @path="/tmp/geth.ipc">
address = cli.deploy_and_wait(contract)
# => "0x2f2faa160420cee087ded96bad52475147136bd8"
Transact with or call the deployed contract.
cli.transact_and_wait(contract, "set", 1234)
# => "0x49ca4c0a5729da19a1d2574de9a444a9cd3219bdad81745b54f9cf3bb83b6a06"
cli.call(contract, "get")
# => 1234
Call an existing contract, e.g., the ENS registry:
ens_registry_abi = '[{"inputs":[{"internalType":"contract ENS","name":"_old","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"NewOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"resolver","type":"address"}],"name":"NewResolver","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"NewTTL","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"Transfer","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"old","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"recordExists","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"resolver","type":"address"}],"name":"setResolver","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setSubnodeOwner","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setSubnodeRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setTTL","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"ttl","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"}]'
ens_registry_address = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
ens_registry_name = "ENSRegistryWithFallback"
ens_registry = Eth::Contract.from_abi(name: ens_registry_name, address: ens_registry_address, abi: ens_registry_abi)
# => #<Eth::Contract::ENSRegistryWithFallback:0x000055bece570980>
ens_registry.address
# => "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"
cli.call(ens_registry, "old")
# => "0x112234455c3a32fd11230c42e7bccd4a84e02010"
Calling a smart contract is a read operation (see above), however, sometimes you want to transact with (write to) a smart contract to update the state. Here is a full example.
First, create a client (here: Goerli testnet through Infura). It's recommended to set fees as you see fit.
# Create a client / RPC endpoint and set default fees
goerli = Client.create "https://goerli.infura.io/v3/31b..30c"
# => #<Eth::Client::Http:0x000055a0ec933dd8
# @gas_limit=21000,
# @host="goerli.infura.io",
# @id=0,
# @max_fee_per_gas=0.2e11,
# @max_priority_fee_per_gas=0,
# @port=443,
# @ssl=true,
# @uri=#<URI::HTTPS https://goerli.infura.io/v3/31b..30c>>
goerli.max_priority_fee_per_gas = 2 * Unit::GWEI
# => 0.2e10
goerli.max_fee_per_gas = 69 * Unit::GWEI
# => 0.69e11
You'll need an account because Infura won't offer you a signer. Let's use a local UTC-JSON file and decrypt it.
# Load some account to sign transactions with later
signer = JSON.load File.open "/path/to/UTC--2019-01-30T13-26-55.927410794Z--e0a2bd4258d2768837baa26a28fe71dc079f84c7"
# => {"address"=>"e0a2bd4258d2768837baa26a28fe71dc079f84c7",
# "crypto"=>
# {"cipher"=>"aes-128-ctr",
# "ciphertext"=>"700..39c",
# "cipherparams"=>{"iv"=>"555..00e"},
# "kdf"=>"scrypt",
# "kdfparams"=>
# {"dklen"=>32,
# "n"=>262144,
# "p"=>1,
# "r"=>8,
# "salt"=>"2bb..93c"},
# "mac"=>"dfc..079"},
# "id"=>"3332ffa0-6511-43dd-a4e3-937b00e180d1",
# "version"=>3}
signer = Key::Decrypter.perform signer, "my-super-secure-password"
# => #<Eth::Key:0x000055b04afad430
# @private_key=
# #<Secp256k1::PrivateKey:0x000055b04afac6c0
# @data="G?*\..\x86">,
# @public_key=#<Secp256k1::PublicKey:0x000055b04afac670>>
signer.address.to_s
# => "0xe0a2Bd4258D2768837BAa26A28fE71Dc079f84c7"
Now, let's load a simple contract from ABI, here: a "House."
# Create a contract from an ABI and address
house_address = Address.new "0x2d234b332E94257f6538290Ae335eEF94B0974F0"
# => #<Eth::Address:0x000055a0ec8b3ed0 @address="0x2d234b332E94257f6538290Ae335eEF94B0974F0">
house_name = "House"
# => "House"
house_abi = '[{"inputs":[{"internalType":"uint64","name":"_subscriptionId","type":"uint64"},{"internalType":"uint256","name":"_minBalance","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"InvalidBet","type":"error"},{"inputs":[],"name":"InvalidWithdraw","type":"error"},{"inputs":[{"internalType":"address","name":"have","type":"address"},{"internalType":"address","name":"want","type":"address"}],"name":"OnlyCoordinatorCanFulfill","type":"error"},{"inputs":[],"name":"RequestError","type":"error"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"bool","name":"rebet","type":"bool"},{"indexed":false,"internalType":"uint8","name":"direction","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"result","type":"uint8"},{"indexed":false,"internalType":"address","name":"better","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Result","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"cashOut","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getBalance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_player","type":"address"}],"name":"getWinnings","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"_direction","type":"uint8"}],"name":"makeBet","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"uint256","name":"requestId","type":"uint256"},{"internalType":"uint256[]","name":"randomWords","type":"uint256[]"}],"name":"rawFulfillRandomWords","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"requestIds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_vrf","type":"address"}],"name":"setVRFCoordinator","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"},{"internalType":"address","name":"_to","type":"address"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]'
# => "[{\"inputs\":[{\"internalType\":\"uint64\",\"name\":\"_subscriptionId\",\"type\":\"uint64\"},{\"internalType\":\"uint256\",\"name\":\"_minBalance\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"InvalidBet\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidWithdraw\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"have\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"want\",\"type\":\"address\"}],\"name\":\"OnlyCoordinatorCanFulfill\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"RequestError\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bool\",\"name\":\"rebet\",\"type\":\"bool\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"direction\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"uint8\",\"name\":\"result\",\"type\":\"uint8\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"better\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"}],\"name\":\"Result\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"}],\"name\":\"cashOut\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_player\",\"type\":\"address\"}],\"name\":\"getWinnings\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint8\",\"name\":\"_direction\",\"type\":\"uint8\"}],\"name\":\"makeBet\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"requestId\",\"type\":\"uint256\"},{\"internalType\":\"uint256[]\",\"name\":\"randomWords\",\"type\":\"uint256[]\"}],\"name\":\"rawFulfillRandomWords\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"name\":\"requestIds\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_newOwner\",\"type\":\"address\"}],\"name\":\"setOwner\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_vrf\",\"type\":\"address\"}],\"name\":\"setVRFCoordinator\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_amount\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"withdraw\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]"
house = Eth::Contract.from_abi(name: house_name, address: house_address, abi: house_abi)
# => #<Eth::Contract::House:0x000055a0ec4bcf98>
house.address.to_s
# => "0x2d234b332E94257f6538290Ae335eEF94B0974F0"
As described in the previous section, calling is free and instant. Try to query your casino balance.
# To read the contract, use `call`
goerli.call(house, "getBalance")
# => 0
goerli.call(house, "getWinnings", Key.new.address.to_s)
# => 0
Finally, to write the contract use transact_and_wait
. Thuis requires your contract, the function name called, the arguments (one or multiple), and finally the sender key as a singer. It will return a transaction hash once it's mined on the chain.
# To write to the contract, use `transact` (or better `transact_and_wait`)
goerli.transact_and_wait(house, "cashOut", 999999999999999999, sender_key: signer)
# => "0x67e9b96b99e51737231c17bc492e37e023e87372253627647ea4a2a7ebf04918"
goerli.transact_and_wait(house, "withdraw", 999999999999999999, Key.new.address.to_s, sender_key: signer)
# => "0x7032d1bcc23b8f6881eabdbf64ddbb22ae333ad6c02027d8dc5924c0445bdf24"
Deploy your own house to start playing around, code is on Etherscan: https://goerli.etherscan.io/address/0x2d234b332E94257f6538290Ae335eEF94B0974F0#code
The gem also comes with an EIP-1271 smart-contract authentification interface.
cli.is_valid_signature contract, hash, signature
# => true