erc20 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01ef1fcd61cd21540d932e28c65ed8afa08891e0cc20b498202eb146809d4993
4
- data.tar.gz: c8e4593fb968dcfbad8e27d24aa974a37946b444eb42b4022139c48c51ffc5ca
3
+ metadata.gz: 6e76d55c6807ddcd06e895bc54cd2edd16ad079b48131eaac1b118005f1b1d5b
4
+ data.tar.gz: 2bd2c637aa1e05d82695eb6581fed32992aabb45ed47dec179b78879380d1869
5
5
  SHA512:
6
- metadata.gz: 38e4e84ea00f176c34a9f3f15ff0e77473f31fb29196b72d7e7b70af38e7aa8877015ff3249822b55665dfadac2cecb909a46d4c62554bcf5c85d8e1a1d30744
7
- data.tar.gz: ffc78c5dfe3817bb59481f6e27e5e5fe7539ffca1e931fe8752fa31e98fee9e4c1880a2d3ce40496549c16b764524c0fad70c147d6f8a7467d8f1ba9c990d6dc
6
+ metadata.gz: d9e574760a4e257e5b8e5131b25a8af9cf5b7dcbb4e32e03093f50114cc84d308faa691032d155e6ebe291c4ce4ad838f0253e46942a8b146b324ef6fe72f88d
7
+ data.tar.gz: fad0c35c9e011414a49cc727d5ff3321b74a4c6424463d7f5c947c74c73d0f8e1e22b91702d5341fea0988c6c5b5ed31ee3e625675393cbf7165e4163f0aa70a
@@ -0,0 +1,16 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
4
+ name: hadolint
5
+ 'on':
6
+ push:
7
+ pull_request:
8
+ jobs:
9
+ hadolint:
10
+ timeout-minutes: 15
11
+ runs-on: ubuntu-24.04
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: hadolint/hadolint-action@v3.1.0
15
+ with:
16
+ dockerfile: hardhat/Dockerfile
@@ -0,0 +1,19 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
4
+ # yamllint disable rule:line-length
5
+ name: typos
6
+ 'on':
7
+ push:
8
+ branches:
9
+ - master
10
+ pull_request:
11
+ branches:
12
+ - master
13
+ jobs:
14
+ typos:
15
+ timeout-minutes: 15
16
+ runs-on: ubuntu-24.04
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: crate-ci/typos@v1.32.0
data/.gitignore CHANGED
@@ -1,8 +1,8 @@
1
- *.gem
2
- .DS_Store
3
1
  .bundle/
2
+ .DS_Store
4
3
  .idea/
5
4
  .yardoc/
5
+ *.gem
6
6
  coverage/
7
7
  doc/
8
8
  node_modules/
data/Gemfile CHANGED
@@ -9,13 +9,12 @@ gemspec
9
9
  gem 'backtrace', '>0', require: false
10
10
  gem 'concurrent-ruby', '~>1.2', require: false
11
11
  gem 'cucumber', '~>9.2', require: false
12
- gem 'donce', '>=0.2.3', require: false
12
+ gem 'donce', '>0', require: false
13
13
  gem 'faraday', '>0', require: false
14
14
  gem 'loog', '>0', require: false
15
15
  gem 'minitest', '~>5.25', require: false
16
16
  gem 'minitest-reporters', '~>1.7', require: false
17
17
  gem 'minitest-retry', '~>0.2', require: false
18
- gem 'os', '>0', require: false
19
18
  gem 'qbash', '>0', require: false
20
19
  gem 'rake', '~>13.2', require: false
21
20
  gem 'random-port', '>0', require: false
@@ -23,6 +22,7 @@ gem 'rubocop', '~>1.75', require: false
23
22
  gem 'rubocop-minitest', '>0', require: false
24
23
  gem 'rubocop-performance', '>0', require: false
25
24
  gem 'rubocop-rake', '>0', require: false
25
+ gem 'rubocop-rspec', '>0', require: false
26
26
  gem 'simplecov', '~>0.22', require: false
27
27
  gem 'simplecov-cobertura', '~>2.1', require: false
28
28
  gem 'threads', '~>0.4', require: false
data/Gemfile.lock CHANGED
@@ -49,9 +49,9 @@ GEM
49
49
  cucumber-messages (> 19, < 28)
50
50
  cucumber-messages (22.0.0)
51
51
  cucumber-tag-expressions (6.1.2)
52
- diff-lcs (1.6.1)
52
+ diff-lcs (1.6.2)
53
53
  docile (1.4.1)
54
- donce (0.2.3)
54
+ donce (0.2.4)
55
55
  backtrace (~> 0.3)
56
56
  os (~> 1.1)
57
57
  qbash (~> 0.3)
@@ -69,7 +69,7 @@ GEM
69
69
  ethon (0.16.0)
70
70
  ffi (>= 1.15.0)
71
71
  eventmachine (1.2.7)
72
- faraday (2.13.0)
72
+ faraday (2.13.1)
73
73
  faraday-net_http (>= 2.0, < 3.5)
74
74
  json
75
75
  logger
