etherlite 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -7
  3. data/etherlite.gemspec +2 -0
  4. data/lib/etherlite.rb +12 -7
  5. data/lib/etherlite/abi.rb +44 -4
  6. data/lib/etherlite/account/anonymous.rb +9 -0
  7. data/lib/etherlite/account/base.rb +56 -0
  8. data/lib/etherlite/account/local.rb +40 -0
  9. data/lib/etherlite/account/private_key.rb +51 -0
  10. data/lib/etherlite/api/node.rb +15 -12
  11. data/lib/etherlite/api/rpc.rb +55 -0
  12. data/lib/etherlite/commands/abi/load_contract.rb +24 -10
  13. data/lib/etherlite/commands/abi/load_function.rb +5 -5
  14. data/lib/etherlite/commands/abi/load_type.rb +1 -1
  15. data/lib/etherlite/commands/contract/event_base/decode_log_inputs.rb +30 -10
  16. data/lib/etherlite/configuration.rb +2 -1
  17. data/lib/etherlite/connection.rb +8 -4
  18. data/lib/etherlite/contract/base.rb +53 -1
  19. data/lib/etherlite/contract/function.rb +18 -14
  20. data/lib/etherlite/errors.rb +27 -0
  21. data/lib/etherlite/nonce_manager.rb +13 -14
  22. data/lib/etherlite/support/array.rb +43 -0
  23. data/lib/etherlite/transaction.rb +33 -1
  24. data/lib/etherlite/types/array_dynamic.rb +19 -5
  25. data/lib/etherlite/types/array_fixed.rb +19 -10
  26. data/lib/etherlite/types/{bool.rb → boolean.rb} +1 -1
  27. data/lib/etherlite/types/byte_string.rb +6 -0
  28. data/lib/etherlite/types/string.rb +6 -1
  29. data/lib/etherlite/utils.rb +4 -2
  30. data/lib/etherlite/version.rb +1 -1
  31. data/lib/generators/etherlite/templates/etherlite.yml +1 -0
  32. metadata +38 -8
  33. data/lib/etherlite/account.rb +0 -85
  34. data/lib/etherlite/commands/base.rb +0 -0
  35. data/lib/etherlite/commands/contract/function/encode_arguments.rb +0 -32
  36. data/lib/etherlite/pk_account.rb +0 -82
  37. data/lib/etherlite/types/array_base.rb +0 -36
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed20095933d11d8e8d7b9cda7dce06cfb6acd1bf
4
- data.tar.gz: aebb4e88d292b3c021cebb1b87318fd8b51abd2d
3
+ metadata.gz: 7c11198a345cd94f0ef94e90a80187b8bd06b382
4
+ data.tar.gz: 871fddbd0b57e525285964b3be91327ab9b6b08d
5
5
  SHA512:
