erc20 0.1.4 → 0.1.6

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: 71eb4c1f0b7e6778be6d2b121792c20aa190cdfdfc62948ce39e1c4ea1cb288f
4
- data.tar.gz: 9dbb23249587fe95bf6681ab9b87a131b4f946343c515df036c1fa1c7f45b200
3
+ metadata.gz: 7843a4cb824e3689fb7a457f8ca0744f1d1059349aa6ea8bfe8805cd597e8b24
4
+ data.tar.gz: d11d0eb794c22737519ef5ee2083763ba8244ce91f01f0f120ee8e6970af53c0
5
5
  SHA512:
6
- metadata.gz: aeb83a009f276a4ead1a30e1d1cb052816067fb1571cca6cb20cc39a7840422342cbf246b498932be850c2330663d7e08356c2b3f35f2365acc933511462a4ab
7
- data.tar.gz: '08b280c073e3d8bd1cc7b335b8d520e1808e190b50aaa227ee5fed17ae78ebc622637cd28c037e5991ea2dd8356cddb4f794ba240313f441cde0e79c3e7163d3'
6
+ metadata.gz: 135c421a05cbc431e20113e8cd6d63294630588b6ba27a63249313455853b83266d697759764b6f82aae857131237fd5c8c85f4f1432fe74cb73a25935b5ca45
7
+ data.tar.gz: ecf62b97b0053bc123de22f600e61a90cbf2e0636c1afa199d96478df0186194a917d27f0234285deb14129715970e2b818be5f6cf7d3a692d131a685a1af41f
data/.rubocop.yml CHANGED
@@ -11,7 +11,6 @@ AllCops:
11
11
  SuggestExtensions: false
12
12
  NewCops: enable
13
13
  plugins:
14
- - rubocop-rspec
15
14
  - rubocop-performance
16
15
  - rubocop-rake
17
16
  - rubocop-minitest
data/Gemfile.lock CHANGED
@@ -51,7 +51,7 @@ GEM
51
51
  cucumber-tag-expressions (6.1.2)
52
52
  diff-lcs (1.6.1)
53
53
  docile (1.4.1)
54
- donce (0.2.0)
54
+ donce (0.2.4)
55
55
  backtrace (~> 0.3)
56
56
  os (~> 1.1)
57
57
  qbash (~> 0.3)
@@ -136,7 +136,7 @@ GEM
136
136
  rubyzip (~> 2.3)
137
137
  regexp_parser (2.10.0)
138
138
  rexml (3.4.1)
139
- rubocop (1.75.2)
139
+ rubocop (1.75.3)
140
140
  json (~> 2.3)
141
141
  language_server-protocol (~> 3.17.0.2)
142
142
  lint_roller (~> 1.1.0)
data/Rakefile CHANGED
@@ -39,7 +39,6 @@ require 'rubocop/rake_task'
39
39
  desc 'Run RuboCop on all directories'
40
40
  RuboCop::RakeTask.new(:rubocop) do |task|
41
41
  task.fail_on_error = true
42
- task.requires << 'rubocop-rspec'
43
42
  end
44
43
 
45
44
  require 'cucumber/rake/task'
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.4'
28
+ VERSION = '0.1.6'
29
29
  end
data/lib/erc20/wallet.rb CHANGED
@@ -308,111 +308,127 @@ class ERC20::Wallet
308
308
  # @param [Boolean] raw TRUE if you need to get JSON events as they arrive from Websockets
309
309
  # @param [Integer] delay How many seconds to wait between +eth_subscribe+ calls
310
310
  # @param [Integer] subscription_id Unique ID of the subscription
311
- def accept(addresses, active = [], raw: false, delay: 1, subscription_id: rand(99_999))
311
+ def accept(addresses, active = [], raw: false, delay: 1, subscription_id: rand(99_999), &)
312
312
  raise 'Addresses can\'t be nil' unless addresses
313
313
  raise 'Addresses must respond to .to_a()' unless addresses.respond_to?(:to_a)
