erc20 0.0.1 → 0.0.3
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 -9
- data/README.md +42 -12
- data/Rakefile +1 -9
- data/erc20.gemspec +4 -1
- 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 +141 -8
- data/lib/erc20.rb +1 -1
- data/test/erc20/test_wallet.rb +162 -5
- data/test/test__helper.rb +4 -0
- metadata +53 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b1ea9500a3c12a4b5f84d2aa2101e16c3cfcf1fef9f0d8ccb95433aba9798cb
|
4
|
+
data.tar.gz: dcd57712fa1fd1adb99511cbc42cb1c2315e7178b9d90862bddfb632ec83b0fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dada085699673561fff510ba8d2071d983c36634b7a7bdf0232536ee4319469a4cd19d7f6b5b48ec6572ec974eeaa5fe192052210b985fb3a094bfbf5b97c8be
|
7
|
+
data.tar.gz: 306828e368f07f87274eeaf1439075bc11a7b858260997419df4620b94682156b3d77000d9d5245aba5397507a0de107339f4d13b354cf3291cf592437884a69
|
@@ -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: 100
|
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,10 @@ PATH
|
|
3
3
|
specs:
|
4
4
|
erc20 (0.0.0)
|
5
5
|
backtrace (> 0)
|
6
|
-
|
6
|
+
eth (~> 0.4)
|
7
|
+
jsonrpc-client (> 0)
|
8
|
+
loog (> 0)
|
9
|
+
websocket-client-simple (> 0)
|
7
10
|
|
8
11
|
GEM
|
9
12
|
remote: https://rubygems.org/
|
@@ -50,8 +53,39 @@ GEM
|
|
50
53
|
date (3.4.1)
|
51
54
|
diff-lcs (1.5.1)
|
52
55
|
docile (1.4.1)
|
56
|
+
donce (0.1.0)
|
57
|
+
backtrace (> 0)
|
58
|
+
os (> 0)
|
59
|
+
qbash (> 0)
|
53
60
|
drb (2.2.1)
|
61
|
+
elapsed (0.0.1)
|
62
|
+
loog (> 0)
|
63
|
+
tago (> 0)
|
54
64
|
erubi (1.13.1)
|
65
|
+
eth (0.5.13)
|
66
|
+
forwardable (~> 1.3)
|
67
|
+
keccak (~> 1.3)
|
68
|
+
konstructor (~> 1.0)
|
69
|
+
openssl (>= 2.2, < 4.0)
|
70
|
+
rbsecp256k1 (~> 6.0)
|
71
|
+
scrypt (~> 3.0)
|
72
|
+
ethon (0.16.0)
|
73
|
+
ffi (>= 1.15.0)
|
74
|
+
event_emitter (0.2.6)
|
75
|
+
faraday (2.12.2)
|
76
|
+
faraday-net_http (>= 2.0, < 3.5)
|
77
|
+
json
|
78
|
+
logger
|
79
|
+
faraday-net_http (3.4.0)
|
80
|
+
net-http (>= 0.5.0)
|
81
|
+
ffi (1.17.1-arm64-darwin)
|
82
|
+
ffi (1.17.1-x64-mingw-ucrt)
|
83
|
+
ffi (1.17.1-x86_64-darwin)
|
84
|
+
ffi (1.17.1-x86_64-linux-gnu)
|
85
|
+
ffi-compiler (1.3.2)
|
86
|
+
ffi (>= 1.15.5)
|
87
|
+
rake
|
88
|
+
forwardable (1.3.3)
|
55
89
|
i18n (1.14.7)
|
56
90
|
concurrent-ruby (~> 1.0)
|
57
91
|
io-console (0.8.0)
|
@@ -59,19 +93,31 @@ GEM
|
|
59
93
|
pp (>= 0.6.0)
|
60
94
|
rdoc (>= 4.0.0)
|
61
95
|
reline (>= 0.4.2)
|
62
|
-
json (2.
|
96
|
+
json (2.10.0)
|
97
|
+
jsonrpc-client (0.1.4)
|
98
|
+
faraday
|
99
|
+
multi_json (>= 1.1.0)
|
100
|
+
keccak (1.3.2)
|
101
|
+
konstructor (1.0.2)
|
63
102
|
language_server-protocol (3.17.0.4)
|
64
103
|
logger (1.6.5)
|
65
104
|
loofah (2.24.0)
|
66
105
|
crass (~> 1.0.2)
|
67
106
|
nokogiri (>= 1.12.0)
|
68
107
|
loog (0.6.0)
|
108
|
+
mini_portile2 (2.8.8)
|
69
109
|
minitest (5.25.4)
|
70
110
|
minitest-reporters (1.7.1)
|
71
111
|
ansi
|
72
112
|
builder
|
73
113
|
minitest (>= 5.0)
|
74
114
|
ruby-progressbar
|
115
|
+
minitest-retry (0.2.5)
|
116
|
+
minitest (>= 5.0)
|
117
|
+
multi_json (1.15.0)
|
118
|
+
mutex_m (0.3.0)
|
119
|
+
net-http (0.6.0)
|
120
|
+
uri
|
75
121
|
nokogiri (1.18.2-arm64-darwin)
|
76
122
|
racc (~> 1.4)
|
77
123
|
nokogiri (1.18.2-x64-mingw-ucrt)
|
@@ -80,16 +126,24 @@ GEM
|
|
80
126
|
racc (~> 1.4)
|
81
127
|
nokogiri (1.18.2-x86_64-linux-gnu)
|
82
128
|
racc (~> 1.4)
|
129
|
+
openssl (3.3.0)
|
130
|
+
os (1.1.4)
|
83
131
|
parallel (1.26.3)
|
84
|
-
parser (3.3.7.
|
132
|
+
parser (3.3.7.1)
|
85
133
|
ast (~> 2.4.1)
|
86
134
|
racc
|
135
|
+
pkg-config (1.5.9)
|
87
136
|
pp (0.6.2)
|
88
137
|
prettyprint
|
89
138
|
prettyprint (0.2.0)
|
90
139
|
psych (5.2.3)
|
91
140
|
date
|
92
141
|
stringio
|
142
|
+
qbash (0.4.0)
|
143
|
+
backtrace (> 0)
|
144
|
+
elapsed (> 0)
|
145
|
+
loog (> 0)
|
146
|
+
tago (> 0)
|
93
147
|
racc (1.8.1)
|
94
148
|
rack (3.1.9)
|
95
149
|
rack-session (2.1.0)
|
@@ -116,13 +170,19 @@ GEM
|
|
116
170
|
zeitwerk (~> 2.6)
|
117
171
|
rainbow (3.1.1)
|
118
172
|
rake (13.2.1)
|
119
|
-
|
173
|
+
random-port (0.7.5)
|
174
|
+
tago (> 0)
|
175
|
+
rbsecp256k1 (6.0.0)
|
176
|
+
mini_portile2 (~> 2.8)
|
177
|
+
pkg-config (~> 1.5)
|
178
|
+
rubyzip (~> 2.3)
|
179
|
+
rdoc (6.12.0)
|
120
180
|
psych (>= 4.0.0)
|
121
181
|
regexp_parser (2.10.0)
|
122
182
|
reline (0.6.0)
|
123
183
|
io-console (~> 0.5)
|
124
184
|
rexml (3.4.0)
|
125
|
-
rspec-core (3.13.
|
185
|
+
rspec-core (3.13.3)
|
126
186
|
rspec-support (~> 3.13.0)
|
127
187
|
rspec-expectations (3.13.3)
|
128
188
|
diff-lcs (>= 1.2.0, < 2.0)
|
@@ -130,7 +190,7 @@ GEM
|
|
130
190
|
rspec-mocks (3.13.2)
|
131
191
|
diff-lcs (>= 1.2.0, < 2.0)
|
132
192
|
rspec-support (~> 3.13.0)
|
133
|
-
rspec-rails (7.1.
|
193
|
+
rspec-rails (7.1.1)
|
134
194
|
actionpack (>= 7.0)
|
135
195
|
activesupport (>= 7.0)
|
136
196
|
railties (>= 7.0)
|
@@ -139,7 +199,7 @@ GEM
|
|
139
199
|
rspec-mocks (~> 3.13)
|
140
200
|
rspec-support (~> 3.13)
|
141
201
|
rspec-support (3.13.2)
|
142
|
-
rubocop (1.71.
|
202
|
+
rubocop (1.71.2)
|
143
203
|
json (~> 2.3)
|
144
204
|
language_server-protocol (>= 3.17.0)
|
145
205
|
parallel (~> 1.10)
|
@@ -157,9 +217,15 @@ GEM
|
|
157
217
|
rubocop-performance (1.23.1)
|
158
218
|
rubocop (>= 1.48.1, < 2.0)
|
159
219
|
rubocop-ast (>= 1.31.1, < 2.0)
|
220
|
+
rubocop-rake (0.6.0)
|
221
|
+
rubocop (~> 1.0)
|
160
222
|
rubocop-rspec (3.4.0)
|
161
223
|
rubocop (~> 1.61)
|
162
224
|
ruby-progressbar (1.13.0)
|
225
|
+
rubyzip (2.4.1)
|
226
|
+
scrypt (3.0.8)
|
227
|
+
ffi-compiler (>= 1.0, < 2.0)
|
228
|
+
rake (>= 9, < 14)
|
163
229
|
securerandom (0.4.1)
|
164
230
|
simplecov (0.22.0)
|
165
231
|
docile (~> 1.1)
|
@@ -176,6 +242,8 @@ GEM
|
|
176
242
|
threads (0.4.1)
|
177
243
|
backtrace (~> 0)
|
178
244
|
concurrent-ruby (~> 1.0)
|
245
|
+
typhoeus (1.4.1)
|
246
|
+
ethon (>= 0.9.0)
|
179
247
|
tzinfo (2.0.6)
|
180
248
|
concurrent-ruby (~> 1.0)
|
181
249
|
unicode-display_width (3.1.4)
|
@@ -183,6 +251,12 @@ GEM
|
|
183
251
|
unicode-emoji (4.0.4)
|
184
252
|
uri (1.0.2)
|
185
253
|
useragent (0.16.11)
|
254
|
+
websocket (1.2.11)
|
255
|
+
websocket-client-simple (0.9.0)
|
256
|
+
base64
|
257
|
+
event_emitter
|
258
|
+
mutex_m
|
259
|
+
websocket
|
186
260
|
yard (0.9.37)
|
187
261
|
zeitwerk (2.7.1)
|
188
262
|
|
@@ -196,19 +270,26 @@ PLATFORMS
|
|
196
270
|
x86_64-linux
|
197
271
|
|
198
272
|
DEPENDENCIES
|
273
|
+
backtrace (> 0)
|
274
|
+
donce (> 0)
|
199
275
|
erc20!
|
200
276
|
loog (> 0)
|
201
277
|
minitest (= 5.25.4)
|
202
278
|
minitest-reporters (= 1.7.1)
|
279
|
+
minitest-retry (= 0.2.5)
|
280
|
+
qbash (> 0)
|
203
281
|
rake (= 13.2.1)
|
204
|
-
|
205
|
-
|
282
|
+
random-port (> 0)
|
283
|
+
rspec-rails (= 7.1.1)
|
284
|
+
rubocop (= 1.71.2)
|
206
285
|
rubocop-minitest (= 0.36.0)
|
207
286
|
rubocop-performance (= 1.23.1)
|
287
|
+
rubocop-rake (> 0)
|
208
288
|
rubocop-rspec (= 3.4.0)
|
209
289
|
simplecov (= 0.22.0)
|
210
290
|
simplecov-cobertura (= 2.1.0)
|
211
291
|
threads (= 0.4.1)
|
292
|
+
typhoeus (> 0)
|
212
293
|
yard (= 0.9.37)
|
213
294
|
|
214
295
|
BUNDLED WITH
|
data/README.md
CHANGED
@@ -11,27 +11,49 @@
|
|
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
|
15
|
-
|
14
|
+
This small Ruby [gem](https://rubygems.org/gems/erc20)
|
15
|
+
makes manipulations with [Etherium] [ERC20] tokens
|
16
|
+
as simple as they can be, if you have a provider of
|
17
|
+
[JSON-RPC] and [WebSocket] Etherium APIs, for example
|
18
|
+
[Infura], [GetBlock], or [Alchemy]:
|
16
19
|
|
17
20
|
```ruby
|
18
|
-
# List of private keys:
|
19
|
-
keys = ['...', '...']
|
20
|
-
|
21
21
|
# Create a wallet:
|
22
|
-
|
22
|
+
require 'erc20'
|
23
|
+
w = ERC20::Wallet.new(
|
24
|
+
contract: ERC20::Wallet.USDT, # hex of it
|
25
|
+
rpc: 'https://mainnet.infura.io/v3/<your-key>',
|
26
|
+
wss: 'wss://mainnet.infura.io/v3/<your-key>',
|
27
|
+
log: $stdout
|
28
|
+
)
|
23
29
|
|
24
|
-
#
|
25
|
-
|
30
|
+
# Check balance on the address:
|
31
|
+
usdt = w.balance(address)
|
26
32
|
|
27
|
-
# Send a few tokens to someone:
|
28
|
-
w.
|
33
|
+
# Send a few tokens to someone and get transaction hash:
|
34
|
+
txn = w.pay(private_key, to_address, amount)
|
29
35
|
|
30
36
|
# Stay waiting, and trigger the block when transactions arrive:
|
31
|
-
|
37
|
+
addresses = ['0x...', '0x...']
|
38
|
+
w.accept(addresses) do |event|
|
39
|
+
puts event[:amount] # how much
|
40
|
+
puts event[:from] # who sent the payment
|
41
|
+
puts event[:to] # who was the receiver
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
To generate a new private key, use [eth](https://rubygems.org/gems/eth):
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'eth'
|
49
|
+
key = Eth::Key.new.private_hex
|
32
50
|
```
|
33
51
|
|
34
|
-
|
52
|
+
To get address from private one:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
public_hex = Eth::Key.new(priv: key).address
|
56
|
+
```
|
35
57
|
|
36
58
|
## How to contribute
|
37
59
|
|
@@ -48,3 +70,11 @@ bundle exec rake
|
|
48
70
|
```
|
49
71
|
|
50
72
|
If it's clean and you don't see any error messages, submit your pull request.
|
73
|
+
|
74
|
+
[Etherium]: https://en.wikipedia.org/wiki/Ethereum
|
75
|
+
[ERC20]: https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
|
76
|
+
[JSON-RPC]: https://ethereum.org/en/developers/docs/apis/json-rpc/
|
77
|
+
[Websocket]: https://ethereum.org/en/developers/tutorials/using-websockets/
|
78
|
+
[Infura]: https://infura.io/
|
79
|
+
[Alchemy]: https://alchemy.com/
|
80
|
+
[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,9 @@ 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 '
|
43
|
+
s.add_dependency 'eth', '~>0.4'
|
44
|
+
s.add_dependency 'jsonrpc-client', '>0'
|
45
|
+
s.add_dependency 'loog', '>0'
|
46
|
+
s.add_dependency 'websocket-client-simple', '>0'
|
44
47
|
s.metadata['rubygems_mfa_required'] = 'true'
|
45
48
|
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,147 @@ 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
|
+
b = r[2..].to_i(16)
|
63
|
+
@log.debug("Balance of #{hex} is #{b}")
|
64
|
+
b
|
65
|
+
end
|
66
|
+
|
67
|
+
# Send a single payment from a private address to a public one.
|
68
|
+
#
|
69
|
+
# @param [String] priv Private key, in hex
|
70
|
+
# @param [String] address Public key, in hex
|
71
|
+
# @param [Integer] amount The amount of ERC20 tokens to send
|
72
|
+
# @param [Integer] gas_limit How much gas you are ready to spend
|
73
|
+
# @param [Integer] gas_price How much gas you pay per computation unit
|
74
|
+
# @return [String] Transaction hash
|
75
|
+
def pay(priv, address, amount, gas_limit: nil, gas_price: nil)
|
76
|
+
func = 'a9059cbb' # transfer(address,uint256)
|
77
|
+
to_clean = address.downcase.sub(/^0x/, '')
|
78
|
+
to_padded = ('0' * (64 - to_clean.size)) + to_clean
|
79
|
+
amt_hex = amount.to_s(16)
|
80
|
+
amt_padded = ('0' * (64 - amt_hex.size)) + amt_hex
|
81
|
+
data = "0x#{func}#{to_padded}#{amt_padded}"
|
82
|
+
key = Eth::Key.new(priv: priv)
|
83
|
+
from = key.address
|
84
|
+
nonce = jsonrpc.eth_getTransactionCount(from, 'pending').to_i(16)
|
85
|
+
tx = Eth::Tx.new(
|
86
|
+
{
|
87
|
+
nonce:,
|
88
|
+
gas_price: gas_price || gas_best_price,
|
89
|
+
gas_limit: gas_limit || gas_estimate(from, data),
|
90
|
+
to: @contract,
|
91
|
+
value: 0,
|
92
|
+
data: data,
|
93
|
+
chain_id: @chain
|
94
|
+
}
|
95
|
+
)
|
96
|
+
tx.sign(key)
|
97
|
+
hex = "0x#{tx.hex}"
|
98
|
+
jsonrpc.eth_sendRawTransaction(hex)
|
99
|
+
@log.debug("Sent #{amount} from #{from} to #{address}: #{hex}")
|
100
|
+
hex
|
101
|
+
end
|
102
|
+
|
103
|
+
# Wait for incoming transactions and let the block know when they
|
104
|
+
# arrive. It's a blocking call, it's better to run it in a separate
|
105
|
+
# thread.
|
106
|
+
#
|
107
|
+
# @param [Array<String>] addresses Addresses to monitor
|
108
|
+
# @param [Array] ready When connected, TRUE will be added to this array
|
109
|
+
# @param [Boolean] raw TRUE if you need to get JSON events as they arrive from Websockets
|
110
|
+
def accept(addresses, connected: [], raw: false)
|
111
|
+
WebSocket::Client::Simple.connect(@wss) do |ws|
|
112
|
+
log = @log
|
113
|
+
contract = @contract
|
114
|
+
wss = @wss
|
115
|
+
ws.on(:open) do
|
116
|
+
log.debug("Connected to #{wss}")
|
117
|
+
msg = {
|
118
|
+
jsonrpc: '2.0',
|
119
|
+
id: 1,
|
120
|
+
method: 'eth_subscribe',
|
121
|
+
params: [
|
122
|
+
'logs',
|
123
|
+
{
|
124
|
+
address: contract,
|
125
|
+
topics: [
|
126
|
+
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
|
127
|
+
nil,
|
128
|
+
addresses.map { |a| "0x000000000000000000000000#{a[2..]}" }
|
129
|
+
]
|
130
|
+
}
|
131
|
+
]
|
132
|
+
}
|
133
|
+
ws.send(msg.to_json)
|
134
|
+
connected.append(1)
|
135
|
+
log.debug("Subscribed to #{addresses.count} addresses")
|
136
|
+
end
|
137
|
+
ws.on(:message) do |msg|
|
138
|
+
data =
|
139
|
+
begin
|
140
|
+
JSON.parse(msg.data)
|
141
|
+
rescue StandardError
|
142
|
+
{}
|
143
|
+
end
|
144
|
+
if data['method'] == 'eth_subscription' && data.dig('params', 'result')
|
145
|
+
event = data['params']['result']
|
146
|
+
unless raw
|
147
|
+
event = {
|
148
|
+
amount: event['data'].to_i(16),
|
149
|
+
from: "0x#{event['topics'][1][26..].downcase}",
|
150
|
+
to: "0x#{event['topics'][2][26..].downcase}"
|
151
|
+
}
|
152
|
+
end
|
153
|
+
log.debug("New event arrived from #{event['address']}")
|
154
|
+
yield event
|
155
|
+
end
|
156
|
+
end
|
157
|
+
ws.on(:close) do |_e|
|
158
|
+
log.debug("Disconnected from #{wss}")
|
159
|
+
end
|
160
|
+
ws.on(:error) do |e|
|
161
|
+
log.debug("Error at #{wss}: #{e}")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def jsonrpc
|
169
|
+
JSONRPC.logger = Loog::NULL
|
170
|
+
JSONRPC::Client.new(@rpc)
|
171
|
+
end
|
172
|
+
|
173
|
+
def gas_estimate(from, data)
|
174
|
+
jsonrpc.eth_estimateGas({ from:, to: @contract, data: }, 'latest').to_i(16)
|
41
175
|
end
|
42
176
|
|
43
|
-
|
44
|
-
|
45
|
-
'none'
|
177
|
+
def gas_best_price
|
178
|
+
jsonrpc.eth_getBlockByNumber('latest', false)['baseFeePerGas'].to_i(16)
|
46
179
|
end
|
47
180
|
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,160 @@ 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[:amount])
|
117
|
+
assert_equal(jeff, event[:from])
|
118
|
+
assert_equal(walter, event[:to])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def wait_for
|
125
|
+
start = Time.now
|
126
|
+
loop do
|
127
|
+
sleep(0.1)
|
128
|
+
break if yield
|
129
|
+
raise 'timeout' if Time.now - start > 60
|
130
|
+
rescue Errno::ECONNREFUSED
|
131
|
+
retry
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def wait_for_port(port)
|
136
|
+
wait_for { Typhoeus::Request.get("http://localhost:#{port}").code == 200 }
|
137
|
+
end
|
138
|
+
|
139
|
+
def env(var)
|
140
|
+
key = ENV.fetch(var, nil)
|
141
|
+
skip("The #{var} environment variable is not set") if key.nil?
|
142
|
+
key
|
143
|
+
end
|
144
|
+
|
145
|
+
def mainnet
|
146
|
+
[
|
147
|
+
"https://mainnet.infura.io/v3/#{env('INFURA_KEY')}",
|
148
|
+
"https://go.getblock.io/#{env('GETBLOCK_KEY')}"
|
149
|
+
].map do |rpc|
|
150
|
+
ERC20::Wallet.new(rpc:, log: Loog::NULL)
|
151
|
+
end.sample
|
152
|
+
end
|
153
|
+
|
154
|
+
def testnet
|
155
|
+
[
|
156
|
+
"https://sepolia.infura.io/v3/#{env('INFURA_KEY')}",
|
157
|
+
"https://go.getblock.io/#{env('GETBLOCK_SEPOILA_KEY')}"
|
158
|
+
].map do |rpc|
|
159
|
+
ERC20::Wallet.new(rpc:, log: Loog::NULL)
|
160
|
+
end.sample
|
161
|
+
end
|
162
|
+
|
163
|
+
def on_hardhat
|
164
|
+
RandomPort::Pool::SINGLETON.acquire do |port|
|
165
|
+
donce(
|
166
|
+
home: File.join(__dir__, '../../hardhat'),
|
167
|
+
ports: { port => 8545 },
|
168
|
+
command: 'npx hardhat node',
|
169
|
+
log: Loog::NULL
|
170
|
+
) do
|
171
|
+
wait_for_port(port)
|
172
|
+
cmd = [
|
173
|
+
'(cat hardhat.config.js)',
|
174
|
+
'(ls -al)',
|
175
|
+
'(echo y | npx hardhat ignition deploy ./ignition/modules/Foo.ts --network foo --deployment-id foo)',
|
176
|
+
'(npx hardhat ignition status foo | tail -1 | cut -d" " -f3)'
|
177
|
+
].join(' && ')
|
178
|
+
contract = donce(
|
179
|
+
home: File.join(__dir__, '../../hardhat'),
|
180
|
+
command: "/bin/bash -c #{Shellwords.escape(cmd)}",
|
181
|
+
build_args: { 'HOST' => donce_host, 'PORT' => port },
|
182
|
+
log: Loog::NULL,
|
183
|
+
root: true
|
184
|
+
).split("\n").last
|
185
|
+
wallet = ERC20::Wallet.new(
|
186
|
+
contract:, chain: 4242,
|
187
|
+
rpc: "http://localhost:#{port}",
|
188
|
+
wss: "ws://localhost:#{port}",
|
189
|
+
log: Loog::NULL
|
190
|
+
)
|
191
|
+
yield wallet
|
192
|
+
end
|
193
|
+
end
|
37
194
|
end
|
38
195
|
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.3
|
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
|
@@ -25,7 +25,49 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
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'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: websocket-client-simple
|
29
71
|
requirement: !ruby/object:Gem::Requirement
|
30
72
|
requirements:
|
31
73
|
- - ">"
|
@@ -55,9 +97,11 @@ files:
|
|
55
97
|
- ".github/workflows/markdown-lint.yml"
|
56
98
|
- ".github/workflows/pdd.yml"
|
57
99
|
- ".github/workflows/rake.yml"
|
100
|
+
- ".github/workflows/shellcheck.yml"
|
58
101
|
- ".github/workflows/xcop.yml"
|
59
102
|
- ".github/workflows/yamllint.yml"
|
60
103
|
- ".gitignore"
|
104
|
+
- ".gitleaksignore"
|
61
105
|
- ".pdd"
|
62
106
|
- ".rubocop.yml"
|
63
107
|
- ".rultor.yml"
|
@@ -69,6 +113,12 @@ files:
|
|
69
113
|
- README.md
|
70
114
|
- Rakefile
|
71
115
|
- erc20.gemspec
|
116
|
+
- hardhat/.gitignore
|
117
|
+
- hardhat/Dockerfile
|
118
|
+
- hardhat/contracts/Foo.sol
|
119
|
+
- hardhat/hardhat.config.js
|
120
|
+
- hardhat/ignition/modules/Foo.ts
|
121
|
+
- hardhat/package.json
|
72
122
|
- lib/erc20.rb
|
73
123
|
- lib/erc20/wallet.rb
|
74
124
|
- renovate.json
|