ethlite 0.2.3 → 0.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed8e8c05d930dceca2c2bf5f4b2e21ebeaaef68309b0c16474b47df667da2a89
4
- data.tar.gz: 203ef7c86aa93e602028b1e0450e70c1c2c3a1388273263ce1e90fc503a18b08
3
+ metadata.gz: 3cd659223f8049ca63302c5bb595da6faf483d29d776ed747b5970dde11b415e
4
+ data.tar.gz: fde6fca03683dac98d474d82bc520a82dc45e74b08eaabbc0c521e2635ca3d3b
5
5
  SHA512:
6
- metadata.gz: 1527a1fccb6908164d7744e0b06772c48a18f2f67bc60068f72c7577f512d2de295e9dfe8d917c5ca3c724963396cfb02ddff0aac6611be65d79575aaa90c34f
7
- data.tar.gz: 92f93f217349b9ef33b1fc1ec7c0da36e50d172e2b44e06b9b8281d008fc770b4da081ad64f46e2be1819bbb7fa423dbe14a6519da4ddae6f782d9ea9f056108
6
+ metadata.gz: 050226ff51c20a38cb8f1db254e246a555d07baf8a6e12766985850400749a463698e89818780cd031e5ed3cab6bb05e2afae37588149084160138d5e6783e0f
7
+ data.tar.gz: 0d16770efc489cf88f08ec8ef471563905b2197c1d8330c58558348f1a9076dd717f654b5c221ea78052d31855bb458105c79f2f7ec1021a04451a202088b722
data/Manifest.txt CHANGED
@@ -10,6 +10,5 @@ lib/ethlite/abi/type.rb
10
10
  lib/ethlite/abi/utils.rb
11
11
  lib/ethlite/constant.rb
12
12
  lib/ethlite/contract.rb
13
- lib/ethlite/utility.rb
14
13
  lib/ethlite/version.rb
15
14
  lib/jsonrpc/jsonrpc.rb
data/README.md CHANGED
@@ -11,10 +11,178 @@ ethlite - light-weight machinery to query / call ethereum (blockchain contract)
11
11
 
12
12
 
13
13
 
14
-
15
14
  ## Usage
16
15
 
