etherlite 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/Guardfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/etherlite.gemspec +31 -0
- data/lib/etherlite.rb +66 -0
- data/lib/etherlite/abi.rb +22 -0
- data/lib/etherlite/account.rb +67 -0
- data/lib/etherlite/address.rb +21 -0
- data/lib/etherlite/api/address.rb +21 -0
- data/lib/etherlite/api/node.rb +36 -0
- data/lib/etherlite/client.rb +11 -0
- data/lib/etherlite/commands/abi/load_contract.rb +66 -0
- data/lib/etherlite/commands/abi/load_function.rb +28 -0
- data/lib/etherlite/commands/abi/load_type.rb +55 -0
- data/lib/etherlite/commands/base.rb +0 -0
- data/lib/etherlite/commands/contract/event_base/decode_log_inputs.rb +21 -0
- data/lib/etherlite/commands/contract/function/encode_arguments.rb +32 -0
- data/lib/etherlite/commands/utils/validate_address.rb +25 -0
- data/lib/etherlite/configuration.rb +33 -0
- data/lib/etherlite/connection.rb +39 -0
- data/lib/etherlite/contract/base.rb +58 -0
- data/lib/etherlite/contract/event_base.rb +36 -0
- data/lib/etherlite/contract/event_input.rb +19 -0
- data/lib/etherlite/contract/function.rb +43 -0
- data/lib/etherlite/railtie.rb +29 -0
- data/lib/etherlite/railties/configuration_extensions.rb +9 -0
- data/lib/etherlite/railties/utils.rb +13 -0
- data/lib/etherlite/types/address.rb +20 -0
- data/lib/etherlite/types/array_base.rb +36 -0
- data/lib/etherlite/types/array_dynamic.rb +13 -0
- data/lib/etherlite/types/array_fixed.rb +23 -0
- data/lib/etherlite/types/base.rb +27 -0
- data/lib/etherlite/types/bool.rb +26 -0
- data/lib/etherlite/types/byte_string.rb +17 -0
- data/lib/etherlite/types/bytes.rb +19 -0
- data/lib/etherlite/types/fixed.rb +38 -0
- data/lib/etherlite/types/integer.rb +38 -0
- data/lib/etherlite/types/string.rb +10 -0
- data/lib/etherlite/utils.rb +63 -0
- data/lib/etherlite/version.rb +3 -0
- data/lib/generators/etherlite/init_generator.rb +10 -0
- data/lib/generators/etherlite/templates/etherlite.yml +11 -0
- metadata +206 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
module Etherlite::Abi
|
2
|
+
class LoadContract < PowerTypes::Command.new(:json)
|
3
|
+
def perform
|
4
|
+
klass = Class.new(Etherlite::Contract::Base)
|
5
|
+
|
6
|
+
abi_definitions.each do |definition|
|
7
|
+
case definition['type']
|
8
|
+
when 'function'
|
9
|
+
define_function klass, definition
|
10
|
+
when 'event'
|
11
|
+
define_event klass, definition
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
klass.functions.freeze
|
16
|
+
klass.events.freeze
|
17
|
+
klass
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def abi_definitions
|
23
|
+
@json['abi'] || []
|
24
|
+
end
|
25
|
+
|
26
|
+
def define_function(_class, _definition)
|
27
|
+
function_args = _definition['inputs'].map { |input| LoadType.for signature: input['type'] }
|
28
|
+
function = Etherlite::Contract::Function.new(
|
29
|
+
_definition['name'],
|
30
|
+
function_args,
|
31
|
+
payable: _definition['payable'],
|
32
|
+
constant: _definition['constant']
|
33
|
+
)
|
34
|
+
|
35
|
+
_class.functions << function
|
36
|
+
_class.class_eval do
|
37
|
+
define_method(_definition['name'].underscore) do |*params|
|
38
|
+
account = (params.last.is_a?(Hash) && params.last[:as]) || default_account
|
39
|
+
|
40
|
+
raise ArgumentError, 'must provide a source account' if account.nil?
|
41
|
+
account.call address, function, *params
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def define_event(_class, _definition)
|
47
|
+
event_inputs = _definition['inputs'].map do |input|
|
48
|
+
Etherlite::Contract::EventInput.new(
|
49
|
+
input['name'], LoadType.for(signature: input['type']), input['indexed']
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
event_class = Class.new(Etherlite::Contract::EventBase) do
|
54
|
+
event_inputs.each do |input|
|
55
|
+
define_method(input.name) { attributes[input.name] }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
event_class.instance_variable_set(:@original_name, _definition['name'])
|
60
|
+
event_class.instance_variable_set(:@inputs, event_inputs)
|
61
|
+
|
62
|
+
_class.events << event_class
|
63
|
+
_class.const_set(_definition['name'], event_class)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Etherlite::Abi
|
2
|
+
class LoadFunction < PowerTypes::Command.new(:signature)
|
3
|
+
MATCHER = /^(payable|onchain|\w[\w\d]*) (\w[\w\d]*)\((.*?)\)$/
|
4
|
+
|
5
|
+
def perform
|
6
|
+
parts = MATCHER.match @signature
|
7
|
+
raise ArgumentError, 'invalid method signature' if parts.nil?
|
8
|
+
|
9
|
+
args = parts[3].split(',').map { |a| LoadType.for(signature: a.strip) }
|
10
|
+
|
11
|
+
case parts[1]
|
12
|
+
when 'payable'
|
13
|
+
build parts[2], args, payable: true
|
14
|
+
when 'onchain'
|
15
|
+
build parts[2], args
|
16
|
+
else
|
17
|
+
return_type = parts[1] == 'void' ? nil : LoadType.for(signature: parts[1])
|
18
|
+
build parts[2], args, constant: true, returns: return_type
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build(*_args)
|
25
|
+
Etherlite::Contract::Function.new *_args
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Etherlite::Abi
|
2
|
+
class LoadType < PowerTypes::Command.new(:signature)
|
3
|
+
MATCHER = /^(?:(?:(?<ty>uint|int|bytes)(?<b1>\d+)|(?<ty>uint|int|bytes)(?<b1>\d+)|(?<ty>fixed|ufixed)(?<b1>\d+)x(?<b2>\d+)|(?<ty>address|bool|uint|int|fixed|ufixed))(?:\[(?<dim>\d*)\]|)|(?<ty>bytes|string))$/
|
4
|
+
|
5
|
+
def perform
|
6
|
+
parts = MATCHER.match @signature
|
7
|
+
raise ArgumentError, "Invalid argument type #{@signature}" if parts.nil?
|
8
|
+
|
9
|
+
type = build_base_type parts
|
10
|
+
build_array_type type, parts
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def build_base_type(_parts) # rubocop:disable Metrics/CyclomaticComplexity
|
16
|
+
case _parts[:ty]
|
17
|
+
when 'uint' then Etherlite::Types::Integer.new(false, b1_256(_parts))
|
18
|
+
when 'int' then Etherlite::Types::Integer.new(true, b1_256(_parts))
|
19
|
+
when 'ufixed' then Etherlite::Types::Fixed.new(false, b1_128(_parts), b2_128(_parts))
|
20
|
+
when 'fixed' then Etherlite::Types::Fixed.new(true, b1_128(_parts), b2_128(_parts))
|
21
|
+
when 'string' then Etherlite::Types::String.new
|
22
|
+
when 'address' then Etherlite::Types::Address.new
|
23
|
+
when 'bool' then Etherlite::Types::Bool.new
|
24
|
+
when 'bytes'
|
25
|
+
if _parts[:b1].present?
|
26
|
+
Etherlite::Types::Bytes.new(_parts[:b1].to_i)
|
27
|
+
else
|
28
|
+
Etherlite::Types::ByteString.new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_array_type(_base_type, _parts)
|
34
|
+
return _base_type if _parts[:dim].nil?
|
35
|
+
|
36
|
+
if _parts[:dim].empty?
|
37
|
+
Etherlite::Types::ArrayDynamic.new _base_type
|
38
|
+
else
|
39
|
+
Etherlite::Types::ArrayFixed.new _base_type, _parts[:dim].to_i
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def b1_256(_parts)
|
44
|
+
_parts[:b1].nil? ? 256 : _parts[:b1].to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def b1_128(_parts)
|
48
|
+
_parts[:b1].nil? ? 128 : _parts[:b1].to_i
|
49
|
+
end
|
50
|
+
|
51
|
+
def b2_128(_parts)
|
52
|
+
_parts[:b2].nil? ? 128 : _parts[:b2].to_i
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Etherlite::Contract::EventBase
|
2
|
+
class DecodeLogInputs < PowerTypes::Command.new(:connection, :inputs, :json)
|
3
|
+
def perform
|
4
|
+
indexed = []
|
5
|
+
non_indexed = []
|
6
|
+
attributes = {}
|
7
|
+
|
8
|
+
@inputs.each { |i| (i.indexed? ? indexed : non_indexed) << i }
|
9
|
+
|
10
|
+
@json['data'][2..-1].scan(/.{64}/).each_with_index do |data, i|
|
11
|
+
attributes[non_indexed[i].name] = non_indexed[i].type.decode(@connection, data)
|
12
|
+
end
|
13
|
+
|
14
|
+
@json['topics'][1..-1].each_with_index do |topic, i|
|
15
|
+
attributes[indexed[i].name] = indexed[i].type.decode(@connection, topic[2..-1])
|
16
|
+
end
|
17
|
+
|
18
|
+
attributes
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Etherlite::Contract::Function
|
2
|
+
class EncodeArguments < PowerTypes::Command.new(:subtypes, :values)
|
3
|
+
def perform # rubocop:disable Metrics/MethodLength
|
4
|
+
if @values.length != @subtypes.count
|
5
|
+
raise ArgumentError, "Expected #{@subtypes.count} arguments, got #{@values.length} "
|
6
|
+
end
|
7
|
+
|
8
|
+
head = []
|
9
|
+
tail = []
|
10
|
+
tail_offset = calculate_head_offset
|
11
|
+
|
12
|
+
@subtypes.each_with_index do |type, i|
|
13
|
+
content = type.encode @values[i]
|
14
|
+
if type.dynamic?
|
15
|
+
head << Etherlite::Utils.uint_to_hex(tail_offset)
|
16
|
+
tail << content
|
17
|
+
tail_offset += content.length / 2 # hex string, 2 characters per byte
|
18
|
+
else
|
19
|
+
head << content
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
head.join + tail.join
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def calculate_head_offset
|
29
|
+
@subtypes.inject(0) { |r, type| r + (type.dynamic? ? 32 : type.size) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Etherlite::Utils
|
2
|
+
class ValidateAddress < PowerTypes::Command.new(:address)
|
3
|
+
MATCHER = /^(0x)?[0-9a-fA-F]{40}$/
|
4
|
+
|
5
|
+
def perform
|
6
|
+
return false unless MATCHER === @address
|
7
|
+
return false if /[A-F]/ === @address && !valid_checksum?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def valid_checksum?
|
14
|
+
trimmed_address = @address.gsub(/^0x/, '')
|
15
|
+
address_hash = Etherlite::Utils.sha3 trimmed_address.downcase
|
16
|
+
|
17
|
+
trimmed_address.chars.each_with_index do |c, i|
|
18
|
+
hash_byte = address_hash[i].to_i(16)
|
19
|
+
return false if (hash_byte > 7 && c.upcase != c) || (hash_byte <= 7 && c.downcase != c)
|
20
|
+
end
|
21
|
+
|
22
|
+
true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Etherlite
|
2
|
+
class Configuration
|
3
|
+
DEFAULTS = {
|
4
|
+
url: 'http://127.0.0.1:8545',
|
5
|
+
logger: nil # set by method
|
6
|
+
}
|
7
|
+
|
8
|
+
attr_accessor :url, :logger
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
assign_attributes DEFAULTS
|
12
|
+
end
|
13
|
+
|
14
|
+
def reset
|
15
|
+
assign_attributes DEFAULTS
|
16
|
+
end
|
17
|
+
|
18
|
+
def assign_attributes(_options)
|
19
|
+
_options.each { |k, v| public_send("#{k}=", v) }
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def logger
|
24
|
+
@logger || default_logger
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def default_logger
|
30
|
+
@default_logger ||= Logger.new(STDOUT)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Etherlite
|
2
|
+
class Connection
|
3
|
+
def initialize(_uri)
|
4
|
+
@uri = _uri
|
5
|
+
end
|
6
|
+
|
7
|
+
def ipc_call(_method, *_params)
|
8
|
+
id = new_unique_id
|
9
|
+
payload = { jsonrpc: "2.0", method: _method, params: _params, id: id }
|
10
|
+
|
11
|
+
# TODO: support ipc
|
12
|
+
Net::HTTP.start(@uri.hostname, @uri.port) do |http|
|
13
|
+
return handle_response http.post(
|
14
|
+
@uri.path.empty? ? '/' : @uri.path,
|
15
|
+
payload.to_json,
|
16
|
+
"Content-Type" => "application/json"
|
17
|
+
), id
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def new_unique_id
|
24
|
+
(Time.now.to_f * 1000.0).to_i
|
25
|
+
end
|
26
|
+
|
27
|
+
def handle_response(_response, _id)
|
28
|
+
case _response
|
29
|
+
when Net::HTTPSuccess
|
30
|
+
# puts _response.body
|
31
|
+
json_body = JSON.parse _response.body
|
32
|
+
# TODO: check id
|
33
|
+
json_body['result']
|
34
|
+
else
|
35
|
+
raise _response
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Etherlite::Contract
|
2
|
+
class Base
|
3
|
+
include Etherlite::Api::Address
|
4
|
+
|
5
|
+
def self.functions
|
6
|
+
@functions ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.events
|
10
|
+
@events ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.at(_address, client: nil, as: nil)
|
14
|
+
client ||= ::Etherlite # use default client if no client is provided
|
15
|
+
|
16
|
+
new(
|
17
|
+
client.connection,
|
18
|
+
Etherlite::Utils.normalize_address_param(_address),
|
19
|
+
as || client.first_account
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :connection
|
24
|
+
|
25
|
+
def initialize(_connection, _normalized_address, _default_account)
|
26
|
+
@connection = _connection
|
27
|
+
@normalized_address = _normalized_address
|
28
|
+
@default_account = _default_account
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_logs(events: nil, from_block: :earliest, to_block: :latest)
|
32
|
+
params = {
|
33
|
+
address: '0x' + @normalized_address,
|
34
|
+
fromBlock: Etherlite::Utils.encode_block_param(from_block),
|
35
|
+
toBlock: Etherlite::Utils.encode_block_param(to_block)
|
36
|
+
}
|
37
|
+
|
38
|
+
params[:topics] = [events.map { |e| event_topic e }] unless events.nil?
|
39
|
+
|
40
|
+
event_map = Hash[(events || self.class.events).map { |e| [event_topic(e), e] }]
|
41
|
+
|
42
|
+
logs = @connection.ipc_call(:eth_getLogs, params)
|
43
|
+
logs.map do |log|
|
44
|
+
event = event_map[log["topics"].first]
|
45
|
+
# TODO: support anonymous events!
|
46
|
+
event.decode(@connection, log) unless event.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :default_account, :normalized_address
|
53
|
+
|
54
|
+
def event_topic(_event)
|
55
|
+
'0x' + Etherlite::Utils.sha3(_event.signature)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'etherlite/commands/contract/event_base/decode_log_inputs'
|
2
|
+
|
3
|
+
module Etherlite::Contract
|
4
|
+
class EventBase
|
5
|
+
def self.inputs
|
6
|
+
@inputs
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.original_name
|
10
|
+
@original_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.signature
|
14
|
+
@signature ||= begin
|
15
|
+
input_sig = @inputs.map { |i| i.type.signature }
|
16
|
+
"#{@original_name}(#{input_sig.join(',')})"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.decode(_connection, _json)
|
21
|
+
new(
|
22
|
+
_json['blockNumber'].nil? ? nil : Etherlite::Utils.hex_to_uint(_json['blockNumber']),
|
23
|
+
_json['transactionHash'],
|
24
|
+
DecodeLogInputs.for(connection: _connection, inputs: inputs, json: _json)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :block_number, :tx_hash, :attributes
|
29
|
+
|
30
|
+
def initialize(_block_number, _tx_hash, _attributes)
|
31
|
+
@block_number = _block_number
|
32
|
+
@tx_hash = _tx_hash
|
33
|
+
@attributes = _attributes
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Etherlite::Contract
|
2
|
+
class EventInput
|
3
|
+
attr_reader :original_name, :type
|
4
|
+
|
5
|
+
def initialize(_original_name, _type, _indexed)
|
6
|
+
@original_name = _original_name
|
7
|
+
@type = _type
|
8
|
+
@indexed = _indexed
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
@name ||= @original_name.underscore
|
13
|
+
end
|
14
|
+
|
15
|
+
def indexed?
|
16
|
+
@indexed
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'etherlite/commands/contract/function/encode_arguments'
|
2
|
+
|
3
|
+
module Etherlite::Contract
|
4
|
+
class Function
|
5
|
+
attr_reader :name, :args
|
6
|
+
|
7
|
+
def initialize(_name, _args, returns: nil, payable: false, constant: false)
|
8
|
+
@name = _name
|
9
|
+
@args = _args
|
10
|
+
|
11
|
+
@returns = returns
|
12
|
+
@payable = payable
|
13
|
+
@constant = constant
|
14
|
+
end
|
15
|
+
|
16
|
+
def constant?
|
17
|
+
@constant
|
18
|
+
end
|
19
|
+
|
20
|
+
def payable?
|
21
|
+
@payable
|
22
|
+
end
|
23
|
+
|
24
|
+
def signature
|
25
|
+
@signature ||= begin
|
26
|
+
arg_signatures = @args.map &:signature
|
27
|
+
"#{@name}(#{arg_signatures.join(',')})"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def encode(_values)
|
32
|
+
signature_hash = Etherlite::Utils.sha3 signature
|
33
|
+
encoded_args = EncodeArguments.for subtypes: @args, values: _values
|
34
|
+
|
35
|
+
'0x' + signature_hash[0..7] + encoded_args
|
36
|
+
end
|
37
|
+
|
38
|
+
def decode(_connection, _values)
|
39
|
+
# TODO: decode return values
|
40
|
+
_values
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|