6
- metadata.gz: dff58138afca0dd99d83692a218a4e410880c6f07cb6bdf0727ff8603f8ce107a5ec9c5ab7886462e831ef4bbd0dcb16bb9b2f254ea5dd7e9a328f3a345ff79d
7
- data.tar.gz: d7c84736ea549eb7b3f59c9bf41e5fc493ebb3c37aaad2b25caff2a7c1d6db5ce196c7809eccf5fe1252ef1b1cd8705f6400819b6345f1af9ae54c851d880ce9
6
+ metadata.gz: 7994ac39cbb16387397ec0829891bc401e9d425bc62051c46b7a47244759b0d018626af088df38456060ba8558ed29836ae666be4f932fc301d093d8233bf33a
7
+ data.tar.gz: db831cc13de2c7f578e0659951ddde2e91697369c63af597e091b2eec1a44eb881ca882ec754184f9facf2128d035eb319a6537c0c9c05fec23a4c42bb8bbf87
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Etherlite
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/etherlite`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Etherlite is a
6
4
 
7
5
  ## Installation
8
6
 
@@ -16,13 +14,101 @@ And then execute:
16
14
 
17
15
  $ bundle
18
16
 
19
- Or install it yourself as:
17
+ Initialize the configuration file and `contracts` folder:
18
+
19
+ $ rails g etherlite:init
20
20
 
21
- $ gem install etherlite
21
+ A `etherlite.yml` file will be added to your app's `config` folder, there you can configure the node locations and some other variables for each enviroment.
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ This gem is intended to be used with contract artifacts generated by Truffle. To start interacting with your contracts, drop the artifacts for the contracts you will be using in the application `contracts` folder. (You can find the contract artifacts on the `build/contracts` folder of your truffle proyect).
26
+
27
+ Rails will automatically load the artifacts as ruby classes.
28
+
29
+ For example, given the following contract:
30
+
31
+ ```solidity
32
+ contract BikeStore {
33
+ event BikeSold(address buyer, string model);
34
+ event BikeReturned(address buyer, string reason);
35
+
36
+ function buyBike(string _model) payable {
37
+ // some code
38
+ }
39
+
40
+ function getBikePrice(string _model) returns (uint price) {
41
+ return _return;
42
+ }
43
+ }
44
+ ```
45
+
46
+ By copying the contract's artifact file in the `contracts` folder you will be able to call:
47
+
48
+ ```ruby
49
+ my_store = BikeStoreContract.at('0xAAAAAAAAAAAAAAAAA') # notice the "Contract" sufix
50
+ price = my_store.get_bike_price
51
+ ```
52
+
53
+ ### Accounts
54
+
55
+ Etherlite supports both node managed accounts and private key accounts (via the `eth` gem).
56
+
57
+ By default etherlite will try to use the first account from the configured node.
58
+
59
+ ```ruby
60
+ Etherlite.default_account # this should reference the node's first account (if present)
61
+ ```
62
+
63
+ When executing operations that need to send a transaction you will need to provide the account's passphrase. You can set the `passphrase` option in the configuration file:
64
+
65
+ ```yml
66
+ development:
67
+ passphrase: <%= ENV['STORE_PASSPHRASE'] %>
68
+ ```
69
+
70
+ or pass it on any transaction-triggering call:
71
+
72
+ ```ruby
73
+ my_store.buy_bike('mongoose', passphrase: 'verysecure')
74
+ ```
75
+
76
+ If you need to use a private key account, then set the `private_key` option in the configuration file with the account's private key. Etherlite will automatically switch to using a private key account instead of the node account:
77
+
78
+ ```yml
79
+ development:
80
+ private_key: <%= ENV['STORE_PRIVATE_KEY'] %>
81
+ ```
82
+
83
+ ```ruby
84
+ Etherlite.default_account.address # this should return the private_key corresponding address
85
+ ```
86
+
87
+ If you wish to programatically load a private key account you can call:
88
+
89
+ ```ruby
90
+ account = Etherlite.account_from_pk(your_private_key)
91
+ ```
92
+
93
+ ### Creating contract instances
94
+
95
+
96
+
97
+ ### Calling contract methods
98
+
99
+ #### Setting gas limit and price
100
+
101
+ ### Transferring ether
102
+
103
+
104
+ ### Waiting for transactions
105
+
106
+
107
+ ### Searching event logs
108
+
109
+ ### Testing with Etherlite, rspec and testrpc
110
+
111
+ Etherlite (will) provides a set of rspec helpers to make
26
112
 
27
113
  ## Development
28
114
 
@@ -34,7 +120,6 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
34
120
 
35
121
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/etherlite. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
122
 
37
-
38
123
  ## License
39
124
 
40
125
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/etherlite.gemspec CHANGED
@@ -29,4 +29,6 @@ Gem::Specification.new do |spec|
29
29
  spec.add_development_dependency "rspec", "~> 3.0"
30
30
  spec.add_development_dependency "guard", "~> 2.14"
31
31
  spec.add_development_dependency "guard-rspec", "~> 4.7"
32
+ spec.add_development_dependency "webmock", "~> 3.0.1"
33
+ spec.add_development_dependency "pry"
32
34
  end
data/lib/etherlite.rb CHANGED
@@ -6,13 +6,15 @@ require "etherlite/version"
6
6
 
7
7
  require "etherlite/api/address"
8
8
  require "etherlite/api/node"
9
+ require "etherlite/api/rpc"
10
+
11
+ require "etherlite/support/array"
9
12
 
10
13
  require "etherlite/types/base"
11
14
  require "etherlite/types/address"
12
- require "etherlite/types/array_base"
13
15
  require "etherlite/types/array_dynamic"
14
16
  require "etherlite/types/array_fixed"
15
- require "etherlite/types/bool"
17
+ require "etherlite/types/boolean"
16
18
  require "etherlite/types/byte_string"
17
19
  require "etherlite/types/bytes"
18
20
  require "etherlite/types/fixed"
@@ -24,14 +26,17 @@ require "etherlite/contract/event_base"
24
26
  require "etherlite/contract/event_input"
25
27
  require "etherlite/contract/function"
26
28
 
29
+ require "etherlite/errors"
27
30
  require "etherlite/configuration"
28
31
  require "etherlite/abi"
29
32
  require "etherlite/utils"
30
33
  require "etherlite/connection"
31
34
  require "etherlite/transaction"
32
35
  require "etherlite/nonce_manager"
33
- require "etherlite/pk_account"
34
- require "etherlite/account"
36
+ require "etherlite/account/base"
37
+ require "etherlite/account/local"
38
+ require "etherlite/account/private_key"
39
+ require "etherlite/account/anonymous"
35
40
  require "etherlite/address"
36
41
  require "etherlite/client"
37
42
 
@@ -42,10 +47,10 @@ module Etherlite
42
47
  Utils.valid_address? _value
43
48
  end
44
49
 
45
- def self.connect(_url)
50
+ def self.connect(_url, _options = {})
46
51
  _url = URI(_url) unless _url.is_a? URI
47
52
 
48
- Client.new Connection.new(_url)
53
+ Client.new Connection.new(_url, _options.fetch(:chain_id, config.chain_id))
49
54
  end
50
55
 
51
56
  def self.config
@@ -62,7 +67,7 @@ module Etherlite
62
67
  end
63
68
 
64
69
  def self.connection
65
- @connection ||= Connection.new URI(config.url)
70
+ @connection ||= Connection.new(URI(config.url), config.chain_id)
66
71
  end
67
72
  end
68
73
 
data/lib/etherlite/abi.rb CHANGED
@@ -3,18 +3,58 @@ require 'etherlite/commands/abi/load_contract'
3
3
  require 'etherlite/commands/abi/load_function'
4
4
 
5
5
  module Etherlite
6
+ ##
7
+ # A set of ABI related utilities
8
+ #
6
9
  module Abi
7
10
  extend self
8
11
 
12
+ ##
13
+ # Load a contract artifact file.
14
+ #
15
+ # This method parses a truffle artifact file and loads a **contract** class.
16
+ #
17
+ # The generated contract class exposes a series of methods to interact with an instance
18
+ # of the contract described by the artifact.
19
+ #
20
+ # ```
21
+ # contract_class = Etherlite::Abi.load_contract_at('/path/to/Contract.json')
22
+ # instance = contract_class.deploy(gas: 1000000)
23
+ # ```
24
+ #
25
+ # @param _path (String) The artifact file absolute path
26
+ #
27
+ # @return [Object] the generated contract class.
28
+ #
9
29
  def load_contract_at(_path)