314
314
  raise 'Active can\'t be nil' unless active
315
+ raise 'Active must respond to .to_a()' unless active.respond_to?(:to_a)
315
316
  raise 'Active must respond to .append()' unless active.respond_to?(:append)
317
+ raise 'Active must respond to .clear()' unless active.respond_to?(:clear)
316
318
  raise 'Delay must be an Integer' unless delay.is_a?(Integer)
317
- raise 'Delay must be a positive Integer' unless delay.positive?
319
+ raise 'Delay must be a positive Integer or positive Float' unless delay.positive?
318
320
  raise 'Subscription ID must be an Integer' unless subscription_id.is_a?(Integer)
319
321
  raise 'Subscription ID must be a positive Integer' unless subscription_id.positive?
320
322
  EventMachine.run do
321
- u = url(http: false)
322
- log_it(:debug, "Connecting to #{u.hostname}:#{u.port}...")
323
- ws = Faye::WebSocket::Client.new(u.to_s, [], proxy: @proxy ? { origin: @proxy } : {})
324
- contract = @contract
325
- attempt = []
326
- log_url = "ws#{@ssl ? 's' : ''}://#{u.hostname}:#{u.port}"
327
- ws.on(:open) do
328
- safe do
329
- verbose do
330
- log_it(:debug, "Connected to #{log_url}")
331
- end
323
+ reaccept(addresses, active, raw:, delay:, subscription_id:, &)
324
+ end
325
+ end
326
+
327
+ private
328
+
329
+ # @param [Array<String>] addresses Addresses to monitor
330
+ # @param [Array] active List of addresses that we are actually listening to
331
+ # @param [Boolean] raw TRUE if you need to get JSON events as they arrive from Websockets
332
+ # @param [Integer] delay How many seconds to wait between +eth_subscribe+ calls
333
+ # @param [Integer] subscription_id Unique ID of the subscription
334
+ # @return [Websocket]
335
+ def reaccept(addresses, active, raw:, delay:, subscription_id:, &)
336
+ u = url(http: false)
337
+ log_it(:debug, "Connecting ##{subscription_id} to #{u.hostname}:#{u.port}...")
338
+ contract = @contract
339
+ log_url = "ws#{@ssl ? 's' : ''}://#{u.hostname}:#{u.port}"
340
+ ws = Faye::WebSocket::Client.new(u.to_s, [], proxy: @proxy ? { origin: @proxy } : {}, ping: 60)
341
+ timer = nil
342
+ ws.on(:open) do
343
+ safe do
344
+ verbose do
345
+ log_it(:debug, "Connected ##{subscription_id} to #{log_url}")
346
+ timer =
347
+ EventMachine.add_periodic_timer(delay) do
348
+ next if active.to_a.sort == addresses.to_a.sort
349
+ ws.send(
350
+ {
351
+ jsonrpc: '2.0',
352
+ id: subscription_id,
353
+ method: 'eth_subscribe',
354
+ params: [
355
+ 'logs',
356
+ {
357
+ address: contract,
358
+ topics: [
359
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
360
+ nil,
361
+ addresses.to_a.map { |a| "0x000000000000000000000000#{a[2..]}" }
362
+ ]
363
+ }
364
+ ]
365
+ }.to_json
366
+ )
367
+ log_it(
368
+ :debug,
369
+ "Requested to subscribe ##{subscription_id} to #{addresses.to_a.size} addresses: " \
370
+ "#{addresses.to_a.map { |a| a[0..6] }.join(', ')}"
371
+ )
372
+ end
332
373
  end
333
374
  end
