erc20 0.2.9 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdb92899afb7e0f3ac74c59c07cdf1f48441d8b8bab6c96fadffe506c49dcedd
4
- data.tar.gz: fd0faec13d2aafdd1cbd02afd0066c17b308b96637469f60eca0cbd1d5102629
3
+ metadata.gz: 9719ca99febb05bab13326b5a1de126b73aa5d923ad7f091ef3c4493a6d4030f
4
+ data.tar.gz: b75de1bc31c3e1e1ac2b3167d92ac9d924841048035e190ec961dfebde81d9c5
5
5
  SHA512:
6
- metadata.gz: 509bdbaa8838f52ad6ea7d8a779001c1bfd87ba9c448ce4b808849967575069b8452e3a1f50b506c38f282b709e31e690a31b4247452b34d8cc097caa72e3b45
7
- data.tar.gz: 8fa48303af7a75dd4019062bb9d3904c84f76f16dd994ee95d3d6cb0f952c9e207030be9063781c3224af6d0a3cde0f107667cdd4447c9fdc4108a05725b3bc3
6
+ metadata.gz: 6c306141b126499fe21d589809a596567439b7ec70ada449a0b7b4feee656fddb08d94f21943b10c6ea8c73526327a337cc056adc861a720fdee0bdce5e3a2f4
7
+ data.tar.gz: adc65e660ef7394c02f0d37979a7568c91b032a07620d25a40d3874e866468b3531e617e353d8bcd48b9e453d10891aa4977f67e21cbd41912123ce470ea3d79
data/Gemfile CHANGED
@@ -20,6 +20,7 @@ gem 'rake', '~>13.2', require: false
20
20
  gem 'random-port', '~>0.7', require: false
21
21
  gem 'rdoc', '~>7.0', require: false
22
22
  gem 'rubocop', '~>1.75', require: false
23
+ gem 'rubocop-elegant', '~>0.5', require: false
23
24
  gem 'rubocop-minitest', '~>0.38', require: false
24
25
  gem 'rubocop-performance', '~>1.25', require: false
25
26
  gem 'rubocop-rake', '~>0.7', require: false
data/Gemfile.lock CHANGED
@@ -13,13 +13,13 @@ PATH
13
13
  GEM
14
14
  remote: https://rubygems.org/
15
15
  specs:
16
- addressable (2.8.8)
16
+ addressable (2.9.0)
17
17
  public_suffix (>= 2.0.2, < 8.0)
18
- ansi (1.5.0)
18
+ ansi (1.6.0)
19
19
  ast (2.4.3)
20
20
  backtrace (0.4.1)
21
21
  base64 (0.3.0)
22
- bigdecimal (4.0.1)
22
+ bigdecimal (4.1.2)
23
23
  builder (3.3.0)
24
24
  concurrent-ruby (1.3.6)
25
25
  crack (1.0.1)
@@ -38,30 +38,31 @@ GEM
38
38
  multi_test (~> 1.1)
39
39
  sys-uname (~> 1.3)
40
40
  cucumber-ci-environment (11.0.0)
41
- cucumber-core (16.1.1)
41
+ cucumber-core (16.2.0)
42
42
  cucumber-gherkin (> 36, < 40)
43
43
  cucumber-messages (> 31, < 33)
44
44
  cucumber-tag-expressions (> 6, < 9)
45
- cucumber-cucumber-expressions (19.0.0)
45
+ cucumber-cucumber-expressions (19.0.1)
46
46
  bigdecimal
47
- cucumber-gherkin (38.0.0)
47
+ cucumber-gherkin (39.1.0)
48
48
  cucumber-messages (>= 31, < 33)
49
49
  cucumber-html-formatter (22.3.0)
50
50
  cucumber-messages (> 23, < 33)
51
- cucumber-messages (32.0.1)
51
+ cucumber-messages (32.3.1)
52
52
  cucumber-tag-expressions (8.1.0)
53
53
  date (3.5.1)
54
54
  diff-lcs (1.6.2)
55
55
  docile (1.4.1)
56
- donce (0.4.0)
56
+ donce (0.7.0)
57
57
  backtrace (~> 0.3)
58
58
  os (~> 1.1)
59
59
  qbash (~> 0.3)
60
- elapsed (0.2.2)
60
+ drb (2.2.3)
61
+ elapsed (0.3.1)
61
62
  loog (~> 0.6)
62
63
  tago (~> 0.1)
63
64
  ellipsized (0.3.0)
64
- erb (6.0.1)
65
+ erb (6.0.4)
65
66
  eth (0.5.13)
66
67
  forwardable (~> 1.3)
67
68
  keccak (~> 1.3)
@@ -69,102 +70,107 @@ GEM
69
70
  openssl (>= 2.2, < 4.0)
70
71
  rbsecp256k1 (~> 6.0)
71
72
  scrypt (~> 3.0)
72
- ethon (0.15.0)
73
+ ethon (0.18.0)
73
74
  ffi (>= 1.15.0)
75
+ logger
74
76
  eventmachine (1.2.7)
75
- faraday (2.14.0)
77
+ faraday (2.14.2)
76
78
  faraday-net_http (>= 2.0, < 3.5)
77
79
  json
78
80
  logger
79
- faraday-net_http (3.4.2)
81
+ faraday-net_http (3.4.3)
80
82
  net-http (~> 0.5)
81
83
  faye-websocket (0.12.0)
82
84
  eventmachine (>= 0.12.0)
83
85
  websocket-driver (>= 0.8.0)
84
- ffi (1.17.3-arm64-darwin)
85
- ffi (1.17.3-x64-mingw-ucrt)
86
- ffi (1.17.3-x86_64-darwin)
87
- ffi (1.17.3-x86_64-linux-gnu)
88
- ffi-compiler (1.3.2)
86
+ ffi (1.17.4-arm64-darwin)
87
+ ffi (1.17.4-x64-mingw-ucrt)
88
+ ffi (1.17.4-x86_64-darwin)
89
+ ffi (1.17.4-x86_64-linux-gnu)
90
+ ffi-compiler (1.4.2)
89
91
  ffi (>= 1.15.5)
90
92
  rake
91
93
  forwardable (1.4.0)