10
- json = JSON.parse File.read(_path)
11
- LoadContract.for json: json
30
+ artifact = JSON.parse File.read(_path)
31
+ LoadContract.for artifact: artifact
12
32
  end
13
33
 
14
- def load_contract(_json)
15
- LoadContract.for json: _json
34
+ ##
35
+ # This method is similar to `load_contract_at` but takes a hash instead of the raw file.
36
+ #
37
+ # @param _artifact (Hash) The artifact hash
38
+ #
39
+ # @return [Object] the generated contract class.
40
+ #
41
+ def load_contract(_artifact)
42
+ LoadContract.for artifact: _artifact
16
43
  end
17
44
 
45
+ ##
46
+ # This method is used internally to load a function definition given a function signature.
47
+ #
48
+ # The function definition can be later used to call a contract's function without need of
49
+ # the artifact file.
50
+ #
51
+ # @param _signature (String) A string that matches:
52
+ #
53
+ # (payable|onchain|<type>) <name>(<type>, <type>, ...), where <name> is the function's name
54
+ # and <type> is any valid solidity type.
55
+ #
56
+ # @return [Object] the generated function definition.
57
+ #
18
58
  def load_function(_signature)
19
59
  LoadFunction.for signature: _signature
20
60
  end
@@ -0,0 +1,9 @@
1
+ module Etherlite
2
+ module Account
3
+ class Anonymous < Base
4
+ def initialize(_connection)
5
+ super(_connection, nil)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,56 @@
1
+ module Etherlite::Account
2
+ class Base
3
+ include Etherlite::Api::Address
4
+
5
+ attr_reader :connection, :normalized_address
6
+
7
+ def initialize(_connection, _normalized_address = nil)
8
+ @connection = _connection
9
+ @normalized_address = _normalized_address
10
+ end
11
+
12
+ def transfer_to(_target, _options = {})
13
+ send_transaction _options.merge(to: _target, value: _options.fetch(:amount, 0))
14
+ end
15
+
16
+ def call(_target, _function, *_params)
17
+ _function = parse_function(_function) if _function.is_a? String
18
+ options = _params.last.is_a?(Hash) ? _params.pop.clone : {}
19
+
20
+ if _function.constant?
21
+ call_constant _target, _function, _params, options
22
+ else
23
+ value = options.fetch(:pay, 0)
24
+ raise 'function is not payable' if value > 0 && !_function.payable?
25
+
26
+ send_transaction options.merge(
27
+ to: _target, data: _function.encode(_params), value: value
28
+ )
29
+ end
30
+ end
31
+
32
+ def send_transaction(_options = {})
33
+ raise NotSupportedError, 'transactions are not supported by this kind of account'
34
+ end
35
+
36
+ private
37
+
38
+ def call_constant(_target, _function, _params, _options)
39
+ params = {
40
+ from: json_encoded_address,
41
+ to: Etherlite::Utils.encode_address_param(_target),
42
+ data: _function.encode(_params)
43
+ }
44
+
45
+ params[:from] = json_encoded_address if @normalized_address.present?
46
+
47
+ block = Etherlite::Utils.encode_block_param _options.fetch(:block, :latest)
48
+
49
+ _function.decode @connection, @connection.eth_call(params, block)
50
+ end
51
+
52
+ def parse_function(_signature)
53
+ Abi::LoadFunction.for signature: _signature
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ module Etherlite
2
+ module Account
3
+ class Local < Base
4
+ def unlock(_passphrase)
5
+ @passphrase = _passphrase
6
+ end
7
+
8
+ def lock
9
+ @passphrase = nil
10
+ end
11
+
12
+ def send_transaction(_options = {})
13
+ params = build_params_from_options _options
14
+ passphrase = _options.fetch(:passphrase, @passphrase)
15
+
16
+ Transaction.new @connection, send_transaction_with_passphrase(params, passphrase)
17
+ end
18
+
19
+ private
20
+
21
+ def build_params_from_options(_opt)
22
+ { from: json_encoded_address }.tap do |pr|
23
+ pr[:to] = Utils.encode_address_param(_opt[:to]) if _opt.key? :to
24
+ pr[:value] = Utils.encode_quantity_param(_opt[:value]) if _opt.key? :value
25
+ pr[:data] = _opt[:data] if _opt.key? :data
26
+ pr[:gas] = Utils.encode_quantity_param(_opt[:gas]) if _opt.key? :gas
27
+ pr[:gasPrice] = Utils.encode_quantity_param(_opt[:gas_price]) if _opt.key? :gas_price
28
+ end
29
+ end
30
+
31
+ def send_transaction_with_passphrase(_params, _passphrase)
32
+ if _passphrase.nil?
33
+ @connection.eth_send_transaction _params
34
+ else
35
+ @connection.personal_send_transaction _params, _passphrase
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ require 'eth'
2
+
3
+ module Etherlite
4
+ module Account
5
+ class PrivateKey < Base
6
+ def initialize(_connection, _pk)
7
+ @key = Eth::Key.new priv: _pk
8
+ super _connection, Etherlite::Utils.normalize_address(@key.address)
9
+ end
10
+
11
+ def send_transaction(_options = {})
12
+ nonce_manager.with_next_nonce_for(normalized_address) do |nonce|
13
+ tx = Eth::Tx.new(
14
+ value: _options.fetch(:value, 0),
15
+ data: _options.fetch(:data, ''),
16
+ gas_limit: _options.fetch(:gas, 90_000),
17
+ gas_price: _options.fetch(:gas_price, gas_price),
18
+ to: Etherlite::Utils.encode_address_param(_options[:to]),
19
+ nonce: nonce
20
+ )
21
+
22
+ # Since eth gem does not allow configuration of chains for every tx, we need
23
+ # to globally configure it before signing. This is not thread safe so a mutex is needed.
24
+ sign_with_connection_chain tx
25
+
26
+ Etherlite::Transaction.new @connection, @connection.eth_send_raw_transaction(tx.hex)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ @@eth_mutex = Mutex.new
33
+
34
+ def gas_price
35
+ # TODO: improve on this
36
+ @gas_price ||= connection.eth_gas_price
37
+ end
38
+
39
+ def nonce_manager
40
+ Etherlite::NonceManager.new @connection
41
+ end
42
+
43
+ def sign_with_connection_chain(_tx)
44
+ @@eth_mutex.synchronize do
45
+ Eth.configure { |c| c.chain_id = @connection.chain_id }
46
+ _tx.sign @key
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -5,37 +5,40 @@ module Etherlite
5
5
  include Address