334
- ws.on(:message) do |msg|
335
- safe do
336
- verbose do
337
- data = to_json(msg)
338
- if data['id']
339
- before = active.to_a
340
- attempt.each do |a|
341
- active.append(a) unless before.include?(a)
342
- end
375
+ end
376
+ ws.on(:message) do |msg|
377
+ safe do
378
+ verbose do
379
+ data = to_json(msg)
380
+ if data['id']
381
+ before = active.to_a.uniq
382
+ addresses.to_a.each do |a|
383
+ next if before.include?(a)
384
+ active.append(a)
385
+ end
386
+ log_it(
387
+ :debug,
388
+ "Subscribed ##{subscription_id} to #{active.to_a.size} addresses at #{log_url}: " \
389
+ "#{active.to_a.map { |a| a[0..6] }.join(', ')}"
390
+ )
391
+ elsif data['method'] == 'eth_subscription' && data.dig('params', 'result')
392
+ event = data['params']['result']
393
+ if raw
394
+ log_it(:debug, "New event arrived from #{event['address']}")
395
+ else
396
+ event = {
397
+ amount: event['data'].to_i(16),
398
+ from: "0x#{event['topics'][1][26..].downcase}",
399
+ to: "0x#{event['topics'][2][26..].downcase}",
400
+ txn: event['transactionHash'].downcase
401
+ }
343
402
  log_it(
344
403
  :debug,
345
- "Subscribed ##{subscription_id} to #{active.to_a.size} addresses at #{log_url}: " \
346
- "#{active.to_a.map { |a| a[0..6] }.join(', ')}"
404
+ "Payment of #{event[:amount]} tokens arrived at ##{subscription_id} " \
405
+ "from #{event[:from]} to #{event[:to]} in #{event[:txn]}"
347
406
  )
348
- elsif data['method'] == 'eth_subscription' && data.dig('params', 'result')
349
- event = data['params']['result']
350
- if raw
351
- log_it(:debug, "New event arrived from #{event['address']}")
352
- else
353
- event = {
354
- amount: event['data'].to_i(16),
355
- from: "0x#{event['topics'][1][26..].downcase}",
356
- to: "0x#{event['topics'][2][26..].downcase}",
357
- txn: event['transactionHash'].downcase
358
- }
359
- log_it(
360
- :debug,
361
- "Payment of #{event[:amount]} tokens arrived " \
362
- "from #{event[:from]} to #{event[:to]} in #{event[:txn]}"
363
- )
364
- end
365
- yield event
366
407
  end
408
+ yield event
367
409
  end
368
410
  end
369
411
  end
370
- ws.on(:close) do
371
- safe do
372
- verbose do
373
- log_it(:debug, "Disconnected from #{log_url}")
374
- end
412
+ end
413
+ ws.on(:close) do
414
+ safe do
415
+ verbose do
416
+ log_it(:debug, "Disconnected ##{subscription_id} from #{log_url}")
417
+ active.clear
418
+ timer&.cancel
419
+ reaccept(addresses, active, raw:, delay:, subscription_id: subscription_id + 1, &)
375
420
  end
376
421
  end
377
- ws.on(:error) do |e|
378
- safe do
379
- verbose do
380
- log_it(:debug, "Error at #{log_url}: #{e.message}")
381
- end
422
+ end
423
+ ws.on(:error) do |e|
424
+ safe do
425
+ verbose do
426
+ log_it(:debug, "Failed ##{subscription_id} at #{log_url}: #{e.message}")
382
427
  end
383
428
  end
384
- EventMachine.add_periodic_timer(delay) do
385
- next if active.to_a.sort == addresses.to_a.sort
386
- attempt = addresses.to_a
387
- ws.send(
388
- {
389
- jsonrpc: '2.0',
390
- id: subscription_id,
391
- method: 'eth_subscribe',
392
- params: [
393
- 'logs',
394
- {
395
- address: contract,
396
- topics: [
397
- '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
398
- nil,
399
- addresses.to_a.map { |a| "0x000000000000000000000000#{a[2..]}" }
400
- ]
401
- }
402
- ]
403
- }.to_json
404
- )
405
- log_it(
406
- :debug,
407
- "Requested to subscribe ##{subscription_id} to #{addresses.to_a.size} addresses at #{log_url}: " \
408
- "#{addresses.to_a.map { |a| a[0..6] }.join(', ')}"
409
- )
410
- end
411
429
  end
