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 +4 -4
- data/.rubocop.yml +0 -1
- data/Gemfile.lock +2 -2
- data/Rakefile +0 -1
- data/lib/erc20/erc20.rb +1 -1
- data/lib/erc20/wallet.rb +97 -81
- data/test/erc20/test_wallet.rb +36 -210
- data/test/erc20/test_wallet_live.rb +126 -0
- data/test/test__helper.rb +126 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7843a4cb824e3689fb7a457f8ca0744f1d1059349aa6ea8bfe8805cd597e8b24
|
4
|
+
data.tar.gz: d11d0eb794c22737519ef5ee2083763ba8244ce91f01f0f120ee8e6970af53c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 135c421a05cbc431e20113e8cd6d63294630588b6ba27a63249313455853b83266d697759764b6f82aae857131237fd5c8c85f4f1432fe74cb73a25935b5ca45
|
7
|
+
data.tar.gz: ecf62b97b0053bc123de22f600e61a90cbf2e0636c1afa199d96478df0186194a917d27f0234285deb14129715970e2b818be5f6cf7d3a692d131a685a1af41f
|
data/.rubocop.yml
CHANGED
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.
|
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.
|
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
data/lib/erc20/erc20.rb
CHANGED
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
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
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
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
"
|
346
|
-
"#{
|
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
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
data/test/erc20/test_wallet.rb
CHANGED
@@ -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(
|
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(
|
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
|
+
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-
|
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:
|