6
6
 
7
7
  def get_block_number
8
- Etherlite::Utils.hex_to_uint connection.ipc_call(:eth_blockNumber)
8
+ connection.eth_block_number
9
9
  end
10
10
 
11
11
  def get_gas_price
12
- Etherlite::Utils.hex_to_uint connection.ipc_call(:eth_gasPrice)
13
- end
14
-
15
- def get_transaction_receipt(_tx_hash)
16
- connection.ipc_call(:eth_getTransactionReceipt, _tx_hash)
12
+ connection.eth_gas_price
17
13
  end
18
14
 
19
15
  def register_account(_passphrase)
20
16
  address = connection.ipc_call(:personal_newAccount, _passphrase)
21
- Etherlite::Account.new @connection, Etherlite::Utils.normalize_address(address)
17
+ Etherlite::Account::Local.new @connection, Etherlite::Utils.normalize_address(address)
22
18
  end
23
19
 
24
20
  def accounts
25
21
  connection.ipc_call(:eth_accounts).map do |address|
26
- Etherlite::Account.new @connection, Etherlite::Utils.normalize_address(address)
22
+ Etherlite::Account::Local.new @connection, Etherlite::Utils.normalize_address(address)
27
23
  end
28
24
  end
29
25
 
30
- def first_account
31
- @first_account ||= accounts.first
26
+ def default_account
27
+ @default_account ||= load_default_account
32
28
  end
33
29
 
34
30
  def account_from_pk(_pk)
35
- Etherlite::PkAccount.new(connection, _pk)
31
+ Etherlite::Account::PrivateKey.new connection, _pk
36
32
  end
37
33
 
38
- def_delegators :first_account, :unlock, :lock, :normalized_address, :transfer_to, :call
34
+ def_delegators :default_account, :unlock, :lock, :normalized_address, :transfer_to, :call
35
+
36
+ private
37
+
38
+ def load_default_account
39
+ # TODO: consider configuring a global PK and allow the default account to use it
40
+ accounts.first || Etherlite::Account::Anonymous.new(connection)
41
+ end
39
42
  end
40
43
  end
41
44
  end