@@ -87,18 +87,19 @@ GEM
87
87
  rake
88
88
  forwardable (1.3.3)
89
89
  hashdiff (1.1.2)
90
- json (2.10.2)
90
+ json (2.12.0)
91
91
  jsonrpc-client (0.1.4)
92
92
  faraday
93
93
  multi_json (>= 1.1.0)
94
94
  keccak (1.3.2)
95
95
  konstructor (1.0.2)
96
- language_server-protocol (3.17.0.4)
96
+ language_server-protocol (3.17.0.5)
97
97
  lint_roller (1.1.0)
98
98
  logger (1.7.0)
99
- loog (0.6.0)
99
+ loog (0.6.1)
100
+ logger (~> 1.0)
100
101
  mini_mime (1.1.5)
101
- mini_portile2 (2.8.8)
102
+ mini_portile2 (2.8.9)
102
103
  minitest (5.25.5)
103
104
  minitest-reporters (1.7.1)
104
105
  ansi
@@ -117,10 +118,10 @@ GEM
117
118
  parser (3.3.8.0)
118
119
  ast (~> 2.4.1)
119
120
  racc
120
- pkg-config (1.6.1)
121
+ pkg-config (1.6.2)
121
122
  prism (1.4.0)
122
- public_suffix (6.0.1)
123
- qbash (0.4.0)
123
+ public_suffix (6.0.2)
124
+ qbash (0.4.5)
124
125
  backtrace (> 0)
125
126
  elapsed (> 0)
126
127
  loog (> 0)
@@ -136,7 +137,7 @@ GEM
136
137
  rubyzip (~> 2.3)
137
138
  regexp_parser (2.10.0)
138
139
  rexml (3.4.1)
139
- rubocop (1.75.3)
140
+ rubocop (1.75.5)
140
141
  json (~> 2.3)
141
142
  language_server-protocol (~> 3.17.0.2)
142
143
  lint_roller (~> 1.1.0)
@@ -161,6 +162,9 @@ GEM
161
162
  rubocop-rake (0.7.1)
162
163
  lint_roller (~> 1.1)
163
164
  rubocop (>= 1.72.1)
165
+ rubocop-rspec (3.6.0)
166
+ lint_roller (~> 1.1)
167
+ rubocop (~> 1.72, >= 1.72.1)
164
168
  ruby-progressbar (1.13.0)
165
169
  rubyzip (2.4.1)
166
170
  scrypt (3.0.8)
@@ -211,14 +215,13 @@ DEPENDENCIES
211
215
  backtrace (> 0)
212
216
  concurrent-ruby (~> 1.2)
213
217
  cucumber (~> 9.2)
214
- donce (>= 0.2.3)
218
+ donce (> 0)
215
219
  erc20!
216
220
  faraday (> 0)
217
221
  loog (> 0)
218
222
  minitest (~> 5.25)
219
223
  minitest-reporters (~> 1.7)
220
224
  minitest-retry (~> 0.2)
221
- os (> 0)
222
225
  qbash (> 0)
223
226
  rake (~> 13.2)
224
227
  random-port (> 0)
@@ -226,6 +229,7 @@ DEPENDENCIES
226
229
  rubocop-minitest (> 0)
227
230
  rubocop-performance (> 0)
228
231
  rubocop-rake (> 0)
232
+ rubocop-rspec (> 0)
229
233
  simplecov (~> 0.22)
230
234
  simplecov-cobertura (~> 2.1)
231
235
  threads (~> 0.4)
data/REUSE.toml CHANGED
@@ -4,10 +4,19 @@
4
4
  version = 1
5
5
  [[annotations]]
