etherlite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|