412
430
  end
413
431
 
414
- private
415
-
416
432
  def to_json(msg)
417
433
  JSON.parse(msg.data)
418
434
  rescue StandardError
@@ -7,7 +7,9 @@ require 'backtrace'
7
7
  require 'donce'
8
8
  require 'eth'
9
9
  require 'faraday'
10
+ require 'fileutils'
10
11
  require 'json'
12
+ require 'os'
11
13
  require 'random-port'
12
14
  require 'shellwords'
13
15
  require 'threads'
@@ -20,58 +22,12 @@ require_relative '../../lib/erc20/wallet'
20
22
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
21
23
  # License:: MIT
22
24
  class TestWallet < ERC20::Test
23
- # At this address, in Ethereum mainnet, there are $8 USDT and 0.0042 ETH. I won't
24
- # move them anyway, that's why tests can use this address forever.
25
- STABLE = '0x7232148927F8a580053792f44D4d59d40Fd00ABD'
26
-
27
25
  # One guy private hex.
28
26
  JEFF = '81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a'
29
27
 
30
28
  # Another guy private hex.
31
29
  WALTER = '91f9111b1744d55361e632771a4e53839e9442a9fef45febc0a5c838c686a15b'
32
30
 
33
- def test_checks_balance_on_mainnet
34
- WebMock.enable_net_connect!
35
- b = mainnet.balance(STABLE)
36
- refute_nil(b)
37
- assert_equal(8_000_000, b) # this is $8 USDT
38
- end
39
-
40
- def test_checks_eth_balance_on_mainnet
41
- WebMock.enable_net_connect!
42
- b = mainnet.eth_balance(STABLE)
43
- refute_nil(b)
44
- assert_equal(4_200_000_000_000_000, b) # this is 0.0042 ETH
45
- end
46
-
47
- def test_checks_balance_of_absent_address
48
- WebMock.enable_net_connect!
49
- a = '0xEB2fE8872A6f1eDb70a2632Effffffffffffffff'
50
- b = mainnet.balance(a)
51
- refute_nil(b)
52
- assert_equal(0, b)
53
- end
54
-
55
- def test_checks_gas_estimate_on_mainnet
56
- WebMock.enable_net_connect!
57
- b = mainnet.gas_estimate(STABLE, Eth::Key.new(priv: JEFF).address.to_s, 44_000)
58
- refute_nil(b)
59
- assert_predicate(b, :positive?)
60
- assert_operator(b, :>, 1000)
61
- end
62
-
63
- def test_fails_with_invalid_infura_key
64
- WebMock.enable_net_connect!
65
- skip('Apparently, even with invalid key, Infura returns balance')
66
- w = ERC20::Wallet.new(
67
- contract: ERC20::Wallet.USDT,
68
- host: 'mainnet.infura.io',
69
- http_path: '/v3/invalid-key-here',
70
- log: fake_loog
71
- )
72
- assert_raises(StandardError) { w.balance(STABLE) }
73
- end
74
-
75
31
  def test_logs_to_stdout
76
32
  WebMock.disable_net_connect!
77
33
  stub_request(:post, 'https://example.org/').to_return(
@@ -83,25 +39,12 @@ class TestWallet < ERC20::Test
83
39
  http_path: '/',
84
40
  log: $stdout
85
41
  )
86
- w.balance(STABLE)
42
+ w.balance(Eth::Key.new(priv: JEFF).address.to_s)
87
43
  end
88
44
 
89
45
  def test_checks_balance_on_testnet
90
46
  WebMock.enable_net_connect!
91
- b = testnet.balance(STABLE)
92
- refute_nil(b)
93
- assert_predicate(b, :zero?)
94
- end
95
-
96
- def test_checks_balance_on_polygon
97
- WebMock.enable_net_connect!
98
- w = ERC20::Wallet.new(
99
- contract: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
100
- host: 'polygon-mainnet.infura.io',
101
- http_path: "/v3/#{env('INFURA_KEY')}",
102
- log: fake_loog
103
- )
104
- b = w.balance(STABLE)
47
+ b = testnet.balance(Eth::Key.new(priv: JEFF).address.to_s)
105
48
  refute_nil(b)
