Skip to content

Commit

Permalink
Smart Contract Support (#68)
Browse files Browse the repository at this point in the history
* Smart Contract Support

* moved files

* remove unused truffle functions

* add contract spec

* deploy and deploy_and_wait

* Secure the address

* fix Contract::Initializer

* Enable create from file

* fix create contract spec

* fix Contract::Event

* Implement call to client

* AUTHORS

* delete create_function_proxies

* remove unused methods

* fixed args

* remove Contract::Deployment

* Avoid active_support dependency

* remove dependency activesupport

* add spec: deploy the contract with key

* add spec: legacy transactions

* use Eth::Util

* Use Eth::Contract for deploy

* set and return the contract address

* .transact .transact_and_wait

* Use keyword argument

* add docs

* Remove encoder dependencies

* Remove decoder dependencies

* remove unused methods

* add test for Contract::Abi.parse_abi()

* remove unused methods

* add test FunctionInput & functionOutput

* add test for Contract::Function

* use Eth::Abi::Type

* add test for contract

* Documentation of contract class

* add test cases for transact and call methods

* rufo

* Move parse_abi() into contract class

* 100.00% documented

* use Eth::Address

* encoded_function_signature

* add docs for client methods

* add test for call()

* If contract.address is set, use it.

* update README

* Update lib/eth/client.rb

Co-authored-by: Afr Schoe <[email protected]>

* Update lib/eth/client.rb

Co-authored-by: Afr Schoe <[email protected]>

* Update spec/fixtures/contracts/test_contract.sol

Co-authored-by: Afr Schoe <[email protected]>

* forwardable does not require

* Use constant values for smart contract costs

Co-authored-by: Afr Schoe <[email protected]>
  • Loading branch information
kurotaky and q9f authored May 6, 2022
1 parent f243cd4 commit 07befbe
Show file tree
Hide file tree
Showing 20 changed files with 891 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ The Ruby-Eth Contributors are:
* Afri Schoedon @q9f
* John Omar @chainoperator
* Joshua Peek @josh
* Yuta Kurotaki @kurotaky

See also:
* https://github.com/q9f/eth.rb/graphs/contributors
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Contents:
- [2.6. Ethereum RLP Encoder and Decoder](#26-ethereum-rlp-encoder-and-decoder)
- [2.7. Ethereum RPC-Client](#27-ethereum-rpc-client)
- [2.8 Solidity Compiler Bindings](#28-solidity-compiler-bindings)
- [2.9 Interact with Smart Contract](#29-interact-with-smart-contract)
- [3. Documentation](#3-documentation)
- [4. Testing](#4-testing)
- [5. Contributing](#5-contributing)
Expand Down Expand Up @@ -253,6 +254,23 @@ contract = solc.compile "spec/fixtures/contracts/greeter.sol"

The `contract["Greeter"]["bin"]` could be directly used to deploy the contract as `Eth::Tx` payload. Check out the [Documentation](https://q9f.github.io/eth.rb/) for more details.

### 2.9 Interact with Smart Contract

Functions to interact with smart contract.

```ruby
contract = Eth::Contract.create(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"
cli.transact_and_wait(contract, "set", 1234)
# => "0x49ca4c0a5729da19a1d2574de9a444a9cd3219bdad81745b54f9cf3bb83b6a06"
cli.call(contract, "get")
# => 1234
```

## 3. Documentation
The documentation can be found at: https://q9f.github.io/eth.rb

Expand Down
6 changes: 6 additions & 0 deletions lib/eth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ module Eth
require "eth/address"
require "eth/chain"
require "eth/constant"
require "eth/contract"
require "eth/contract/event"
require "eth/contract/function"
require "eth/contract/function_input"
require "eth/contract/function_output"
require "eth/contract/initializer"
require "eth/client"
require "eth/client/http"
require "eth/client/ipc"
Expand Down
236 changes: 236 additions & 0 deletions lib/eth/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,242 @@ def transfer(destination, amount, sender_key = nil, legacy = false)
end
end

# Deploy contract and waits for it to be mined.
# Uses `eth_coinbase` or external signer
# if no sender key is provided.
#
# @overload deploy(contract)
# @param contract [Eth::Contract] contracts to deploy.
# @overload deploy(contract, sender_key)
# @param contract [Eth::Contract] contracts to deploy.
# @param sender_key [Eth::Key] the sender private key.
# @overload deploy(contract, sender_key, legacy)
# @param contract [Eth::Contract] contracts to deploy.
# @param sender_key [Eth::Key] the sender private key.
# @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @return [String] the contract address.
def deploy_and_wait(contract, sender_key: nil, legacy: false)
hash = wait_for_tx(deploy(contract, sender_key: sender_key, legacy: legacy))
contract.address = eth_get_transaction_receipt(hash)["result"]["contractAddress"]
end

# Deploy contract. Uses `eth_coinbase` or external signer
# if no sender key is provided.
#
# @overload deploy(contract)
# @param contract [Eth::Contract] contracts to deploy.
# @overload deploy(contract, sender_key)
# @param contract [Eth::Contract] contracts to deploy.
# @param sender_key [Eth::Key] the sender private key.
# @overload deploy(contract, sender_key, legacy)
# @param contract [Eth::Contract] contracts to deploy.
# @param sender_key [Eth::Key] the sender private key.
# @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @return [String] the transaction hash.
def deploy(contract, sender_key: nil, legacy: false)
gas_limit = Tx.estimate_intrinsic_gas(contract.bin) + Tx::CREATE_GAS
params = {
value: 0,
gas_limit: gas_limit,
chain_id: chain_id,
data: contract.bin,
}
if legacy
params.merge!({
gas_price: max_fee_per_gas,
})
else
params.merge!({
priority_fee: max_priority_fee_per_gas,
max_gas_fee: max_fee_per_gas,
})
end
unless sender_key.nil?
# use the provided key as sender and signer
params.merge!({
from: sender_key.address,
nonce: get_nonce(sender_key.address),
})
tx = Eth::Tx.new(params)
tx.sign sender_key
return eth_send_raw_transaction(tx.hex)["result"]
else
# use the default account as sender and external signer
params.merge!({
from: default_account,
nonce: get_nonce(default_account),
})
return eth_send_transaction(params)["result"]
end
end

# Encoding for function calls.
def call_payload(fun, args)
types = fun.inputs.map { |i| i.type }
encoded_str = Util.bin_to_hex(Eth::Abi.encode(types, args))
"0x" + fun.signature + (encoded_str.empty? ? "0" * 64 : encoded_str)
end

# Non-transactional function call called from call().
#
# @overload call_raw(contract, func)
# @param contract [Eth::Contract] subject contract to call.
# @param func [Eth::Contract::Function] method name to be called.
# @overload call_raw(contract, func, value)
# @param contract [Eth::Contract] subject contract to call.
# @param func [Eth::Contract::Function] method name to be called.
# @param value [Integer|String] function arguments.
# @overload call_raw(contract, func, value, sender_key, legacy)
# @param contract [Eth::Contract] subject contract to call.
# @param func [Eth::Contract::Function] method name to be called.
# @param value [Integer|String] function arguments.
# @param sender_key [Eth::Key] the sender private key.
# @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @return [Object] returns the result of the call.
def call_raw(contract, func, *args, **kwargs)
gas_limit = Tx.estimate_intrinsic_gas(contract.bin) + Tx::CREATE_GAS
params = {
gas_limit: gas_limit,
chain_id: chain_id,
data: call_payload(func, args),
}
if kwargs[:address] || contract.address
params.merge!({ to: kwargs[:address] || contract.address })
end
if kwargs[:legacy]
params.merge!({
gas_price: max_fee_per_gas,
})
else
params.merge!({
priority_fee: max_priority_fee_per_gas,
max_gas_fee: max_fee_per_gas,
})
end
unless kwargs[:sender_key].nil?
# use the provided key as sender and signer
params.merge!({
from: kwargs[:sender_key].address,
nonce: get_nonce(kwargs[:sender_key].address),
})
tx = Eth::Tx.new(params)
tx.sign kwargs[:sender_key]
else
# use the default account as sender and external signer
params.merge!({
from: default_account,
nonce: get_nonce(default_account),
})
end
raw_result = eth_call(params)["result"]
types = func.outputs.map { |i| i.type }
Eth::Abi.decode(types, raw_result)
end

# Non-transactional function calls.
#
# @overload call(contract, function_name)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @overload call(contract, function_name, value)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @param value [Integer|String] function arguments.
# @overload call(contract, function_name, value, sender_key, legacy)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @param value [Integer|String] function arguments.
# @param sender_key [Eth::Key] the sender private key.
# @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @return [Object] returns the result of the call.
def call(contract, function_name, *args, **kwargs)
func = contract.functions.select { |func| func.name == function_name }[0]
raise ArgumentError, "function_name does not exist!" if func.nil?
output = call_raw(contract, func, *args, **kwargs)
if output.length == 1
return output[0]
else
return output
end
end

# Function call with transaction.
#
# @overload transact(contract, function_name)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @overload transact(contract, function_name, value)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @param value [Integer|String] function arguments.
# @overload transact(contract, function_name, value, sender_key, legacy, address)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @param value [Integer|String] function arguments.
# @param sender_key [Eth::Key] the sender private key.
# @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @param address [String] contract address.
# @return [Object] returns the result of the call.
def transact(contract, function_name, *args, **kwargs)
gas_limit = Tx.estimate_intrinsic_gas(contract.bin) + Tx::CREATE_GAS
fun = contract.functions.select { |func| func.name == function_name }[0]
params = {
value: 0,
gas_limit: gas_limit,
chain_id: chain_id,
to: kwargs[:address] || contract.address,
data: call_payload(fun, args),
}
if kwargs[:legacy]
params.merge!({
gas_price: max_fee_per_gas,
})
else
params.merge!({
priority_fee: max_priority_fee_per_gas,
max_gas_fee: max_fee_per_gas,
})
end
unless kwargs[:sender_key].nil?
# use the provided key as sender and signer
params.merge!({
from: kwargs[:sender_key].address,
nonce: get_nonce(kwargs[:sender_key].address),
})
tx = Eth::Tx.new(params)
tx.sign kwargs[:sender_key]
return eth_send_raw_transaction(tx.hex)["result"]
else
# use the default account as sender and external signer
params.merge!({
from: default_account,
nonce: get_nonce(default_account),
})
return eth_send_transaction(params)["result"]
end
end

# Function call with transaction and waits for it to be mined.
#
# @overload transact_and_wait(contract, function_name)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @overload transact_and_wait(contract, function_name, value)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @param value [Integer|String] function arguments.
# @overload transact_and_wait(contract, function_name, value, sender_key, legacy, address)
# @param contract [Eth::Contract] subject contract to call.
# @param function_name [String] method name to be called.
# @param value [Integer|String] function arguments.
# @param sender_key [Eth::Key] the sender private key.
# @param legacy [Boolean] enables legacy transactions (pre-EIP-1559).
# @param address [String] contract address.
# @return [Object] returns the result of the call.
def transact_and_wait(contract, function_name, *args, **kwargs)
wait_for_tx(transact(contract, function_name, *args, **kwargs))
end

# Gives control over resetting the RPC request ID back to zero.
# Usually not needed.
#
Expand Down
Loading

0 comments on commit 07befbe

Please sign in to comment.