web3-eth 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,224 @@
1
+ # -*- encoding : ascii-8bit -*-
2
+
3
+ require 'digest'
4
+ require 'digest/sha3'
5
+ require 'openssl'
6
+ require 'rlp'
7
+
8
+ module Web3::Eth::Abi
9
+ module Utils
10
+
11
+ extend self
12
+
13
+ include Constant
14
+
15
+ ##
16
+ # Not the keccak in sha3, although it's underlying lib named SHA3
17
+ #
18
+ def keccak256(x)
19
+ Digest::SHA3.new(256).digest(x)
20
+ end
21
+
22
+ def keccak512(x)
23
+ Digest::SHA3.new(512).digest(x)
24
+ end
25
+
26
+ def keccak256_rlp(x)
27
+ keccak256 RLP.encode(x)
28
+ end
29
+
30
+ def sha256(x)
31
+ Digest::SHA256.digest x
32
+ end
33
+
34
+ def double_sha256(x)
35
+ sha256 sha256(x)
36
+ end
37
+
38
+ def ripemd160(x)
39
+ Digest::RMD160.digest x
40
+ end
41
+
42
+ def hash160(x)
43
+ ripemd160 sha256(x)
44
+ end
45
+
46
+ def hash160_hex(x)
47
+ encode_hex hash160(x)
48
+ end
49
+
50
+ def mod_exp(x, y, n)
51
+ x.to_bn.mod_exp(y, n).to_i
52
+ end
53
+
54
+ def mod_mul(x, y, n)
55
+ x.to_bn.mod_mul(y, n).to_i
56
+ end
57
+
58
+ def to_signed(i)
59
+ i > Constant::INT_MAX ? (i-Constant::TT256) : i
60
+ end
61
+
62
+ def base58_check_to_bytes(s)
63
+ leadingzbytes = s.match(/\A1*/)[0]
64
+ data = Constant::BYTE_ZERO * leadingzbytes.size + BaseConvert.convert(s, 58, 256)
65
+
66
+ raise ChecksumError, "double sha256 checksum doesn't match" unless double_sha256(data[0...-4])[0,4] == data[-4..-1]
67
+ data[1...-4]
68
+ end
69
+
70
+ def bytes_to_base58_check(bytes, magicbyte=0)
71
+ bs = "#{magicbyte.chr}#{bytes}"
72
+ leadingzbytes = bs.match(/\A#{Constant::BYTE_ZERO}*/)[0]
73
+ checksum = double_sha256(bs)[0,4]
74
+ '1'*leadingzbytes.size + BaseConvert.convert("#{bs}#{checksum}", 256, 58)
75
+ end
76
+
77
+ def ceil32(x)
78
+ x % 32 == 0 ? x : (x + 32 - x%32)
79
+ end
80
+
81
+ def encode_hex(b)
82
+ RLP::Utils.encode_hex b
83
+ end
84
+
85
+ def decode_hex(s)
86
+ RLP::Utils.decode_hex s
87
+ end
88
+
89
+ def big_endian_to_int(s)
90
+ RLP::Sedes.big_endian_int.deserialize s.sub(/\A(\x00)+/, '')
91
+ end
92
+
93
+ def int_to_big_endian(n)
94
+ RLP::Sedes.big_endian_int.serialize n
95
+ end
96
+
97
+ def lpad(x, symbol, l)
98
+ return x if x.size >= l
99
+ symbol * (l - x.size) + x
100
+ end
101
+
102
+ def rpad(x, symbol, l)
103
+ return x if x.size >= l
104
+ x + symbol * (l - x.size)
105
+ end
106
+
107
+ def zpad(x, l)
108
+ lpad x, BYTE_ZERO, l
109
+ end
110
+
111
+ def zunpad(x)
112
+ x.sub /\A\x00+/, ''
113
+ end
114
+
115
+ def zpad_int(n, l=32)
116
+ zpad encode_int(n), l
117
+ end
118
+
119
+ def zpad_hex(s, l=32)
120
+ zpad decode_hex(s), l
121
+ end
122
+
123
+ def int_to_addr(x)
124
+ zpad_int x, 20
125
+ end
126
+
127
+ def encode_int(n)
128
+ raise ArgumentError, "Integer invalid or out of range: #{n}" unless n.is_a?(Integer) && n >= 0 && n <= UINT_MAX
129
+ int_to_big_endian n
130
+ end
131
+
132
+ def decode_int(v)
133
+ raise ArgumentError, "No leading zero bytes allowed for integers" if v.size > 0 && (v[0] == Constant::BYTE_ZERO || v[0] == 0)
134
+ big_endian_to_int v
135
+ end
136
+
137
+ def bytearray_to_int(arr)
138
+ o = 0
139
+ arr.each {|x| o = (o << 8) + x }
140
+ o
141
+ end
142
+
143
+ def int_array_to_bytes(arr)
144
+ arr.pack('C*')
145
+ end
146
+
147
+ def bytes_to_int_array(bytes)
148
+ bytes.unpack('C*')
149
+ end
150
+
151
+ def coerce_to_int(x)
152
+ if x.is_a?(Numeric)
153
+ x
154
+ elsif x.size == 40
155
+ big_endian_to_int decode_hex(x)
156
+ else
157
+ big_endian_to_int x
158
+ end
159
+ end
160
+
161
+ def coerce_to_bytes(x)
162
+ if x.is_a?(Numeric)
163
+ int_to_big_endian x
164
+ elsif x.size == 40
165
+ decode_hex(x)
166
+ else
167
+ x
168
+ end
169
+ end
170
+
171
+ def coerce_addr_to_hex(x)
172
+ if x.is_a?(Numeric)
173
+ encode_hex zpad(int_to_big_endian(x), 20)
174
+ elsif x.size == 40 || x.size == 0
175
+ x
176
+ else
177
+ encode_hex zpad(x, 20)[-20..-1]
178
+ end
179
+ end
180
+
181
+ def normalize_address(x, allow_blank: false)
182
+ address = Address.new(x)
183
+ raise ValueError, "address is blank" if !allow_blank && address.blank?
184
+ address.to_bytes
185
+ end
186
+
187
+ def mk_contract_address(sender, nonce)
188
+ keccak256_rlp([normalize_address(sender), nonce])[12..-1]
189
+ end
190
+
191
+ def mk_metropolis_contract_address(sender, initcode)
192
+ keccak256(normalize_address(sender) + initcode)[12..-1]
193
+ end
194
+
195
+
196
+ def parse_int_or_hex(s)
197
+ if s.is_a?(Numeric)
198
+ s
199
+ elsif s[0,2] == '0x'
200
+ big_endian_to_int decode_hex(normalize_hex_without_prefix(s))
201
+ else
202
+ s.to_i
203
+ end
204
+ end
205
+
206
+ def normalize_hex_without_prefix(s)
207
+ if s[0,2] == '0x'
208
+ (s.size % 2 == 1 ? '0' : '') + s[2..-1]
209
+ else
210
+ s
211
+ end
212
+ end
213
+
214
+ def function_signature method_name, arg_types
215
+ "#{method_name}(#{arg_types.join(',')})"
216
+ end
217
+
218
+ def signature_hash signature, length=64
219
+ encode_hex(keccak256(signature))[0...length]
220
+ end
221
+
222
+
223
+ end
224
+ end
@@ -15,9 +15,7 @@ module Web3
15
15
  self.class.send(:define_method, k, proc {self.instance_variable_get("@#{k}")})
16
16
  end
17
17
 
18
- @transactions = block_data['transactions'].collect {|t|
19
- Transaction.new t
20
- }
18
+ @transactions = @transactions.collect {|t| Web3::Eth::Transaction.new t }
21
19
 
22
20
  end
23
21
 
@@ -0,0 +1,42 @@
1
+ module Web3
2
+ module Eth
3
+
4
+ class CallTrace
5
+
6
+ include Web3::Eth::Utility
7
+
8
+ attr_reader :raw_data
9
+
10
+ def initialize trace_data
11
+ @raw_data = trace_data
12
+
13
+ trace_data.each do |k, v|
14
+ self.instance_variable_set("@#{k}", v)
15
+ self.class.send(:define_method, k, proc {self.instance_variable_get("@#{k}")})
16
+ end
17
+
18
+ end
19
+
20
+ def value_eth
21
+ wei_to_ether from_hex action['value']
22
+ end
23
+
24
+ def from
25
+ action['from']
26
+ end
27
+
28
+ def to
29
+ action['to']
30
+ end
31
+
32
+ def input
33
+ action['input']
34
+ end
35
+
36
+ def output
37
+ result['output']
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,134 @@
1
+ module Web3
2
+ module Eth
3
+ class Contract
4
+
5
+ include Abi::AbiCoder
6
+ include Abi::Utils
7
+ include Utility
8
+
9
+
10
+ class ContractInstance
11
+
12
+ def initialize contract, address
13
+ @contract = contract
14
+ @address = address
15
+ end
16
+
17
+ def method_missing m, *args
18
+ @contract.call_contract @address, m.to_s, args
19
+ end
20
+
21
+ def __contract__
22
+ @contract
23
+ end
24
+
25
+ def __address__
26
+ @address
27
+ end
28
+
29
+ end
30
+
31
+ class ContractMethod
32
+ attr_reader :abi, :signature, :name, :signature_hash, :input_types, :output_types, :constant
33
+
34
+ def initialize abi
35
+ @abi = abi
36
+ @name = abi['name']
37
+ @constant = !!abi['constant']
38
+ @input_types = abi['inputs'].map{|a| a['type']}
39
+ @output_types = abi['outputs'].map{|a| a['type']} if abi['outputs']
40
+ @signature = Abi::Utils.function_signature @name, @input_types
41
+ @signature_hash = Abi::Utils.signature_hash @signature, (abi['type']=='event' ? 64 : 8)
42
+ end
43
+
44
+ end
45
+
46
+ attr_reader :web3_rpc, :abi, :functions, :events, :constructor
47
+
48
+ def initialize abi, web_rpc = nil
49
+ @web3_rpc = web_rpc
50
+ @abi = abi.kind_of?(String) ? JSON.parse(abi) : abi
51
+ parse_abi @abi
52
+ end
53
+
54
+ def at address
55
+ ContractInstance.new self, address
56
+ end
57
+
58
+ def call_contract contract_address, method_name, args
59
+
60
+ function = functions[method_name]
61
+ raise "No method found in ABI: #{method_name}" unless function
62
+ raise "Function #{method_name} is not constant: #{method_name}, requires to sign transaction" unless function.constant
63
+
64
+ data = '0x' + function.signature_hash + encode_hex(encode_abi(function.input_types, args) )
65
+
66
+ response = web3_rpc.request "eth_call", [{ to: contract_address, data: data}]
67
+
68
+ string_data = [remove_0x_head(response)].pack('H*')
69
+ return nil if string_data.empty?
70
+
71
+ result = decode_abi function.output_types, string_data
72
+ result.length==1 ? result.first : result
73
+
74
+ end
75
+
76
+ def find_event_by_hash method_hash
77
+ events.values.detect{|e| e.signature_hash==method_hash}
78
+ end
79
+
80
+ def find_function_by_hash method_hash
81
+ functions.values.detect{|e| e.signature_hash==method_hash}
82
+ end
83
+
84
+ def parse_log_args log
85
+
86
+ event = find_event_by_hash log.method_hash
87
+ raise "No event found by hash #{log.method_hash}, probably ABI is not related to log event" unless event
88
+
89
+ not_indexed_types = event.abi['inputs'].select{|a| !a['indexed']}.collect{|a| a['type']}
90
+ not_indexed_values = not_indexed_types.empty? ? [] :
91
+ decode_abi(not_indexed_types, [remove_0x_head(log.raw_data['data'])].pack('H*') )
92
+
93
+ indexed_types = event.abi['inputs'].select{|a| a['indexed']}.collect{|a| a['type']}
94
+ indexed_values = [indexed_types, log.indexed_args].transpose.collect{|arg|
95
+ decode_abi([arg.first], [arg.second].pack('H*') ).first
96
+ }
97
+
98
+ i = j = 0
99
+
100
+ args = event.abi['inputs'].collect{|input|
101
+ input['indexed'] ? (i+=1; indexed_values[i-1]) : (j+=1;not_indexed_values[j-1])
102
+ }
103
+
104
+ {event: event, args: args}
105
+
106
+ end
107
+
108
+ def parse_call_args transaction
109
+ function = find_function_by_hash transaction.method_hash
110
+ raise "No function found by hash #{transaction.method_hash}, probably ABI is not related to call" unless function
111
+ [function.input_types, transaction.method_arguments].transpose.collect{|arg|
112
+ decode_abi([arg.first], [arg.second].pack('H*') ).first
113
+ }
114
+ end
115
+
116
+
117
+ def parse_constructor_args transaction
118
+ # suffix # 0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29
119
+ # look http://solidity.readthedocs.io/en/latest/metadata.html for details
120
+ args = transaction.input[/a165627a7a72305820\w{64}0029(\w*)/,1]
121
+ args ? decode_abi(constructor.input_types, [args].pack('H*') ) : []
122
+ end
123
+
124
+ private
125
+
126
+ def parse_abi abi
127
+ @functions = Hash[abi.select{|a| a['type']=='function'}.map{|a| [a['name'], ContractMethod.new(a)]}]
128
+ @events = Hash[abi.select{|a| a['type']=='event'}.map{|a| [a['name'], ContractMethod.new(a)]}]
129
+ @constructor = ContractMethod.new abi.detect{|a| a['type']=='constructor'}
130
+ end
131
+
132
+ end
133
+ end
134
+ end
@@ -1,7 +1,7 @@
1
1
  module Web3
2
2
  module Eth
3
3
 
4
- class Ethereum
4
+ class EthModule
5
5
 
6
6
  include Web3::Eth::Utility
7
7
 
@@ -33,6 +33,15 @@ module Web3
33
33
  TransactionReceipt.new @web3_rpc.request("#{PREFIX}#{__method__}", [tx_hash])
34
34
  end
35
35
 
36
+ def contract abi
37
+ Web3::Eth::Contract.new abi, @web3_rpc
38
+ end
39
+
40
+ def load_contract etherscan_api, contract_address
41
+ contract(etherscan_api.contract_getabi address: contract_address).at contract_address
42
+ end
43
+
44
+
36
45
  def method_missing m, *args
37
46
  @web3_rpc.request "#{PREFIX}#{m}", args[0]
38
47
  end
@@ -0,0 +1,71 @@
1
+ module Web3
2
+ module Eth
3
+ class Etherscan
4
+
5
+
6
+ DEFAULT_CONNECT_OPTIONS = {
7
+ open_timeout: 10,
8
+ read_timeout: 70,
9
+ parse_result: true,
10
+ url: 'https://api.etherscan.io/api'
11
+ }
12
+
13
+ attr_reader :api_key, :connect_options
14
+
15
+ def initialize api_key, connect_options: DEFAULT_CONNECT_OPTIONS
16
+ @api_key = api_key
17
+ @connect_options = connect_options
18
+ end
19
+
20
+ def method_missing m, *args
21
+ api_module, action = m.to_s.split '_', 2
22
+ raise "Calling method must be in form <module>_<action>" unless action
23
+
24
+ arguments = args[0].kind_of?(String) ? { address: args[0] } : args[0]
25
+ result = request api_module, action, arguments
26
+
27
+ if connect_options[:parse_result]
28
+ begin
29
+ JSON.parse result
30
+ rescue
31
+ result
32
+ end
33
+ else
34
+ result
35
+ end
36
+
37
+ end
38
+
39
+
40
+ private
41
+
42
+ def request api_module, action, args = {}
43
+
44
+ uri = URI connect_options[:url]
45
+ uri.query = URI.encode_www_form({
46
+ module: api_module,
47
+ action: action,
48
+ apikey: api_key
49
+ }.merge(args))
50
+
51
+ Net::HTTP.start(uri.host, uri.port,
52
+ connect_options.merge(use_ssl: uri.scheme=='https' )) do |http|
53
+
54
+ request = Net::HTTP::Get.new uri
55
+ response = http.request request
56
+
57
+ raise "Error code #{response.code} on request #{uri.to_s} #{request.body}" unless response.kind_of? Net::HTTPOK
58
+
59
+ json = JSON.parse(response.body)
60
+
61
+ raise "Response #{json['message']} on request #{uri.to_s}" unless json['status']=='1'
62
+
63
+ json['result']
64
+
65
+ end
66
+ end
67
+
68
+
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ module Web3
2
+ module Eth
3
+
4
+ class Log
5
+
6
+ attr_reader :raw_data
7
+
8
+ def initialize log
9
+ @raw_data = log
10
+
11
+ log.each do |k, v|
12
+ self.instance_variable_set("@#{k}", v)
13
+ self.class.send(:define_method, k, proc {self.instance_variable_get("@#{k}")})
14
+ end
15
+
16
+ end
17
+
18
+ def method_hash
19
+ topics.first[2..65]
20
+ end
21
+
22
+ def indexed_args
23
+ topics[1...topics.size].collect{|x| x[2..65]}
24
+ end
25
+
26
+
27
+ end
28
+
29
+ end
30
+ end
data/lib/web3/eth/rpc.rb CHANGED
@@ -17,6 +17,8 @@ module Web3
17
17
  DEFAULT_HOST = 'localhost'
18
18
  DEFAULT_PORT = 8545
19
19
 
20
+ attr_reader :eth, :trace
21
+
20
22
  def initialize host: DEFAULT_HOST, port: DEFAULT_PORT, connect_options: DEFAULT_CONNECT_OPTIONS
21
23
 
22
24
  @client_id = Random.rand 10000000
@@ -24,12 +26,9 @@ module Web3
24
26
  @uri = URI((connect_options[:use_ssl] ? 'https' : 'http')+ "://#{host}:#{port}")
25
27
  @connect_options = connect_options
26
28
 
27
- @eth = Ethereum.new self
28
- end
29
-
29
+ @eth = EthModule.new self
30
+ @trace = TraceModule.new self
30
31
 
31
- def eth
32
- @eth
33
32
  end
34
33
 
35
34
 
@@ -0,0 +1,26 @@
1
+ module Web3
2
+ module Eth
3
+
4
+ class TraceModule
5
+
6
+ include Web3::Eth::Utility
7
+
8
+ PREFIX = 'trace_'
9
+
10
+ def initialize web3_rpc
11
+ @web3_rpc = web3_rpc
12
+ end
13
+
14
+ def method_missing m, *args
15
+ @web3_rpc.request "#{PREFIX}#{m}", args[0]
16
+ end
17
+
18
+ def internalCallsByHash tx_hash
19
+ @web3_rpc.request("#{PREFIX}transaction", [tx_hash]).select{|t| t['traceAddress']!=[]}.collect{|t|
20
+ CallTrace.new t
21
+ }
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -18,6 +18,24 @@ module Web3
18
18
 
19
19
  end
20
20
 
21
+ def method_hash
22
+ if input && input.length>=10
23
+ input[2...10]
24
+ else
25
+ nil
26
+ end
27
+ end
28
+
29
+ def method_arguments
30
+ if input && input.length>10
31
+ (0...(input.length-10)/ 64).to_a.collect{|index|
32
+ input[index*64+10..index*64+73]
33
+ }
34
+ else
35
+ []
36
+ end
37
+ end
38
+
21
39
  def block_number
22
40
  from_hex blockNumber
23
41
  end
@@ -15,6 +15,8 @@ module Web3
15
15
  self.class.send(:define_method, k, proc {self.instance_variable_get("@#{k}")})
16
16
  end
17
17
 
18
+ @logs = @logs.collect {|log| Web3::Eth::Log.new log }
19
+
18
20
  end
19
21
 
20
22
  def block_number
@@ -22,7 +24,7 @@ module Web3
22
24
  end
23
25
 
24
26
  def success?
25
- status==1 || status.nil?
27
+ status==1 || status=='0x1' || status.nil?
26
28
  end
27
29
 
28
30
  def gas_used_eth
@@ -15,6 +15,9 @@ module Web3
15
15
  h.to_i 16
16
16
  end
17
17
 
18
+ def remove_0x_head(s)
19
+ s[0,2] == '0x' ? s[2..-1] : s
20
+ end
18
21
 
19
22
  end
20
23
  end
@@ -1,5 +1,5 @@
1
1
  module Web3
2
2
  module Eth
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/web3/eth.rb CHANGED
@@ -1,7 +1,13 @@
1
1
  require "web3/eth/version"
2
+ require "web3/eth/abi/abi_coder"
2
3
  require "web3/eth/utility"
3
4
  require "web3/eth/block"
4
5
  require "web3/eth/transaction"
6
+ require "web3/eth/contract"
7
+ require "web3/eth/call_trace"
8
+ require "web3/eth/log"
5
9
  require "web3/eth/transaction_receipt"
6
- require "web3/eth/ethereum"
7
- require "web3/eth/rpc"
10
+ require "web3/eth/eth_module"
11
+ require "web3/eth/trace_module"
12
+ require "web3/eth/etherscan"
13
+ require "web3/eth/rpc"
data/web3-eth.gemspec CHANGED
@@ -23,6 +23,8 @@ Gem::Specification.new do |spec|
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ["lib"]
25
25
 
26
+ spec.add_dependency('rlp', '~> 0.7.3')
27
+ spec.add_dependency('digest-sha3', '~> 1.1.0')
26
28
  spec.add_development_dependency "bundler", "~> 1.14"
27
29
  spec.add_development_dependency "rake", "~> 10.0"
28
30
  spec.add_development_dependency "minitest", "~> 5.0"