106
49
  assert_predicate(b, :zero?)
107
50
  end
@@ -231,6 +174,38 @@ class TestWallet < ERC20::Test
231
174
  end
232
175
  end
233
176
 
177
+ def test_accepts_payments_on_hardhat_after_disconnect
178
+ skip('Works only on macOS') unless OS.mac?
179
+ WebMock.enable_net_connect!
180
+ walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
181
+ Dir.mktmpdir do |home|
182
+ die = File.join(home, 'die.txt')
183
+ on_hardhat(die:) do |wallet|
184
+ active = []
185
+ events = []
186
+ daemon =
187
+ Thread.new do
188
+ wallet.accept([walter], active, subscription_id: 42) do |e|
189
+ events.append(e)
190
+ end
191
+ rescue StandardError => e
192
+ fake_loog.error(Backtrace.new(e))
193
+ end
194
+ wait_for { !active.empty? }
195
+ wallet.pay(JEFF, walter, 4_567)
196
+ wait_for { events.size == 1 }
197
+ FileUtils.touch(die)
198
+ on_hardhat(port: wallet.port) do
199
+ wallet.pay(JEFF, walter, 3_456)
200
+ wait_for { events.size > 1 }
201
+ daemon.kill
202
+ daemon.join(30)
203
+ assert_equal(3, events.size)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
234
209
  def test_accepts_many_payments_on_hardhat
235
210
  WebMock.enable_net_connect!
236
211
  walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
@@ -345,26 +320,6 @@ class TestWallet < ERC20::Test
345
320
  end
346
321
  end
347
322
 
348
- def test_accepts_payments_on_mainnet
349
- WebMock.enable_net_connect!
350
- active = []
351
- failed = false
352
- net = mainnet
353
- daemon =
354
- Thread.new do
355
- net.accept([STABLE], active) do |_|
356
- # ignore it
357
- end
358
- rescue StandardError => e
359
- failed = true
360
- fake_loog.error(Backtrace.new(e))
361
- end
362
- wait_for { !active.empty? }
363
- daemon.kill
364
- daemon.join(30)
365
- refute(failed)
366
- end
367
-
368
323
  def test_checks_balance_via_proxy
369
324
  WebMock.enable_net_connect!
370
325
  b = nil
@@ -376,133 +331,4 @@ class TestWallet < ERC20::Test
376
331
  end
377
332
  assert_equal(123_000_100_000, b)
378
333
  end
