web3ethereum 1.0.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/.github/workflows/main.yml +18 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/web3/eth.rb +13 -0
- data/lib/web3/eth/abi/abi_coder.rb +375 -0
- data/lib/web3/eth/abi/constant.rb +30 -0
- data/lib/web3/eth/abi/exceptions.rb +28 -0
- data/lib/web3/eth/abi/type.rb +118 -0
- data/lib/web3/eth/abi/utils.rb +224 -0
- data/lib/web3/eth/block.rb +33 -0
- data/lib/web3/eth/call_trace.rb +87 -0
- data/lib/web3/eth/contract.rb +171 -0
- data/lib/web3/eth/eth_module.rb +52 -0
- data/lib/web3/eth/etherscan.rb +71 -0
- data/lib/web3/eth/log.rb +34 -0
- data/lib/web3/eth/rpc.rb +71 -0
- data/lib/web3/eth/trace_module.rb +26 -0
- data/lib/web3/eth/transaction.rb +73 -0
- data/lib/web3/eth/transaction_receipt.rb +41 -0
- data/lib/web3/eth/utility.rb +24 -0
- data/lib/web3/eth/version.rb +5 -0
- data/web3eth.gemspec +39 -0
- metadata +107 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
module Web3
|
2
|
+
module Eth
|
3
|
+
class Contract
|
4
|
+
|
5
|
+
class ContractInstance
|
6
|
+
|
7
|
+
def initialize contract, address
|
8
|
+
@contract = contract
|
9
|
+
@address = address
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing m, *args
|
13
|
+
@contract.call_contract @address, m.to_s, args
|
14
|
+
end
|
15
|
+
|
16
|
+
def __contract__
|
17
|
+
@contract
|
18
|
+
end
|
19
|
+
|
20
|
+
def __address__
|
21
|
+
@address
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class ContractMethod
|
27
|
+
|
28
|
+
include Abi::AbiCoder
|
29
|
+
include Abi::Utils
|
30
|
+
include Utility
|
31
|
+
|
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
|
+
def parse_event_args log
|
45
|
+
|
46
|
+
log_data = remove_0x_head log.raw_data['data']
|
47
|
+
indexed_types = abi['inputs'].select{|a| a['indexed']}.collect{|a| a['type']}
|
48
|
+
not_indexed_types = abi['inputs'].select{|a| !a['indexed']}.collect{|a| a['type']}
|
49
|
+
|
50
|
+
indexed_args = log.indexed_args
|
51
|
+
|
52
|
+
if indexed_args.size==indexed_types.size
|
53
|
+
|
54
|
+
indexed_values = [indexed_types, indexed_args].transpose.collect{|arg|
|
55
|
+
decode_typed_data( arg.first, [arg.second].pack('H*') )
|
56
|
+
}
|
57
|
+
|
58
|
+
not_indexed_values = not_indexed_types.empty? ? [] :
|
59
|
+
decode_abi(not_indexed_types, [log_data].pack('H*') )
|
60
|
+
|
61
|
+
i = j = 0
|
62
|
+
|
63
|
+
abi['inputs'].collect{|input|
|
64
|
+
input['indexed'] ? (i+=1; indexed_values[i-1]) : (j+=1;not_indexed_values[j-1])
|
65
|
+
}
|
66
|
+
|
67
|
+
elsif !indexed_args.empty? || !log_data.empty?
|
68
|
+
all_types = abi['inputs'].collect{|a| a['type']}
|
69
|
+
[all_types[0...indexed_args.size], indexed_args].transpose.collect{|arg|
|
70
|
+
decode_typed_data( arg.first, [arg.second].pack('H*') )
|
71
|
+
} + decode_abi(all_types[indexed_args.size..-1], [log_data].pack('H*') )
|
72
|
+
else
|
73
|
+
[]
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def parse_method_args transaction
|
80
|
+
d = transaction.call_input_data
|
81
|
+
(!d || d.empty?) ? [] : decode_abi(input_types, [d].pack('H*'))
|
82
|
+
end
|
83
|
+
|
84
|
+
def do_call web3_rpc, contract_address, args
|
85
|
+
data = '0x' + signature_hash + encode_hex(encode_abi(input_types, args) )
|
86
|
+
|
87
|
+
response = web3_rpc.request "eth_call", [{ to: contract_address, data: data}, 'latest']
|
88
|
+
|
89
|
+
string_data = [remove_0x_head(response)].pack('H*')
|
90
|
+
return nil if string_data.empty?
|
91
|
+
|
92
|
+
result = decode_abi output_types, string_data
|
93
|
+
result.length==1 ? result.first : result
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_reader :web3_rpc, :abi, :functions, :events, :constructor, :functions_by_hash, :events_by_hash
|
99
|
+
|
100
|
+
def initialize abi, web_rpc = nil
|
101
|
+
@web3_rpc = web_rpc
|
102
|
+
@abi = abi.kind_of?(String) ? JSON.parse(abi) : abi
|
103
|
+
parse_abi @abi
|
104
|
+
end
|
105
|
+
|
106
|
+
def at address
|
107
|
+
ContractInstance.new self, address
|
108
|
+
end
|
109
|
+
|
110
|
+
def call_contract contract_address, method_name, args
|
111
|
+
function = functions[method_name]
|
112
|
+
raise "No method found in ABI: #{method_name}" unless function
|
113
|
+
raise "Function #{method_name} is not constant: #{method_name}, requires to sign transaction" unless function.constant
|
114
|
+
function.do_call web3_rpc, contract_address, args
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_event_by_hash method_hash
|
118
|
+
@events_by_hash[method_hash]
|
119
|
+
end
|
120
|
+
|
121
|
+
def find_function_by_hash method_hash
|
122
|
+
@functions_by_hash[method_hash]
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_log_args log
|
126
|
+
event = find_event_by_hash log.method_hash
|
127
|
+
raise "No event found by hash #{log.method_hash}, probably ABI is not related to log event" unless event
|
128
|
+
event.parse_event_args log
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_call_args transaction
|
132
|
+
function = find_function_by_hash transaction.method_hash
|
133
|
+
raise "No function found by hash #{transaction.method_hash}, probably ABI is not related to call" unless function
|
134
|
+
function.parse_method_args transaction
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def parse_constructor_args transaction
|
139
|
+
constructor ? constructor.parse_method_args(transaction) : []
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
def parse_abi abi
|
145
|
+
@functions = {}
|
146
|
+
@events = {}
|
147
|
+
|
148
|
+
@functions_by_hash = {}
|
149
|
+
@events_by_hash = {}
|
150
|
+
|
151
|
+
abi.each{|a|
|
152
|
+
|
153
|
+
case a['type']
|
154
|
+
when 'function'
|
155
|
+
method = ContractMethod.new(a)
|
156
|
+
@functions[method.name] = method
|
157
|
+
@functions_by_hash[method.signature_hash] = method
|
158
|
+
when 'event'
|
159
|
+
method = ContractMethod.new(a)
|
160
|
+
@events[method.name] = method
|
161
|
+
@events_by_hash[method.signature_hash] = method
|
162
|
+
when 'constructor'
|
163
|
+
method = ContractMethod.new(a)
|
164
|
+
@constructor = method
|
165
|
+
end
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Web3
|
2
|
+
module Eth
|
3
|
+
|
4
|
+
class EthModule
|
5
|
+
|
6
|
+
include Web3::Eth::Utility
|
7
|
+
|
8
|
+
PREFIX = 'eth_'
|
9
|
+
|
10
|
+
def initialize web3_rpc
|
11
|
+
@web3_rpc = web3_rpc
|
12
|
+
end
|
13
|
+
|
14
|
+
def getBalance address, block = 'latest', convert_to_eth = true
|
15
|
+
wei = @web3_rpc.request("#{PREFIX}#{__method__}", [address, block]).to_i 16
|
16
|
+
convert_to_eth ? wei_to_ether(wei) : wei
|
17
|
+
end
|
18
|
+
|
19
|
+
def getBlockByNumber block, full = true, convert_to_object = true
|
20
|
+
resp = @web3_rpc.request("#{PREFIX}#{__method__}", [hex(block), full])
|
21
|
+
convert_to_object ? Block.new(resp) : resp
|
22
|
+
end
|
23
|
+
|
24
|
+
def blockNumber
|
25
|
+
from_hex @web3_rpc.request("#{PREFIX}#{__method__}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def getTransactionByHash tx_hash
|
29
|
+
Transaction.new @web3_rpc.request("#{PREFIX}#{__method__}", [tx_hash])
|
30
|
+
end
|
31
|
+
|
32
|
+
def getTransactionReceipt tx_hash
|
33
|
+
TransactionReceipt.new @web3_rpc.request("#{PREFIX}#{__method__}", [tx_hash])
|
34
|
+
end
|
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
|
+
|
45
|
+
def method_missing m, *args
|
46
|
+
@web3_rpc.request "#{PREFIX}#{m}", args[0]
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
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
|
data/lib/web3/eth/log.rb
ADDED
@@ -0,0 +1,34 @@
|
|
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 has_topics?
|
19
|
+
!!topics.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def method_hash
|
23
|
+
topics.first && topics.first[2..65]
|
24
|
+
end
|
25
|
+
|
26
|
+
def indexed_args
|
27
|
+
topics[1...topics.size].collect{|x| x[2..65]}
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/web3/eth/rpc.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
module Web3
|
2
|
+
module Eth
|
3
|
+
|
4
|
+
class Rpc
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'net/http'
|
8
|
+
|
9
|
+
JSON_RPC_VERSION = '2.0'
|
10
|
+
|
11
|
+
DEFAULT_CONNECT_OPTIONS = {
|
12
|
+
use_ssl: false,
|
13
|
+
open_timeout: 10,
|
14
|
+
read_timeout: 70
|
15
|
+
}
|
16
|
+
|
17
|
+
DEFAULT_HOST = 'localhost'
|
18
|
+
DEFAULT_PORT = 8545
|
19
|
+
|
20
|
+
attr_reader :eth, :trace
|
21
|
+
|
22
|
+
def initialize host: DEFAULT_HOST, port: DEFAULT_PORT, connect_options: DEFAULT_CONNECT_OPTIONS
|
23
|
+
|
24
|
+
@client_id = Random.rand 10000000
|
25
|
+
|
26
|
+
@uri = URI((connect_options[:use_ssl] ? 'https' : 'http')+ "://#{host}:#{port}#{connect_options[:rpc_path]}")
|
27
|
+
@connect_options = connect_options
|
28
|
+
|
29
|
+
@eth = EthModule.new self
|
30
|
+
@trace = TraceModule.new self
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def request method, params = nil
|
36
|
+
|
37
|
+
|
38
|
+
Net::HTTP.start(@uri.host, @uri.port, @connect_options) do |http|
|
39
|
+
|
40
|
+
request = Net::HTTP::Post.new @uri, {"Content-Type" => "application/json"}
|
41
|
+
request.body = {:jsonrpc => JSON_RPC_VERSION, method: method, params: params, id: @client_id}.compact.to_json
|
42
|
+
response = http.request request
|
43
|
+
|
44
|
+
raise "Error code #{response.code} on request #{@uri.to_s} #{request.body}" unless response.kind_of? Net::HTTPOK
|
45
|
+
|
46
|
+
body = JSON.parse(response.body)
|
47
|
+
|
48
|
+
if body['result']
|
49
|
+
body['result']
|
50
|
+
elsif body['error']
|
51
|
+
raise "Error #{@uri.to_s} #{body['error']} on request #{@uri.to_s} #{request.body}"
|
52
|
+
else
|
53
|
+
raise "No response on request #{@uri.to_s} #{request.body}"
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
unless Hash.method_defined?(:compact)
|
66
|
+
class Hash
|
67
|
+
def compact
|
68
|
+
self.reject{ |_k, v| v.nil? }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -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
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Web3
|
2
|
+
module Eth
|
3
|
+
|
4
|
+
class Transaction
|
5
|
+
|
6
|
+
include Web3::Eth::Utility
|
7
|
+
|
8
|
+
attr_reader :raw_data
|
9
|
+
|
10
|
+
def initialize transaction_data
|
11
|
+
@raw_data = transaction_data
|
12
|
+
transaction_data.each do |k, v|
|
13
|
+
self.instance_variable_set("@#{k}", v)
|
14
|
+
self.class.send(:define_method, k, proc {self.instance_variable_get("@#{k}")})
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_hash
|
19
|
+
if input && input.length>=10
|
20
|
+
input[2...10]
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# suffix # 0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29
|
27
|
+
# look http://solidity.readthedocs.io/en/latest/metadata.html for details
|
28
|
+
def call_input_data
|
29
|
+
if raw_data['creates'] && input
|
30
|
+
fetch_constructor_data input
|
31
|
+
elsif input && input.length>10
|
32
|
+
input[10..input.length]
|
33
|
+
else
|
34
|
+
[]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def block_number
|
39
|
+
# if transaction is less than 12 seconds old, blockNumber will be nil
|
40
|
+
# :. nil check before calling `to_hex` to avoid argument error
|
41
|
+
blockNumber && from_hex(blockNumber)
|
42
|
+
end
|
43
|
+
|
44
|
+
def value_wei
|
45
|
+
from_hex value
|
46
|
+
end
|
47
|
+
|
48
|
+
def value_eth
|
49
|
+
wei_to_ether from_hex value
|
50
|
+
end
|
51
|
+
|
52
|
+
def gas_limit
|
53
|
+
from_hex gas
|
54
|
+
end
|
55
|
+
|
56
|
+
def gasPrice_eth
|
57
|
+
wei_to_ether from_hex gasPrice
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
CONSTRUCTOR_SEQ = /a165627a7a72305820\w{64}0029(\w*)$/
|
63
|
+
def fetch_constructor_data input
|
64
|
+
data = input[CONSTRUCTOR_SEQ,1]
|
65
|
+
while data && (d = data[CONSTRUCTOR_SEQ,1])
|
66
|
+
data = d
|
67
|
+
end
|
68
|
+
data
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|