erc20 0.0.1 → 0.0.2
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/actionlint.yml +1 -0
- data/.github/workflows/codecov.yml +2 -1
- data/.github/workflows/copyrights.yml +1 -0
- data/.github/workflows/markdown-lint.yml +1 -0
- data/.github/workflows/pdd.yml +1 -0
- data/.github/workflows/rake.yml +12 -3
- data/.github/workflows/shellcheck.yml +37 -0
- data/.github/workflows/xcop.yml +1 -0
- data/.github/workflows/yamllint.yml +1 -0
- data/.gitignore +1 -0
- data/.gitleaksignore +2 -0
- data/.rubocop.yml +2 -2
- data/.rultor.yml +2 -2
- data/Gemfile +9 -2
- data/Gemfile.lock +90 -8
- data/README.md +40 -12
- data/Rakefile +1 -9
- data/erc20.gemspec +4 -0
- data/hardhat/.gitignore +5 -0
- data/hardhat/Dockerfile +44 -0
- data/hardhat/contracts/Foo.sol +95 -0
- data/hardhat/hardhat.config.js +52 -0
- data/hardhat/ignition/modules/Foo.ts +28 -0
- data/hardhat/package.json +7 -0
- data/lib/erc20/wallet.rb +127 -8
- data/lib/erc20.rb +1 -1
- data/test/erc20/test_wallet.rb +164 -5
- data/test/test__helper.rb +4 -0
- metadata +66 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bf1f0dbee9d090079bd263a79eeafc0093866fc5503d7a1130b26e7b77ca8630
|
4
|
+
data.tar.gz: 105f04b7df7e9b8f45a3c34acb739fb6b74238e9f7180b624ceb724f8a4c1570
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e86283d48937fca5b956c0963b19c6b25b25226d6a60bd737e37d6201cc5dc82879d33ab79ff3ef919728762da601aa9d1bc56aaa85b94532f7edcafd29f04eb
|
7
|
+
data.tar.gz: f11fd8b7d8b017c4fbdc70b4442aa054344fee9451fd029e7826da8fe12961ac60447522945e83b941ab7ccf28a67299a9077c47b114f8736336d42c6b16088f
|
@@ -25,6 +25,7 @@ name: codecov
|
|
25
25
|
- master
|
26
26
|
jobs:
|
27
27
|
codecov:
|
28
|
+
timeout-minutes: 15
|
28
29
|
runs-on: ubuntu-24.04
|
29
30
|
steps:
|
30
31
|
- uses: actions/checkout@v4
|
@@ -33,7 +34,7 @@ jobs:
|
|
33
34
|
ruby-version: 3.3
|
34
35
|
bundler-cache: true
|
35
36
|
- run: bundle config set --global path "$(pwd)/vendor/bundle"
|
36
|
-
- run: bundle install
|
37
|
+
- run: bundle install --no-color
|
37
38
|
- run: bundle exec rake
|
38
39
|
- uses: codecov/codecov-action@v5
|
39
40
|
with:
|
data/.github/workflows/pdd.yml
CHANGED
data/.github/workflows/rake.yml
CHANGED
@@ -28,18 +28,27 @@ name: rake
|
|
28
28
|
- master
|
29
29
|
jobs:
|
30
30
|
rake:
|
31
|
-
name: test
|
32
31
|
strategy:
|
33
32
|
matrix:
|
34
|
-
os: [ubuntu-24.04
|
33
|
+
os: [ubuntu-24.04]
|
35
34
|
ruby: [3.3]
|
36
35
|
runs-on: ${{ matrix.os }}
|
36
|
+
env:
|
37
|
+
INFURA_KEY: ${{ secrets.INFURA_KEY }}
|
38
|
+
GETBLOCK_KEY: ${{ secrets.GETBLOCK_KEY }}
|
39
|
+
GETBLOCK_SEPOILA_KEY: ${{ secrets.GETBLOCK_SEPOILA_KEY }}
|
37
40
|
steps:
|
38
41
|
- uses: actions/checkout@v4
|
42
|
+
- run: |
|
43
|
+
sudo apt-get update --yes --fix-missing
|
44
|
+
sudo apt-get install --yes autoconf
|
45
|
+
if: matrix.os == 'ubuntu-24.04'
|
46
|
+
- run: brew install automake
|
47
|
+
if: matrix.os == 'macos-15'
|
39
48
|
- uses: ruby/setup-ruby@v1
|
40
49
|
with:
|
41
50
|
ruby-version: ${{ matrix.ruby }}
|
42
51
|
bundler-cache: true
|
43
52
|
- run: bundle config set --global path "$(pwd)/vendor/bundle"
|
44
|
-
- run: bundle install
|
53
|
+
- run: bundle install --no-color
|
45
54
|
- run: bundle exec rake
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# (The MIT License)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2025 Yegor Bugayenko
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the 'Software'), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
---
|
23
|
+
name: shellcheck
|
24
|
+
'on':
|
25
|
+
push:
|
26
|
+
branches:
|
27
|
+
- master
|
28
|
+
pull_request:
|
29
|
+
branches:
|
30
|
+
- master
|
31
|
+
jobs:
|
32
|
+
shellcheck:
|
33
|
+
timeout-minutes: 15
|
34
|
+
runs-on: ubuntu-24.04
|
35
|
+
steps:
|
36
|
+
- uses: actions/checkout@v4
|
37
|
+
- uses: ludeeus/action-shellcheck@master
|
data/.github/workflows/xcop.yml
CHANGED
data/.gitignore
CHANGED
data/.gitleaksignore
ADDED
data/.rubocop.yml
CHANGED
@@ -30,7 +30,7 @@ AllCops:
|
|
30
30
|
require:
|
31
31
|
- rubocop-minitest
|
32
32
|
- rubocop-performance
|
33
|
-
|
33
|
+
- rubocop-rake
|
34
34
|
Minitest/EmptyLineBeforeAssertionMethods:
|
35
35
|
Enabled: false
|
36
36
|
Gemspec/RequiredRubyVersion:
|
@@ -44,7 +44,7 @@ Layout/MultilineMethodCallIndentation:
|
|
44
44
|
Metrics/AbcSize:
|
45
45
|
Enabled: false
|
46
46
|
Metrics/BlockLength:
|
47
|
-
Max:
|
47
|
+
Max: 50
|
48
48
|
Metrics/CyclomaticComplexity:
|
49
49
|
Max: 25
|
50
50
|
Metrics/PerceivedComplexity:
|
data/.rultor.yml
CHANGED
@@ -29,7 +29,7 @@ release:
|
|
29
29
|
pre: false
|
30
30
|
script: |-
|
31
31
|
[[ "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit -1
|
32
|
-
bundle exec rake
|
32
|
+
DONCE_SUDO=true bundle exec rake
|
33
33
|
rm -rf *.gem
|
34
34
|
sed -i "s/0\.0\.0/${tag}/g" lib/erc20.rb
|
35
35
|
git add lib/erc20.rb
|
@@ -39,4 +39,4 @@ release:
|
|
39
39
|
gem push *.gem --config-file ../rubygems.yml
|
40
40
|
merge:
|
41
41
|
script: |-
|
42
|
-
bundle exec rake
|
42
|
+
DONCE_SUDO=true bundle exec rake
|
data/Gemfile
CHANGED
@@ -23,16 +23,23 @@
|
|
23
23
|
source 'https://rubygems.org'
|
24
24
|
gemspec
|
25
25
|
|
26
|
+
gem 'backtrace', '>0', require: false
|
27
|
+
gem 'donce', '>0', require: false
|
26
28
|
gem 'loog', '>0', require: false
|
27
29
|
gem 'minitest', '5.25.4', require: false
|
28
30
|
gem 'minitest-reporters', '1.7.1', require: false
|
31
|
+
gem 'minitest-retry', '0.2.5', require: false
|
32
|
+
gem 'qbash', '>0', require: false
|
29
33
|
gem 'rake', '13.2.1', require: false
|
30
|
-
gem '
|
31
|
-
gem '
|
34
|
+
gem 'random-port', '>0', require: false
|
35
|
+
gem 'rspec-rails', '7.1.1', require: false
|
36
|
+
gem 'rubocop', '1.71.2', require: false
|
32
37
|
gem 'rubocop-minitest', '0.36.0', require: false
|
33
38
|
gem 'rubocop-performance', '1.23.1', require: false
|
39
|
+
gem 'rubocop-rake', '>0', require: false
|
34
40
|
gem 'rubocop-rspec', '3.4.0', require: false
|
35
41
|
gem 'simplecov', '0.22.0', require: false
|
36
42
|
gem 'simplecov-cobertura', '2.1.0', require: false
|
37
43
|
gem 'threads', '0.4.1', require: false
|
44
|
+
gem 'typhoeus', '>0', require: false
|
38
45
|
gem 'yard', '0.9.37', require: false
|
data/Gemfile.lock
CHANGED
@@ -3,7 +3,11 @@ PATH
|
|
3
3
|
specs:
|
4
4
|
erc20 (0.0.0)
|
5
5
|
backtrace (> 0)
|
6
|
+
eth (~> 0.4)
|
7
|
+
jsonrpc-client (> 0)
|
8
|
+
loog (> 0)
|
6
9
|
tago (> 0)
|
10
|
+
websocket-client-simple (> 0)
|
7
11
|
|
8
12
|
GEM
|
9
13
|
remote: https://rubygems.org/
|
@@ -50,8 +54,39 @@ GEM
|
|
50
54
|
date (3.4.1)
|
51
55
|
diff-lcs (1.5.1)
|
52
56
|
docile (1.4.1)
|
57
|
+
donce (0.1.0)
|
58
|
+
backtrace (> 0)
|
59
|
+
os (> 0)
|
60
|
+
qbash (> 0)
|
53
61
|
drb (2.2.1)
|
62
|
+
elapsed (0.0.1)
|
63
|
+
loog (> 0)
|
64
|
+
tago (> 0)
|
54
65
|
erubi (1.13.1)
|
66
|
+
eth (0.5.13)
|
67
|
+
forwardable (~> 1.3)
|
68
|
+
keccak (~> 1.3)
|
69
|
+
konstructor (~> 1.0)
|
70
|
+
openssl (>= 2.2, < 4.0)
|
71
|
+
rbsecp256k1 (~> 6.0)
|
72
|
+
scrypt (~> 3.0)
|
73
|
+
ethon (0.16.0)
|
74
|
+
ffi (>= 1.15.0)
|
75
|
+
event_emitter (0.2.6)
|
76
|
+
faraday (2.12.2)
|
77
|
+
faraday-net_http (>= 2.0, < 3.5)
|
78
|
+
json
|
79
|
+
logger
|
80
|
+
faraday-net_http (3.4.0)
|
81
|
+
net-http (>= 0.5.0)
|
82
|
+
ffi (1.17.1-arm64-darwin)
|
83
|
+
ffi (1.17.1-x64-mingw-ucrt)
|
84
|
+
ffi (1.17.1-x86_64-darwin)
|
85
|
+
ffi (1.17.1-x86_64-linux-gnu)
|
86
|
+
ffi-compiler (1.3.2)
|
87
|
+
ffi (>= 1.15.5)
|
88
|
+
rake
|
89
|
+
forwardable (1.3.3)
|
55
90
|
i18n (1.14.7)
|
56
91
|
concurrent-ruby (~> 1.0)
|
57
92
|
io-console (0.8.0)
|
@@ -59,19 +94,31 @@ GEM
|
|
59
94
|
pp (>= 0.6.0)
|
60
95
|
rdoc (>= 4.0.0)
|
61
96
|
reline (>= 0.4.2)
|
62
|
-
json (2.
|
97
|
+
json (2.10.0)
|
98
|
+
jsonrpc-client (0.1.4)
|
99
|
+
faraday
|
100
|
+
multi_json (>= 1.1.0)
|
101
|
+
keccak (1.3.2)
|
102
|
+
konstructor (1.0.2)
|
63
103
|
language_server-protocol (3.17.0.4)
|
64
104
|
logger (1.6.5)
|
65
105
|
loofah (2.24.0)
|
66
106
|
crass (~> 1.0.2)
|
67
107
|
nokogiri (>= 1.12.0)
|
68
108
|
loog (0.6.0)
|
109
|
+
mini_portile2 (2.8.8)
|
69
110
|
minitest (5.25.4)
|
70
111
|
minitest-reporters (1.7.1)
|
71
112
|
ansi
|
72
113
|
builder
|
73
114
|
minitest (>= 5.0)
|
74
115
|
ruby-progressbar
|
116
|
+
minitest-retry (0.2.5)
|
117
|
+
minitest (>= 5.0)
|
118
|
+
multi_json (1.15.0)
|
119
|
+
mutex_m (0.3.0)
|
120
|
+
net-http (0.6.0)
|
121
|
+
uri
|
75
122
|
nokogiri (1.18.2-arm64-darwin)
|
76
123
|
racc (~> 1.4)
|
77
124
|
nokogiri (1.18.2-x64-mingw-ucrt)
|
@@ -80,16 +127,24 @@ GEM
|
|
80
127
|
racc (~> 1.4)
|
81
128
|
nokogiri (1.18.2-x86_64-linux-gnu)
|
82
129
|
racc (~> 1.4)
|
130
|
+
openssl (3.3.0)
|
131
|
+
os (1.1.4)
|
83
132
|
parallel (1.26.3)
|
84
|
-
parser (3.3.7.
|
133
|
+
parser (3.3.7.1)
|
85
134
|
ast (~> 2.4.1)
|
86
135
|
racc
|
136
|
+
pkg-config (1.5.9)
|
87
137
|
pp (0.6.2)
|
88
138
|
prettyprint
|
89
139
|
prettyprint (0.2.0)
|
90
140
|
psych (5.2.3)
|
91
141
|
date
|
92
142
|
stringio
|
143
|
+
qbash (0.4.0)
|
144
|
+
backtrace (> 0)
|
145
|
+
elapsed (> 0)
|
146
|
+
loog (> 0)
|
147
|
+
tago (> 0)
|
93
148
|
racc (1.8.1)
|
94
149
|
rack (3.1.9)
|
95
150
|
rack-session (2.1.0)
|
@@ -116,13 +171,19 @@ GEM
|
|
116
171
|
zeitwerk (~> 2.6)
|
117
172
|
rainbow (3.1.1)
|
118
173
|
rake (13.2.1)
|
119
|
-
|
174
|
+
random-port (0.7.5)
|
175
|
+
tago (> 0)
|
176
|
+
rbsecp256k1 (6.0.0)
|
177
|
+
mini_portile2 (~> 2.8)
|
178
|
+
pkg-config (~> 1.5)
|
179
|
+
rubyzip (~> 2.3)
|
180
|
+
rdoc (6.12.0)
|
120
181
|
psych (>= 4.0.0)
|
121
182
|
regexp_parser (2.10.0)
|
122
183
|
reline (0.6.0)
|
123
184
|
io-console (~> 0.5)
|
124
185
|
rexml (3.4.0)
|
125
|
-
rspec-core (3.13.
|
186
|
+
rspec-core (3.13.3)
|
126
187
|
rspec-support (~> 3.13.0)
|
127
188
|
rspec-expectations (3.13.3)
|
128
189
|
diff-lcs (>= 1.2.0, < 2.0)
|
@@ -130,7 +191,7 @@ GEM
|
|
130
191
|
rspec-mocks (3.13.2)
|
131
192
|
diff-lcs (>= 1.2.0, < 2.0)
|
132
193
|
rspec-support (~> 3.13.0)
|
133
|
-
rspec-rails (7.1.
|
194
|
+
rspec-rails (7.1.1)
|
134
195
|
actionpack (>= 7.0)
|
135
196
|
activesupport (>= 7.0)
|
136
197
|
railties (>= 7.0)
|
@@ -139,7 +200,7 @@ GEM
|
|
139
200
|
rspec-mocks (~> 3.13)
|
140
201
|
rspec-support (~> 3.13)
|
141
202
|
rspec-support (3.13.2)
|
142
|
-
rubocop (1.71.
|
203
|
+
rubocop (1.71.2)
|
143
204
|
json (~> 2.3)
|
144
205
|
language_server-protocol (>= 3.17.0)
|
145
206
|
parallel (~> 1.10)
|
@@ -157,9 +218,15 @@ GEM
|
|
157
218
|
rubocop-performance (1.23.1)
|
158
219
|
rubocop (>= 1.48.1, < 2.0)
|
159
220
|
rubocop-ast (>= 1.31.1, < 2.0)
|
221
|
+
rubocop-rake (0.6.0)
|
222
|
+
rubocop (~> 1.0)
|
160
223
|
rubocop-rspec (3.4.0)
|
161
224
|
rubocop (~> 1.61)
|
162
225
|
ruby-progressbar (1.13.0)
|
226
|
+
rubyzip (2.4.1)
|
227
|
+
scrypt (3.0.8)
|
228
|
+
ffi-compiler (>= 1.0, < 2.0)
|
229
|
+
rake (>= 9, < 14)
|
163
230
|
securerandom (0.4.1)
|
164
231
|
simplecov (0.22.0)
|
165
232
|
docile (~> 1.1)
|
@@ -176,6 +243,8 @@ GEM
|
|
176
243
|
threads (0.4.1)
|
177
244
|
backtrace (~> 0)
|
178
245
|
concurrent-ruby (~> 1.0)
|
246
|
+
typhoeus (1.4.1)
|
247
|
+
ethon (>= 0.9.0)
|
179
248
|
tzinfo (2.0.6)
|
180
249
|
concurrent-ruby (~> 1.0)
|
181
250
|
unicode-display_width (3.1.4)
|
@@ -183,6 +252,12 @@ GEM
|
|
183
252
|
unicode-emoji (4.0.4)
|
184
253
|
uri (1.0.2)
|
185
254
|
useragent (0.16.11)
|
255
|
+
websocket (1.2.11)
|
256
|
+
websocket-client-simple (0.9.0)
|
257
|
+
base64
|
258
|
+
event_emitter
|
259
|
+
mutex_m
|
260
|
+
websocket
|
186
261
|
yard (0.9.37)
|
187
262
|
zeitwerk (2.7.1)
|
188
263
|
|
@@ -196,19 +271,26 @@ PLATFORMS
|
|
196
271
|
x86_64-linux
|
197
272
|
|
198
273
|
DEPENDENCIES
|
274
|
+
backtrace (> 0)
|
275
|
+
donce (> 0)
|
199
276
|
erc20!
|
200
277
|
loog (> 0)
|
201
278
|
minitest (= 5.25.4)
|
202
279
|
minitest-reporters (= 1.7.1)
|
280
|
+
minitest-retry (= 0.2.5)
|
281
|
+
qbash (> 0)
|
203
282
|
rake (= 13.2.1)
|
204
|
-
|
205
|
-
|
283
|
+
random-port (> 0)
|
284
|
+
rspec-rails (= 7.1.1)
|
285
|
+
rubocop (= 1.71.2)
|
206
286
|
rubocop-minitest (= 0.36.0)
|
207
287
|
rubocop-performance (= 1.23.1)
|
288
|
+
rubocop-rake (> 0)
|
208
289
|
rubocop-rspec (= 3.4.0)
|
209
290
|
simplecov (= 0.22.0)
|
210
291
|
simplecov-cobertura (= 2.1.0)
|
211
292
|
threads (= 0.4.1)
|
293
|
+
typhoeus (> 0)
|
212
294
|
yard (= 0.9.37)
|
213
295
|
|
214
296
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -11,27 +11,47 @@
|
|
11
11
|
[](https://hitsofcode.com/view/github/yegor256/erc20)
|
12
12
|
[](https://github.com/yegor256/erc20/blob/master/LICENSE.txt)
|
13
13
|
|
14
|
-
This small Ruby library makes Etherium ERC20
|
15
|
-
as simple as they can be,
|
14
|
+
This small Ruby library makes manipulations with [Etherium] [ERC20] tokens
|
15
|
+
as simple as they can be, if you have a provider of
|
16
|
+
[JSON-RPC] and [WebSocket] Etherium APIs, for example
|
17
|
+
[Infura], [GetBlock], or [Alchemy]:
|
16
18
|
|
17
19
|
```ruby
|
18
|
-
# List of private keys:
|
19
|
-
keys = ['...', '...']
|
20
|
-
|
21
20
|
# Create a wallet:
|
22
|
-
w = ERC20::Wallet.new(
|
21
|
+
w = ERC20::Wallet.new(
|
22
|
+
contract: ERC20::Wallet.USDT, # hex of it
|
23
|
+
rpc: 'https://mainnet.infura.io/v3/<your-key>',
|
24
|
+
wss: 'wss://mainnet.infura.io/v3/<your-key>',
|
25
|
+
log: $stdout
|
26
|
+
)
|
23
27
|
|
24
|
-
#
|
25
|
-
|
28
|
+
# Check balance on the address:
|
29
|
+
usdt = w.balance(address)
|
26
30
|
|
27
|
-
# Send a few tokens to someone:
|
28
|
-
w.
|
31
|
+
# Send a few tokens to someone and get transaction hash:
|
32
|
+
txn = w.pay(private_key, to_address, amount)
|
29
33
|
|
30
34
|
# Stay waiting, and trigger the block when transactions arrive:
|
31
|
-
|
35
|
+
addresses = ['0x...', '0x...']
|
36
|
+
w.accept(addresses) do |event|
|
37
|
+
puts event['data'].to_i(16)) # how much
|
38
|
+
puts "0x#{event['topics'][1][26..]}" # who sent the payment
|
39
|
+
puts "0x#{event['topics'][2][26..]}" # who was the receiver
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
To generate a new private key, use [eth](https://rubygems.org/gems/eth):
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
require 'eth'
|
47
|
+
key = Eth::Key.new.private_hex
|
32
48
|
```
|
33
49
|
|
34
|
-
|
50
|
+
To get address from private one:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
public_hex = Eth::Key.new(priv: key).address
|
54
|
+
```
|
35
55
|
|
36
56
|
## How to contribute
|
37
57
|
|
@@ -48,3 +68,11 @@ bundle exec rake
|
|
48
68
|
```
|
49
69
|
|
50
70
|
If it's clean and you don't see any error messages, submit your pull request.
|
71
|
+
|
72
|
+
[Etherium]: https://en.wikipedia.org/wiki/Ethereum
|
73
|
+
[ERC20]: https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
|
74
|
+
[JSON-RPC]: https://ethereum.org/en/developers/docs/apis/json-rpc/
|
75
|
+
[Websocket]: https://ethereum.org/en/developers/tutorials/using-websockets/
|
76
|
+
[Infura]: https://infura.io/
|
77
|
+
[Alchemy]: https://alchemy.com/
|
78
|
+
[GetBlock]: https://getblock.io/
|
data/Rakefile
CHANGED
@@ -32,7 +32,7 @@ def version
|
|
32
32
|
Gem::Specification.load(Dir['*.gemspec'].first).version
|
33
33
|
end
|
34
34
|
|
35
|
-
task default: %i[clean test rubocop yard
|
35
|
+
task default: %i[clean test rubocop yard]
|
36
36
|
|
37
37
|
require 'rake/testtask'
|
38
38
|
desc 'Run all unit tests'
|
@@ -61,11 +61,3 @@ desc 'Run benchmark script'
|
|
61
61
|
task :benchmark do
|
62
62
|
ruby 'benchmarks/simple.rb'
|
63
63
|
end
|
64
|
-
|
65
|
-
task :copyright do
|
66
|
-
sh "grep -q -r '#{Date.today.strftime('%Y')}' \
|
67
|
-
--include '*.rb' \
|
68
|
-
--include '*.txt' \
|
69
|
-
--include 'Rakefile' \
|
70
|
-
."
|
71
|
-
end
|
data/erc20.gemspec
CHANGED
@@ -40,6 +40,10 @@ Gem::Specification.new do |s|
|
|
40
40
|
s.rdoc_options = ['--charset=UTF-8']
|
41
41
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
42
42
|
s.add_dependency 'backtrace', '>0'
|
43
|
+
s.add_dependency 'eth', '~>0.4'
|
44
|
+
s.add_dependency 'jsonrpc-client', '>0'
|
45
|
+
s.add_dependency 'loog', '>0'
|
43
46
|
s.add_dependency 'tago', '>0'
|
47
|
+
s.add_dependency 'websocket-client-simple', '>0'
|
44
48
|
s.metadata['rubygems_mfa_required'] = 'true'
|
45
49
|
end
|
data/hardhat/.gitignore
ADDED
data/hardhat/Dockerfile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Copyright (c) 2025 Yegor Bugayenko
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the 'Software'), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all
|
11
|
+
# copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
# SOFTWARE.
|
20
|
+
|
21
|
+
FROM node:22
|
22
|
+
|
23
|
+
ENV HOME=/hh
|
24
|
+
WORKDIR /app
|
25
|
+
EXPOSE 8545
|
26
|
+
|
27
|
+
COPY package.json .
|
28
|
+
RUN npm install
|
29
|
+
|
30
|
+
# These two must be provided via '--build-arg':
|
31
|
+
ARG PORT=8080
|
32
|
+
ARG HOST=localhost
|
33
|
+
COPY hardhat.config.js .
|
34
|
+
RUN sed -i "s/PORT/$PORT/g" hardhat.config.js
|
35
|
+
RUN sed -i "s/HOST/$HOST/g" hardhat.config.js
|
36
|
+
|
37
|
+
COPY contracts contracts
|
38
|
+
COPY ignition ignition
|
39
|
+
|
40
|
+
RUN rm -rf ignition/deployments
|
41
|
+
|
42
|
+
RUN npx hardhat compile
|
43
|
+
|
44
|
+
RUN rm -rf cache
|
@@ -0,0 +1,95 @@
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
2
|
+
pragma solidity ^0.8.28;
|
3
|
+
|
4
|
+
contract Foo {
|
5
|
+
string public name = "Foo";
|
6
|
+
string public symbol = "FOO";
|
7
|
+
uint8 public decimals = 18;
|
8
|
+
|
9
|
+
// Tracks the total token supply
|
10
|
+
uint256 public totalSupply;
|
11
|
+
|
12
|
+
// Owner of the token contract (for minting, etc., if desired)
|
13
|
+
address public owner;
|
14
|
+
|
15
|
+
// Balances for each account
|
16
|
+
mapping(address => uint256) private _balances;
|
17
|
+
|
18
|
+
// Allowances for each account, where allowances[from][spender] = amount
|
19
|
+
mapping(address => mapping(address => uint256)) private _allowances;
|
20
|
+
|
21
|
+
// Events
|
22
|
+
event Transfer(address indexed from, address indexed to, uint256 value);
|
23
|
+
event Approval(address indexed owner, address indexed spender, uint256 value);
|
24
|
+
|
25
|
+
// Constructor: Mint an initial supply to the deployer
|
26
|
+
constructor() {
|
27
|
+
owner = msg.sender;
|
28
|
+
_mint(owner, 100000);
|
29
|
+
_mint(0xd5fF1bFCDE7A03Da61ad229d962c74F1eA2f16A5, 123000000000); // Jeff
|
30
|
+
_mint(0xd7a63Ac9DD3d7878bc042A4bADA214EE4aff8c85, 456000000000); // Walter
|
31
|
+
}
|
32
|
+
|
33
|
+
function balanceOf(address account) external view returns (uint256) {
|
34
|
+
return _balances[account];
|
35
|
+
}
|
36
|
+
|
37
|
+
function transfer(address recipient, uint256 amount) external returns (bool) {
|
38
|
+
_transfer(msg.sender, recipient, amount);
|
39
|
+
return true;
|
40
|
+
}
|
41
|
+
|
42
|
+
function allowance(address _owner, address _spender) external view returns (uint256) {
|
43
|
+
return _allowances[_owner][_spender];
|
44
|
+
}
|
45
|
+
|
46
|
+
function approve(address spender, uint256 amount) external returns (bool) {
|
47
|
+
_approve(msg.sender, spender, amount);
|
48
|
+
return true;
|
49
|
+
}
|
50
|
+
|
51
|
+
function transferFrom(
|
52
|
+
address sender,
|
53
|
+
address recipient,
|
54
|
+
uint256 amount
|
55
|
+
) external returns (bool) {
|
56
|
+
uint256 currentAllowance = _allowances[sender][msg.sender];
|
57
|
+
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
|
58
|
+
_approve(sender, msg.sender, currentAllowance - amount);
|
59
|
+
_transfer(sender, recipient, amount);
|
60
|
+
|
61
|
+
return true;
|
62
|
+
}
|
63
|
+
|
64
|
+
function mint(address account, uint256 amount) public {
|
65
|
+
require(msg.sender == owner, "ERC20: only owner can mint");
|
66
|
+
_mint(account, amount);
|
67
|
+
}
|
68
|
+
|
69
|
+
function _transfer(address sender, address recipient, uint256 amount) internal {
|
70
|
+
require(sender != address(0), "ERC20: transfer from the zero address");
|
71
|
+
require(recipient != address(0), "ERC20: transfer to the zero address");
|
72
|
+
require(_balances[sender] >= amount, "ERC20: transfer amount exceeds balance");
|
73
|
+
_balances[sender] -= amount;
|
74
|
+
_balances[recipient] += amount;
|
75
|
+
emit Transfer(sender, recipient, amount);
|
76
|
+
}
|
77
|
+
|
78
|
+
function _approve(
|
79
|
+
address tokenOwner,
|
80
|
+
address spender,
|
81
|
+
uint256 amount
|
82
|
+
) internal {
|
83
|
+
require(tokenOwner != address(0), "ERC20: approve from the zero address");
|
84
|
+
require(spender != address(0), "ERC20: approve to the zero address");
|
85
|
+
_allowances[tokenOwner][spender] = amount;
|
86
|
+
emit Approval(tokenOwner, spender, amount);
|
87
|
+
}
|
88
|
+
|
89
|
+
function _mint(address account, uint256 amount) internal {
|
90
|
+
require(account != address(0), "ERC20: mint to the zero address");
|
91
|
+
totalSupply += amount;
|
92
|
+
_balances[account] += amount;
|
93
|
+
emit Transfer(address(0), account, amount);
|
94
|
+
}
|
95
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
// Copyright (c) 2025 Yegor Bugayenko
|
2
|
+
//
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
// of this software and associated documentation files (the "Software"), to deal
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
8
|
+
// furnished to do so, subject to the following conditions:
|
9
|
+
//
|
10
|
+
// The above copyright notice and this permission notice shall be included in all
|
11
|
+
// copies or substantial portions of the Software.
|
12
|
+
//
|
13
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
// SOFTWARE.
|
20
|
+
|
21
|
+
require("@nomicfoundation/hardhat-toolbox");
|
22
|
+
|
23
|
+
/** @type import("hardhat/config").HardhatUserConfig */
|
24
|
+
|
25
|
+
module.exports = {
|
26
|
+
solidity: "0.8.28",
|
27
|
+
defaultNetwork: "foo",
|
28
|
+
networks: {
|
29
|
+
hardhat: {
|
30
|
+
chainId: 4242,
|
31
|
+
gas: 10,
|
32
|
+
gasPrice: 4,
|
33
|
+
maxFeePerGas: 100,
|
34
|
+
maxPriorityFeePerGas: 100,
|
35
|
+
initialBaseFeePerGas: 100,
|
36
|
+
accounts: [
|
37
|
+
{
|
38
|
+
privateKey: "81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a",
|
39
|
+
balance: "55555555555555555555555"
|
40
|
+
},
|
41
|
+
{
|
42
|
+
privateKey: "91f9111b1744d55361e632771a4e53839e9442a9fef45febc0a5c838c686a15b",
|
43
|
+
balance: "66666666666666666666666"
|
44
|
+
}
|
45
|
+
]
|
46
|
+
},
|
47
|
+
foo: {
|
48
|
+
chainId: 4242,
|
49
|
+
url: "http://HOST:PORT"
|
50
|
+
}
|
51
|
+
}
|
52
|
+
};
|
@@ -0,0 +1,28 @@
|
|
1
|
+
// Copyright (c) 2025 Yegor Bugayenko
|
2
|
+
//
|
3
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
// of this software and associated documentation files (the 'Software'), to deal
|
5
|
+
// in the Software without restriction, including without limitation the rights
|
6
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
// copies of the Software, and to permit persons to whom the Software is
|
8
|
+
// furnished to do so, subject to the following conditions:
|
9
|
+
//
|
10
|
+
// The above copyright notice and this permission notice shall be included in all
|
11
|
+
// copies or substantial portions of the Software.
|
12
|
+
//
|
13
|
+
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
// SOFTWARE.
|
20
|
+
|
21
|
+
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
|
22
|
+
|
23
|
+
const FooModule = buildModule("FooModule", (m) => {
|
24
|
+
const foo = m.contract("Foo");
|
25
|
+
return { foo };
|
26
|
+
});
|
27
|
+
|
28
|
+
export default FooModule;
|
data/lib/erc20/wallet.rb
CHANGED
@@ -20,6 +20,10 @@
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
21
|
# SOFTWARE.
|
22
22
|
|
23
|
+
require 'eth'
|
24
|
+
require 'jsonrpc/client'
|
25
|
+
require 'loog'
|
26
|
+
require 'websocket-client-simple'
|
23
27
|
require_relative '../erc20'
|
24
28
|
|
25
29
|
# A wallet.
|
@@ -30,18 +34,133 @@ require_relative '../erc20'
|
|
30
34
|
# Copyright:: Copyright (c) 2025 Yegor Bugayenko
|
31
35
|
# License:: MIT
|
32
36
|
class ERC20::Wallet
|
37
|
+
# Address of USDT contract.
|
38
|
+
USDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'
|
39
|
+
|
33
40
|
# Constructor.
|
34
|
-
# @param [
|
35
|
-
# @param [String]
|
41
|
+
# @param [String] contract Hex of the contract in Etherium
|
42
|
+
# @param [String] rpc The URL of Etherium JSON-RPC provider
|
43
|
+
# @param [Integer] chain The ID of the chain (1 for mainnet)
|
36
44
|
# @param [Object] log The destination for logs
|
37
|
-
def initialize(
|
38
|
-
@
|
39
|
-
@
|
45
|
+
def initialize(contract: USDT, rpc: nil, wss: nil, chain: 1, log: $stdout)
|
46
|
+
@contract = contract
|
47
|
+
@rpc = rpc
|
48
|
+
@wss = wss
|
40
49
|
@log = log
|
50
|
+
@chain = chain
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get balance of a public address.
|
54
|
+
#
|
55
|
+
# @param [String] hex Public key, in hex, starting from '0x'
|
56
|
+
# @return [Integer] Balance, in
|
57
|
+
def balance(hex)
|
58
|
+
func = '70a08231' # balanceOf
|
59
|
+
padded = "000000000000000000000000#{hex[2..].downcase}"
|
60
|
+
data = "0x#{func}#{padded}"
|
61
|
+
r = jsonrpc.eth_call({ to: @contract, data: data }, 'latest')
|
62
|
+
r[2..].to_i(16)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Send a single payment from a private address to a public one.
|
66
|
+
#
|
67
|
+
# @param [String] priv Private key, in hex
|
68
|
+
# @param [String] address Public key, in hex
|
69
|
+
# @param [Integer] amount The amount of ERC20 tokens to send
|
70
|
+
# @param [Integer] gas_limit How much gas you are ready to spend
|
71
|
+
# @param [Integer] gas_price How much gas you pay per computation unit
|
72
|
+
# @return [String] Transaction hash
|
73
|
+
def pay(priv, address, amount, gas_limit: nil, gas_price: nil)
|
74
|
+
func = 'a9059cbb' # transfer(address,uint256)
|
75
|
+
to_clean = address.downcase.sub(/^0x/, '')
|
76
|
+
to_padded = ('0' * (64 - to_clean.size)) + to_clean
|
77
|
+
amt_hex = amount.to_s(16)
|
78
|
+
amt_padded = ('0' * (64 - amt_hex.size)) + amt_hex
|
79
|
+
data = "0x#{func}#{to_padded}#{amt_padded}"
|
80
|
+
key = Eth::Key.new(priv: priv)
|
81
|
+
from = key.address
|
82
|
+
nonce = jsonrpc.eth_getTransactionCount(from, 'pending').to_i(16)
|
83
|
+
tx = Eth::Tx.new(
|
84
|
+
{
|
85
|
+
nonce:,
|
86
|
+
gas_price: gas_price || gas_best_price,
|
87
|
+
gas_limit: gas_limit || gas_estimate(from, data),
|
88
|
+
to: @contract,
|
89
|
+
value: 0,
|
90
|
+
data: data,
|
91
|
+
chain_id: @chain
|
92
|
+
}
|
93
|
+
)
|
94
|
+
tx.sign(key)
|
95
|
+
jsonrpc.eth_sendRawTransaction("0x#{tx.hex}")
|
96
|
+
end
|
97
|
+
|
98
|
+
# Wait for incoming transactions and let the block know when they
|
99
|
+
# arrive. It's a blocking call, it's better to run it in a separate
|
100
|
+
# thread.
|
101
|
+
#
|
102
|
+
# @param [Array<String>] addresses Addresses to monitor
|
103
|
+
# @param [Array] ready When connected, TRUE will be added to this array
|
104
|
+
def accept(addresses, connected: [])
|
105
|
+
WebSocket::Client::Simple.connect(@wss) do |ws|
|
106
|
+
log = @log
|
107
|
+
contract = @contract
|
108
|
+
wss = @wss
|
109
|
+
ws.on(:open) do
|
110
|
+
log.debug("Connected to #{wss}")
|
111
|
+
msg = {
|
112
|
+
jsonrpc: '2.0',
|
113
|
+
id: 1,
|
114
|
+
method: 'eth_subscribe',
|
115
|
+
params: [
|
116
|
+
'logs',
|
117
|
+
{
|
118
|
+
address: contract,
|
119
|
+
topics: [
|
120
|
+
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
|
121
|
+
nil,
|
122
|
+
addresses.map { |a| "0x000000000000000000000000#{a[2..]}" }
|
123
|
+
]
|
124
|
+
}
|
125
|
+
]
|
126
|
+
}
|
127
|
+
ws.send(msg.to_json)
|
128
|
+
connected.append(1)
|
129
|
+
end
|
130
|
+
ws.on(:message) do |msg|
|
131
|
+
data =
|
132
|
+
begin
|
133
|
+
JSON.parse(msg.data)
|
134
|
+
rescue StandardError
|
135
|
+
{}
|
136
|
+
end
|
137
|
+
if data['method'] == 'eth_subscription' && data.dig('params', 'result')
|
138
|
+
event = data['params']['result']
|
139
|
+
log.debug("New transaction from: #{event['address']}")
|
140
|
+
yield event
|
141
|
+
end
|
142
|
+
end
|
143
|
+
ws.on(:close) do |_e|
|
144
|
+
log.debug("Disconnected from #{wss}")
|
145
|
+
end
|
146
|
+
ws.on(:error) do |e|
|
147
|
+
log.debug("Error at #{wss}: #{e}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
def jsonrpc
|
155
|
+
JSONRPC.logger = Loog::NULL
|
156
|
+
JSONRPC::Client.new(@rpc)
|
157
|
+
end
|
158
|
+
|
159
|
+
def gas_estimate(from, data)
|
160
|
+
jsonrpc.eth_estimateGas({ from:, to: @contract, data: }, 'latest').to_i(16)
|
41
161
|
end
|
42
162
|
|
43
|
-
|
44
|
-
|
45
|
-
'none'
|
163
|
+
def gas_best_price
|
164
|
+
jsonrpc.eth_getBlockByNumber('latest', false)['baseFeePerGas'].to_i(16)
|
46
165
|
end
|
47
166
|
end
|
data/lib/erc20.rb
CHANGED
data/test/erc20/test_wallet.rb
CHANGED
@@ -20,8 +20,14 @@
|
|
20
20
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
21
|
# SOFTWARE.
|
22
22
|
|
23
|
-
require '
|
23
|
+
require 'backtrace'
|
24
|
+
require 'donce'
|
25
|
+
require 'eth'
|
24
26
|
require 'loog'
|
27
|
+
require 'random-port'
|
28
|
+
require 'shellwords'
|
29
|
+
require 'typhoeus'
|
30
|
+
require 'minitest/autorun'
|
25
31
|
require_relative '../../lib/erc20'
|
26
32
|
require_relative '../../lib/erc20/wallet'
|
27
33
|
|
@@ -30,9 +36,162 @@ require_relative '../../lib/erc20/wallet'
|
|
30
36
|
# Copyright:: Copyright (c) 2025 Yegor Bugayenko
|
31
37
|
# License:: MIT
|
32
38
|
class TestWallet < Minitest::Test
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
39
|
+
# At this address, in the mainnet, there are a few USDT tokens. I won't
|
40
|
+
# move them anyway, that's why tests can use this address forever.
|
41
|
+
STABLE_ADDRESS = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
|
42
|
+
|
43
|
+
# One guy private hex.
|
44
|
+
JEFF = '81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a'
|
45
|
+
|
46
|
+
# Another guy private hex.
|
47
|
+
WALTER = '91f9111b1744d55361e632771a4e53839e9442a9fef45febc0a5c838c686a15b'
|
48
|
+
|
49
|
+
def test_checks_balance_on_mainnet
|
50
|
+
b = mainnet.balance(STABLE_ADDRESS)
|
51
|
+
refute_nil(b)
|
52
|
+
assert_equal(27_258_889, b)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_checks_balance_of_absent_address
|
56
|
+
a = '0xEB2fE8872A6f1eDb70a2632Effffffffffffffff'
|
57
|
+
b = mainnet.balance(a)
|
58
|
+
refute_nil(b)
|
59
|
+
assert_equal(0, b)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_fails_with_invalid_infura_key
|
63
|
+
w = ERC20::Wallet.new(
|
64
|
+
rpc: 'https://mainnet.infura.io/v3/invalid-key-here',
|
65
|
+
log: Loog::NULL
|
66
|
+
)
|
67
|
+
assert_raises(StandardError) { w.balance(STABLE_ADDRESS) }
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_checks_balance_on_testnet
|
71
|
+
skip('does not work')
|
72
|
+
b = testnet.balance(STABLE_ADDRESS)
|
73
|
+
refute_nil(b)
|
74
|
+
assert_predicate(b, :positive?)
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_checks_balance_on_hardhat
|
78
|
+
on_hardhat do |wallet|
|
79
|
+
b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
|
80
|
+
assert_equal(123_000_100_000, b)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_pays_on_hardhat
|
85
|
+
on_hardhat do |wallet|
|
86
|
+
to = Eth::Key.new(priv: WALTER).address.to_s
|
87
|
+
before = wallet.balance(to)
|
88
|
+
sum = 42_000
|
89
|
+
from = Eth::Key.new(priv: JEFF).address.to_s
|
90
|
+
assert_operator(wallet.balance(from), :>, sum * 2)
|
91
|
+
wallet.pay(JEFF, to, sum)
|
92
|
+
assert_equal(before + sum, wallet.balance(to))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_accepts_payments_on_hardhat
|
97
|
+
walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
|
98
|
+
jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
|
99
|
+
on_hardhat do |wallet|
|
100
|
+
connected = []
|
101
|
+
event = nil
|
102
|
+
daemon =
|
103
|
+
Thread.new do
|
104
|
+
wallet.accept([walter, jeff], connected:) do |e|
|
105
|
+
event = e
|
106
|
+
end
|
107
|
+
rescue StandardError => e
|
108
|
+
puts Backtrace.new(e)
|
109
|
+
end
|
110
|
+
wait_for { !connected.empty? }
|
111
|
+
sum = 77_000
|
112
|
+
wallet.pay(JEFF, walter, sum)
|
113
|
+
wait_for { !event.nil? }
|
114
|
+
daemon.kill
|
115
|
+
daemon.join(30)
|
116
|
+
assert_equal(sum, event['data'].to_i(16))
|
117
|
+
from = "0x#{event['topics'][1][26..].downcase}"
|
118
|
+
assert_equal(jeff, from)
|
119
|
+
to = "0x#{event['topics'][2][26..].downcase}"
|
120
|
+
assert_equal(walter, to)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def wait_for
|
127
|
+
start = Time.now
|
128
|
+
loop do
|
129
|
+
sleep(0.1)
|
130
|
+
break if yield
|
131
|
+
raise 'timeout' if Time.now - start > 15
|
132
|
+
rescue Errno::ECONNREFUSED
|
133
|
+
retry
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def wait_for_port(port)
|
138
|
+
wait_for { Typhoeus::Request.get("http://localhost:#{port}").code == 200 }
|
139
|
+
end
|
140
|
+
|
141
|
+
def env(var)
|
142
|
+
key = ENV.fetch(var, nil)
|
143
|
+
skip("The #{var} environment variable is not set") if key.nil?
|
144
|
+
key
|
145
|
+
end
|
146
|
+
|
147
|
+
def mainnet
|
148
|
+
[
|
149
|
+
"https://mainnet.infura.io/v3/#{env('INFURA_KEY')}",
|
150
|
+
"https://go.getblock.io/#{env('GETBLOCK_KEY')}"
|
151
|
+
].map do |rpc|
|
152
|
+
ERC20::Wallet.new(rpc:, log: Loog::NULL)
|
153
|
+
end.sample
|
154
|
+
end
|
155
|
+
|
156
|
+
def testnet
|
157
|
+
[
|
158
|
+
"https://sepolia.infura.io/v3/#{env('INFURA_KEY')}",
|
159
|
+
"https://go.getblock.io/#{env('GETBLOCK_SEPOILA_KEY')}"
|
160
|
+
].map do |rpc|
|
161
|
+
ERC20::Wallet.new(rpc:, log: Loog::NULL)
|
162
|
+
end.sample
|
163
|
+
end
|
164
|
+
|
165
|
+
def on_hardhat
|
166
|
+
RandomPort::Pool::SINGLETON.acquire do |port|
|
167
|
+
donce(
|
168
|
+
home: File.join(__dir__, '../../hardhat'),
|
169
|
+
ports: { port => 8545 },
|
170
|
+
command: 'npx hardhat node',
|
171
|
+
log: Loog::NULL
|
172
|
+
) do
|
173
|
+
wait_for_port(port)
|
174
|
+
cmd = [
|
175
|
+
'(cat hardhat.config.js)',
|
176
|
+
'(ls -al)',
|
177
|
+
'(echo y | npx hardhat ignition deploy ./ignition/modules/Foo.ts --network foo --deployment-id foo)',
|
178
|
+
'(npx hardhat ignition status foo | tail -1 | cut -d" " -f3)'
|
179
|
+
].join(' && ')
|
180
|
+
contract = donce(
|
181
|
+
home: File.join(__dir__, '../../hardhat'),
|
182
|
+
command: "/bin/bash -c #{Shellwords.escape(cmd)}",
|
183
|
+
build_args: { 'HOST' => donce_host, 'PORT' => port },
|
184
|
+
log: Loog::NULL,
|
185
|
+
root: true
|
186
|
+
).split("\n").last
|
187
|
+
wallet = ERC20::Wallet.new(
|
188
|
+
contract:, chain: 4242,
|
189
|
+
rpc: "http://localhost:#{port}",
|
190
|
+
wss: "ws://localhost:#{port}",
|
191
|
+
log: Loog::NULL
|
192
|
+
)
|
193
|
+
yield wallet
|
194
|
+
end
|
195
|
+
end
|
37
196
|
end
|
38
197
|
end
|
data/test/test__helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: erc20
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Yegor Bugayenko
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: backtrace
|
@@ -24,6 +24,48 @@ dependencies:
|
|
24
24
|
- - ">"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: eth
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: jsonrpc-client
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: loog
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
27
69
|
- !ruby/object:Gem::Dependency
|
28
70
|
name: tago
|
29
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +80,20 @@ dependencies:
|
|
38
80
|
- - ">"
|
39
81
|
- !ruby/object:Gem::Version
|
40
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: websocket-client-simple
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
41
97
|
description: A simple library for making ERC20 manipulations as easy as theycan be
|
42
98
|
for cryptocurrency newbies
|
43
99
|
email: yegor256@gmail.com
|
@@ -55,9 +111,11 @@ files:
|
|
55
111
|
- ".github/workflows/markdown-lint.yml"
|
56
112
|
- ".github/workflows/pdd.yml"
|
57
113
|
- ".github/workflows/rake.yml"
|
114
|
+
- ".github/workflows/shellcheck.yml"
|
58
115
|
- ".github/workflows/xcop.yml"
|
59
116
|
- ".github/workflows/yamllint.yml"
|
60
117
|
- ".gitignore"
|
118
|
+
- ".gitleaksignore"
|
61
119
|
- ".pdd"
|
62
120
|
- ".rubocop.yml"
|
63
121
|
- ".rultor.yml"
|
@@ -69,6 +127,12 @@ files:
|
|
69
127
|
- README.md
|
70
128
|
- Rakefile
|
71
129
|
- erc20.gemspec
|
130
|
+
- hardhat/.gitignore
|
131
|
+
- hardhat/Dockerfile
|
132
|
+
- hardhat/contracts/Foo.sol
|
133
|
+
- hardhat/hardhat.config.js
|
134
|
+
- hardhat/ignition/modules/Foo.ts
|
135
|
+
- hardhat/package.json
|
72
136
|
- lib/erc20.rb
|
73
137
|
- lib/erc20/wallet.rb
|
74
138
|
- renovate.json
|