379
-
380
- def test_checks_balance_via_proxy_on_mainnet
381
- WebMock.enable_net_connect!
382
- via_proxy do |proxy|
383
- w = ERC20::Wallet.new(
384
- host: 'mainnet.infura.io',
385
- http_path: "/v3/#{env('INFURA_KEY')}",
386
- proxy:, log: fake_loog
387
- )
388
- assert_equal(8_000_000, w.balance(STABLE))
389
- end
390
- end
391
-
392
- def test_pays_on_mainnet
393
- WebMock.enable_net_connect!
394
- skip('This is live, must be run manually')
395
- w = mainnet
396
- print 'Enter Ethereum ERC20 private key (64 chars): '
397
- priv = gets.chomp
398
- to = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
399
- txn = w.pay(priv, to, 1_990_000)
400
- assert_equal(66, txn.length)
401
- end
402
-
403
- private
404
-
405
- def env(var)
406
- key = ENV.fetch(var, nil)
407
- skip("The #{var} environment variable is not set") if key.nil?
408
- skip("The #{var} environment variable is empty") if key.empty?
409
- key
410
- end
411
-
412
- def mainnet
413
- [
414
- {
415
- host: 'mainnet.infura.io',
416
- http_path: "/v3/#{env('INFURA_KEY')}",
417
- ws_path: "/ws/v3/#{env('INFURA_KEY')}"
418
- },
419
- {
420
- host: 'go.getblock.io',
421
- http_path: "/#{env('GETBLOCK_KEY')}",
422
- ws_path: "/#{env('GETBLOCK_WS_KEY')}"
423
- }
424
- ].map do |server|
425
- ERC20::Wallet.new(
426
- host: server[:host],
427
- http_path: server[:http_path],
428
- ws_path: server[:ws_path],
429
- log: fake_loog
430
- )
431
- end.sample
432
- end
433
-
434
- def testnet
435
- [
436
- {
437
- host: 'sepolia.infura.io',
438
- http_path: "/v3/#{env('INFURA_KEY')}",
439
- ws_path: "/ws/v3/#{env('INFURA_KEY')}"
440
- },
441
- {
442
- host: 'go.getblock.io',
443
- http_path: "/#{env('GETBLOCK_SEPOILA_KEY')}",
444
- ws_path: "/#{env('GETBLOCK_SEPOILA_KEY')}"
445
- }
446
- ].map do |server|
447
- ERC20::Wallet.new(
448
- host: server[:host],
449
- http_path: server[:http_path],
450
- ws_path: server[:ws_path],
451
- log: fake_loog
452
- )
453
- end.sample
454
- end
455
-
456
- def through_proxy(wallet, proxy)
457
- ERC20::Wallet.new(
458
- contract: wallet.contract, chain: wallet.chain,
459
- host: donce_host, port: wallet.port, http_path: wallet.http_path, ws_path: wallet.ws_path,
460
- ssl: wallet.ssl, proxy:, log: fake_loog
461
- )
462
- end
463
-
464
- def via_proxy
465
- RandomPort::Pool::SINGLETON.acquire do |port|
466
- donce(
467
- image: 'yegor256/squid-proxy:latest',
468
- ports: { port => 3128 },
469
- env: { 'USERNAME' => 'jeffrey', 'PASSWORD' => 'swordfish' },
470
- root: true, log: fake_loog
471
- ) do
472
- yield "http://jeffrey:swordfish@localhost:#{port}"
473
- end
474
- end
475
- end
476
-
477
- def on_hardhat
478
- RandomPort::Pool::SINGLETON.acquire do |port|
479
- donce(
480
- home: File.join(__dir__, '../../hardhat'),
481
- ports: { port => 8545 },
482
- command: 'npx hardhat node',
483
- log: fake_loog
484
- ) do
485
- wait_for_port(port)
486
- cmd = [
487
- '(cat hardhat.config.js)',
488
- '(ls -al)',
489
- '(echo y | npx hardhat ignition deploy ./ignition/modules/Foo.ts --network foo --deployment-id foo)',
490
- '(npx hardhat ignition status foo | tail -1 | cut -d" " -f3)'
491
- ].join(' && ')
492
- contract = donce(
493
- home: File.join(__dir__, '../../hardhat'),
494
- command: "/bin/bash -c #{Shellwords.escape(cmd)}",
495
- build_args: { 'HOST' => donce_host, 'PORT' => port },
496
- log: fake_loog,
497
- root: true
498
- ).split("\n").last
499
- wallet = ERC20::Wallet.new(
500
- contract:, chain: 4242,
501
- host: 'localhost', port:, http_path: '/', ws_path: '/', ssl: false,
502
- log: fake_loog
503
- )
504
- yield wallet
505
- end
506
- end
507
- end
508
334
  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.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-21 00:00:00.000000000 Z
10
+ date: 2025-04-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: eth
@@ -149,6 +149,7 @@ files:
149
149
  - renovate.json
150
150
  - test/erc20/test_fake_wallet.rb
151
151
  - test/erc20/test_wallet.rb
152
+ - test/erc20/test_wallet_live.rb
152
153
  - test/test__helper.rb
153
154
  homepage: http://github.com/yegor256/erc20.rb
154
155
  licenses: