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 +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +57 -45
- data/README.md +7 -4
- data/Rakefile +2 -2
- data/bin/erc20 +5 -0
- data/erc20.gemspec +8 -8
- data/features/step_definitions/steps.rb +5 -5
- data/features/support/env.rb +1 -1
- data/lib/erc20/erc20.rb +1 -2
- data/lib/erc20/fake_wallet.rb +4 -9
- data/lib/erc20/wallet.rb +153 -133
- data/lib/erc20.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9719ca99febb05bab13326b5a1de126b73aa5d923ad7f091ef3c4493a6d4030f
|
|
4
|
+
data.tar.gz: b75de1bc31c3e1e1ac2b3167d92ac9d924841048035e190ec961dfebde81d9c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
16
|
+
addressable (2.9.0)
|
|
17
17
|
public_suffix (>= 2.0.2, < 8.0)
|
|
18
|
-
ansi (1.
|
|
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.
|
|
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.
|
|
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.
|
|
45
|
+
cucumber-cucumber-expressions (19.0.1)
|
|
46
46
|
bigdecimal
|
|
47
|
-
cucumber-gherkin (
|
|
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.
|
|
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.
|
|
56
|
+
donce (0.7.0)
|
|
57
57
|
backtrace (~> 0.3)
|
|
58
58
|
os (~> 1.1)
|
|
59
59
|
qbash (~> 0.3)
|
|
60
|
-
|
|
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.
|
|
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.
|
|
73
|
+
ethon (0.18.0)
|
|
73
74
|
ffi (>= 1.15.0)
|
|
75
|
+
logger
|
|
74
76
|
eventmachine (1.2.7)
|
|
75
|
-
faraday (2.14.
|
|
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.
|
|
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.
|
|
85
|
-
ffi (1.17.
|
|
86
|
-
ffi (1.17.
|
|
87
|
-
ffi (1.17.
|
|
88
|
-
ffi-compiler (1.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
110
|
+
minitest (6.0.6)
|
|
111
|
+
drb (~> 2.0)
|
|
109
112
|
prism (~> 1.5)
|
|
110
|
-
minitest-reporters (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.
|
|
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.
|
|
124
|
+
openssl (3.3.3)
|
|
122
125
|
os (1.1.4)
|
|
123
|
-
parallel (1.
|
|
124
|
-
parser (3.3.
|
|
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.
|
|
131
|
+
prism (1.9.0)
|
|
129
132
|
psych (5.3.1)
|
|
130
133
|
date
|
|
131
134
|
stringio
|
|
132
|
-
public_suffix (7.0.
|
|
133
|
-
qbash (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.
|
|
141
|
-
random-port (0.
|
|
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.
|
|
150
|
+
rdoc (7.2.0)
|
|
148
151
|
erb
|
|
149
152
|
psych (>= 4.0.0)
|
|
150
153
|
tsort
|
|
151
|
-
regexp_parser (2.
|
|
154
|
+
regexp_parser (2.12.0)
|
|
152
155
|
rexml (3.4.4)
|
|
153
|
-
rubocop (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 (
|
|
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.
|
|
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.
|
|
167
|
+
rubocop-ast (1.49.1)
|
|
165
168
|
parser (>= 3.3.7.2)
|
|
166
169
|
prism (~> 1.7)
|
|
167
|
-
rubocop-
|
|
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.
|
|
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.
|
|
203
|
-
ethon (>= 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.
|
|
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
|
-
|
|
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
|
|
79
|
-
|
|
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?
|
|
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
|
|
29
|
-
s.add_dependency
|
|
30
|
-
s.add_dependency
|
|
31
|
-
s.add_dependency
|
|
32
|
-
s.add_dependency
|
|
33
|
-
s.add_dependency
|
|
34
|
-
s.add_dependency
|
|
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|
|
data/features/support/env.rb
CHANGED
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
|
-
|
|
28
|
-
VERSION = '0.2.9' unless defined?(VERSION)
|
|
27
|
+
VERSION = '0.3.0' unless defined?(VERSION)
|
|
29
28
|
end
|
data/lib/erc20/fake_wallet.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
raise '
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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] ==
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
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
|
|
500
|
+
raise(e)
|
|
486
501
|
end
|
|
487
502
|
|
|
488
503
|
def safe
|
|
489
504
|
yield
|
|
490
505
|
rescue StandardError
|
|
491
|
-
|
|
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
|
-
|
|
514
|
-
|
|
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
|
-
|
|
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