diff --git a/README.md b/README.md index 0167b24..b1f687d 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/izetex/web3-eth. 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. +Bug reports and pull requests are welcome on GitHub at https://github.com/Bloxy-info/web3-eth. 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. ## License diff --git a/lib/web3/eth.rb b/lib/web3/eth.rb index f98d545..f3e11e3 100644 --- a/lib/web3/eth.rb +++ b/lib/web3/eth.rb @@ -8,6 +8,9 @@ require "web3/eth/log" require "web3/eth/transaction_receipt" require "web3/eth/eth_module" +require "web3/eth/debug/debug_module" +require "web3/eth/debug/transaction_call_trace" require "web3/eth/trace_module" +require "web3/eth/parity_module" require "web3/eth/etherscan" require "web3/eth/rpc" diff --git a/lib/web3/eth/abi/abi_coder.rb b/lib/web3/eth/abi/abi_coder.rb index a131b39..7b93213 100644 --- a/lib/web3/eth/abi/abi_coder.rb +++ b/lib/web3/eth/abi/abi_coder.rb @@ -192,10 +192,15 @@ def encode_primitive_type(type, arg) end end + + def min_data_size types + types.size*32 + end + ## # Decodes multiple arguments using the head/tail mechanism. # - def decode_abi(types, data) + def decode_abi types, data, raise_errors = false parsed_types = types.map {|t| Type.parse(t) } outputs = [nil] * types.size @@ -207,8 +212,17 @@ def decode_abi(types, data) # If a type is static, grab the data directly, otherwise record its # start position if t.dynamic? + + if raise_errors && pos>data.size-1 + raise DecodingError, "Position out of bounds #{pos}>#{data.size-1}" + end + start_positions[i] = Utils.big_endian_to_int(data[pos, 32]) + if raise_errors && start_positions[i]>data.size-1 + raise DecodingError, "Start position out of bounds #{start_positions[i]}>#{data.size-1}" + end + j = i - 1 while j >= 0 && start_positions[j].nil? start_positions[j] = start_positions[i] @@ -217,7 +231,7 @@ def decode_abi(types, data) pos += 32 else - outputs[i] = data[pos, t.size] + outputs[i] = zero_padding data, pos, t.size, start_positions pos += t.size end end @@ -230,36 +244,61 @@ def decode_abi(types, data) j -= 1 end -# raise DecodingError, "Not enough data for head" unless pos <= data.size + if raise_errors && pos > data.size + raise DecodingError, "Not enough data for head" + end + parsed_types.each_with_index do |t, i| if t.dynamic? offset, next_offset = start_positions[i, 2] - outputs[i] = data[offset...next_offset] + if offset<=data.size && next_offset<=data.size + outputs[i] = data[offset...next_offset] + end end end + if raise_errors && outputs.include?(nil) + raise DecodingError, "Not all data can be parsed" + end + parsed_types.zip(outputs).map {|(type, out)| decode_type(type, out) } end alias :decode :decode_abi + def zero_padding data, pos, count, start_positions + if pos >= data.size + start_positions[start_positions.size-1] += count + "\x00"*count + elsif pos + count > data.size + start_positions[start_positions.size-1] += ( count - (data.size-pos)) + data[pos,data.size-pos] + "\x00"*( count - (data.size-pos)) + else + data[pos, count] + end + end + def decode_typed_data type_name, data decode_primitive_type Type.parse(type_name), data end def decode_type(type, arg) - if %w(string bytes).include?(type.base) && type.sub.empty? + return nil if arg.nil? || arg.empty? + if type.kind_of?(Tuple) && type.dims.empty? + arg ? decode_abi(type.types, arg) : [] + elsif %w(string bytes).include?(type.base) && type.sub.empty? l = Utils.big_endian_to_int arg[0,32] data = arg[32..-1] data[0, l] elsif type.dynamic? l = Utils.big_endian_to_int arg[0,32] + raise DecodingError, "Too long length: #{l}" if l>100000 subtype = type.subtype if subtype.dynamic? raise DecodingError, "Not enough data for head" unless arg.size >= 32 + 32*l - start_positions = (1..l).map {|i| Utils.big_endian_to_int arg[32*i, 32] } + start_positions = (1..l).map {|i| 32+Utils.big_endian_to_int(arg[32*i, 32]) } start_positions.push arg.size outputs = (0...l).map {|i| arg[start_positions[i]...start_positions[i+1]] } diff --git a/lib/web3/eth/abi/type.rb b/lib/web3/eth/abi/type.rb index c0c01ad..e29fecd 100644 --- a/lib/web3/eth/abi/type.rb +++ b/lib/web3/eth/abi/type.rb @@ -12,6 +12,13 @@ class <= 8 && total <= 256 - raise ParseError, "Fixed high/low sizes must be multiples of 8" unless high % 8 == 0 && low % 8 == 0 + raise ParseError, "Fixed high size must be multiple of 8" unless high % 8 == 0 + raise ParseError, "Low sizes must be 0 to 80" unless low>0 && low<=80 when 'hash' raise ParseError, "Hash type must have numerical suffix" unless sub =~ /\A[0-9]+\z/ when 'address' @@ -115,4 +121,82 @@ def subtype end end + + class Tuple < Type + + def self.parse types, dims + + depth = 0 + collected = [] + current = '' + + types.split('').each do |c| + case c + when ',' then + if depth==0 + collected << current + current = '' + else + current += c + end + when '(' then + depth += 1 + current += c + when ')' then + depth -= 1 + current += c + else + current += c + end + + end + collected << current unless current.empty? + + Tuple.new collected, dims.map {|x| x[1...-1].to_i} + + end + + attr_reader :types, :parsed_types + def initialize types, dims + super('tuple', '', dims) + @types = types + @parsed_types = types.map{|t| Type.parse t} + end + + def ==(another_type) + another_type.kind_of?(Tuple) && + another_type.types == types && + another_type.dims == dims + end + + def size + @size ||= calculate_size + end + + def calculate_size + if dims.empty? + s = 0 + parsed_types.each do |type| + ts = type.size + return nil if ts.nil? + s += ts + end + s + else + if dims.last == 0 # 0 for dynamic array [] + nil + else + subtype.dynamic? ? nil : dims.last * subtype.size + end + end + + + end + + def subtype + @subtype ||= Tuple.new(types, dims[0...-1]) + end + + end + end diff --git a/lib/web3/eth/block.rb b/lib/web3/eth/block.rb index 17dc3c6..7a15dd0 100644 --- a/lib/web3/eth/block.rb +++ b/lib/web3/eth/block.rb @@ -27,6 +27,30 @@ def block_number from_hex number end + def block_difficulty + self.respond_to?(:difficulty) ? from_hex(difficulty) : 0 + end + + def block_gasLimit + self.respond_to?(:gasLimit) ? from_hex(gasLimit) : 0 + end + + def block_gasUsed + self.respond_to?(:gasUsed) ? from_hex(gasUsed) : 0 + end + + def block_nonce + self.respond_to?(:nonce) ? from_hex(nonce) : 0 + end + + def block_size + self.respond_to?(:size) ? from_hex(size) : 0 + end + + def block_totalDifficulty + self.respond_to?(:totalDifficulty) ? from_hex(totalDifficulty) : 0 + end + end end diff --git a/lib/web3/eth/contract.rb b/lib/web3/eth/contract.rb index 5dfaf51..9ed4082 100644 --- a/lib/web3/eth/contract.rb +++ b/lib/web3/eth/contract.rb @@ -34,18 +34,26 @@ class ContractMethod def initialize abi @abi = abi @name = abi['name'] - @constant = !!abi['constant'] - @input_types = abi['inputs'].map{|a| a['type']} - @output_types = abi['outputs'].map{|a| a['type']} if abi['outputs'] + @constant = !!abi['constant'] || abi['stateMutability']=='view' + @input_types = abi['inputs'] ? abi['inputs'].map{|a| parse_component_type a } : [] + @output_types = abi['outputs'].map{|a| parse_component_type a } if abi['outputs'] @signature = Abi::Utils.function_signature @name, @input_types - @signature_hash = Abi::Utils.signature_hash @signature, (abi['type']=='event' ? 64 : 8) + @signature_hash = Abi::Utils.signature_hash @signature, (abi['type'].try(:downcase)=='event' ? 64 : 8) + end + + def parse_component_type argument + if argument['type']=~/^tuple((\[[0-9]*\])*)/ + argument['components'] ? "(#{argument['components'].collect{|c| parse_component_type c }.join(',')})#{$1}" : "()#{$1}" + else + argument['type'] + end end def parse_event_args log log_data = remove_0x_head log.raw_data['data'] - indexed_types = abi['inputs'].select{|a| a['indexed']}.collect{|a| a['type']} - not_indexed_types = abi['inputs'].select{|a| !a['indexed']}.collect{|a| a['type']} + indexed_types = abi['inputs'].select{|a| a['indexed']}.collect{|a| parse_component_type a } + not_indexed_types = abi['inputs'].select{|a| !a['indexed']}.collect{|a| parse_component_type a } indexed_args = log.indexed_args @@ -65,7 +73,7 @@ def parse_event_args log } elsif !indexed_args.empty? || !log_data.empty? - all_types = abi['inputs'].collect{|a| a['type']} + all_types = abi['inputs'].collect{|a| parse_component_type a } [all_types[0...indexed_args.size], indexed_args].transpose.collect{|arg| decode_typed_data( arg.first, [arg.second].pack('H*') ) } + decode_abi(all_types[indexed_args.size..-1], [log_data].pack('H*') ) @@ -84,7 +92,7 @@ def parse_method_args transaction def do_call web3_rpc, contract_address, args data = '0x' + signature_hash + encode_hex(encode_abi(input_types, args) ) - response = web3_rpc.request "eth_call", [{ to: contract_address, data: data}, 'latest'] + response = web3_rpc.eth.call [{ to: contract_address, data: data}, 'latest'] string_data = [remove_0x_head(response)].pack('H*') return nil if string_data.empty? @@ -95,6 +103,52 @@ def do_call web3_rpc, contract_address, args end + class ContractConstructor < ContractMethod + + def initialize abi + super abi + end + + def parse_method_args transaction + return [] if input_types.empty? + + input = transaction.input + + d = fetch_constructor_data input + result = (d && !d.empty? && try_parse(d)) + + unless result + start = input.length-1-min_data_size(input_types) + while start>=0 && !result + result = try_parse input, start + start -= 1 + end + end + + result + end + + private + + CONSTRUCTOR_SEQ = /a165627a7a72305820\w{64}0029(\w*)$/ + def fetch_constructor_data input + data = input[CONSTRUCTOR_SEQ,1] + while data && (d = data[CONSTRUCTOR_SEQ,1]) + data = d + end + data + end + + def try_parse input, start = 0 + d = start==0 ? input : input.slice(start, input.length-start-1) + decode_abi input_types, [d].pack('H*'), true + rescue Exception => err + nil + end + + end + + attr_reader :web3_rpc, :abi, :functions, :events, :constructor, :functions_by_hash, :events_by_hash def initialize abi, web_rpc = nil @@ -150,7 +204,7 @@ def parse_abi abi abi.each{|a| - case a['type'] + case a['type'].try(:downcase) when 'function' method = ContractMethod.new(a) @functions[method.name] = method @@ -160,8 +214,7 @@ def parse_abi abi @events[method.name] = method @events_by_hash[method.signature_hash] = method when 'constructor' - method = ContractMethod.new(a) - @constructor = method + @constructor = ContractConstructor.new(a) end } end diff --git a/lib/web3/eth/debug/debug_module.rb b/lib/web3/eth/debug/debug_module.rb new file mode 100644 index 0000000..460dbc6 --- /dev/null +++ b/lib/web3/eth/debug/debug_module.rb @@ -0,0 +1,39 @@ +module Web3::Eth::Debug + + class DebugModule + + include Web3::Eth::Utility + + PREFIX = 'debug_' + + def initialize web3_rpc + @web3_rpc = web3_rpc + end + + def traceTransaction hash, tracer = 'callTracer', convert_to_object = true + raw = @web3_rpc.request("#{PREFIX}#{__method__}", [hash, {tracer: tracer}]) + convert_to_object ? TransactionCallTrace.new(raw) : raw + end + + def traceBlockByNumber number, tracer = 'callTracer', convert_to_object = true + timeout = @web3_rpc.connect_options[:read_timeout] || 120 + raw = @web3_rpc.request("#{PREFIX}#{__method__}", [hex(number), {tracer: tracer, + timeout: "#{timeout}s"}]) + raise raw.first['error'] if (raw.first && raw.first['error']) + convert_to_object ? raw.map{|r| TransactionCallTrace.new(r['result'])} : raw + end + + def traceBlockByHash hash, tracer = 'callTracer', convert_to_object = true + raw = @web3_rpc.request("#{PREFIX}#{__method__}", [hash, {tracer: tracer}]) + convert_to_object ? raw.map{|r| TransactionCallTrace.new(r['result'])} : raw + end + + + def method_missing m, *args + @web3_rpc.request "#{PREFIX}#{m}", args[0] + end + + + end + +end diff --git a/lib/web3/eth/debug/transaction_call_trace.rb b/lib/web3/eth/debug/transaction_call_trace.rb new file mode 100644 index 0000000..9f5f2ba --- /dev/null +++ b/lib/web3/eth/debug/transaction_call_trace.rb @@ -0,0 +1,106 @@ +module Web3::Eth::Debug + + class TransactionCallTrace + + include Web3::Eth::Utility + + attr_reader :raw_data, :calls, :traceAddress, :parent + def initialize raw, traceAddress = [], parent = nil + @raw_data = raw + @traceAddress = traceAddress + @parent = parent + @calls = raw['calls'] ? raw['calls'].each_with_index.map{|c,i| TransactionCallTrace.new c, (traceAddress + [i]), self } : [] + end + + # CALL STATICCALL DELEGATECALL CREATE SELFDESTRUCT + def type + raw_data['type'] + end + + def action + { + 'callType' => type.downcase, + 'address' => ( suicide? ? parent.smart_contract : raw_data['to']) + } + end + + def smart_contract + ['STATICCALL','CALL'].include?(type) ? to : from + end + + def creates + (type=='CREATE' || type=='CREATE2') ? to : nil + end + + def method_hash + if input && input.length>=10 + input[2...10] + else + nil + end + end + + def suicide? + type=='SELFDESTRUCT' + end + + def from + raw_data['from'] + end + + def to + raw_data['to'] + end + + def value_wei + from_hex raw_data['value'] + end + + def value_eth + wei_to_ether value_wei + end + + def gas + from_hex raw_data['gas'] + end + + def gas_used + from_hex raw_data['gasUsed'] + end + + def input + raw_data['input'] + end + + def output + raw_data['output'] + end + + def time + raw_data['time'] + end + + def error + raw_data['error'] + end + + def success? + !raw_data['error'] + end + + # suffix # 0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29 + # look http://solidity.readthedocs.io/en/latest/metadata.html for details + def call_input_data + if creates && input + input[/a165627a7a72305820\w{64}0029(\w*)/,1] + elsif input && input.length>10 + input[10..input.length] + else + [] + end + end + + end + + +end diff --git a/lib/web3/eth/etherscan.rb b/lib/web3/eth/etherscan.rb index eea92f7..385ab1d 100644 --- a/lib/web3/eth/etherscan.rb +++ b/lib/web3/eth/etherscan.rb @@ -58,7 +58,7 @@ def request api_module, action, args = {} json = JSON.parse(response.body) - raise "Response #{json['message']} on request #{uri.to_s}" unless json['status']=='1' + raise "Response #{json['message']} on request #{uri.to_s}" if json['status'] && json['status']!='1' json['result'] @@ -68,4 +68,4 @@ def request api_module, action, args = {} end end -end \ No newline at end of file +end diff --git a/lib/web3/eth/parity_module.rb b/lib/web3/eth/parity_module.rb new file mode 100644 index 0000000..b34cb49 --- /dev/null +++ b/lib/web3/eth/parity_module.rb @@ -0,0 +1,26 @@ +module Web3 + module Eth + + class ParityModule + + include Web3::Eth::Utility + + PREFIX = 'parity_' + + def initialize web3_rpc + @web3_rpc = web3_rpc + end + + def method_missing m, *args + @web3_rpc.request "#{PREFIX}#{m}", args[0] + end + + def getBlockReceiptsByBlockNumber block + @web3_rpc.request("#{PREFIX}getBlockReceipts", [hex(block)]).collect{|tr| + TransactionReceipt.new tr + } + end + + end + end +end diff --git a/lib/web3/eth/rpc.rb b/lib/web3/eth/rpc.rb index 095dcc2..8e50da1 100644 --- a/lib/web3/eth/rpc.rb +++ b/lib/web3/eth/rpc.rb @@ -17,7 +17,7 @@ class Rpc DEFAULT_HOST = 'localhost' DEFAULT_PORT = 8545 - attr_reader :eth, :trace + attr_reader :eth, :connect_options def initialize host: DEFAULT_HOST, port: DEFAULT_PORT, connect_options: DEFAULT_CONNECT_OPTIONS @@ -27,10 +27,20 @@ def initialize host: DEFAULT_HOST, port: DEFAULT_PORT, connect_options: DEFAULT_ @connect_options = connect_options @eth = EthModule.new self - @trace = TraceModule.new self end + def trace + @trace ||= TraceModule.new(self) + end + + def parity + @parity ||= ParityModule.new(self) + end + + def debug + @debug ||= Debug::DebugModule.new(self) + end def request method, params = nil @@ -43,7 +53,7 @@ def request method, params = nil raise "Error code #{response.code} on request #{@uri.to_s} #{request.body}" unless response.kind_of? Net::HTTPOK - body = JSON.parse(response.body) + body = JSON.parse(response.body, max_nesting: 1500) if body['result'] body['result'] diff --git a/lib/web3/eth/trace_module.rb b/lib/web3/eth/trace_module.rb index 19ac43f..8060206 100644 --- a/lib/web3/eth/trace_module.rb +++ b/lib/web3/eth/trace_module.rb @@ -21,6 +21,12 @@ def internalCallsByHash tx_hash } end + def tracesByBlockNumber block + @web3_rpc.request("#{PREFIX}block", [hex(block)]).collect{|t| + CallTrace.new t + } + end + end end end diff --git a/lib/web3/eth/transaction.rb b/lib/web3/eth/transaction.rb index d648fbe..4586d61 100644 --- a/lib/web3/eth/transaction.rb +++ b/lib/web3/eth/transaction.rb @@ -26,9 +26,7 @@ def method_hash # suffix # 0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29 # look http://solidity.readthedocs.io/en/latest/metadata.html for details def call_input_data - if raw_data['creates'] && input - fetch_constructor_data input - elsif input && input.length>10 + if input && input.length>10 input[10..input.length] else [] @@ -57,17 +55,16 @@ def gasPrice_eth wei_to_ether from_hex gasPrice end - private + def gasPrice_weth + from_hex gasPrice + end - CONSTRUCTOR_SEQ = /a165627a7a72305820\w{64}0029(\w*)$/ - def fetch_constructor_data input - data = input[CONSTRUCTOR_SEQ,1] - while data && (d = data[CONSTRUCTOR_SEQ,1]) - data = d - end - data + + def transaction_nonce + from_hex nonce end + end end end diff --git a/lib/web3/eth/transaction_receipt.rb b/lib/web3/eth/transaction_receipt.rb index 0ac8a09..779ba41 100644 --- a/lib/web3/eth/transaction_receipt.rb +++ b/lib/web3/eth/transaction_receipt.rb @@ -31,11 +31,14 @@ def gas_used from_hex gasUsed end - def cumulative_gas_used from_hex cumulativeGasUsed end + def transaction_index + from_hex transactionIndex + end + end end end \ No newline at end of file diff --git a/lib/web3/eth/utility.rb b/lib/web3/eth/utility.rb index 500699c..1354a52 100644 --- a/lib/web3/eth/utility.rb +++ b/lib/web3/eth/utility.rb @@ -12,10 +12,11 @@ def wei_to_ether(wei) end def from_hex h - h.to_i 16 + h.nil? ? 0 : (h.kind_of?(String) ? h.to_i(16) : h) end def remove_0x_head(s) + return s if !s || s.length<2 s[0,2] == '0x' ? s[2..-1] : s end diff --git a/lib/web3/eth/version.rb b/lib/web3/eth/version.rb index de858ed..a0ddfab 100644 --- a/lib/web3/eth/version.rb +++ b/lib/web3/eth/version.rb @@ -1,5 +1,5 @@ module Web3 module Eth - VERSION = "0.2.16" + VERSION = "0.2.46" end end diff --git a/web3-eth.gemspec b/web3-eth.gemspec index ee4cec1..bbfacec 100644 --- a/web3-eth.gemspec +++ b/web3-eth.gemspec @@ -7,11 +7,11 @@ Gem::Specification.new do |spec| spec.name = "web3-eth" spec.version = Web3::Eth::VERSION spec.authors = ["studnev"] - spec.email = ["studnev@izx.io"] + spec.email = ["aleksey@bloxy.info"] spec.summary = %q{Web3 client to connect to Ethereum node by RPC.} spec.description = %q{Calling RPC methods of Ethereum node with Ruby.} - spec.homepage = "https://github.com/izetex/web3-eth" + spec.homepage = "https://github.com/Bloxy-info/web3-eth" spec.license = "MIT"