17
- to be done
16
+
17
+ ### Step 0: Setup JSON RPC Client
18
+
19
+ ```ruby
20
+ ## let's use a simple JSON RPC client
21
+ ## get the eth node uri via the INFURA_URI enviroment variable / key
22
+ ## e.g. https://mainnet.infura.io/v3/<YOUR_KEY_HERE>
23
+ ETH_NODE = JsonRpc.new( ENV['INFURA_URI'] )
24
+ ```
25
+
26
+
27
+
28
+ ### Do-It-Yourself (DIY) JSON RPC eth_call With "Hand-Coded" To-The-Metal ABI Encoding/Decoding
29
+
30
+
31
+ Let's try to build a JSON-RPC request from scratch calling
32
+ a (constant/read-only/view) blockchain contract method via `eth_call`.
33
+
34
+ Let's try `function tokenURI(uint256 tokenId) returns (string)`
35
+ that lets you request the metdata uri for a (non-fungible) token.
36
+
37
+ Let's try the Moonbirds contract @ [0x23581767a106ae21c074b2276d25e5c3e136a68b](https://etherscan.io/address/0x23581767a106ae21c074b2276d25e5c3e136a68b):
38
+
39
+ ```ruby
40
+ ## contract address - let's try moonbirds
41
+ contract_address = '0x23581767a106ae21c074b2276d25e5c3e136a68b'
42
+
43
+ token_ids = (0..9)
44
+ token_ids.each do |token_id|
45
+
46
+ puts "==> calling tokenURI(#{token_id})..."
47
+ tokenURI = eth_call( contract_address,
48
+ 'tokenURI', ['uint256'], ['string'],
49
+ [token_id] )
50
+ puts " ...returns: #{tokenURI}"
51
+ end
52
+ ```
53
+
54
+
55
+ And the "magic" hand-coded to-the-metal `eth_call` machinery:
56
+
57
+ ```ruby
58
+ ## construct a json-rpc call from scratch
59
+ def eth_call( contract_address,
60
+ name, inputs, outputs,
61
+ args )
62
+
63
+ ## binary encode method sig(nature)
64
+ signature = "#{name}(#{inputs.join(',')})"
65
+ signature_hash = Ethlite::Abi::Utils.encode_hex(
66
+ Ethlite::Abi::Utils.keccak256(signature))[0..7]
67
+
68
+ pp signature
69
+ # => "tokenURI(uint256)"
70
+ pp signature_hash
71
+ # => "c87b56dd"
72
+
73
+ ## binary encode method arg(ument)s
74
+ args_encoded = Ethlite::Abi::Utils.encode_hex(
75
+ Ethlite::Abi::AbiCoder.encode_abi( inputs, args) )
76
+
77
+ data = '0x' + signature_hash + args_encoded
78
+
79
+ ## json-rpc method and params
80
+ method = 'eth_call'
81
+ params = [{ to: contract_address,
82
+ data: data},
83
+ 'latest'
84
+ ]
85
+
86
+ ## do the json-rpc request
87
+ response = ETH_NODE.request( method, params )
88
+
89
+ puts "response:"
90
+ pp response
91
+
92
+ ## decode binary result
93
+ string_data = Ethlite::Abi::Utils.decode_hex(
94
+ Ethlite::Abi::Utils.remove_0x_head(response))
95
+ result = Ethlite::Abi::AbiCoder.decode_abi( outputs, string_data )
96
+ result.length == 1 ? result[0] : result
97
+ end
98
+ ```
99
+
100
+ resulting in (with debug json rpc request/reponse output):
101
+
102
+
103
+ calling `tokenURI(0)`... json_rpc POST payload request:
104
+
105
+ ``` json
106
+ {"jsonrpc":"2.0",
107
+ "method":"eth_call",
108
+ "params":[
109
+ {"to":"0x23581767a106ae21c074b2276d25e5c3e136a68b",
110
+ "data":"0xc87b56dd0000000000000000000000000000000000000000000000000000000000000000"},
111
+ "latest"],
112
+ "id":1
113
+ }
114
+ ```
115
+
116
+ json_rpc response:
117
+
118
+ ``` json
119
+ {"jsonrpc":"2.0",
120
+ "id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003a68747470733a2f2f6c6976652d2d2d6d657461646174612d35636f767071696a61612d75632e612e72756e2e6170702f6d657461646174612f30000000000000"
121
+ }
122
+ ```
123
+
124
+ ...returns: `https://live---metadata-5covpqijaa-uc.a.run.app/metadata/0`
125
+
126
+
127
+ calling `tokenURI(1)...` json_rpc POST payload request:
128
+
129
+ ``` json
130
+ {"jsonrpc":"2.0",
131
+ "method":"eth_call",
132
+ "params":[
133
+ {"to":"0x23581767a106ae21c074b2276d25e5c3e136a68b",
134
+ "data":"0xc87b56dd0000000000000000000000000000000000000000000000000000000000000001"},
135
+ "latest"],
136
+ "id":2
137
+ }
138
+ ```
139
+
140
+ json_rpc response:
141
+
142
+ ``` json
143
+ {"jsonrpc":"2.0",
144
+ "id":2,"result":"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003a68747470733a2f2f6c6976652d2d2d6d657461646174612d35636f767071696a61612d75632e612e72756e2e6170702f6d657461646174612f31000000000000"
145
+ }
146
+ ```
147
+
148
+ ...returns: `https://live---metadata-5covpqijaa-uc.a.run.app/metadata/1`
149
+
150
+
151
+
152
+ ### JSON RPC eth_call With ContractMethod Helper
153
+
154
+ Let's retry using the convenience builtin
155
+ `ContractMethod` helper:
156
+
157
+ ```ruby
158
+ ## contract address - let's try moonbirds
159
+ contract_address = '0x23581767a106ae21c074b2276d25e5c3e136a68b'
160
+
161
+ ETH_tokenURI = Ethlite::ContractMethod.new( 'tokenURI',
162
+ inputs: ['uint256'],
163
+ outputs: ['string'] )
164
+
165
+ token_ids = (0..9)
166
+ token_ids.each do |token_id|
167
+ puts "==> tokenURI(#{token_id}) returns:"
168
+ pp ETH_tokenURI.do_call( ETH_NODE, contract_address, [token_id] )
169
+ end
170
+ ```
171
+
172
+ resulting in:
173
+
174
+ ```
175
+ ==> tokenURI(0) returns:
176
+ "https://live---metadata-5covpqijaa-uc.a.run.app/metadata/0"
177
+ ==> tokenURI(1) returns:
178
+ "https://live---metadata-5covpqijaa-uc.a.run.app/metadata/1"
179
+ ==> tokenURI(2) returns:
180
+ "https://live---metadata-5covpqijaa-uc.a.run.app/metadata/3"
181
+ ...
182
+ ```
183
+
184
+
185
+ And so on and so forth.
18
186
 
19
187
 
20
188
 
data/Rakefile CHANGED
@@ -21,7 +21,7 @@ Hoe.spec 'ethlite' do
21
21
  self.extra_deps = [
22
22
  ['cocos'],
23
23
  ## ['keccak'], -- try using bundled ruby version for now
24
- ['rlp'],
24
+ ['rlp-lite'],
25
25
  ]
26
26
 
27
27
  self.licenses = ['Public Domain']
@@ -56,44 +56,50 @@ module Abi
56
56
  end
57
57
 
58
58
  def to_signed(i)
59
- i > Constant::INT_MAX ? (i-Constant::TT256) : i
59
+ i > INT_MAX ? (i-TT256) : i
60
60
  end
61
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
62
 
77
63
  def ceil32(x)
78
64
  x % 32 == 0 ? x : (x + 32 - x%32)
79
65
  end
80
66
 
81
67
  def encode_hex(b)
82
- RLP::Utils.encode_hex b
68
+ raise TypeError, "Value must be an instance of String" unless b.instance_of?(String)
69
+ b.unpack("H*").first
70
+ end
71
+
72
+ def decode_hex(str)
73
+ raise TypeError, "Value must be an instance of string" unless str.instance_of?(String)
74
+ raise TypeError, 'Non-hexadecimal digit found' unless str =~ /\A[0-9a-fA-F]*\z/
75
+ [str].pack("H*")
83
76
  end
84
77
 
85
- def decode_hex(s)
86
- RLP::Utils.decode_hex s
78
+
79
+
80
+
81
+ def int_to_big_endian(n)
82
+ RLP::Sedes.big_endian_int.serialize n
87
83
  end
88
84
 
89
85
  def big_endian_to_int(s)
90
86
  RLP::Sedes.big_endian_int.deserialize s.sub(/\A(\x00)+/, '')
91
87
  end
92
88
 
93
- def int_to_big_endian(n)
94
- RLP::Sedes.big_endian_int.serialize n
89
+
90
+ def encode_int(n)
91
+ raise ArgumentError, "Integer invalid or out of range: #{n}" unless n.is_a?(Integer) && n >= 0 && n <= UINT_MAX
92
+ int_to_big_endian n
93
+ end
94
+
95
+ def decode_int(v)
96
+ raise ArgumentError, "No leading zero bytes allowed for integers" if v.size > 0 && (v[0] == BYTE_ZERO || v[0] == 0)
97
+ big_endian_to_int v
95
98
  end
96
99
 
100
+
101
+
102
+
97
103
  def lpad(x, symbol, l)
98
104
  return x if x.size >= l
99
105
  symbol * (l - x.size) + x
@@ -124,15 +130,6 @@ module Abi
124
130
  zpad_int x, 20
125
131
  end
126
132
 
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
133
 
137
134
  def bytearray_to_int(arr)
138
135
  o = 0
@@ -148,6 +145,7 @@ module Abi
148
145
  bytes.unpack('C*')
149
146
  end
150
147
 
148
+
151
149
  def coerce_to_int(x)
152
150
  if x.is_a?(Numeric)
153
151
  x
@@ -178,19 +176,6 @@ module Abi
178
176
  end
179
177
  end
180
178
 
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
179
 
195
180
 
196
181
  def parse_int_or_hex(s)
@@ -203,6 +188,24 @@ module Abi
203
188
  end
204
189
  end
205
190
 
191
+
192
+ =begin
193
+ ## add? moved over from the old module Utility
194
+ def hex( num )
195
+ '0x' + num.to_s(16)
196
+ end
197
+
198
+ def from_hex( h )
199
+ h.nil? ? 0 : (h.kind_of?(String) ? h.to_i(16) : h)
200
+ end
201
+ =end
202
+
203
+
204
+ def remove_0x_head( s )
205
+ return s if !s || s.length<2
206
+ s[0,2] == '0x' ? s[2..-1] : s
207
+ end
208
+
206
209
  def normalize_hex_without_prefix(s)