92
94
  hashdiff (1.2.1)
93
- json (2.18.0)
95
+ json (2.19.5)
94
96
  jsonrpc-client (0.1.4)
95
97
  faraday
96
98
  multi_json (>= 1.1.0)
97
- keccak (1.3.2)
99
+ keccak (1.3.3)
98
100
  konstructor (1.0.2)
99
101
  language_server-protocol (3.17.0.5)
100
102
  lint_roller (1.1.0)
101
103
  logger (1.7.0)
102
- loog (0.7.2)
104
+ loog (0.8.0)
103
105
  ellipsized
104
106
  logger (~> 1.0)
105
107
  memoist3 (1.0.0)
106
108
  mini_mime (1.1.5)
107
109
  mini_portile2 (2.8.9)
108
- minitest (6.0.1)
110
+ minitest (6.0.6)
111
+ drb (~> 2.0)
109
112
  prism (~> 1.5)
110
- minitest-reporters (1.7.1)
113
+ minitest-reporters (1.8.0)
111
114
  ansi
112
115
  builder
113
- minitest (>= 5.0)
116
+ minitest (>= 5.0, < 7)
114
117
  ruby-progressbar
115
118
  minitest-retry (0.3.1)
116
119
  minitest (>= 5.0)
117
- multi_json (1.19.1)
120
+ multi_json (1.21.1)
118
121
  multi_test (1.1.0)
119
122
  net-http (0.9.1)
120
123
  uri (>= 0.11.1)
121
- openssl (3.3.2)
124
+ openssl (3.3.3)
122
125
  os (1.1.4)
123
- parallel (1.27.0)
124
- parser (3.3.10.1)
126
+ parallel (2.1.0)
127
+ parser (3.3.11.1)
125
128
  ast (~> 2.4.1)
126
129
  racc
127
130
  pkg-config (1.6.5)
128
- prism (1.8.0)
131
+ prism (1.9.0)
129
132
  psych (5.3.1)
130
133
  date
131
134
  stringio
132
- public_suffix (7.0.2)
133
- qbash (0.6.0)
135
+ public_suffix (7.0.5)
136
+ qbash (0.8.4)
134
137
  backtrace (> 0)
135
138
  elapsed (> 0)
136
139
  loog (> 0)
137
140
  tago (> 0)
138
141
  racc (1.8.1)
139
142
  rainbow (3.1.1)
140
- rake (13.3.1)
141
- random-port (0.7.6)
143
+ rake (13.4.2)
144
+ random-port (0.8.2)
142
145
  tago (~> 0.0)
143
146
  rbsecp256k1 (6.0.0)
144
147
  mini_portile2 (~> 2.8)
145
148
  pkg-config (~> 1.5)
146
149
  rubyzip (~> 2.3)
147
- rdoc (7.1.0)
150
+ rdoc (7.2.0)
148
151
  erb
149
152
  psych (>= 4.0.0)
150
153
  tsort
151
- regexp_parser (2.11.3)
154
+ regexp_parser (2.12.0)
152
155
  rexml (3.4.4)
153
- rubocop (1.82.1)
156
+ rubocop (1.86.2)
154
157
  json (~> 2.3)
155
158
  language_server-protocol (~> 3.17.0.2)
156
159
  lint_roller (~> 1.1.0)
157
- parallel (~> 1.10)
160
+ parallel (>= 1.10)
158
161
  parser (>= 3.3.0.2)
159
162
  rainbow (>= 2.2.2, < 4.0)
160
163
  regexp_parser (>= 2.9.3, < 3.0)
161
- rubocop-ast (>= 1.48.0, < 2.0)
164
+ rubocop-ast (>= 1.49.0, < 2.0)
162
165
  ruby-progressbar (~> 1.7)
163
166
  unicode-display_width (>= 2.4.0, < 4.0)
164
- rubocop-ast (1.49.0)
167
+ rubocop-ast (1.49.1)
165
168
  parser (>= 3.3.7.2)
166
169
  prism (~> 1.7)
167
- rubocop-minitest (0.38.2)
170
+ rubocop-elegant (0.5.1)
171
+ lint_roller (~> 1.1)
172
+ rubocop (~> 1.75)
173
+ rubocop-minitest (0.39.1)
168
174
  lint_roller (~> 1.1)
169
175
  rubocop (>= 1.75.0, < 2.0)
170
176
  rubocop-ast (>= 1.38.0, < 2.0)
@@ -190,22 +196,26 @@ GEM
190
196
  simplecov-html (0.13.2)
191
197
  simplecov_json_formatter (0.1.4)
192
198
  stringio (3.2.0)
193
- sys-uname (1.4.1)
199
+ sys-uname (1.5.1)
200
+ ffi (~> 1.1)
201
+ memoist3 (~> 1.0.0)
202
+ sys-uname (1.5.1-universal-mingw32)
194
203
  ffi (~> 1.1)
195
204
  memoist3 (~> 1.0.0)
205
+ win32ole
196
206
  tago (0.7.0)
197
207
  thor (1.5.0)
198
208
  threads (0.5.0)
199
209
  backtrace (~> 0)
200
210
  concurrent-ruby (~> 1.0)
201
211
  tsort (0.2.0)
202
- typhoeus (1.5.0)
203
- ethon (>= 0.9.0, < 0.16.0)
212
+ typhoeus (1.6.0)
213
+ ethon (>= 0.18.0)
204
214
  unicode-display_width (3.2.0)
205
215
  unicode-emoji (~> 4.1)
206
216
  unicode-emoji (4.2.0)
207
217
  uri (1.1.1)
208
- webmock (3.26.1)
218
+ webmock (3.26.2)
209
219
  addressable (>= 2.8.0)
210
220
  crack (>= 0.3.2)
211
221
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -213,7 +223,8 @@ GEM
213
223
  base64
214
224
  websocket-extensions (>= 0.1.0)
215
225
  websocket-extensions (0.1.5)
216
- yard (0.9.38)
226
+ win32ole (1.9.3)
227
+ yard (0.9.44)
217
228
 
218
229
  PLATFORMS
219
230
  arm64-darwin-22