6
6
  path = [
7
+ ".DS_Store",
8
+ ".gitattributes",
9
+ ".gitignore",
10
+ ".gitleaksignore",
11
+ ".pdd",
7
12
  "**.json",
8
13
  "**.md",
9
14
  "**.png",
10
15
  "**.txt",
16
+ "**/.DS_Store",
17
+ "**/.gitignore",
18
+ "**/.gitleaksignore",
19
+ "**/.pdd",
11
20
  "**/*.csv",
12
21
  "**/*.jpg",
13
22
  "**/*.json",
@@ -17,23 +26,14 @@ path = [
17
26
  "**/*.svg",
18
27
  "**/*.txt",
19
28
  "**/*.vm",
20
- "**/.DS_Store",
21
- "**/.gitignore",
22
- "**/.gitleaksignore",
23
- "**/.pdd",
24
- "**/CNAME",
25
29
  "**/Cargo.toml",
30
+ "**/CNAME",
26
31
  "**/Gemfile.lock",
27
- ".DS_Store",
28
- ".gitattributes",
29
- ".gitignore",
30
- ".gitleaksignore",
31
- ".pdd",
32
32
  "Cargo.toml",
33
33
  "Gemfile.lock",
34
- "README.md",
35
34
  "hardhat/.gitignore",
36
35
  "hardhat/package.json",
36
+ "README.md",
37
37
  "renovate.json",
38
38
  ]
39
39
  precedence = "override"
data/hardhat/Dockerfile CHANGED
@@ -14,14 +14,12 @@ RUN npm install
14
14
  ARG PORT=8080
15
15
  ARG HOST=localhost
16
16
  COPY hardhat.config.js .
17
- RUN sed -i "s/PORT/$PORT/g" hardhat.config.js
18
- RUN sed -i "s/HOST/$HOST/g" hardhat.config.js
17
+ RUN sed -i "s/PORT/$PORT/g" hardhat.config.js \
18
+ && sed -i "s/HOST/$HOST/g" hardhat.config.js
19
19
 
20
20
  COPY contracts contracts
21
21
  COPY ignition ignition
22
22
 
23
- RUN rm -rf ignition/deployments
24
-
25
- RUN npx hardhat compile
26
-
27
- RUN rm -rf cache
23
+ RUN rm -rf ignition/deployments \
24
+ && npx hardhat compile \
25
+ && rm -rf cache
data/lib/erc20/erc20.rb CHANGED
@@ -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.1.5'
28
+ VERSION = '0.2.0'
29
29
  end
@@ -69,6 +69,14 @@ class ERC20::FakeWallet
69
69
  b
70
70
  end
71
71
 
72
+ # Get ERC20 amount (in tokens) that was sent in the given transaction.
73
+ #
74
+ # @param [String] txn Hex of transaction
75
+ # @return [Integer] Balance, in ERC20 tokens
76
+ def sum_of(_txn)
77
+ 42_000_000
78
+ end
79
+
72
80
  # How much gas units is required in order to send ERC20 transaction.
73
81
  #
74
82
  # @param [String] from The departing address, in hex
data/lib/erc20/wallet.rb CHANGED
@@ -142,6 +142,28 @@ class ERC20::Wallet
142
142
  b
143
143
  end
144
144
 
145
+ # Get ERC20 amount (in tokens) that was sent in the given transaction.
146
+ #
147
+ # @param [String] txn Hex of transaction
148
+ # @return [Integer] Balance, in ERC20 tokens
149
+ def sum_of(txn)
150
+ raise 'Transaction hash can\'t be nil' unless txn
151
+ raise 'Transaction hash must be a String' unless txn.is_a?(String)
152
+ raise 'Invalid format of the transaction hash' unless /^0x[0-9a-fA-F]{64}$/.match?(txn)
153
+ receipt = jsonrpc.eth_getTransactionReceipt(txn)
154
+ raise "Transaction not found: #{txn}" if receipt.nil?
155
+ logs = receipt['logs'] || []
156
+ transfer_event = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
157
+ logs.each do |log|
158
+ next unless log['topics'] && log['topics'][0] == transfer_event
159
+ next unless log['address'].downcase == @contract.downcase
160
+ amount = log['data'].to_i(16)
161
+ log_it(:debug, "Found transfer of #{amount} tokens in transaction #{txn}")
162
+ return amount
163
+ end
164
+ raise "No transfer event found in transaction #{txn}"
165
+ end
166
+
145
167
  # How many gas units are required to send an ERC20 transaction.
146
168
  #
147
169
  # @param [String] from The sending address, in hex
@@ -312,9 +334,11 @@ class ERC20::Wallet
312
334
  raise 'Addresses can\'t be nil' unless addresses
313
335
  raise 'Addresses must respond to .to_a()' unless addresses.respond_to?(:to_a)
314
336
  raise 'Active can\'t be nil' unless active
337
+ raise 'Active must respond to .to_a()' unless active.respond_to?(:to_a)
315
338
  raise 'Active must respond to .append()' unless active.respond_to?(:append)
339
+ raise 'Active must respond to .clear()' unless active.respond_to?(:clear)
316
340
  raise 'Delay must be an Integer' unless delay.is_a?(Integer)
317
- raise 'Delay must be a positive Integer' unless delay.positive?
341
+ raise 'Delay must be a positive Integer or positive Float' unless delay.positive?
318
342
  raise 'Subscription ID must be an Integer' unless subscription_id.is_a?(Integer)
319
343
  raise 'Subscription ID must be a positive Integer' unless subscription_id.positive?
320
344
  EventMachine.run do
@@ -336,37 +360,38 @@ class ERC20::Wallet
336
360
  contract = @contract
337
361
  log_url = "ws#{@ssl ? 's' : ''}://#{u.hostname}:#{u.port}"
338
362
  ws = Faye::WebSocket::Client.new(u.to_s, [], proxy: @proxy ? { origin: @proxy } : {}, ping: 60)
339
- timer =
340
- EventMachine.add_periodic_timer(delay) do
341
- next if active.to_a.sort == addresses.to_a.sort
342
- ws.send(
343
- {
344
- jsonrpc: '2.0',
345
- id: subscription_id,
346
- method: 'eth_subscribe',
347
- params: [
348
- 'logs',
349
- {
350
- address: contract,
351
- topics: [
352
- '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
353
- nil,
354
- addresses.to_a.map { |a| "0x000000000000000000000000#{a[2..]}" }
355
- ]
356
- }
357
- ]
358
- }.to_json
359
- )
360
- log_it(
361
- :debug,
362
- "Requested to subscribe ##{subscription_id} to #{addresses.to_a.size} addresses: " \
363
- "#{addresses.to_a.map { |a| a[0..6] }.join(', ')}"
364
- )
365
- end
363
+ timer = nil
366
364
  ws.on(:open) do
367
365
  safe do
368
366
  verbose do
369
367
  log_it(:debug, "Connected ##{subscription_id} to #{log_url}")
368
+ timer =
369
+ EventMachine.add_periodic_timer(delay) do
370
+ next if active.to_a.sort == addresses.to_a.sort
371
+ ws.send(
372
+ {
373
+ jsonrpc: '2.0',
374
+ id: subscription_id,
375
+ method: 'eth_subscribe',
376
+ params: [
377
+ 'logs',
378
+ {
379
+ address: contract,
380
+ topics: [
381
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
382
+ nil,
383
+ addresses.to_a.map { |a| "0x000000000000000000000000#{a[2..]}" }
384
+ ]
385
+ }
386
+ ]
387
+ }.to_json
388
+ )
389
+ log_it(
390
+ :debug,
391
+ "Requested to subscribe ##{subscription_id} to #{addresses.to_a.size} addresses: " \
392
+ "#{addresses.to_a.map { |a| a[0..6] }.join(', ')}"
393
+ )
394
+ end
370
395
  end
371
396
  end
372
397
  end
@@ -411,9 +436,8 @@ class ERC20::Wallet
411
436
  safe do
412
437
  verbose do
413
438
  log_it(:debug, "Disconnected ##{subscription_id} from #{log_url}")
414
- sleep(delay)
415
439
  active.clear
416
- timer.cancel
440
+ timer&.cancel
417
441
  reaccept(addresses, active, raw:, delay:, subscription_id: subscription_id + 1, &)
418
442
  end
419
443
  end
@@ -68,6 +68,11 @@ class TestFakeWallet < ERC20::Test
68
68
  assert_equal(b, w.eth_balance(a))
69
69
  end
70
70
 
71
+ def test_reads_sum_of_payment
72
+ txn = '0xcf0598d640d4bea3367e6af28a08c54342a39156afd292a31453778e4755945d'
73
+ assert_predicate(ERC20::FakeWallet.new.sum_of(txn), :positive?)
74
+ end
75
+
71
76
  def test_returns_host
72
77
  assert_equal('example.com', ERC20::FakeWallet.new.host)
73
78
  end
@@ -22,58 +22,12 @@ require_relative '../../lib/erc20/wallet'
22
22
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
23
23
  # License:: MIT
24
24
  class TestWallet < ERC20::Test
25
- # At this address, in Ethereum mainnet, there are $8 USDT and 0.0042 ETH. I won't
26
- # move them anyway, that's why tests can use this address forever.
27
- STABLE = '0x7232148927F8a580053792f44D4d59d40Fd00ABD'
28
-
29
25
  # One guy private hex.
30
26
  JEFF = '81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a'
31
27
 
32
28
  # Another guy private hex.
33
29
  WALTER = '91f9111b1744d55361e632771a4e53839e9442a9fef45febc0a5c838c686a15b'
34
30
 
35
- def test_checks_balance_on_mainnet
36
- WebMock.enable_net_connect!
37
- b = mainnet.balance(STABLE)
38
- refute_nil(b)
39
- assert_equal(8_000_000, b) # this is $8 USDT
40
- end
41
-
42
- def test_checks_eth_balance_on_mainnet
43
- WebMock.enable_net_connect!
44
- b = mainnet.eth_balance(STABLE)
45
- refute_nil(b)
46
- assert_equal(4_200_000_000_000_000, b) # this is 0.0042 ETH
47
- end
48
-
49
- def test_checks_balance_of_absent_address
50
- WebMock.enable_net_connect!
51
- a = '0xEB2fE8872A6f1eDb70a2632Effffffffffffffff'
52
- b = mainnet.balance(a)
53
- refute_nil(b)
54
- assert_equal(0, b)
55
- end
56
-
57
- def test_checks_gas_estimate_on_mainnet
58
- WebMock.enable_net_connect!
59
- b = mainnet.gas_estimate(STABLE, Eth::Key.new(priv: JEFF).address.to_s, 44_000)
60
- refute_nil(b)
61
- assert_predicate(b, :positive?)
62
- assert_operator(b, :>, 1000)
63
- end
64
-
65
- def test_fails_with_invalid_infura_key
66
- WebMock.enable_net_connect!
67
- skip('Apparently, even with invalid key, Infura returns balance')
68
- w = ERC20::Wallet.new(
69
- contract: ERC20::Wallet.USDT,
70
- host: 'mainnet.infura.io',
71
- http_path: '/v3/invalid-key-here',
72
- log: fake_loog
73
- )
74
- assert_raises(StandardError) { w.balance(STABLE) }
75
- end
76
-
77
31
  def test_logs_to_stdout
78
32
  WebMock.disable_net_connect!
79
33
  stub_request(:post, 'https://example.org/').to_return(
@@ -85,25 +39,12 @@ class TestWallet < ERC20::Test
85
39
  http_path: '/',
86
40
  log: $stdout
87
41
  )
88
- w.balance(STABLE)
42
+ w.balance(Eth::Key.new(priv: JEFF).address.to_s)
89
43
  end
90
44
 
91
45
  def test_checks_balance_on_testnet
92
46
  WebMock.enable_net_connect!
93
- b = testnet.balance(STABLE)
94
- refute_nil(b)
95
- assert_predicate(b, :zero?)
96
- end
97
-
98
- def test_checks_balance_on_polygon
99
- WebMock.enable_net_connect!
100
- w = ERC20::Wallet.new(
101
- contract: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
102
- host: 'polygon-mainnet.infura.io',
103
- http_path: "/v3/#{env('INFURA_KEY')}",
104
- log: fake_loog
105
- )
106
- b = w.balance(STABLE)
47
+ b = testnet.balance(Eth::Key.new(priv: JEFF).address.to_s)
107
48
  refute_nil(b)
108
49
  assert_predicate(b, :zero?)
109
50
  end
@@ -162,6 +103,15 @@ class TestWallet < ERC20::Test
162
103
  end
163
104
  end
164
105
 
106
+ def test_reads_payment_amount_on_hardhat
107
+ WebMock.enable_net_connect!
108
+ on_hardhat do |wallet|
109
+ sum = 33_330
110
+ txn = wallet.pay(JEFF, Eth::Key.new(priv: WALTER).address.to_s, sum)
111
+ assert_equal(sum, wallet.sum_of(txn))
112
+ end
113
+ end
114
+
165
115
  def test_eth_pays_on_hardhat
166
116
  WebMock.enable_net_connect!
167
117
  on_hardhat do |wallet|
@@ -379,26 +329,6 @@ class TestWallet < ERC20::Test
379
329
  end
380
330
  end
381
331
 
382
- def test_accepts_payments_on_mainnet
383
- WebMock.enable_net_connect!
384
- active = []
385
- failed = false
386
- net = mainnet
387
- daemon =
388
- Thread.new do
389
- net.accept([STABLE], active) do |_|
390
- # ignore it
391
- end
392
- rescue StandardError => e
393
- failed = true
394
- fake_loog.error(Backtrace.new(e))
395
- end
396
- wait_for { !active.empty? }
397
- daemon.kill
398
- daemon.join(30)
399
- refute(failed)
400
- end
401
-
402
332
  def test_checks_balance_via_proxy
403
333
  WebMock.enable_net_connect!
404
334
  b = nil
@@ -410,151 +340,4 @@ class TestWallet < ERC20::Test
410
340
  end
411
341
  assert_equal(123_000_100_000, b)
412
342
  end
413
-
414
- def test_checks_balance_via_proxy_on_mainnet
415
- WebMock.enable_net_connect!
416
- via_proxy do |proxy|
417
- w = ERC20::Wallet.new(
418
- host: 'mainnet.infura.io',
419
- http_path: "/v3/#{env('INFURA_KEY')}",
420
- proxy:, log: fake_loog
421
- )
422
- assert_equal(8_000_000, w.balance(STABLE))
423
- end
424
- end
425
-
426
- def test_pays_on_mainnet
427
- WebMock.enable_net_connect!
428
- skip('This is live, must be run manually')
429
- w = mainnet
430
- print 'Enter Ethereum ERC20 private key (64 chars): '
431
- priv = gets.chomp
432
- to = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
433
- txn = w.pay(priv, to, 1_990_000)
434
- assert_equal(66, txn.length)
435
- end
436
-
437
- private
438
-
439
- def env(var)
440
- key = ENV.fetch(var, nil)
441
- skip("The #{var} environment variable is not set") if key.nil?
442
- skip("The #{var} environment variable is empty") if key.empty?
443
- key
444
- end
445
-
446
- def mainnet
447
- [
448
- {
449
- host: 'mainnet.infura.io',
450
- http_path: "/v3/#{env('INFURA_KEY')}",
451
- ws_path: "/ws/v3/#{env('INFURA_KEY')}"
452
- },
453
- {
454
- host: 'go.getblock.io',
455
- http_path: "/#{env('GETBLOCK_KEY')}",
456
- ws_path: "/#{env('GETBLOCK_WS_KEY')}"
457
- }
458
- ].map do |server|
459
- ERC20::Wallet.new(
460
- host: server[:host],
461
- http_path: server[:http_path],
462
- ws_path: server[:ws_path],
463
- log: fake_loog
464
- )
465
- end.sample
466
- end
467
-
468
- def testnet
469
- [
470
- {
471
- host: 'sepolia.infura.io',
472
- http_path: "/v3/#{env('INFURA_KEY')}",
473
- ws_path: "/ws/v3/#{env('INFURA_KEY')}"
474
- },
475
- {
476
- host: 'go.getblock.io',
477
- http_path: "/#{env('GETBLOCK_SEPOILA_KEY')}",
478
- ws_path: "/#{env('GETBLOCK_SEPOILA_KEY')}"
479
- }
480
- ].map do |server|
481
- ERC20::Wallet.new(
482
- host: server[:host],
483
- http_path: server[:http_path],
484
- ws_path: server[:ws_path],
485
- log: fake_loog
486
- )
487
- end.sample
488
- end
489
-
490
- def through_proxy(wallet, proxy)
491
- ERC20::Wallet.new(
492
- contract: wallet.contract, chain: wallet.chain,
493
- host: donce_host, port: wallet.port, http_path: wallet.http_path, ws_path: wallet.ws_path,
494
- ssl: wallet.ssl, proxy:, log: fake_loog
495
- )
496
- end
497
-
498
- def via_proxy
499
- RandomPort::Pool::SINGLETON.acquire do |port|
500
- donce(
501
- image: 'yegor256/squid-proxy:latest',
502
- ports: { port => 3128 },
503
- env: { 'USERNAME' => 'jeffrey', 'PASSWORD' => 'swordfish' },
504
- root: true, log: fake_loog
505
- ) do
506
- yield "http://jeffrey:swordfish@localhost:#{port}"
507
- end
508
- end
509
- end
510
-
511
- def on_hardhat(port: nil, die: nil)
512
- RandomPort::Pool::SINGLETON.acquire do |rnd|
513
- port = rnd if port.nil?
514
- if die
515
- killer = [
516
- '&',
517
- 'HARDHAT_PID=$!;',
518
- 'export HARDHAT_PID;',
519
- 'while true; do',
520
- " if [ -e #{Shellwords.escape(File.join('/die', File.basename(die)))} ]; then",
521
- ' kill -9 "${HARDHAT_PID}";',
522
- ' break;',
523
- ' else',
524
- ' sleep 0.1;',
525
- ' fi;',
526
- 'done'
527
- ].join(' ')
528
- end
529
- cmd = "npx hardhat node #{killer if die}"
530
- donce(
531
- home: File.join(__dir__, '../../hardhat'),
532
- ports: { port => 8545 },
533
- volumes: die ? { File.dirname(die) => '/die' } : {},
534
- command: "/bin/bash -c #{Shellwords.escape(cmd)}",
535
- log: fake_loog
536
- ) do
537
- wait_for_port(port)
538
- cmd = [
539
- '(cat hardhat.config.js)',
540
- '(ls -al)',
541
- '(echo y | npx hardhat ignition deploy ./ignition/modules/Foo.ts --network foo --deployment-id foo)',
542
- '(npx hardhat ignition status foo | tail -1 | cut -d" " -f3)'
543
- ].join(' && ')
544
- contract = donce(
545
- home: File.join(__dir__, '../../hardhat'),
546
- command: "/bin/bash -c #{Shellwords.escape(cmd)}",
547
- build_args: { 'HOST' => donce_host, 'PORT' => port },
548
- log: fake_loog,
549
- root: true
550
- ).split("\n").last
551
- wallet = ERC20::Wallet.new(
552
- contract:, chain: 4242,
553
- host: 'localhost', port:, http_path: '/', ws_path: '/', ssl: false,
554
- log: fake_loog
555
- )
556
- yield wallet
557
- end
558
- end
559
- end
560
343
  end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'backtrace'
7
+ require 'donce'
8
+ require 'eth'
9
+ require 'faraday'
10
+ require 'fileutils'
11
+ require 'json'
12
+ require 'os'
13
+ require 'random-port'
14
+ require 'shellwords'
15
+ require 'threads'
16
+ require 'typhoeus'
17
+ require_relative '../test__helper'
18
+ require_relative '../../lib/erc20/wallet'
19
+
20
+ # Test.
21
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
22
+ # Copyright:: Copyright (c) 2025 Yegor Bugayenko
23
+ # License:: MIT
24
+ class TestWalletLive < ERC20::Test
25
+ # At this address, in Ethereum mainnet, there are $8 USDT and 0.0042 ETH. I won't
26
+ # move them anyway, that's why tests can use this address forever.
27
+ STABLE = '0x7232148927F8a580053792f44D4d59d40Fd00ABD'
28
+
29
+ def test_checks_balance_on_mainnet
30
+ WebMock.enable_net_connect!
31
+ b = mainnet.balance(STABLE)
32
+ refute_nil(b)
33
+ assert_equal(8_000_000, b) # this is $8 USDT
34
+ end
35
+
36
+ def test_checks_eth_balance_on_mainnet
37
+ WebMock.enable_net_connect!
38
+ b = mainnet.eth_balance(STABLE)
39
+ refute_nil(b)
40
+ assert_equal(4_200_000_000_000_000, b) # this is 0.0042 ETH
41
+ end
42
+
43
+ def test_checks_balance_of_absent_address
44
+ WebMock.enable_net_connect!
45
+ a = '0xEB2fE8872A6f1eDb70a2632Effffffffffffffff'
46
+ b = mainnet.balance(a)
47
+ refute_nil(b)
48
+ assert_equal(0, b)
49
+ end
50
+
51
+ def test_checks_gas_estimate_on_mainnet
52
+ WebMock.enable_net_connect!
53
+ b = mainnet.gas_estimate(STABLE, '0x7232148927F8a580053792f44D4d5FFFFFFFFFFF', 44_000)
54
+ refute_nil(b)
55
+ assert_predicate(b, :positive?)
56
+ assert_operator(b, :>, 1000)
57
+ end
58
+
59
+ def test_fails_with_invalid_infura_key
60
+ WebMock.enable_net_connect!
61
+ skip('Apparently, even with invalid key, Infura returns balance')
62
+ w = ERC20::Wallet.new(
63
+ contract: ERC20::Wallet.USDT,
64
+ host: 'mainnet.infura.io',
65
+ http_path: '/v3/invalid-key-here',
66
+ log: fake_loog
67
+ )
68
+ assert_raises(StandardError) { w.balance(STABLE) }
69
+ end
70
+
71
+ def test_checks_balance_on_polygon
72
+ WebMock.enable_net_connect!
73
+ w = ERC20::Wallet.new(
74
+ contract: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
75
+ host: 'polygon-mainnet.infura.io',
76
+ http_path: "/v3/#{env('INFURA_KEY')}",
77
+ log: fake_loog
78
+ )
79
+ b = w.balance(STABLE)
80
+ refute_nil(b)
81
+ assert_predicate(b, :zero?)
82
+ end
83
+
84
+ def test_accepts_payments_on_mainnet
85
+ WebMock.enable_net_connect!
86
+ active = []
87
+ failed = false
88
+ net = mainnet
89
+ daemon =
90
+ Thread.new do
91
+ net.accept([STABLE], active) do |_|
92
+ # ignore it
93
+ end
94
+ rescue StandardError => e
95
+ failed = true
96
+ fake_loog.error(Backtrace.new(e))
97
+ end
98
+ wait_for { !active.empty? }
99
+ daemon.kill
100
+ daemon.join(30)
101
+ refute(failed)
102
+ end
103
+
104
+ def test_checks_balance_via_proxy_on_mainnet
105
+ WebMock.enable_net_connect!
106
+ via_proxy do |proxy|
107
+ w = ERC20::Wallet.new(
108
+ host: 'mainnet.infura.io',
109
+ http_path: "/v3/#{env('INFURA_KEY')}",
110
+ proxy:, log: fake_loog
111
+ )
112
+ assert_equal(8_000_000, w.balance(STABLE))
113
+ end
114
+ end
115
+
116
+ def test_pays_on_mainnet
117
+ WebMock.enable_net_connect!
118
+ skip('This is live, must be run manually')
119
+ w = mainnet
120
+ print 'Enter Ethereum ERC20 private key (64 chars): '
121
+ priv = gets.chomp
122
+ to = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
123
+ txn = w.pay(priv, to, 1_990_000)
124
+ assert_equal(66, txn.length)
125
+ end
126
+ end
data/test/test__helper.rb CHANGED
@@ -42,6 +42,10 @@ class Primitivo
42
42
  @array = array
43
43
  end
44
44
 
45
+ def clear
46
+ @array.clear
47
+ end
48
+
45
49
  def to_a
46
50
  @array.to_a
47
51
  end
@@ -77,4 +81,126 @@ class ERC20::Test < Minitest::Test
77
81
  def wait_for_port(port)
78
82
  wait_for { Typhoeus::Request.get("http://localhost:#{port}").code == 200 }
79
83
  end
84
+
85
+ def env(var)
86
+ key = ENV.fetch(var, nil)
87
+ skip("The #{var} environment variable is not set") if key.nil?
88
+ skip("The #{var} environment variable is empty") if key.empty?
89
+ key
90
+ end
91
+
92
+ def mainnet
93
+ [
94
+ {
95
+ host: 'mainnet.infura.io',
96
+ http_path: "/v3/#{env('INFURA_KEY')}",
97
+ ws_path: "/ws/v3/#{env('INFURA_KEY')}"
98
+ },
99
+ {
100
+ host: 'go.getblock.io',
101
+ http_path: "/#{env('GETBLOCK_KEY')}",
102
+ ws_path: "/#{env('GETBLOCK_WS_KEY')}"
103
+ }
104
+ ].map do |server|
105
+ ERC20::Wallet.new(
106
+ host: server[:host],
107
+ http_path: server[:http_path],
108
+ ws_path: server[:ws_path],
109
+ log: fake_loog
110
+ )
111
+ end.sample
112
+ end
113
+
114
+ def testnet
115
+ [
116
+ {
117
+ host: 'sepolia.infura.io',
118
+ http_path: "/v3/#{env('INFURA_KEY')}",
119
+ ws_path: "/ws/v3/#{env('INFURA_KEY')}"
120
+ },
121
+ {
122
+ host: 'go.getblock.io',
123
+ http_path: "/#{env('GETBLOCK_SEPOILA_KEY')}",
124
+ ws_path: "/#{env('GETBLOCK_SEPOILA_KEY')}"
125
+ }
126
+ ].map do |server|
127
+ ERC20::Wallet.new(
128
+ host: server[:host],
129
+ http_path: server[:http_path],
130
+ ws_path: server[:ws_path],
131
+ log: fake_loog
132
+ )
133
+ end.sample
134
+ end
135
+
136
+ def through_proxy(wallet, proxy)
137
+ ERC20::Wallet.new(
138
+ contract: wallet.contract, chain: wallet.chain,
139
+ host: donce_host, port: wallet.port, http_path: wallet.http_path, ws_path: wallet.ws_path,
140
+ ssl: wallet.ssl, proxy:, log: fake_loog
141
+ )
142
+ end
143
+
144
+ def via_proxy
145
+ RandomPort::Pool::SINGLETON.acquire do |port|
146
+ donce(
147
+ image: 'yegor256/squid-proxy:latest',
148
+ ports: { port => 3128 },
149
+ env: { 'USERNAME' => 'jeffrey', 'PASSWORD' => 'swordfish' },
150
+ root: true, log: fake_loog
151
+ ) do
152
+ yield "http://jeffrey:swordfish@localhost:#{port}"
153
+ end
154
+ end
155
+ end
156
+
157
+ def on_hardhat(port: nil, die: nil)
158
+ RandomPort::Pool::SINGLETON.acquire do |rnd|
159
+ port = rnd if port.nil?
160
+ if die
161
+ killer = [
162
+ '&',
163
+ 'HARDHAT_PID=$!;',
164
+ 'export HARDHAT_PID;',
165
+ 'while true; do',
166
+ " if [ -e #{Shellwords.escape(File.join('/die', File.basename(die)))} ]; then",
167
+ ' kill -9 "${HARDHAT_PID}";',
168
+ ' break;',
169
+ ' else',
170
+ ' sleep 0.1;',
171
+ ' fi;',
172
+ 'done'
173
+ ].join(' ')
174
+ end
175
+ cmd = "npx hardhat node #{killer if die}"
176
+ donce(
177
+ home: File.join(__dir__, '../hardhat'),
178
+ ports: { port => 8545 },
179
+ volumes: die ? { File.dirname(die) => '/die' } : {},
180
+ command: "/bin/bash -c #{Shellwords.escape(cmd)}",
181
+ log: fake_loog
182
+ ) do
183
+ wait_for_port(port)
184
+ cmd = [
185
+ '(cat hardhat.config.js)',
186
+ '(ls -al)',
187
+ '(echo y | npx hardhat ignition deploy ./ignition/modules/Foo.ts --network foo --deployment-id foo)',
188
+ '(npx hardhat ignition status foo | tail -1 | cut -d" " -f3)'
189
+ ].join(' && ')
190
+ contract = donce(
191
+ home: File.join(__dir__, '../hardhat'),
192
+ command: "/bin/bash -c #{Shellwords.escape(cmd)}",
193
+ build_args: { 'HOST' => donce_host, 'PORT' => port },
194
+ log: fake_loog,
195
+ root: true
196
+ ).split("\n").last
197
+ wallet = ERC20::Wallet.new(
198
+ contract:, chain: 4242,
199
+ host: 'localhost', port:, http_path: '/', ws_path: '/', ssl: false,
200
+ log: fake_loog
201
+ )
202
+ yield wallet
203
+ end
204
+ end
205
+ end
80
206
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erc20
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: eth
@@ -102,19 +102,21 @@ executables:
102
102
  - erc20
103
103
  extensions: []
104
104
  extra_rdoc_files:
105
- - README.md
106
105
  - LICENSE.txt
106
+ - README.md
107
107
  files:
108
108
  - ".0pdd.yml"
109
109
  - ".gitattributes"
110
110
  - ".github/workflows/actionlint.yml"
111
111
  - ".github/workflows/codecov.yml"
112
112
  - ".github/workflows/copyrights.yml"
113
+ - ".github/workflows/hadolint.yml"
113
114
  - ".github/workflows/markdown-lint.yml"
114
115
  - ".github/workflows/pdd.yml"
115
116
  - ".github/workflows/rake.yml"
116
117
  - ".github/workflows/reuse.yml"
117
118
  - ".github/workflows/shellcheck.yml"
119
+ - ".github/workflows/typos.yml"
118
120
  - ".github/workflows/xcop.yml"
119
121
  - ".github/workflows/yamllint.yml"
120
122
  - ".gitignore"
@@ -149,6 +151,7 @@ files:
149
151
  - renovate.json
150
152
  - test/erc20/test_fake_wallet.rb
151
153
  - test/erc20/test_wallet.rb
154
+ - test/erc20/test_wallet_live.rb
152
155
  - test/test__helper.rb
153
156
  homepage: http://github.com/yegor256/erc20.rb
154
157
  licenses:
@@ -170,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
173
  - !ruby/object:Gem::Version
171
174
  version: '0'
172
175
  requirements: []
173
- rubygems_version: 3.6.2
176
+ rubygems_version: 3.6.7
174
177
  specification_version: 4
175
178
  summary: Sending and receiving ERC20 tokens in Ethereum network
176
179
  test_files: []