207
210
  if s[0,2] == '0x'
208
211
  (s.size % 2 == 1 ? '0' : '') + s[2..-1]
@@ -211,6 +214,7 @@ module Abi
211
214
  end
212
215
  end
213
216
 
217
+
214
218
  def function_signature method_name, arg_types
215
219
  "#{method_name}(#{arg_types.join(',')})"
216
220
  end
@@ -2,9 +2,15 @@
2
2
  module Ethlite
3
3
  module Constant
4
4
 
5
- BYTE_EMPTY = "".freeze
6
- BYTE_ZERO = "\x00".freeze
7
- BYTE_ONE = "\x01".freeze
5
+
6
+ ## todo/check - use encoding -ascii-8bit for source file or ? - why? why not?
7
+ ## use #b/.b to ensure binary encoding? - why? why not?
8
+
9
+ ## todo/check: use auto-freeze string literals magic comment - why? why not?
10
+ BYTE_EMPTY = "".b.freeze
11
+ BYTE_ZERO = "\x00".b.freeze
12
+ BYTE_ONE = "\x01".b.freeze
13
+
8
14
 
9
15
  TT32 = 2**32
10
16
  TT40 = 2**40
@@ -17,35 +23,16 @@ module Ethlite
17
23
  INT_MAX = 2**255 - 1
18
24
  INT_MIN = -2**255
19
25
 
20
- HASH_ZERO = ("\x00"*32).freeze
26
+ HASH_ZERO = ("\x00"*32).b.freeze
21
27
 
22
28
 
23
- PUBKEY_ZERO = ("\x00"*32).freeze
24
- PRIVKEY_ZERO = ("\x00"*32).freeze
29
+ PUBKEY_ZERO = ("\x00"*32).b.freeze
30
+ PRIVKEY_ZERO = ("\x00"*32).b.freeze
31
+
25
32
  PRIVKEY_ZERO_HEX = ('0'*64).freeze
26
33
 
27
34
  CONTRACT_CODE_SIZE_LIMIT = 0x6000
28
35
 
29
36
 
30
- =begin
31
- # The RLP short length limit.
32
- SHORT_LENGTH_LIMIT = 56.freeze
33
-
34
- # The RLP long length limit.
35
- LONG_LENGTH_LIMIT = (256 ** 8).freeze
36
-
37
- # The RLP primitive type offset.
38
- PRIMITIVE_PREFIX_OFFSET = 0x80.freeze
39
-
40
- # The RLP array type offset.
41
- LIST_PREFIX_OFFSET = 0xc0.freeze
42
-
43
- # The binary encoding is ASCII (8-bit).
44
- BINARY_ENCODING = "ASCII-8BIT".freeze
45
-
46
- # Infinity as constant for convenience.
47
- INFINITY = (1.0 / 0.0).freeze
48
-
49
- =end
50
37
  end # module Constant
51
38
  end # module Ethlite
@@ -1,7 +1,5 @@
1
1
  module Ethlite
2
2
  class ContractMethod
3
- include Utility
4
-
5
3
 
6
4
  def self.parse_abi( abi )
7
5
  ## convenience helper - auto-convert to json if string passed in
@@ -31,8 +29,7 @@
31
29
 
32
30
 
33
31
 
34
- attr_reader :abi,
35
- :signature,
32
+ attr_reader :signature,
36
33
  :name,
37
34
  :signature_hash,
38
35
  :input_types,
@@ -66,7 +63,8 @@
66
63
  puts "response:"
67
64
  pp response
68
65
 
69
- string_data = [remove_0x_head(response)].pack('H*')
66
+ string_data = Abi::Utils.decode_hex(
67
+ Abi::Utils.remove_0x_head(response))
70
68
  return nil if string_data.empty?
71
69
 
72
70
  result = Abi::AbiCoder.decode_abi( @output_types, string_data )
@@ -3,7 +3,7 @@
3
3
  module Ethlite
4
4
  MAJOR = 0
5
5
  MINOR = 2
6
- PATCH = 3
6
+ PATCH = 5
7
7
  VERSION = [MAJOR,MINOR,PATCH].join('.')
8
8
 
9
9
  def self.version
data/lib/ethlite.rb CHANGED
@@ -10,7 +10,7 @@ require 'openssl'
10
10
  require 'digest'
11
11
 
12
12
  ## 3rd party gems
13
- require 'rlp' ## gem rlp - see https://rubygems.org/gems/rlp
13
+ require 'rlp-lite'
14
14
 
15
15
  ## bundled require 'digest/keccak' ## gem keccak - see https://rubygems.org/gems/keccak
16
16
  require_relative 'digest/keccak'
@@ -31,7 +31,6 @@ require_relative 'ethlite/abi/utils'
31
31
  require_relative 'ethlite/abi/abi_coder'
32
32
 
33
33
 
34
- require_relative 'ethlite/utility'
35
34
  require_relative 'ethlite/contract'
36
35
 
37
36
 
@@ -9,49 +9,43 @@
9
9
 
10
10
  class JsonRpc
11
11
  def initialize( uri )
12
- @client_id = Random.rand( 10000000 )
13
-
14
- @uri = URI.parse( uri )
12
+ @uri = uri ## assume uri always as string for now
13
+ @request_id = 1
15
14
  end
16
15
 
17
16
 
18
17
  def request( method, params=[] )
19
- opts = {}
20
- if @uri.instance_of?( URI::HTTPS )
21
- opts[:use_ssl] = true
22
- opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
23
- end
24
-
25
- Net::HTTP.start( @uri.host, @uri.port, **opts ) do |http|
26
- headers = {"Content-Type" => "application/json"}
27
- request = Net::HTTP::Post.new( @uri.request_uri, headers )
28
18
 
29
- json = { jsonrpc: '2.0',
19
+ data = { jsonrpc: '2.0',
30
20
  method: method,
31
21
  params: params,
32
- id: @client_id }.to_json
22
+ id: @request_id }
23
+
24
+ @request_id += 1
33
25
 
34
- puts "json POST payload:"
35
- puts json
26
+ puts "json_rpc POST payload:"
27
+ puts data.to_json
36
28
 
37
- request.body = json
38
- response = http.request( request )
29
+ response = Webclient.post( @uri, json: data )
39
30
 
40
- unless response.kind_of?( Net::HTTPOK )
41
- raise "Error code #{response.code} on request #{@uri.to_s} #{request.body}"
31
+
32
+ if response.status.nok?
33
+ raise "Error code #{response.status.code} on request #{@uri} #{data}"
42
34
  end
43
35
 
36
+ puts "json_rpc response.body:"
37
+ puts response.body
38
+
44
39
 
45
40
  body = JSON.parse( response.body, max_nesting: 1500 )
46
41
 
47
42
  if body['result']
48
43
  body['result']
49
44
  elsif body['error']
50
- raise "Error #{@uri.to_s} #{body['error']} on request #{@uri.to_s} #{request.body}"
45
+ raise "Error #{@uri} #{body['error']} on request #{@uri} #{data}"
51
46
  else
52
- raise "No response on request #{@uri.to_s} #{request.body}"
47
+ raise "No response on request #{@uri} #{data}"
53
48
  end
54
- end
55
- end
49
+ end # method request
56
50
  end # class JsonRpc
57
51
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ethlite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-15 00:00:00.000000000 Z
11
+ date: 2022-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cocos
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: rlp
28
+ name: rlp-lite
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -94,7 +94,6 @@ files:
94
94
  - lib/ethlite/abi/utils.rb
95
95
  - lib/ethlite/constant.rb
96
96
  - lib/ethlite/contract.rb
97
- - lib/ethlite/utility.rb
98
97
  - lib/ethlite/version.rb
99
98
  - lib/jsonrpc/jsonrpc.rb
100
99
  homepage: https://github.com/pixelartexchange/artbase
@@ -1,23 +0,0 @@
1
- module Ethlite
2
-
3
- module Utility
4
-
5
- def hex( num )
6
- '0x' + num.to_s(16)
7
- end
8
-
9
- def from_hex( h )
10
- h.nil? ? 0 : (h.kind_of?(String) ? h.to_i(16) : h)
11
- end
12
-
13
- def remove_0x_head( s )
14
- return s if !s || s.length<2
15
- s[0,2] == '0x' ? s[2..-1] : s
16
- end
17
-
18
-
19
- def wei_to_ether( wei )
20
- 1.0 * wei / 10**18
21
- end
22
- end
23
- end # module Ethlite