@@ -240,6 +251,7 @@ DEPENDENCIES
240
251
  random-port (~> 0.7)
241
252
  rdoc (~> 7.0)
242
253
  rubocop (~> 1.75)
254
+ rubocop-elegant (~> 0.5)
243
255
  rubocop-minitest (~> 0.38)
244
256
  rubocop-performance (~> 1.25)
245
257
  rubocop-rake (~> 0.7)
data/README.md CHANGED
@@ -29,7 +29,7 @@ Or simply add this to your Gemfile:
29
29
  gem 'erc20'
30
30
  ```
31
31
 
32
- Then, make an instance of the main class and use to read
32
+ Then, make an instance of the main class and use it to read
33
33
  balances, send and receive payments:
34
34
 
35
35
  ```ruby
@@ -40,6 +40,8 @@ w = ERC20::Wallet.new(
40
40
  host: 'mainnet.infura.io',
41
41
  http_path: '/v3/<your-infura-key>',
42
42
  ws_path: '/ws/v3/<your-infura-key>',
43
+ attempts: 3, # retry failed HTTP RPC calls up to 3 times, with backoff
44
+ fallbacks: ['https://eth.drpc.org'], # alternative RPC endpoints to try
43
45
  log: $stdout
44
46
  )
45
47
 
@@ -75,8 +77,8 @@ To check the price of a gas unit and the expected cost of a payment:
75
77
  # How many gas units required to send this payment:
76
78
  units = w.gas_estimate(from, to, amount)
77
79
 
