erc20 0.0.21 → 0.1.1
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/.github/workflows/shellcheck.yml +0 -2
- data/.gitignore +6 -5
- data/.rultor.yml +1 -1
- data/Gemfile +10 -10
- data/Gemfile.lock +60 -137
- data/README.md +31 -11
- data/REUSE.toml +23 -1
- data/Rakefile +9 -1
- data/bin/erc20 +152 -0
- data/erc20.gemspec +4 -2
- data/features/cli.feature +30 -0
- data/features/gem_package.feature +27 -0
- data/features/step_definitions/steps.rb +72 -0
- data/features/support/env.rb +8 -0
- data/lib/erc20/erc20.rb +3 -3
- data/lib/erc20/fake_wallet.rb +6 -3
- data/lib/erc20/wallet.rb +34 -17
- data/test/erc20/test_fake_wallet.rb +1 -1
- data/test/erc20/test_wallet.rb +12 -11
- data/test/test__helper.rb +1 -1
- metadata +26 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fbce7bc57948f9c8acfeec7c0af2cf67e70e2447e064040485e6868d466828dc
|
4
|
+
data.tar.gz: cc521c55db79dca85e58235c886172a7ca81cf0f59ee61f5c134cc888eb3db4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c712231ba46caabca78b45e9be366c120e2f5275bc734c874f69c7306d1f961c7b7c3a5b8ae58341cb4d48776a91c5a8bc8ec2360dfcbc294d46ae329d9bf8a4
|
7
|
+
data.tar.gz: 7a074720cfec0773a6e4e0cc70fe07fde201ae9897cb395a2b91a84429683354b822bef0be10650290b554ffe59b150034a1ea320eed34a396d574267954c130
|
data/.gitignore
CHANGED
data/.rultor.yml
CHANGED
data/Gemfile
CHANGED
@@ -7,23 +7,23 @@ source 'https://rubygems.org'
|
|
7
7
|
gemspec
|
8
8
|
|
9
9
|
gem 'backtrace', '>0', require: false
|
10
|
+
gem 'cucumber', '~>9.2', require: false
|
10
11
|
gem 'donce', '>0', require: false
|
11
12
|
gem 'faraday', '>0', require: false
|
12
13
|
gem 'loog', '>0', require: false
|
13
|
-
gem 'minitest', '5.25
|
14
|
-
gem 'minitest-reporters', '1.7
|
15
|
-
gem 'minitest-retry', '0.2
|
14
|
+
gem 'minitest', '~>5.25', require: false
|
15
|
+
gem 'minitest-reporters', '~>1.7', require: false
|
16
|
+
gem 'minitest-retry', '~>0.2', require: false
|
16
17
|
gem 'qbash', '>0', require: false
|
17
|
-
gem 'rake', '13.2
|
18
|
+
gem 'rake', '~>13.2', require: false
|
18
19
|
gem 'random-port', '>0', require: false
|
19
|
-
gem '
|
20
|
-
gem 'rubocop', '1.72.2', require: false
|
20
|
+
gem 'rubocop', '~>1.75', require: false
|
21
21
|
gem 'rubocop-minitest', '>0', require: false
|
22
22
|
gem 'rubocop-performance', '>0', require: false
|
23
23
|
gem 'rubocop-rake', '>0', require: false
|
24
24
|
gem 'rubocop-rspec', '>0', require: false
|
25
|
-
gem 'simplecov', '0.22
|
26
|
-
gem 'simplecov-cobertura', '2.1
|
27
|
-
gem 'threads', '0.4
|
25
|
+
gem 'simplecov', '~>0.22', require: false
|
26
|
+
gem 'simplecov-cobertura', '~>2.1', require: false
|
27
|
+
gem 'threads', '~>0.4', require: false
|
28
28
|
gem 'typhoeus', '>0', require: false
|
29
|
-
gem 'yard', '0.9
|
29
|
+
gem 'yard', '~>0.9', require: false
|
data/Gemfile.lock
CHANGED
@@ -7,61 +7,52 @@ PATH
|
|
7
7
|
json (>= 2.10.1)
|
8
8
|
jsonrpc-client (>= 0.1.4)
|
9
9
|
loog (> 0)
|
10
|
+
slop (> 4)
|
10
11
|
|
11
12
|
GEM
|
12
13
|
remote: https://rubygems.org/
|
13
14
|
specs:
|
14
|
-
actionpack (8.0.1)
|
15
|
-
actionview (= 8.0.1)
|
16
|
-
activesupport (= 8.0.1)
|
17
|
-
nokogiri (>= 1.8.5)
|
18
|
-
rack (>= 2.2.4)
|
19
|
-
rack-session (>= 1.0.1)
|
20
|
-
rack-test (>= 0.6.3)
|
21
|
-
rails-dom-testing (~> 2.2)
|
22
|
-
rails-html-sanitizer (~> 1.6)
|
23
|
-
useragent (~> 0.16)
|
24
|
-
actionview (8.0.1)
|
25
|
-
activesupport (= 8.0.1)
|
26
|
-
builder (~> 3.1)
|
27
|
-
erubi (~> 1.11)
|
28
|
-
rails-dom-testing (~> 2.2)
|
29
|
-
rails-html-sanitizer (~> 1.6)
|
30
|
-
activesupport (8.0.1)
|
31
|
-
base64
|
32
|
-
benchmark (>= 0.3)
|
33
|
-
bigdecimal
|
34
|
-
concurrent-ruby (~> 1.0, >= 1.3.1)
|
35
|
-
connection_pool (>= 2.2.5)
|
36
|
-
drb
|
37
|
-
i18n (>= 1.6, < 2)
|
38
|
-
logger (>= 1.4.2)
|
39
|
-
minitest (>= 5.1)
|
40
|
-
securerandom (>= 0.3)
|
41
|
-
tzinfo (~> 2.0, >= 2.0.5)
|
42
|
-
uri (>= 0.13.1)
|
43
15
|
ansi (1.5.0)
|
44
|
-
ast (2.4.
|
16
|
+
ast (2.4.3)
|
45
17
|
backtrace (0.4.0)
|
46
18
|
base64 (0.2.0)
|
47
|
-
benchmark (0.4.0)
|
48
19
|
bigdecimal (3.1.9)
|
49
20
|
builder (3.3.0)
|
50
21
|
concurrent-ruby (1.3.5)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
22
|
+
cucumber (9.2.1)
|
23
|
+
builder (~> 3.2)
|
24
|
+
cucumber-ci-environment (> 9, < 11)
|
25
|
+
cucumber-core (> 13, < 14)
|
26
|
+
cucumber-cucumber-expressions (~> 17.0)
|
27
|
+
cucumber-gherkin (> 24, < 28)
|
28
|
+
cucumber-html-formatter (> 20.3, < 22)
|
29
|
+
cucumber-messages (> 19, < 25)
|
30
|
+
diff-lcs (~> 1.5)
|
31
|
+
mini_mime (~> 1.1)
|
32
|
+
multi_test (~> 1.1)
|
33
|
+
sys-uname (~> 1.2)
|
34
|
+
cucumber-ci-environment (10.0.1)
|
35
|
+
cucumber-core (13.0.3)
|
36
|
+
cucumber-gherkin (>= 27, < 28)
|
37
|
+
cucumber-messages (>= 20, < 23)
|
38
|
+
cucumber-tag-expressions (> 5, < 7)
|
39
|
+
cucumber-cucumber-expressions (17.1.0)
|
40
|
+
bigdecimal
|
41
|
+
cucumber-gherkin (27.0.0)
|
42
|
+
cucumber-messages (>= 19.1.4, < 23)
|
43
|
+
cucumber-html-formatter (21.9.0)
|
44
|
+
cucumber-messages (> 19, < 28)
|
45
|
+
cucumber-messages (22.0.0)
|
46
|
+
cucumber-tag-expressions (6.1.2)
|
47
|
+
diff-lcs (1.6.1)
|
55
48
|
docile (1.4.1)
|
56
49
|
donce (0.1.0)
|
57
50
|
backtrace (> 0)
|
58
51
|
os (> 0)
|
59
52
|
qbash (> 0)
|
60
|
-
drb (2.2.1)
|
61
53
|
elapsed (0.0.1)
|
62
54
|
loog (> 0)
|
63
55
|
tago (> 0)
|
64
|
-
erubi (1.13.1)
|
65
56
|
eth (0.5.13)
|
66
57
|
forwardable (~> 1.3)
|
67
58
|
keccak (~> 1.3)
|
@@ -89,14 +80,7 @@ GEM
|
|
89
80
|
ffi (>= 1.15.5)
|
90
81
|
rake
|
91
82
|
forwardable (1.3.3)
|
92
|
-
|
93
|
-
concurrent-ruby (~> 1.0)
|
94
|
-
io-console (0.8.0)
|
95
|
-
irb (1.15.1)
|
96
|
-
pp (>= 0.6.0)
|
97
|
-
rdoc (>= 4.0.0)
|
98
|
-
reline (>= 0.4.2)
|
99
|
-
json (2.10.1)
|
83
|
+
json (2.10.2)
|
100
84
|
jsonrpc-client (0.1.4)
|
101
85
|
faraday
|
102
86
|
multi_json (>= 1.1.0)
|
@@ -104,13 +88,11 @@ GEM
|
|
104
88
|
konstructor (1.0.2)
|
105
89
|
language_server-protocol (3.17.0.4)
|
106
90
|
lint_roller (1.1.0)
|
107
|
-
logger (1.
|
108
|
-
loofah (2.24.0)
|
109
|
-
crass (~> 1.0.2)
|
110
|
-
nokogiri (>= 1.12.0)
|
91
|
+
logger (1.7.0)
|
111
92
|
loog (0.6.0)
|
93
|
+
mini_mime (1.1.5)
|
112
94
|
mini_portile2 (2.8.8)
|
113
|
-
minitest (5.25.
|
95
|
+
minitest (5.25.5)
|
114
96
|
minitest-reporters (1.7.1)
|
115
97
|
ansi
|
116
98
|
builder
|
@@ -119,58 +101,23 @@ GEM
|
|
119
101
|
minitest-retry (0.2.5)
|
120
102
|
minitest (>= 5.0)
|
121
103
|
multi_json (1.15.0)
|
104
|
+
multi_test (1.1.0)
|
122
105
|
net-http (0.6.0)
|
123
106
|
uri
|
124
|
-
nokogiri (1.18.3-arm64-darwin)
|
125
|
-
racc (~> 1.4)
|
126
|
-
nokogiri (1.18.3-x64-mingw-ucrt)
|
127
|
-
racc (~> 1.4)
|
128
|
-
nokogiri (1.18.3-x86_64-darwin)
|
129
|
-
racc (~> 1.4)
|
130
|
-
nokogiri (1.18.3-x86_64-linux-gnu)
|
131
|
-
racc (~> 1.4)
|
132
107
|
openssl (3.3.0)
|
133
108
|
os (1.1.4)
|
134
109
|
parallel (1.26.3)
|
135
|
-
parser (3.3.7.
|
110
|
+
parser (3.3.7.4)
|
136
111
|
ast (~> 2.4.1)
|
137
112
|
racc
|
138
|
-
pkg-config (1.
|
139
|
-
|
140
|
-
prettyprint
|
141
|
-
prettyprint (0.2.0)
|
142
|
-
psych (5.2.3)
|
143
|
-
date
|
144
|
-
stringio
|
113
|
+
pkg-config (1.6.0)
|
114
|
+
prism (1.4.0)
|
145
115
|
qbash (0.4.0)
|
146
116
|
backtrace (> 0)
|
147
117
|
elapsed (> 0)
|
148
118
|
loog (> 0)
|
149
119
|
tago (> 0)
|
150
120
|
racc (1.8.1)
|
151
|
-
rack (3.1.10)
|
152
|
-
rack-session (2.1.0)
|
153
|
-
base64 (>= 0.1.0)
|
154
|
-
rack (>= 3.0.0)
|
155
|
-
rack-test (2.2.0)
|
156
|
-
rack (>= 1.3)
|
157
|
-
rackup (2.2.1)
|
158
|
-
rack (>= 3)
|
159
|
-
rails-dom-testing (2.2.0)
|
160
|
-
activesupport (>= 5.0.0)
|
161
|
-
minitest
|
162
|
-
nokogiri (>= 1.6)
|
163
|
-
rails-html-sanitizer (1.6.2)
|
164
|
-
loofah (~> 2.21)
|
165
|
-
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
166
|
-
railties (8.0.1)
|
167
|
-
actionpack (= 8.0.1)
|
168
|
-
activesupport (= 8.0.1)
|
169
|
-
irb (~> 1.13)
|
170
|
-
rackup (>= 1.0.0)
|
171
|
-
rake (>= 12.2)
|
172
|
-
thor (~> 1.0, >= 1.2.2)
|
173
|
-
zeitwerk (~> 2.6)
|
174
121
|
rainbow (3.1.1)
|
175
122
|
rake (13.2.1)
|
176
123
|
random-port (0.7.5)
|
@@ -179,30 +126,9 @@ GEM
|
|
179
126
|
mini_portile2 (~> 2.8)
|
180
127
|
pkg-config (~> 1.5)
|
181
128
|
rubyzip (~> 2.3)
|
182
|
-
rdoc (6.12.0)
|
183
|
-
psych (>= 4.0.0)
|
184
129
|
regexp_parser (2.10.0)
|
185
|
-
reline (0.6.0)
|
186
|
-
io-console (~> 0.5)
|
187
130
|
rexml (3.4.1)
|
188
|
-
|
189
|
-
rspec-support (~> 3.13.0)
|
190
|
-
rspec-expectations (3.13.3)
|
191
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
192
|
-
rspec-support (~> 3.13.0)
|
193
|
-
rspec-mocks (3.13.2)
|
194
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
195
|
-
rspec-support (~> 3.13.0)
|
196
|
-
rspec-rails (7.1.1)
|
197
|
-
actionpack (>= 7.0)
|
198
|
-
activesupport (>= 7.0)
|
199
|
-
railties (>= 7.0)
|
200
|
-
rspec-core (~> 3.13)
|
201
|
-
rspec-expectations (~> 3.13)
|
202
|
-
rspec-mocks (~> 3.13)
|
203
|
-
rspec-support (~> 3.13)
|
204
|
-
rspec-support (3.13.2)
|
205
|
-
rubocop (1.72.2)
|
131
|
+
rubocop (1.75.2)
|
206
132
|
json (~> 2.3)
|
207
133
|
language_server-protocol (~> 3.17.0.2)
|
208
134
|
lint_roller (~> 1.1.0)
|
@@ -210,18 +136,19 @@ GEM
|
|
210
136
|
parser (>= 3.3.0.2)
|
211
137
|
rainbow (>= 2.2.2, < 4.0)
|
212
138
|
regexp_parser (>= 2.9.3, < 3.0)
|
213
|
-
rubocop-ast (>= 1.
|
139
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
214
140
|
ruby-progressbar (~> 1.7)
|
215
141
|
unicode-display_width (>= 2.4.0, < 4.0)
|
216
|
-
rubocop-ast (1.
|
217
|
-
parser (>= 3.3.
|
218
|
-
|
142
|
+
rubocop-ast (1.44.0)
|
143
|
+
parser (>= 3.3.7.2)
|
144
|
+
prism (~> 1.4)
|
145
|
+
rubocop-minitest (0.38.0)
|
219
146
|
lint_roller (~> 1.1)
|
220
|
-
rubocop (>= 1.
|
147
|
+
rubocop (>= 1.75.0, < 2.0)
|
221
148
|
rubocop-ast (>= 1.38.0, < 2.0)
|
222
|
-
rubocop-performance (1.
|
149
|
+
rubocop-performance (1.25.0)
|
223
150
|
lint_roller (~> 1.1)
|
224
|
-
rubocop (>= 1.
|
151
|
+
rubocop (>= 1.75.0, < 2.0)
|
225
152
|
rubocop-ast (>= 1.38.0, < 2.0)
|
226
153
|
rubocop-rake (0.7.1)
|
227
154
|
lint_roller (~> 1.1)
|
@@ -234,7 +161,6 @@ GEM
|
|
234
161
|
scrypt (3.0.8)
|
235
162
|
ffi-compiler (>= 1.0, < 2.0)
|
236
163
|
rake (>= 9, < 14)
|
237
|
-
securerandom (0.4.1)
|
238
164
|
simplecov (0.22.0)
|
239
165
|
docile (~> 1.1)
|
240
166
|
simplecov-html (~> 0.11)
|
@@ -244,27 +170,24 @@ GEM
|
|
244
170
|
simplecov (~> 0.19)
|
245
171
|
simplecov-html (0.13.1)
|
246
172
|
simplecov_json_formatter (0.1.4)
|
247
|
-
|
248
|
-
|
249
|
-
|
173
|
+
slop (4.10.1)
|
174
|
+
sys-uname (1.3.1)
|
175
|
+
ffi (~> 1.1)
|
176
|
+
tago (0.1.0)
|
250
177
|
threads (0.4.1)
|
251
178
|
backtrace (~> 0)
|
252
179
|
concurrent-ruby (~> 1.0)
|
253
180
|
typhoeus (1.4.1)
|
254
181
|
ethon (>= 0.9.0)
|
255
|
-
tzinfo (2.0.6)
|
256
|
-
concurrent-ruby (~> 1.0)
|
257
182
|
unicode-display_width (3.1.4)
|
258
183
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
259
184
|
unicode-emoji (4.0.4)
|
260
|
-
uri (1.0.
|
261
|
-
useragent (0.16.11)
|
185
|
+
uri (1.0.3)
|
262
186
|
websocket-driver (0.7.7)
|
263
187
|
base64
|
264
188
|
websocket-extensions (>= 0.1.0)
|
265
189
|
websocket-extensions (0.1.5)
|
266
190
|
yard (0.9.37)
|
267
|
-
zeitwerk (2.7.2)
|
268
191
|
|
269
192
|
PLATFORMS
|
270
193
|
arm64-darwin-22
|
@@ -277,27 +200,27 @@ PLATFORMS
|
|
277
200
|
|
278
201
|
DEPENDENCIES
|
279
202
|
backtrace (> 0)
|
203
|
+
cucumber (~> 9.2)
|
280
204
|
donce (> 0)
|
281
205
|
erc20!
|
282
206
|
faraday (> 0)
|
283
207
|
loog (> 0)
|
284
|
-
minitest (
|
285
|
-
minitest-reporters (
|
286
|
-
minitest-retry (
|
208
|
+
minitest (~> 5.25)
|
209
|
+
minitest-reporters (~> 1.7)
|
210
|
+
minitest-retry (~> 0.2)
|
287
211
|
qbash (> 0)
|
288
|
-
rake (
|
212
|
+
rake (~> 13.2)
|
289
213
|
random-port (> 0)
|
290
|
-
|
291
|
-
rubocop (= 1.72.2)
|
214
|
+
rubocop (~> 1.75)
|
292
215
|
rubocop-minitest (> 0)
|
293
216
|
rubocop-performance (> 0)
|
294
217
|
rubocop-rake (> 0)
|
295
218
|
rubocop-rspec (> 0)
|
296
|
-
simplecov (
|
297
|
-
simplecov-cobertura (
|
298
|
-
threads (
|
219
|
+
simplecov (~> 0.22)
|
220
|
+
simplecov-cobertura (~> 2.1)
|
221
|
+
threads (~> 0.4)
|
299
222
|
typhoeus (> 0)
|
300
|
-
yard (
|
223
|
+
yard (~> 0.9)
|
301
224
|
|
302
225
|
BUNDLED WITH
|
303
226
|
2.5.16
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Ethereum ERC20 Manipulations in Ruby
|
2
2
|
|
3
3
|
[](http://www.rultor.com/p/yegor256/erc20)
|
4
4
|
[](https://www.jetbrains.com/ruby/)
|
@@ -12,9 +12,9 @@
|
|
12
12
|
[](https://github.com/yegor256/erc20/blob/master/LICENSE.txt)
|
13
13
|
|
14
14
|
This small Ruby [gem](https://rubygems.org/gems/erc20)
|
15
|
-
makes manipulations with [
|
16
|
-
as simple as
|
17
|
-
[JSON-RPC] and [WebSockets]
|
15
|
+
makes manipulations with [Ethereum] [ERC20] tokens
|
16
|
+
as simple as possible, when you have a provider of
|
17
|
+
[JSON-RPC] and [WebSockets] Ethereum APIs, such as
|
18
18
|
[Infura], [GetBlock], or [Alchemy]:
|
19
19
|
|
20
20
|
```ruby
|
@@ -44,10 +44,10 @@ w.accept(addresses) do |event|
|
|
44
44
|
end
|
45
45
|
```
|
46
46
|
|
47
|
-
|
47
|
+
You can also check ETH balance and send ETH transactions:
|
48
48
|
|
49
49
|
```ruby
|
50
|
-
# Check how many ETHs are
|
50
|
+
# Check how many ETHs are on the given address:
|
51
51
|
eth = w.eth_balance(address)
|
52
52
|
|
53
53
|
# Send a few ETHs to someone and get transaction hash:
|
@@ -91,13 +91,32 @@ w = ERC20::Wallet.new(
|
|
91
91
|
You can use [squid-proxy] [Docker] image to set up your own [HTTP proxy] server.
|
92
92
|
|
93
93
|
Of course, this library works with [Polygon], [Optimism],
|
94
|
-
and other
|
94
|
+
and other EVM compatible blockchains.
|
95
|
+
|
96
|
+
## How to use in command line
|
97
|
+
|
98
|
+
This gem also provides a command line tool for sending ETH and ERC20 payments
|
99
|
+
and checking balances.
|
100
|
+
|
101
|
+
First, you install it, via [gem]:
|
102
|
+
|
103
|
+
```bash
|
104
|
+
gem install erc20
|
105
|
+
```
|
106
|
+
|
107
|
+
Then, run it:
|
108
|
+
|
109
|
+
```bash
|
110
|
+
erc20 --help
|
111
|
+
```
|
112
|
+
|
113
|
+
Usage should be straightforward. If you have questions, please submit an issue.
|
95
114
|
|
96
115
|
## How to use in tests
|
97
116
|
|
98
|
-
You can use `ERC20::FakeWallet` class that behaves exactly like
|
117
|
+
You can use the `ERC20::FakeWallet` class that behaves exactly like
|
99
118
|
`ERC20::Wallet`, but doesn't make any network connections to the provider.
|
100
|
-
|
119
|
+
Additionally, it records all requests sent to it:
|
101
120
|
|
102
121
|
```ruby
|
103
122
|
require 'erc20'
|
@@ -110,7 +129,7 @@ assert w.history.include?({ method: :pay, priv:, address:, amount: 42_000 })
|
|
110
129
|
|
111
130
|
Read
|
112
131
|
[these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).
|
113
|
-
Make sure
|
132
|
+
Make sure your build is green before you contribute
|
114
133
|
your pull request. You will need to have
|
115
134
|
[Ruby](https://www.ruby-lang.org/en/) 3.2+ and
|
116
135
|
[Bundler](https://bundler.io/) installed. Then:
|
@@ -122,7 +141,8 @@ bundle exec rake
|
|
122
141
|
|
123
142
|
If it's clean and you don't see any error messages, submit your pull request.
|
124
143
|
|
125
|
-
[
|
144
|
+
[gem]: https://github.com/rubygems/rubygems
|
145
|
+
[Ethereum]: https://en.wikipedia.org/wiki/Ethereum
|
126
146
|
[ERC20]: https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
|
127
147
|
[JSON-RPC]: https://ethereum.org/en/developers/docs/apis/json-rpc/
|
128
148
|
[Websockets]: https://ethereum.org/en/developers/tutorials/using-websockets/
|
data/REUSE.toml
CHANGED
@@ -4,15 +4,37 @@
|
|
4
4
|
version = 1
|
5
5
|
[[annotations]]
|
6
6
|
path = [
|
7
|
+
"**.json",
|
8
|
+
"**.md",
|
9
|
+
"**.png",
|
10
|
+
"**.txt",
|
11
|
+
"**/*.csv",
|
12
|
+
"**/*.jpg",
|
13
|
+
"**/*.json",
|
14
|
+
"**/*.md",
|
15
|
+
"**/*.pdf",
|
16
|
+
"**/*.png",
|
17
|
+
"**/*.svg",
|
18
|
+
"**/*.txt",
|
19
|
+
"**/*.vm",
|
20
|
+
"**/.DS_Store",
|
21
|
+
"**/.gitignore",
|
22
|
+
"**/.gitleaksignore",
|
23
|
+
"**/.pdd",
|
24
|
+
"**/CNAME",
|
25
|
+
"**/Cargo.toml",
|
26
|
+
"**/Gemfile.lock",
|
27
|
+
".DS_Store",
|
7
28
|
".gitattributes",
|
8
29
|
".gitignore",
|
9
30
|
".gitleaksignore",
|
10
31
|
".pdd",
|
32
|
+
"Cargo.toml",
|
11
33
|
"Gemfile.lock",
|
12
34
|
"README.md",
|
13
35
|
"hardhat/.gitignore",
|
14
36
|
"hardhat/package.json",
|
15
|
-
"renovate.json"
|
37
|
+
"renovate.json",
|
16
38
|
]
|
17
39
|
precedence = "override"
|
18
40
|
SPDX-FileCopyrightText = "Copyright (c) 2025 Yegor Bugayenko"
|
data/Rakefile
CHANGED
@@ -17,7 +17,7 @@ def version
|
|
17
17
|
Gem::Specification.load(Dir['*.gemspec'].first).version
|
18
18
|
end
|
19
19
|
|
20
|
-
task default: %i[clean test rubocop yard]
|
20
|
+
task default: %i[clean test features rubocop yard]
|
21
21
|
|
22
22
|
require 'rake/testtask'
|
23
23
|
desc 'Run all unit tests'
|
@@ -42,6 +42,14 @@ RuboCop::RakeTask.new(:rubocop) do |task|
|
|
42
42
|
task.requires << 'rubocop-rspec'
|
43
43
|
end
|
44
44
|
|
45
|
+
require 'cucumber/rake/task'
|
46
|
+
Cucumber::Rake::Task.new(:features) do
|
47
|
+
Rake::Cleaner.cleanup_files(['coverage'])
|
48
|
+
end
|
49
|
+
Cucumber::Rake::Task.new(:'features:html') do |t|
|
50
|
+
t.profile = 'html_report'
|
51
|
+
end
|
52
|
+
|
45
53
|
desc 'Run benchmark script'
|
46
54
|
task :benchmark do
|
47
55
|
ruby 'benchmarks/simple.rb'
|
data/bin/erc20
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
5
|
+
# SPDX-License-Identifier: MIT
|
6
|
+
|
7
|
+
$stdout.sync = true
|
8
|
+
|
9
|
+
require 'backtrace'
|
10
|
+
require 'loog'
|
11
|
+
require 'slop'
|
12
|
+
require_relative '../lib/erc20'
|
13
|
+
require_relative '../lib/erc20/erc20'
|
14
|
+
require_relative '../lib/erc20/wallet'
|
15
|
+
|
16
|
+
begin
|
17
|
+
begin
|
18
|
+
opts = Slop.parse(ARGV, strict: true, help: true) do |o|
|
19
|
+
o.banner = "Usage (#{ERC20::VERSION}): erc20 [options] command [args]
|
20
|
+
Commands are:
|
21
|
+
key: Generate a new Ethereum private key (64 symbols)
|
22
|
+
address: Turn private key into a public address (44 symbols)
|
23
|
+
price: Get current price of one gas unit, in gwei
|
24
|
+
pay: Send ERC20 payment
|
25
|
+
eth_pay: Send ETH payment
|
26
|
+
balance: Get ERC20 balance
|
27
|
+
eth_balance: Get ETH balance
|
28
|
+
Options are:"
|
29
|
+
o.string(
|
30
|
+
'--contract',
|
31
|
+
'Public address of ERC20 contract',
|
32
|
+
default: ERC20::Wallet::USDT
|
33
|
+
)
|
34
|
+
o.integer(
|
35
|
+
'--chain',
|
36
|
+
'Ethereum chain ID',
|
37
|
+
default: 1
|
38
|
+
)
|
39
|
+
o.string(
|
40
|
+
'--host',
|
41
|
+
'Host name of the provider',
|
42
|
+
default: 'eth.llamarpc.com'
|
43
|
+
)
|
44
|
+
o.string(
|
45
|
+
'--port',
|
46
|
+
'TCP port of the provider',
|
47
|
+
default: 443
|
48
|
+
)
|
49
|
+
o.string(
|
50
|
+
'--http_path',
|
51
|
+
'URL path for the HTTP RPC entry point of the provider',
|
52
|
+
default: '/'
|
53
|
+
)
|
54
|
+
o.string(
|
55
|
+
'--ws_path',
|
56
|
+
'URL path for the Websockets entry point of the provider',
|
57
|
+
default: '/'
|
58
|
+
)
|
59
|
+
o.string(
|
60
|
+
'--proxy',
|
61
|
+
'HTTP/S proxy for all requests, e.g. "localhost:3128"'
|
62
|
+
)
|
63
|
+
o.integer(
|
64
|
+
'--attempts',
|
65
|
+
'How many times should we try before failing',
|
66
|
+
default: 1
|
67
|
+
)
|
68
|
+
o.bool(
|
69
|
+
'--dry',
|
70
|
+
'Don\'t send a real payment, run in a read-only mode'
|
71
|
+
)
|
72
|
+
o.bool('--help', 'Read this: https://github.com/yegor256/erc20') do
|
73
|
+
puts o
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
o.bool('--verbose', 'Print all possible debug messages')
|
77
|
+
end
|
78
|
+
rescue Slop::Error => e
|
79
|
+
raise e.message
|
80
|
+
end
|
81
|
+
raise 'Try --help' if opts.arguments.empty?
|
82
|
+
log = opts[:verbose] ? Loog::VERBOSE : Loog::REGULAR
|
83
|
+
wallet = ERC20::Wallet.new(
|
84
|
+
contract: opts[:contract],
|
85
|
+
host: opts[:host], port: opts[:port],
|
86
|
+
http_path: opts[:http_path], ws_path: opts[:ws_path],
|
87
|
+
log:
|
88
|
+
)
|
89
|
+
case opts.arguments[0]
|
90
|
+
when 'key'
|
91
|
+
puts Eth::Key.new.private_hex
|
92
|
+
when 'address'
|
93
|
+
puts Eth::Key.new(priv: opts.arguments[1]).address.to_s
|
94
|
+
when 'price'
|
95
|
+
puts wallet.gas_price
|
96
|
+
when 'balance'
|
97
|
+
address = opts.arguments[1]
|
98
|
+
raise 'Address is required' if address.nil?
|
99
|
+
log.debug("Checking ERC20 balance of #{address}")
|
100
|
+
tokens = wallet.balance(address)
|
101
|
+
log.debug("The balance is #{tokens} ERC20 tokens (#{tokens.to_f / 1_000_000} USDT)")
|
102
|
+
puts tokens
|
103
|
+
when 'eth_balance'
|
104
|
+
address = opts.arguments[1]
|
105
|
+
raise 'Address is required' if address.nil?
|
106
|
+
log.debug("Checking ETH balance of #{address}")
|
107
|
+
wei = wallet.eth_balance(address)
|
108
|
+
log.debug("The balance of #{address} is #{wei} wei (#{format('%0.4f', wei.to_f / 1_000_000_000_000_000_000)} ETH)")
|
109
|
+
puts wei
|
110
|
+
when 'pay'
|
111
|
+
pkey = opts.arguments[1]
|
112
|
+
raise 'Private key is required' if pkey.nil?
|
113
|
+
priv = Eth::Key.new(priv: pkey)
|
114
|
+
log.debug("Sending ERC20 tokens from #{priv.address.to_s}")
|
115
|
+
address = opts.arguments[2]
|
116
|
+
raise 'Address is required' if address.nil?
|
117
|
+
log.debug("Sending ERC20 tokens to #{address}")
|
118
|
+
amount = opts.arguments[3]
|
119
|
+
raise 'Amount argument is required' if amount.nil?
|
120
|
+
raise 'Amount is not valid' unless /^[0-9]+(usdt)?$/.match?(amount)
|
121
|
+
amount = amount.to_i if /^[0-9]+$/.match?(amount)
|
122
|
+
amount = (amount.gsub(/usdt^/, '').to_f * 1_000_000).to_i if /^[0-9]usdt+$/.match?(amount)
|
123
|
+
log.debug("Sending #{amount} ERC20 tokens")
|
124
|
+
puts wallet.pay(priv.private_hex, address, amount)
|
125
|
+
when 'eth_pay'
|
126
|
+
pkey = opts.arguments[1]
|
127
|
+
raise 'Private key is required' if pkey.nil?
|
128
|
+
priv = Eth::Key.new(priv: pkey)
|
129
|
+
log.debug("Sending ETH from #{priv.address.to_s}")
|
130
|
+
address = opts.arguments[2]
|
131
|
+
raise 'Address is required' if address.nil?
|
132
|
+
log.debug("Sending ETH to #{address}")
|
133
|
+
amount = opts.arguments[3]
|
134
|
+
raise 'Amount argument is required' if amount.nil?
|
135
|
+
raise "Amount #{amount.inspect} is not valid" unless /^[0-9]+(\.[0-9]+)?(eth|wei|gwei)?$/.match?(amount)
|
136
|
+
amount = amount.to_i if /^[0-9]+$/.match?(amount)
|
137
|
+
amount = amount.gsub(/wei^/, '').to_i if /[0-9]wei+$/.match?(amount)
|
138
|
+
amount = (amount.gsub(/gwei^/, '').to_f * 1_000_000_000).to_i if /[0-9]gwei+$/.match?(amount)
|
139
|
+
amount = (amount.gsub(/eth^/, '').to_f * 1_000_000_000_000_000_000).to_i if /[0-9]eth+$/.match?(amount)
|
140
|
+
log.debug("Sending #{amount} wei...")
|
141
|
+
puts wallet.eth_pay(priv.private_hex, address, amount)
|
142
|
+
else
|
143
|
+
raise "Command #{opts.arguments[0]} is not supported"
|
144
|
+
end
|
145
|
+
rescue StandardError => e
|
146
|
+
if opts[:verbose]
|
147
|
+
puts Backtrace.new(e)
|
148
|
+
else
|
149
|
+
puts "ERROR: #{e.message}"
|
150
|
+
end
|
151
|
+
exit(255)
|
152
|
+
end
|
data/erc20.gemspec
CHANGED
@@ -12,22 +12,24 @@ Gem::Specification.new do |s|
|
|
12
12
|
s.name = 'erc20'
|
13
13
|
s.version = ERC20::VERSION
|
14
14
|
s.license = 'MIT'
|
15
|
-
s.summary = 'Sending and receiving ERC20 tokens in
|
15
|
+
s.summary = 'Sending and receiving ERC20 tokens in Ethereum network'
|
16
16
|
s.description =
|
17
17
|
'A simple library for making ERC20 manipulations as easy as they ' \
|
18
18
|
'can be for cryptocurrency newbies: checking balance, sending payments, ' \
|
19
19
|
'and monitoring addresses for incoming payments. The library expects ' \
|
20
|
-
'
|
20
|
+
'Ethereum node to provide JSON RPC and Websockets API.'
|
21
21
|
s.authors = ['Yegor Bugayenko']
|
22
22
|
s.email = 'yegor256@gmail.com'
|
23
23
|
s.homepage = 'http://github.com/yegor256/erc20.rb'
|
24
24
|
s.files = `git ls-files`.split($RS)
|
25
25
|
s.rdoc_options = ['--charset=UTF-8']
|
26
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
26
27
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
27
28
|
s.add_dependency 'eth', '>=0.5.13'
|
28
29
|
s.add_dependency 'faye-websocket', '>=0.11.3'
|
29
30
|
s.add_dependency 'json', '>=2.10.1'
|
30
31
|
s.add_dependency 'jsonrpc-client', '>=0.1.4'
|
31
32
|
s.add_dependency 'loog', '>0'
|
33
|
+
s.add_dependency 'slop', '>4'
|
32
34
|
s.metadata['rubygems_mfa_required'] = 'true'
|
33
35
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
|
4
|
+
Feature: Command Line Processing
|
5
|
+
As a simple ETH/ERC20 user I want to send payments
|
6
|
+
|
7
|
+
Scenario: Help can be printed
|
8
|
+
When I run bin/erc20 with "--help"
|
9
|
+
Then Exit code is zero
|
10
|
+
And Stdout contains "--help"
|
11
|
+
|
12
|
+
Scenario: Gas price price can be retrieved
|
13
|
+
When I run bin/erc20 with "price --attempts=4"
|
14
|
+
Then Exit code is zero
|
15
|
+
|
16
|
+
Scenario: ETH private key can be generated
|
17
|
+
When I run bin/erc20 with "key"
|
18
|
+
Then Exit code is zero
|
19
|
+
|
20
|
+
Scenario: ETH address can be created
|
21
|
+
When I run bin/erc20 with "address 46feba063e9b59a8ae0dba68abd39a3cb8f52089e776576d6eb1bb5bfec123d1"
|
22
|
+
Then Exit code is zero
|
23
|
+
|
24
|
+
Scenario: ERC20 balance can be checked
|
25
|
+
When I run bin/erc20 with "balance 0x7232148927F8a580053792f44D4d59d40Fd00ABD --verbose"
|
26
|
+
Then Exit code is zero
|
27
|
+
|
28
|
+
Scenario: ETH balance can be checked
|
29
|
+
When I run bin/erc20 with "eth_balance 0x7232148927F8a580053792f44D4d59d40Fd00ABD --verbose"
|
30
|
+
Then Exit code is zero
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
|
4
|
+
Feature: Gem Package
|
5
|
+
As a source code writer I want to be able to
|
6
|
+
package the Gem into .gem file
|
7
|
+
|
8
|
+
Scenario: Gem can be packaged
|
9
|
+
Given It is Unix
|
10
|
+
Given I have a "execs.rb" file with content:
|
11
|
+
"""
|
12
|
+
#!/usr/bin/env ruby
|
13
|
+
require 'rubygems'
|
14
|
+
spec = Gem::Specification::load('./spec.rb')
|
15
|
+
if spec.executables.empty?
|
16
|
+
fail 'no executables: ' + File.read('./spec.rb')
|
17
|
+
end
|
18
|
+
"""
|
19
|
+
When I run bash with:
|
20
|
+
"""
|
21
|
+
cd erc20
|
22
|
+
gem build erc20.gemspec
|
23
|
+
gem specification --ruby erc20-*.gem > ../spec.rb
|
24
|
+
cd ..
|
25
|
+
ruby execs.rb
|
26
|
+
"""
|
27
|
+
Then Exit code is zero
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
4
|
+
# SPDX-License-Identifier: MIT
|
5
|
+
|
6
|
+
require 'tmpdir'
|
7
|
+
require 'slop'
|
8
|
+
require 'English'
|
9
|
+
require_relative '../../lib/erc20'
|
10
|
+
|
11
|
+
Before do
|
12
|
+
@cwd = Dir.pwd
|
13
|
+
@dir = Dir.mktmpdir('test')
|
14
|
+
FileUtils.mkdir_p(@dir)
|
15
|
+
Dir.chdir(@dir)
|
16
|
+
@opts =
|
17
|
+
Slop.parse ['-v'] do |o|
|
18
|
+
o.bool '-v', '--verbose'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
After do
|
23
|
+
Dir.chdir(@cwd)
|
24
|
+
FileUtils.rm_rf(@dir)
|
25
|
+
end
|
26
|
+
|
27
|
+
Given(/^I have a "([^"]*)" file with content:$/) do |file, text|
|
28
|
+
FileUtils.mkdir_p(File.dirname(file)) unless File.exist?(file)
|
29
|
+
File.write(file, text.gsub('\\xFF', 0xFF.chr))
|
30
|
+
end
|
31
|
+
|
32
|
+
When(%r{^I run bin/erc20 with "([^"]*)"$}) do |arg|
|
33
|
+
home = File.join(File.dirname(__FILE__), '../..')
|
34
|
+
@stdout = `ruby -I#{home}/lib #{home}/bin/erc20 #{arg}`
|
35
|
+
@exitstatus = $CHILD_STATUS.exitstatus
|
36
|
+
end
|
37
|
+
|
38
|
+
Then(/^Stdout contains "([^"]*)"$/) do |txt|
|
39
|
+
raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
|
40
|
+
end
|
41
|
+
|
42
|
+
Then(/^Stdout is empty$/) do
|
43
|
+
raise "STDOUT is not empty:\n#{@stdout}" unless @stdout == ''
|
44
|
+
end
|
45
|
+
|
46
|
+
Then(/^Exit code is zero$/) do
|
47
|
+
raise "Non-zero exit #{@exitstatus}:\n#{@stdout}" unless @exitstatus.zero?
|
48
|
+
end
|
49
|
+
|
50
|
+
Then(/^Exit code is not zero$/) do
|
51
|
+
raise 'Zero exit code' if @exitstatus.zero?
|
52
|
+
end
|
53
|
+
|
54
|
+
When(/^I run bash with "([^"]*)"$/) do |text|
|
55
|
+
FileUtils.copy_entry(@cwd, File.join(@dir, 'erc20'))
|
56
|
+
@stdout = `#{text}`
|
57
|
+
@exitstatus = $CHILD_STATUS.exitstatus
|
58
|
+
end
|
59
|
+
|
60
|
+
When(/^I run bash with:$/) do |text|
|
61
|
+
FileUtils.copy_entry(@cwd, File.join(@dir, 'erc20'))
|
62
|
+
@stdout = `#{text}`
|
63
|
+
@exitstatus = $CHILD_STATUS.exitstatus
|
64
|
+
end
|
65
|
+
|
66
|
+
Given(/^It is Unix$/) do
|
67
|
+
pending if Gem.win_platform?
|
68
|
+
end
|
69
|
+
|
70
|
+
Given(/^It is Windows$/) do
|
71
|
+
pending unless Gem.win_platform?
|
72
|
+
end
|
data/lib/erc20/erc20.rb
CHANGED
@@ -3,9 +3,9 @@
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
4
4
|
# SPDX-License-Identifier: MIT
|
5
5
|
|
6
|
-
# This module makes manipulations with
|
6
|
+
# This module makes manipulations with Ethereum ERC20 tokens
|
7
7
|
# as simple as they can be, if you have a provider of
|
8
|
-
# JSON-RPC and WebSockets
|
8
|
+
# JSON-RPC and WebSockets Ethereum APIs, for example
|
9
9
|
# Infura, GetBlock, or Alchemy.
|
10
10
|
#
|
11
11
|
# Start like this:
|
@@ -25,5 +25,5 @@
|
|
25
25
|
# License:: MIT
|
26
26
|
module ERC20
|
27
27
|
# Current version of the gem (changed by the +.rultor.yml+ on every release)
|
28
|
-
VERSION = '0.
|
28
|
+
VERSION = '0.1.1'
|
29
29
|
end
|
data/lib/erc20/fake_wallet.rb
CHANGED
@@ -91,9 +91,11 @@ class ERC20::FakeWallet
|
|
91
91
|
|
92
92
|
# Send a single ERC20 payment from a private address to a public one.
|
93
93
|
#
|
94
|
-
# @param [String]
|
95
|
-
# @param [String]
|
96
|
-
# @param [Integer]
|
94
|
+
# @param [String] priv Private key, in hex
|
95
|
+
# @param [String] address Public key, in hex
|
96
|
+
# @param [Integer] amount The amount of ERC20 tokens to send
|
97
|
+
# @param [Integer] gas_limit Optional gas limit
|
98
|
+
# @param [Integer] gas_price Optional gas price in gwei
|
97
99
|
# @return [String] Transaction hash
|
98
100
|
def pay(priv, address, amount, gas_limit: nil, gas_price: nil)
|
99
101
|
hex = TXN_HASH
|
@@ -106,6 +108,7 @@ class ERC20::FakeWallet
|
|
106
108
|
# @param [String] priv Private key, in hex
|
107
109
|
# @param [String] address Public key, in hex
|
108
110
|
# @param [Integer] amount The amount of ETHs to send
|
111
|
+
# @param [Integer] gas_price Optional gas price in gwei
|
109
112
|
# @return [String] Transaction hash
|
110
113
|
def eth_pay(priv, address, amount, gas_price: nil)
|
111
114
|
hex = TXN_HASH
|
data/lib/erc20/wallet.rb
CHANGED
@@ -12,7 +12,7 @@ require 'loog'
|
|
12
12
|
require 'uri'
|
13
13
|
require_relative 'erc20'
|
14
14
|
|
15
|
-
# A wallet with ERC20 tokens on
|
15
|
+
# A wallet with ERC20 tokens on Ethereum.
|
16
16
|
#
|
17
17
|
# Objects of this class are thread-safe.
|
18
18
|
#
|
@@ -64,7 +64,7 @@ class ERC20::Wallet
|
|
64
64
|
attr_reader :host, :port, :ssl, :chain, :contract, :ws_path, :http_path
|
65
65
|
|
66
66
|
# Constructor.
|
67
|
-
# @param [String] contract Hex of the contract in
|
67
|
+
# @param [String] contract Hex of the contract in Ethereum
|
68
68
|
# @param [Integer] chain The ID of the chain (1 for mainnet)
|
69
69
|
# @param [String] host The host to connect to
|
70
70
|
# @param [Integer] port TCP port to use
|
@@ -107,9 +107,9 @@ class ERC20::Wallet
|
|
107
107
|
|
108
108
|
# Get ERC20 balance of a public address (it's not the same as ETH balance!).
|
109
109
|
#
|
110
|
-
# An address in
|
111
|
-
# balance in ETH crypto. Another balance is the one kept by ERC20 contract
|
112
|
-
# in its own
|
110
|
+
# An address in Ethereum may have many balances. One of them is the main
|
111
|
+
# balance in ETH crypto. Another balance is the one kept by the ERC20 contract
|
112
|
+
# in its own ledger in root storage. This balance is checked by this method.
|
113
113
|
#
|
114
114
|
# @param [String] address Public key, in hex, starting from '0x'
|
115
115
|
# @return [Integer] Balance, in tokens
|
@@ -127,7 +127,7 @@ class ERC20::Wallet
|
|
127
127
|
|
128
128
|
# Get ETH balance of a public address.
|
129
129
|
#
|
130
|
-
# An address in
|
130
|
+
# An address in Ethereum may have many balances. One of them is the main
|
131
131
|
# balance in ETH crypto. This balance is checked by this method.
|
132
132
|
#
|
133
133
|
# @param [String] hex Public key, in hex, starting from '0x'
|
@@ -142,12 +142,12 @@ class ERC20::Wallet
|
|
142
142
|
b
|
143
143
|
end
|
144
144
|
|
145
|
-
# How
|
145
|
+
# How many gas units are required to send an ERC20 transaction.
|
146
146
|
#
|
147
|
-
# @param [String] from The
|
148
|
-
# @param [String] to
|
147
|
+
# @param [String] from The sending address, in hex
|
148
|
+
# @param [String] to The receiving address, in hex
|
149
149
|
# @param [Integer] amount How many ERC20 tokens to send
|
150
|
-
# @return [Integer]
|
150
|
+
# @return [Integer] Number of gas units required
|
151
151
|
def gas_estimate(from, to, amount)
|
152
152
|
raise 'Address can\'t be nil' unless from
|
153
153
|
raise 'Address must be a String' unless from.is_a?(String)
|
@@ -164,15 +164,33 @@ class ERC20::Wallet
|
|
164
164
|
end
|
165
165
|
|
166
166
|
# What is the price of gas unit in gwei?
|
167
|
+
#
|
168
|
+
# In Ethereum, gas is a unit that measures the computational work required to
|
169
|
+
# execute operations on the network. Every transaction and smart contract
|
170
|
+
# interaction consumes gas. Gas price is the amount of ETH you're willing to pay
|
171
|
+
# for each unit of gas, denominated in gwei (1 gwei = 0.000000001 ETH). Higher
|
172
|
+
# gas prices incentivize miners to include your transaction sooner, while lower
|
173
|
+
# prices may result in longer confirmation times.
|
174
|
+
#
|
167
175
|
# @return [Integer] Price of gas unit, in gwei (0.000000001 ETH)
|
168
176
|
def gas_price
|
169
|
-
|
177
|
+
block = jsonrpc.eth_getBlockByNumber('latest', false)
|
178
|
+
raise "Can't get gas price, try again later" if block.nil?
|
179
|
+
gwei = block['baseFeePerGas'].to_i(16)
|
170
180
|
@log.debug("The cost of one gas unit is #{gwei} gwei")
|
171
181
|
gwei
|
172
182
|
end
|
173
183
|
|
174
184
|
# Send a single ERC20 payment from a private address to a public one.
|
175
185
|
#
|
186
|
+
# ERC20 payments differ fundamentally from native ETH transfers. While ETH transfers
|
187
|
+
# simply move the cryptocurrency directly between addresses, ERC20 token transfers
|
188
|
+
# are actually interactions with a smart contract. When you transfer ERC20 tokens,
|
189
|
+
# you're not sending anything directly to another user - instead, you're calling
|
190
|
+
# the token contract's transfer function, which updates its internal ledger to
|
191
|
+
# decrease your balance and increase the recipient's balance. This requires more
|
192
|
+
# gas than ETH transfers since it involves executing contract code.
|
193
|
+
#
|
176
194
|
# @param [String] priv Private key, in hex
|
177
195
|
# @param [String] address Public key, in hex
|
178
196
|
# @param [Integer] amount The amount of ERC20 tokens to send
|
@@ -225,8 +243,7 @@ class ERC20::Wallet
|
|
225
243
|
#
|
226
244
|
# @param [String] priv Private key, in hex
|
227
245
|
# @param [String] address Public key, in hex
|
228
|
-
# @param [Integer] amount The amount of
|
229
|
-
# @param [Integer] limit How much gas you're ready to spend
|
246
|
+
# @param [Integer] amount The amount of ETH to send
|
230
247
|
# @param [Integer] price How much gas you pay per computation unit
|
231
248
|
# @return [String] Transaction hash
|
232
249
|
def eth_pay(priv, address, amount, price: gas_price)
|
@@ -272,7 +289,7 @@ class ERC20::Wallet
|
|
272
289
|
# +Thread.kill+.
|
273
290
|
#
|
274
291
|
# The array with the list of addresses (+addresses+) may change its
|
275
|
-
# content on-fly. The +accept()+ method will +
|
292
|
+
# content on-the-fly. The +accept()+ method will +eth_subscribe+ to the addresses
|
276
293
|
# that are added and will +eth_unsubscribe+ from those that are removed.
|
277
294
|
# Once we actually start listening, the +active+ array will be updated
|
278
295
|
# with the list of addresses.
|
@@ -291,8 +308,8 @@ class ERC20::Wallet
|
|
291
308
|
raise 'Addresses must respond to .to_a()' unless addresses.respond_to?(:to_a)
|
292
309
|
raise 'Active can\'t be nil' unless active
|
293
310
|
raise 'Active must respond to .append()' unless active.respond_to?(:append)
|
294
|
-
raise '
|
295
|
-
raise '
|
311
|
+
raise 'Delay must be an Integer' unless delay.is_a?(Integer)
|
312
|
+
raise 'Delay must be a positive Integer' unless delay.positive?
|
296
313
|
raise 'Subscription ID must be an Integer' unless subscription_id.is_a?(Integer)
|
297
314
|
raise 'Subscription ID must be a positive Integer' unless subscription_id.positive?
|
298
315
|
EventMachine.run do
|
@@ -332,7 +349,7 @@ class ERC20::Wallet
|
|
332
349
|
txn: event['transactionHash'].downcase
|
333
350
|
}
|
334
351
|
log.debug(
|
335
|
-
"Payment of #{event[:amount]} tokens arrived" \
|
352
|
+
"Payment of #{event[:amount]} tokens arrived " \
|
336
353
|
"from #{event[:from]} to #{event[:to]} in #{event[:txn]}"
|
337
354
|
)
|
338
355
|
end
|
@@ -13,8 +13,8 @@ require 'random-port'
|
|
13
13
|
require 'shellwords'
|
14
14
|
require 'threads'
|
15
15
|
require 'typhoeus'
|
16
|
-
require_relative '../../lib/erc20/fake_wallet'
|
17
16
|
require_relative '../test__helper'
|
17
|
+
require_relative '../../lib/erc20/fake_wallet'
|
18
18
|
|
19
19
|
# Test.
|
20
20
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
data/test/erc20/test_wallet.rb
CHANGED
@@ -13,15 +13,15 @@ require 'random-port'
|
|
13
13
|
require 'shellwords'
|
14
14
|
require 'threads'
|
15
15
|
require 'typhoeus'
|
16
|
-
require_relative '../../lib/erc20/wallet'
|
17
16
|
require_relative '../test__helper'
|
17
|
+
require_relative '../../lib/erc20/wallet'
|
18
18
|
|
19
19
|
# Test.
|
20
20
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
21
21
|
# Copyright:: Copyright (c) 2025 Yegor Bugayenko
|
22
22
|
# License:: MIT
|
23
23
|
class TestWallet < Minitest::Test
|
24
|
-
# At this address, in
|
24
|
+
# At this address, in Ethereum mainnet, there are $8 USDT and 0.0042 ETH. I won't
|
25
25
|
# move them anyway, that's why tests can use this address forever.
|
26
26
|
STABLE = '0x7232148927F8a580053792f44D4d59d40Fd00ABD'
|
27
27
|
|
@@ -76,7 +76,8 @@ class TestWallet < Minitest::Test
|
|
76
76
|
def test_checks_balance_on_polygon
|
77
77
|
w = ERC20::Wallet.new(
|
78
78
|
contract: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
|
79
|
-
host: 'polygon-mainnet.infura.io',
|
79
|
+
host: 'polygon-mainnet.infura.io',
|
80
|
+
http_path: "/v3/#{env('INFURA_KEY')}",
|
80
81
|
log: loog
|
81
82
|
)
|
82
83
|
b = w.balance(STABLE)
|
@@ -290,20 +291,19 @@ class TestWallet < Minitest::Test
|
|
290
291
|
|
291
292
|
def test_checks_balance_via_proxy_on_mainnet
|
292
293
|
via_proxy do |proxy|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
end
|
294
|
+
w = ERC20::Wallet.new(
|
295
|
+
host: 'mainnet.infura.io',
|
296
|
+
http_path: "/v3/#{env('INFURA_KEY')}",
|
297
|
+
proxy:, log: loog
|
298
|
+
)
|
299
|
+
assert_equal(8_000_000, w.balance(STABLE))
|
300
300
|
end
|
301
301
|
end
|
302
302
|
|
303
303
|
def test_pays_on_mainnet
|
304
304
|
skip('This is live, must be run manually')
|
305
305
|
w = mainnet
|
306
|
-
print 'Enter
|
306
|
+
print 'Enter Ethereum ERC20 private key (64 chars): '
|
307
307
|
priv = gets.chomp
|
308
308
|
to = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
|
309
309
|
txn = w.pay(priv, to, 1_990_000)
|
@@ -315,6 +315,7 @@ class TestWallet < Minitest::Test
|
|
315
315
|
def env(var)
|
316
316
|
key = ENV.fetch(var, nil)
|
317
317
|
skip("The #{var} environment variable is not set") if key.nil?
|
318
|
+
skip("The #{var} environment variable is empty") if key.empty?
|
318
319
|
key
|
319
320
|
end
|
320
321
|
|
data/test/test__helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: erc20
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date: 2025-
|
10
|
+
date: 2025-04-05 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: eth
|
@@ -80,12 +79,27 @@ dependencies:
|
|
80
79
|
- - ">"
|
81
80
|
- !ruby/object:Gem::Version
|
82
81
|
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: slop
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '4'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '4'
|
83
96
|
description: 'A simple library for making ERC20 manipulations as easy as they can
|
84
97
|
be for cryptocurrency newbies: checking balance, sending payments, and monitoring
|
85
|
-
addresses for incoming payments. The library expects
|
98
|
+
addresses for incoming payments. The library expects Ethereum node to provide JSON
|
86
99
|
RPC and Websockets API.'
|
87
100
|
email: yegor256@gmail.com
|
88
|
-
executables:
|
101
|
+
executables:
|
102
|
+
- erc20
|
89
103
|
extensions: []
|
90
104
|
extra_rdoc_files:
|
91
105
|
- README.md
|
@@ -117,7 +131,12 @@ files:
|
|
117
131
|
- README.md
|
118
132
|
- REUSE.toml
|
119
133
|
- Rakefile
|
134
|
+
- bin/erc20
|
120
135
|
- erc20.gemspec
|
136
|
+
- features/cli.feature
|
137
|
+
- features/gem_package.feature
|
138
|
+
- features/step_definitions/steps.rb
|
139
|
+
- features/support/env.rb
|
121
140
|
- hardhat/.gitignore
|
122
141
|
- hardhat/Dockerfile
|
123
142
|
- hardhat/contracts/Foo.sol
|
@@ -137,7 +156,6 @@ licenses:
|
|
137
156
|
- MIT
|
138
157
|
metadata:
|
139
158
|
rubygems_mfa_required: 'true'
|
140
|
-
post_install_message:
|
141
159
|
rdoc_options:
|
142
160
|
- "--charset=UTF-8"
|
143
161
|
require_paths:
|
@@ -153,8 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
171
|
- !ruby/object:Gem::Version
|
154
172
|
version: '0'
|
155
173
|
requirements: []
|
156
|
-
rubygems_version: 3.
|
157
|
-
signing_key:
|
174
|
+
rubygems_version: 3.6.2
|
158
175
|
specification_version: 4
|
159
|
-
summary: Sending and receiving ERC20 tokens in
|
176
|
+
summary: Sending and receiving ERC20 tokens in Ethereum network
|
160
177
|
test_files: []
|