web3-eth 0.1.0 → 0.2.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.
@@ -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"