78
- # What is the price of a gas unit, in gwei:
79
- gwei = w.gas_price
80
+ # What is the price of a gas unit, in wei:
81
+ price = w.gas_price
80
82
  ```
81
83
 
82
84
  To generate a new private key, use [eth](https://rubygems.org/gems/eth):
@@ -106,7 +108,7 @@ w = ERC20::Wallet.new(
106
108
  You can use [squid-proxy] [Docker] image to set up your own [HTTP proxy] server.
107
109
 
108
110
  Of course, this library works with [Polygon], [Optimism],
109
- and other EVM compatible blockchains.
111
+ and other [EVM] compatible blockchains.
110
112
 
111
113
  ## How to use in command line
112
114
 
@@ -170,3 +172,4 @@ If it's clean and you don't see any error messages, submit your pull request.
170
172
  [Docker]: https://www.docker.com/
171
173
  [Polygon]: https://polygon.technology/
172
174
  [Optimism]: https://www.optimism.io/
175
+ [EVM]: https://ethereum.org/developers/docs/evm/
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rake'
4
+ require 'rake/clean'
3
5
  # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
4
6
  # SPDX-License-Identifier: MIT
5
7
 
6
8
  require 'rubygems'
7
- require 'rake'
8
- require 'rake/clean'
9
9
 
10
10
  ENV['RAKE'] = 'true'
11
11
 
data/bin/erc20 CHANGED
@@ -44,6 +44,9 @@ class Bin < Thor
44
44
  desc: 'HTTP/S proxy for all requests, e.g. "localhost:3128"'
45
45
  class_option :attempts, type: :numeric, default: 1,
46
46
  desc: 'How many times should we try before failing'
47
+ class_option :fallbacks, type: :array,
48
+ default: ['https://ethereum.publicnode.com', 'https://eth.drpc.org', 'https://rpc.ankr.com/eth'],
49
+ desc: 'Fallback HTTP RPC endpoint URLs to try when the primary host fails'
47
50
  class_option :dry, type: :boolean, default: false,
48
51
  desc: "Don't send a real payment, run in a read-only mode"
49
52
  class_option :verbose, type: :boolean, default: false,
@@ -136,6 +139,8 @@ class Bin < Thor
136
139
  host: options[:host], port: options[:port].to_i,
137
140
  http_path: options[:http_path], ws_path: options[:ws_path],
138
141
  ssl: options[:ssl],
142
+ attempts: options[:attempts].to_i,
143
+ fallbacks: options[:fallbacks],
139
144
  log: log
140
145
  )
141
146
  end
data/erc20.gemspec CHANGED
@@ -7,7 +7,7 @@ require 'English'
7
7
  require_relative 'lib/erc20/erc20'
8
8
 
9
9
  Gem::Specification.new do |s|
10
- s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
10
+ s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
11
11
  s.required_ruby_version = '~>3.0'
12
12
  s.name = 'erc20'
13
13
  s.version = ERC20::VERSION
@@ -25,12 +25,12 @@ Gem::Specification.new do |s|
25
25
  s.rdoc_options = ['--charset=UTF-8']
26
26
  s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
27
27
  s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
28
- s.add_dependency 'elapsed', '~>0.2'
29
- s.add_dependency 'eth', '~>0.5'
30
- s.add_dependency 'faye-websocket', '~>0.11'
31
- s.add_dependency 'json', '~>2.10'
32
- s.add_dependency 'jsonrpc-client', '~>0.1'
33
- s.add_dependency 'loog', '~>0.4'
34
- s.add_dependency 'thor', '~>1.3'
28
+ s.add_dependency('elapsed', '~>0.2')
29
+ s.add_dependency('eth', '~>0.5')
30
+ s.add_dependency('faye-websocket', '~>0.11')
31
+ s.add_dependency('json', '~>2.10')
32
+ s.add_dependency('jsonrpc-client', '~>0.1')
33
+ s.add_dependency('loog', '~>0.4')
34
+ s.add_dependency('thor', '~>1.3')
35
35
  s.metadata['rubygems_mfa_required'] = 'true'
36
36
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
4
5
  # SPDX-License-Identifier: MIT
5
6
 
6
7
  require 'tmpdir'
7
- require 'English'
8
8
  require_relative '../../lib/erc20'
9
9
 
10
10
  Before do
@@ -31,19 +31,19 @@ When(%r{^I run bin/erc20 with "([^"]*)"$}) do |arg|
31
31
  end
32
32
 
33
33
  Then(/^Stdout contains "([^"]*)"$/) do |txt|
34
- raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
34
+ raise(StandardError, "STDOUT doesn't contain '#{txt}':\n#{@stdout}") unless @stdout.include?(txt)
35
35
  end
36
36
 
37
37
  Then(/^Stdout is empty$/) do
38
- raise "STDOUT is not empty:\n#{@stdout}" unless @stdout == ''
38
+ raise(StandardError, "STDOUT is not empty:\n#{@stdout}") unless @stdout == ''
39
39
  end
40
40
 
41
41
  Then(/^Exit code is zero$/) do
42
- raise "Non-zero exit #{@exitstatus}:\n#{@stdout}" unless @exitstatus.zero?
42
+ raise(StandardError, "Non-zero exit #{@exitstatus}:\n#{@stdout}") unless @exitstatus.zero?
43
43
  end
44
44
 
45
45
  Then(/^Exit code is not zero$/) do
46
- raise 'Zero exit code' if @exitstatus.zero?
46
+ raise(StandardError, 'Zero exit code') if @exitstatus.zero?
47
47
  end
48
48
 
49
49
  When(/^I run bash with "([^"]*)"$/) do |text|
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cucumber'
3
4
  # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
4
5
  # SPDX-License-Identifier: MIT
5
6
 
6
7
  require 'simplecov'
7
- require 'cucumber'
8
8
  require_relative '../../lib/erc20'
data/lib/erc20/erc20.rb CHANGED
@@ -24,6 +24,5 @@
24
24
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
25
25
  # License:: MIT
26
26
  module ERC20
27
- # Current version of the gem (changed by the +.rultor.yml+ on every release)
28
- VERSION = '0.2.9' unless defined?(VERSION)
27
+ VERSION = '0.3.0' unless defined?(VERSION)
29
28
  end
@@ -12,14 +12,9 @@ require_relative 'wallet'
12
12
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
13
13
  # License:: MIT
14
14
  class ERC20::FakeWallet
15
- # Transaction hash always returned:
16
15
  TXN_HASH = '0x172de9cda30537eae68ab4a96163ebbb8f8a85293b8737dd2e5deb4714b14623'
17
16
 
18
- # Fakes:
19
- attr_reader :host, :port, :ssl, :chain, :contract, :ws_path, :http_path
20
-
21
- # Full history of all method calls:
22
- attr_reader :history
17
+ attr_reader :host, :port, :ssl, :chain, :contract, :ws_path, :http_path, :history
23
18
 
24
19
  # Ctor.
25
20
  def initialize
@@ -77,7 +72,7 @@ class ERC20::FakeWallet
77
72
  42_000_000
78
73
  end
79
74
 
80
- # How much gas units is required in order to send ERC20 transaction.
75
+ # How many gas units are required to send an ERC20 transaction.
81
76
  #
82
77
  # @param [String] from The departing address, in hex
83
78
  # @param [String] to Arriving address, in hex
@@ -137,7 +132,7 @@ class ERC20::FakeWallet
137
132
  sleep(delay)
138
133
  a = addresses.to_a.sample
139
134
  next if a.nil?
140
- event =
135
+ yield(
141
136
  if raw
142
137
  {}
143
138
  else
@@ -148,7 +143,7 @@ class ERC20::FakeWallet
148
143
  txn: TXN_HASH
149
144
  }
150
145
  end
151
- yield event
146
+ )
152
147
  end
153
148
  end
154
149
  end
data/lib/erc20/wallet.rb CHANGED
@@ -58,10 +58,9 @@ require_relative 'erc20'
58
58
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
59
59
  # License:: MIT
60
60
  class ERC20::Wallet
61
- # Address of USDT contract.
62
61
  USDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'
62
+ TRANSFER = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
63
63
 
64
- # These properties are read-only:
65
64
  attr_reader :host, :port, :ssl, :chain, :contract, :ws_path, :http_path
66
65
 
67
66
  # Constructor.
@@ -73,36 +72,50 @@ class ERC20::Wallet
73
72
  # @param [String] ws_path The path in the connection URL, for Websockets
74
73
  # @param [Boolean] ssl Should we use SSL (for https and wss)
75
74
  # @param [String] proxy The URL of the proxy to use
75
+ # @param [Integer] attempts How many times to retry a failed HTTP RPC call before giving up
76
+ # @param [Array<String>] fallbacks Alternative HTTP RPC endpoint URLs to try when the primary one fails
76
77
  # @param [Object] log The destination for logs
77
- def initialize(contract: USDT, chain: 1, log: $stdout,
78
- host: nil, port: 443, http_path: '/', ws_path: '/',
79
- ssl: true, proxy: nil)
80
- raise 'Contract can\'t be nil' unless contract
81
- raise 'Contract must be a String' unless contract.is_a?(String)
82
- raise 'Invalid format of the contract' unless /^0x[0-9a-fA-F]{40}$/.match?(contract)
78
+ def initialize(
79
+ contract: USDT, chain: 1, log: $stdout,
80
+ host: nil, port: 443, http_path: '/', ws_path: '/',
81
+ ssl: true, proxy: nil, attempts: 1, fallbacks: []
82
+ )
83
+ raise(ArgumentError, 'Contract can\'t be nil') unless contract
84
+ raise(ArgumentError, 'Contract must be a String') unless contract.is_a?(String)
85
+ raise(ArgumentError, 'Invalid format of the contract') unless /^0x[0-9a-fA-F]{40}$/.match?(contract)
83
86
  @contract = contract
84
- raise 'Host can\'t be nil' unless host
85
- raise 'Host must be a String' unless host.is_a?(String)
87
+ raise(ArgumentError, 'Host can\'t be nil') unless host
88
+ raise(ArgumentError, 'Host must be a String') unless host.is_a?(String)
86
89
  @host = host
87
- raise 'Port can\'t be nil' unless port
88
- raise 'Port must be an Integer' unless port.is_a?(Integer)
89
- raise 'Port must be a positive Integer' unless port.positive?
90
+ raise(ArgumentError, 'Port can\'t be nil') unless port
91
+ raise(ArgumentError, 'Port must be an Integer') unless port.is_a?(Integer)
92
+ raise(ArgumentError, 'Port must be a positive Integer') unless port.positive?
90
93
  @port = port
91
- raise 'Ssl can\'t be nil' if ssl.nil?
94
+ raise(ArgumentError, 'Ssl can\'t be nil') if ssl.nil?
92
95
  @ssl = ssl
93
- raise 'Http_path can\'t be nil' unless http_path
94
- raise 'Http_path must be a String' unless http_path.is_a?(String)
96
+ raise(ArgumentError, 'Http_path can\'t be nil') unless http_path
97
+ raise(ArgumentError, 'Http_path must be a String') unless http_path.is_a?(String)
95
98
  @http_path = http_path
96
- raise 'Ws_path can\'t be nil' unless ws_path
97
- raise 'Ws_path must be a String' unless ws_path.is_a?(String)
99
+ raise(ArgumentError, 'Ws_path can\'t be nil') unless ws_path
100
+ raise(ArgumentError, 'Ws_path must be a String') unless ws_path.is_a?(String)
98
101
  @ws_path = ws_path
99
- raise 'Log can\'t be nil' unless log
102
+ raise(ArgumentError, 'Log can\'t be nil') unless log
100
103
  @log = log
101
- raise 'Chain can\'t be nil' unless chain
102
- raise 'Chain must be an Integer' unless chain.is_a?(Integer)
103
- raise 'Chain must be a positive Integer' unless chain.positive?
104
+ raise(ArgumentError, 'Chain can\'t be nil') unless chain
105
+ raise(ArgumentError, 'Chain must be an Integer') unless chain.is_a?(Integer)
106
+ raise(ArgumentError, 'Chain must be a positive Integer') unless chain.positive?
104
107
  @chain = chain
105
108
  @proxy = proxy
109
+ raise(ArgumentError, 'Attempts can\'t be nil') unless attempts
110
+ raise(ArgumentError, 'Attempts must be an Integer') unless attempts.is_a?(Integer)
111
+ raise(ArgumentError, 'Attempts must be a positive Integer') unless attempts.positive?
112
+ @attempts = attempts
113
+ raise(ArgumentError, 'Fallbacks can\'t be nil') if fallbacks.nil?
114
+ raise(ArgumentError, 'Fallbacks must be an Array') unless fallbacks.is_a?(Array)
115
+ fallbacks.each do |f|
116
+ raise(ArgumentError, 'Each fallback must be a String') unless f.is_a?(String)
117
+ end
118
+ @fallbacks = fallbacks
106
119
  @mutex = Mutex.new
107
120
  end
108
121
 
@@ -115,16 +128,11 @@ class ERC20::Wallet
115
128
  # @param [String] address Public key, in hex, starting from '0x'
116
129
  # @return [Integer] Balance, in tokens
117
130
  def balance(address)
118
- raise 'Address can\'t be nil' unless address
119
- raise 'Address must be a String' unless address.is_a?(String)
120
- raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
121
- func = '70a08231' # balanceOf
122
- data = "0x#{func}000000000000000000000000#{address[2..].downcase}"
123
- r =
124
- with_jsonrpc do |jr|
125
- jr.eth_call({ to: @contract, data: data }, 'latest')
126
- end
127
- b = r[2..].to_i(16)
131
+ raise(ArgumentError, 'Address can\'t be nil') unless address
132
+ raise(ArgumentError, 'Address must be a String') unless address.is_a?(String)
133
+ raise(ArgumentError, 'Invalid format of the address') unless /^0x[0-9a-fA-F]{40}$/.match?(address)
134
+ data = "0x70a08231000000000000000000000000#{address[2..].downcase}"
135
+ b = with_jsonrpc { |jr| jr.eth_call({ to: @contract, data: data }, 'latest') }[2..].to_i(16)
128
136
  log_it(:debug, "The balance of #{address} is #{b} ERC20 tokens")
129
137
  b
130
138
  end
@@ -137,14 +145,10 @@ class ERC20::Wallet
137
145
  # @param [String] address Public key, in hex, starting from '0x'
138
146
  # @return [Integer] Balance, in ETH
139
147
  def eth_balance(address)
140
- raise 'Address can\'t be nil' unless address
141
- raise 'Address must be a String' unless address.is_a?(String)
142
- raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
143
- r =
144
- with_jsonrpc do |jr|
145
- jr.eth_getBalance(address, 'latest')
146
- end
147
- b = r[2..].to_i(16)
148
+ raise(ArgumentError, 'Address can\'t be nil') unless address
149
+ raise(ArgumentError, 'Address must be a String') unless address.is_a?(String)
150
+ raise(ArgumentError, 'Invalid format of the address') unless /^0x[0-9a-fA-F]{40}$/.match?(address)
151
+ b = with_jsonrpc { |jr| jr.eth_getBalance(address, 'latest') }[2..].to_i(16)
148
152
  log_it(:debug, "The balance of #{address} is #{b} ETHs")
149
153
  b
150
154
  end
@@ -154,24 +158,23 @@ class ERC20::Wallet
154
158
  # @param [String] txn Hex of transaction
155
159
  # @return [Integer] Balance, in ERC20 tokens
156
160
  def sum_of(txn)
157
- raise 'Transaction hash can\'t be nil' unless txn
158
- raise 'Transaction hash must be a String' unless txn.is_a?(String)
159
- raise 'Invalid format of the transaction hash' unless /^0x[0-9a-fA-F]{64}$/.match?(txn)
161
+ raise(ArgumentError, 'Transaction hash can\'t be nil') unless txn
162
+ raise(ArgumentError, 'Transaction hash must be a String') unless txn.is_a?(String)
163
+ raise(ArgumentError, 'Invalid format of the transaction hash') unless /^0x[0-9a-fA-F]{64}$/.match?(txn)
160
164
  receipt =
161
165
  with_jsonrpc do |jr|
162
166
  jr.eth_getTransactionReceipt(txn)
163
167
  end
164
- raise "Transaction not found: #{txn}" if receipt.nil?
168
+ raise(StandardError, "Transaction not found: #{txn}") if receipt.nil?
165
169
  logs = receipt['logs'] || []
166
- transfer_event = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
167
170
  logs.each do |log|
168
- next unless log['topics'] && log['topics'][0] == transfer_event
171
+ next unless log['topics'] && log['topics'][0] == TRANSFER
169
172
  next unless log['address'].downcase == @contract.downcase
170
173
  amount = log['data'].to_i(16)
171
174
  log_it(:debug, "Found transfer of #{amount} tokens in transaction #{txn}")
172
175
  return amount
173
176
  end
174
- raise "No transfer event found in transaction #{txn}"
177
+ raise(StandardError, "No transfer event found in transaction #{txn}")
175
178
  end
176
179
 
177
180
  # How many gas units are required to send an ERC20 transaction.
@@ -181,15 +184,15 @@ class ERC20::Wallet
181
184
  # @param [Integer] amount How many ERC20 tokens to send
182
185
  # @return [Integer] Number of gas units required
183
186
  def gas_estimate(from, to, amount)
184
- raise 'Address can\'t be nil' unless from
185
- raise 'Address must be a String' unless from.is_a?(String)
186
- raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(from)
187
- raise 'Address can\'t be nil' unless to
188
- raise 'Address must be a String' unless to.is_a?(String)
189
- raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(to)
190
- raise 'Amount can\'t be nil' unless amount
191
- raise "Amount (#{amount}) must be an Integer" unless amount.is_a?(Integer)
192
- raise "Amount (#{amount}) must be a positive Integer" unless amount.positive?
187
+ raise(ArgumentError, 'Address can\'t be nil') unless from
188
+ raise(ArgumentError, 'Address must be a String') unless from.is_a?(String)
189
+ raise(ArgumentError, 'Invalid format of the address') unless /^0x[0-9a-fA-F]{40}$/.match?(from)
190
+ raise(ArgumentError, 'Address can\'t be nil') unless to
191
+ raise(ArgumentError, 'Address must be a String') unless to.is_a?(String)
192
+ raise(ArgumentError, 'Invalid format of the address') unless /^0x[0-9a-fA-F]{40}$/.match?(to)
193
+ raise(ArgumentError, 'Amount can\'t be nil') unless amount
194
+ raise(ArgumentError, "Amount (#{amount}) must be an Integer") unless amount.is_a?(Integer)
195
+ raise(ArgumentError, "Amount (#{amount}) must be a positive Integer") unless amount.positive?
193
196
  gas =
194
197
  with_jsonrpc do |jr|
195
198
  jr.eth_estimateGas({ from:, to: @contract, data: to_pay_data(to, amount) }, 'latest').to_i(16)
@@ -198,25 +201,35 @@ class ERC20::Wallet
198
201
  gas
199
202
  end
200
203
 
201
- # What is the price of gas unit in gwei?
204
+ GAS_PRICE_TIP = 1_000_000_000
205
+
206
+ # What is the price of gas unit in wei?
202
207
  #
203
208
  # In Ethereum, gas is a unit that measures the computational work required to
204
209
  # execute operations on the network. Every transaction and smart contract
205
210
  # interaction consumes gas. Gas price is the amount of ETH you're willing to pay
206
- # for each unit of gas, denominated in gwei (1 gwei = 0.000000001 ETH). Higher
211
+ # for each unit of gas, denominated in wei (1 gwei = 0.000000001 ETH). Higher
207
212
  # gas prices incentivize miners to include your transaction sooner, while lower
208
213
  # prices may result in longer confirmation times.
209
214
  #
210
- # @return [Integer] Price of gas unit, in gwei (0.000000001 ETH)
215
+ # The returned price is not the bare EIP-1559 base fee. The base fee alone
216
+ # leaves a zero miner tip (+tip = gasPrice - baseFee = 0+), so proposers have
217
+ # no incentive to include the transaction, and it becomes unmineable the
218
+ # moment the base fee rises (it may grow up to 12.5% per block). To make the
219
+ # price mineable, we double the base fee (a buffer that absorbs several blocks
220
+ # of base-fee growth) and add a priority tip (+GAS_PRICE_TIP+).
221
+ #
222
+ # @return [Integer] Price of gas unit, in wei (1 gwei = 0.000000001 ETH)
211
223
  def gas_price
212
224
  block =
213
225
  with_jsonrpc do |jr|
214
226
  jr.eth_getBlockByNumber('latest', false)
215
227
  end
216
- raise "Can't get gas price, try again later" if block.nil?
217
- gwei = block['baseFeePerGas'].to_i(16)
218
- log_it(:debug, "The cost of one gas unit is #{gwei} gwei")
219
- gwei
228
+ raise(StandardError, "Can't get gas price, try again later") if block.nil?
229
+ base = block['baseFeePerGas'].to_i(16)
230
+ price = (base * 2) + GAS_PRICE_TIP
231
+ log_it(:debug, "The base fee is #{base} wei, the cost of one gas unit is #{price} wei")
232
+ price
220
233
  end
221
234
 
222
235
  # Send a single ERC20 payment from a private address to a public one.
@@ -236,40 +249,40 @@ class ERC20::Wallet
236
249
  # @param [Integer] price How much gas you pay per computation unit
237
250
  # @return [String] Transaction hash
238
251
  def pay(priv, address, amount, limit: nil, price: gas_price)
239
- raise 'Private key can\'t be nil' unless priv
240
- raise 'Private key must be a String' unless priv.is_a?(String)
241
- raise 'Invalid format of private key' unless /^[0-9a-fA-F]{64}$/.match?(priv)
242
- raise 'Address can\'t be nil' unless address
243
- raise 'Address must be a String' unless address.is_a?(String)
244
- raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
245
- raise 'Amount can\'t be nil' unless amount
246
- raise "Amount (#{amount}) must be an Integer" unless amount.is_a?(Integer)
247
- raise "Amount (#{amount}) must be a positive Integer" unless amount.positive?
252
+ raise(ArgumentError, 'Private key can\'t be nil') unless priv
253
+ raise(ArgumentError, 'Private key must be a String') unless priv.is_a?(String)
254
+ raise(ArgumentError, 'Invalid format of private key') unless /^[0-9a-fA-F]{64}$/.match?(priv)
255
+ raise(ArgumentError, 'Address can\'t be nil') unless address
256
+ raise(ArgumentError, 'Address must be a String') unless address.is_a?(String)
257
+ raise(ArgumentError, 'Invalid format of the address') unless /^0x[0-9a-fA-F]{40}$/.match?(address)
258
+ raise(ArgumentError, 'Amount can\'t be nil') unless amount
259
+ raise(ArgumentError, "Amount (#{amount}) must be an Integer") unless amount.is_a?(Integer)
260
+ raise(ArgumentError, "Amount (#{amount}) must be a positive Integer") unless amount.positive?
248
261
  if limit
249
- raise 'Gas limit must be an Integer' unless limit.is_a?(Integer)
250
- raise "Gas limit #{limit} is below #{Eth::Tx::DEFAULT_GAS_LIMIT}" if limit < Eth::Tx::DEFAULT_GAS_LIMIT
251
- raise "Gas limit #{limit} is above #{Eth::Tx::BLOCK_GAS_LIMIT}" if limit > Eth::Tx::BLOCK_GAS_LIMIT
262
+ raise(ArgumentError, 'Gas limit must be an Integer') unless limit.is_a?(Integer)
263
+ raise(ArgumentError, "Gas limit #{limit} is below #{Eth::Tx::DEFAULT_GAS_LIMIT}") if limit < Eth::Tx::DEFAULT_GAS_LIMIT
264
+ raise(ArgumentError, "Gas limit #{limit} is above #{Eth::Tx::BLOCK_GAS_LIMIT}") if limit > Eth::Tx::BLOCK_GAS_LIMIT
252
265
  end
253
266
  if price
254
- raise 'Gas price must be an Integer' unless price.is_a?(Integer)
255
- raise 'Gas price must be a positive Integer' unless price.positive?
267
+ raise(ArgumentError, 'Gas price must be an Integer') unless price.is_a?(Integer)
268
+ raise(ArgumentError, 'Gas price must be a positive Integer') unless price.positive?
256
269
  end
257
270
  key = Eth::Key.new(priv: priv)
258
271
  from = key.address.to_s
259
272
  tnx =
260
273
  @mutex.synchronize do
261
274
  with_jsonrpc do |jr|
262
- nonce = jr.eth_getTransactionCount(from, 'pending').to_i(16)
263
- h = {
264
- nonce:,
265
- gas_price: price,
266
- gas_limit: limit || gas_estimate(from, address, amount),
267
- to: @contract,
268
- value: 0,
269
- data: to_pay_data(address, amount),
270
- chain_id: @chain
271
- }
272
- tx = Eth::Tx.new(h)
275
+ tx = Eth::Tx.new(
276
+ {
277
+ nonce: jr.eth_getTransactionCount(from, 'pending').to_i(16),
278
+ gas_price: price,
279
+ gas_limit: limit || gas_estimate(from, address, amount),
280
+ to: @contract,
281
+ value: 0,
282
+ data: to_pay_data(address, amount),
283
+ chain_id: @chain
284
+ }
285
+ )
273
286
  tx.sign(key)
274
287
  hex = "0x#{tx.hex}"
275
288
  log_it(:debug, "Sending ERC20 transaction #{hex}")
@@ -288,34 +301,34 @@ class ERC20::Wallet
288
301
  # @param [Integer] price How much gas you pay per computation unit
289
302
  # @return [String] Transaction hash
290
303
  def eth_pay(priv, address, amount, price: gas_price)
291
- raise 'Private key can\'t be nil' unless priv
292
- raise 'Private key must be a String' unless priv.is_a?(String)
293
- raise 'Invalid format of private key' unless /^[0-9a-fA-F]{64}$/.match?(priv)
294
- raise 'Address can\'t be nil' unless address
295
- raise 'Address must be a String' unless address.is_a?(String)
296
- raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
297
- raise 'Amount can\'t be nil' unless amount
298
- raise "Amount (#{amount}) must be an Integer" unless amount.is_a?(Integer)
299
- raise "Amount (#{amount}) must be a positive Integer" unless amount.positive?
304
+ raise(ArgumentError, 'Private key can\'t be nil') unless priv
305
+ raise(ArgumentError, 'Private key must be a String') unless priv.is_a?(String)
306
+ raise(ArgumentError, 'Invalid format of private key') unless /^[0-9a-fA-F]{64}$/.match?(priv)
307
+ raise(ArgumentError, 'Address can\'t be nil') unless address
308
+ raise(ArgumentError, 'Address must be a String') unless address.is_a?(String)
309
+ raise(ArgumentError, 'Invalid format of the address') unless /^0x[0-9a-fA-F]{40}$/.match?(address)
310
+ raise(ArgumentError, 'Amount can\'t be nil') unless amount
311
+ raise(ArgumentError, "Amount (#{amount}) must be an Integer") unless amount.is_a?(Integer)
312
+ raise(ArgumentError, "Amount (#{amount}) must be a positive Integer") unless amount.positive?
300
313
  if price
301
- raise 'Gas price must be an Integer' unless price.is_a?(Integer)
302
- raise 'Gas price must be a positive Integer' unless price.positive?
314
+ raise(ArgumentError, 'Gas price must be an Integer') unless price.is_a?(Integer)
315
+ raise(ArgumentError, 'Gas price must be a positive Integer') unless price.positive?
303
316
  end
304
317
  key = Eth::Key.new(priv: priv)
305
318
  from = key.address.to_s
306
319
  tnx =
307
320
  @mutex.synchronize do
308
321
  with_jsonrpc do |jr|
309
- nonce = jr.eth_getTransactionCount(from, 'pending').to_i(16)
310
- h = {
311
- chain_id: @chain,
312
- nonce:,
313
- gas_price: price,
314
- gas_limit: 22_000,
315
- to: address,
316
- value: amount
317
- }
318
- tx = Eth::Tx.new(h)
322
+ tx = Eth::Tx.new(
323
+ {
324
+ chain_id: @chain,
325
+ nonce: jr.eth_getTransactionCount(from, 'pending').to_i(16),
326
+ gas_price: price,
327
+ gas_limit: 22_000,
328
+ to: address,
329
+ value: amount
330
+ }
331
+ )
319
332
  tx.sign(key)
320
333
  hex = "0x#{tx.hex}"
321
334
  log_it(:debug, "Sending ETH transaction #{hex}")
@@ -352,16 +365,16 @@ class ERC20::Wallet
352
365
  # @param [Integer] delay How many seconds to wait between +eth_subscribe+ calls
353
366
  # @param [Integer] subscription_id Unique ID of the subscription
354
367
  def accept(addresses, active = [], raw: false, delay: 1, subscription_id: rand(99_999), &)
355
- raise 'Addresses can\'t be nil' unless addresses
356
- raise 'Addresses must respond to .to_a()' unless addresses.respond_to?(:to_a)
357
- raise 'Active can\'t be nil' unless active
358
- raise 'Active must respond to .to_a()' unless active.respond_to?(:to_a)
359
- raise 'Active must respond to .append()' unless active.respond_to?(:append)
360
- raise 'Active must respond to .clear()' unless active.respond_to?(:clear)
361
- raise 'Delay must be an Integer' unless delay.is_a?(Integer)
362
- raise 'Delay must be a positive Integer or positive Float' unless delay.positive?
363
- raise 'Subscription ID must be an Integer' unless subscription_id.is_a?(Integer)
364
- raise 'Subscription ID must be a positive Integer' unless subscription_id.positive?
368
+ raise(ArgumentError, 'Addresses can\'t be nil') unless addresses
369
+ raise(ArgumentError, 'Addresses must respond to .to_a()') unless addresses.respond_to?(:to_a)
370
+ raise(ArgumentError, 'Active can\'t be nil') unless active
371
+ raise(ArgumentError, 'Active must respond to .to_a()') unless active.respond_to?(:to_a)
372
+ raise(ArgumentError, 'Active must respond to .append()') unless active.respond_to?(:append)
373
+ raise(ArgumentError, 'Active must respond to .clear()') unless active.respond_to?(:clear)
374
+ raise(ArgumentError, 'Delay must be an Integer') unless delay.is_a?(Integer)
375
+ raise(ArgumentError, 'Delay must be a positive Integer or positive Float') unless delay.positive?
376
+ raise(ArgumentError, 'Subscription ID must be an Integer') unless subscription_id.is_a?(Integer)
377
+ raise(ArgumentError, 'Subscription ID must be a positive Integer') unless subscription_id.positive?
365
378
  EventMachine.run do
366
379
  reaccept(addresses, active, raw:, delay:, subscription_id:, &)
367
380
  end
@@ -389,6 +402,7 @@ class ERC20::Wallet
389
402
  timer =
390
403
  EventMachine.add_periodic_timer(delay) do
391
404
  next if active.to_a.sort == addresses.to_a.sort
405
+ # rubocop:disable Style/Send
392
406
  ws.send(
393
407
  {
394
408
  jsonrpc: '2.0',
@@ -407,6 +421,7 @@ class ERC20::Wallet
407
421
  ]
408
422
  }.to_json
409
423
  )
424
+ # rubocop:enable Style/Send
410
425
  log_it(
411
426
  :debug,
412
427
  "Requested to subscribe ##{subscription_id} to #{addresses.to_a.size} addresses: " \
@@ -448,7 +463,7 @@ class ERC20::Wallet
448
463
  "from #{event[:from]} to #{event[:to]} in #{event[:txn]}"
449
464
  )
450
465
  end
451
- yield event
466
+ yield(event)
452
467
  end
453
468
  end
454
469
  end
@@ -482,13 +497,13 @@ class ERC20::Wallet
482
497
  yield
483
498
  rescue StandardError => e
484
499
  log_it(:error, Backtrace.new(e).to_s)
485
- raise e
500
+ raise(e)
486
501
  end
487
502
 
488
503
  def safe
489
504
  yield
490
505
  rescue StandardError
491
- # ignore it
506
+ nil
492
507
  end
493
508
 
494
509
  def url(http: true)
@@ -503,25 +518,30 @@ class ERC20::Wallet
503
518
  opts[:connection] =
504
519
  Faraday.new do |f|
505
520
  f.adapter(Faraday.default_adapter)
506
- f.proxy = {
507
- uri: "#{uri.scheme}://#{uri.hostname}:#{uri.port}",
508
- user: uri.user,
509
- password: uri.password
510
- }
521
+ f.proxy = { uri: "#{uri.scheme}://#{uri.hostname}:#{uri.port}", user: uri.user, password: uri.password }
511
522
  end
512
523
  end
513
- elapsed(@log, good: "Talked to #{url.host}:#{url.port}") do
514
- yield JSONRPC::Client.new(url.to_s, opts)
524
+ endpoints = [url.to_s] + @fallbacks
525
+ attempt = 0
526
+ begin
527
+ attempt += 1
528
+ u = URI.parse(endpoints[(attempt - 1) % endpoints.size])
529
+ elapsed(@log, good: "Talked to #{u.host}:#{u.port}") do
530
+ yield(JSONRPC::Client.new(u.to_s, opts))
531
+ end
532
+ rescue StandardError => e
533
+ raise if attempt >= @attempts
534
+ pause = 2**(attempt - 1)
535
+ log_it(:debug, "Attempt #{attempt}/#{@attempts} to #{u.host} failed (#{e.class}), retrying in #{pause}s")
536
+ sleep(pause)
537
+ retry
515
538
  end
516
539
  end
517
540
 
518
541
  def to_pay_data(address, amount)
519
- func = 'a9059cbb' # transfer(address,uint256)
520
542
  to_clean = address.downcase.sub(/^0x/, '')
521
- to_padded = ('0' * (64 - to_clean.size)) + to_clean
522
543
  amt_hex = amount.to_s(16)
523
- amt_padded = ('0' * (64 - amt_hex.size)) + amt_hex
524
- "0x#{func}#{to_padded}#{amt_padded}"
544
+ "0xa9059cbb#{('0' * (64 - to_clean.size)) + to_clean}#{('0' * (64 - amt_hex.size)) + amt_hex}"
525
545
  end
526
546
 
527
547
  def log_it(method, msg)
data/lib/erc20.rb CHANGED
@@ -4,5 +4,5 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require_relative 'erc20/erc20'
7
- require_relative 'erc20/wallet'
8
7
  require_relative 'erc20/fake_wallet'
8
+ require_relative 'erc20/wallet'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erc20
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko