erc20 0.2.0 → 0.2.1

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.
@@ -1,343 +0,0 @@
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 TestWallet < ERC20::Test
25
- # One guy private hex.
26
- JEFF = '81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a'
27
-
28
- # Another guy private hex.
29
- WALTER = '91f9111b1744d55361e632771a4e53839e9442a9fef45febc0a5c838c686a15b'
30
-
31
- def test_logs_to_stdout
32
- WebMock.disable_net_connect!
33
- stub_request(:post, 'https://example.org/').to_return(
34
- body: { jsonrpc: '2.0', id: 42, result: '0x1F1F1F' }.to_json,
35
- headers: { 'Content-Type' => 'application/json' }
36
- )
37
- w = ERC20::Wallet.new(
38
- host: 'example.org',
39
- http_path: '/',
40
- log: $stdout
41
- )
42
- w.balance(Eth::Key.new(priv: JEFF).address.to_s)
43
- end
44
-
45
- def test_checks_balance_on_testnet
46
- WebMock.enable_net_connect!
47
- b = testnet.balance(Eth::Key.new(priv: JEFF).address.to_s)
48
- refute_nil(b)
49
- assert_predicate(b, :zero?)
50
- end
51
-
52
- def test_checks_gas_estimate_on_hardhat
53
- WebMock.enable_net_connect!
54
- sum = 100_000
55
- on_hardhat do |wallet|
56
- b1 = wallet.gas_estimate(
57
- Eth::Key.new(priv: JEFF).address.to_s,
58
- Eth::Key.new(priv: WALTER).address.to_s,
59
- sum
60
- )
61
- assert_operator(b1, :>, 21_000)
62
- end
63
- end
64
-
65
- def test_checks_balance_on_hardhat
66
- WebMock.enable_net_connect!
67
- on_hardhat do |wallet|
68
- b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
69
- assert_equal(123_000_100_000, b)
70
- end
71
- end
72
-
73
- def test_checks_eth_balance_on_hardhat
74
- WebMock.enable_net_connect!
75
- on_hardhat do |wallet|
76
- b = wallet.balance(Eth::Key.new(priv: WALTER).address.to_s)
77
- assert_equal(456_000_000_000, b)
78
- end
79
- end
80
-
81
- def test_checks_balance_on_hardhat_in_threads
82
- WebMock.enable_net_connect!
83
- on_hardhat do |wallet|
84
- Threads.new.assert do
85
- b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
86
- assert_equal(123_000_100_000, b)
87
- end
88
- end
89
- end
90
-
91
- def test_pays_on_hardhat
92
- WebMock.enable_net_connect!
93
- on_hardhat do |wallet|
94
- to = Eth::Key.new(priv: WALTER).address.to_s
95
- before = wallet.balance(to)
96
- sum = 42_000
97
- from = Eth::Key.new(priv: JEFF).address.to_s
98
- assert_operator(wallet.balance(from), :>, sum * 2)
99
- txn = wallet.pay(JEFF, to, sum)
100
- assert_equal(66, txn.length)
101
- assert_match(/^0x[a-f0-9]{64}$/, txn)
102
- assert_equal(before + sum, wallet.balance(to))
103
- end
104
- end
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
-
115
- def test_eth_pays_on_hardhat
116
- WebMock.enable_net_connect!
117
- on_hardhat do |wallet|
118
- to = Eth::Key.new(priv: WALTER).address.to_s
119
- before = wallet.eth_balance(to)
120
- sum = 42_000
121
- from = Eth::Key.new(priv: JEFF).address.to_s
122
- assert_operator(wallet.eth_balance(from), :>, sum * 2)
123
- txn = wallet.eth_pay(JEFF, to, sum)
124
- assert_equal(66, txn.length)
125
- assert_match(/^0x[a-f0-9]{64}$/, txn)
126
- assert_equal(before + sum, wallet.eth_balance(to))
127
- end
128
- end
129
-
130
- def test_pays_on_hardhat_in_threads
131
- WebMock.enable_net_connect!
132
- on_hardhat do |wallet|
133
- to = Eth::Key.new(priv: WALTER).address.to_s
134
- before = wallet.balance(to)
135
- sum = 42_000
136
- mul = 10
137
- Threads.new(mul).assert do
138
- wallet.pay(JEFF, to, sum)
139
- end
140
- assert_equal(before + (sum * mul), wallet.balance(to))
141
- end
142
- end
143
-
144
- def test_pays_eth_on_hardhat_in_threads
145
- WebMock.enable_net_connect!
146
- on_hardhat do |wallet|
147
- to = Eth::Key.new(priv: WALTER).address.to_s
148
- before = wallet.eth_balance(to)
149
- sum = 42_000
150
- mul = 10
151
- Threads.new(mul).assert do
152
- wallet.eth_pay(JEFF, to, sum)
153
- end
154
- assert_equal(before + (sum * mul), wallet.eth_balance(to))
155
- end
156
- end
157
-
158
- def test_accepts_payments_on_hardhat
159
- WebMock.enable_net_connect!
160
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
161
- jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
162
- on_hardhat do |wallet|
163
- active = []
164
- event = nil
165
- daemon =
166
- Thread.new do
167
- wallet.accept([walter, jeff], active) do |e|
168
- event = e
169
- end
170
- rescue StandardError => e
171
- fake_loog.error(Backtrace.new(e))
172
- end
173
- wait_for { !active.empty? }
174
- sum = 77_000
175
- wallet.pay(JEFF, walter, sum)
176
- wait_for { !event.nil? }
177
- daemon.kill
178
- daemon.join(30)
179
- assert_equal(sum, event[:amount])
180
- assert_equal(jeff, event[:from])
181
- assert_equal(walter, event[:to])
182
- assert_equal(66, event[:txn].length)
183
- end
184
- end
185
-
186
- def test_accepts_payments_on_hardhat_after_disconnect
187
- skip('Works only on macOS') unless OS.mac?
188
- WebMock.enable_net_connect!
189
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
190
- Dir.mktmpdir do |home|
191
- die = File.join(home, 'die.txt')
192
- on_hardhat(die:) do |wallet|
193
- active = []
194
- events = []
195
- daemon =
196
- Thread.new do
197
- wallet.accept([walter], active, subscription_id: 42) do |e|
198
- events.append(e)
199
- end
200
- rescue StandardError => e
201
- fake_loog.error(Backtrace.new(e))
202
- end
203
- wait_for { !active.empty? }
204
- wallet.pay(JEFF, walter, 4_567)
205
- wait_for { events.size == 1 }
206
- FileUtils.touch(die)
207
- on_hardhat(port: wallet.port) do
208
- wallet.pay(JEFF, walter, 3_456)
209
- wait_for { events.size > 1 }
210
- daemon.kill
211
- daemon.join(30)
212
- assert_equal(3, events.size)
213
- end
214
- end
215
- end
216
- end
217
-
218
- def test_accepts_many_payments_on_hardhat
219
- WebMock.enable_net_connect!
220
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
221
- on_hardhat do |wallet|
222
- active = []
223
- events = Concurrent::Set.new
224
- total = 10
225
- daemon =
226
- Thread.new do
227
- wallet.accept([walter], active) do |e|
228
- events.add(e)
229
- end
230
- rescue StandardError => e
231
- fake_loog.error(Backtrace.new(e))
232
- end
233
- wait_for { !active.empty? }
234
- sum = 1_234
235
- Threads.new(total).assert do
236
- wallet.pay(JEFF, walter, sum)
237
- end
238
- wait_for { events.size == total }
239
- daemon.kill
240
- daemon.join(30)
241
- assert_equal(total, events.size)
242
- end
243
- end
244
-
245
- def test_accepts_payments_with_failures_on_hardhat
246
- WebMock.enable_net_connect!
247
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
248
- on_hardhat do |wallet|
249
- active = []
250
- events = Concurrent::Set.new
251
- total = 10
252
- daemon =
253
- Thread.new do
254
- wallet.accept([walter], active) do |e|
255
- events.add(e)
256
- raise 'intentional'
257
- end
258
- end
259
- wait_for { !active.empty? }
260
- sum = 1_234
261
- Threads.new(total).assert do
262
- wallet.pay(JEFF, walter, sum)
263
- end
264
- wait_for { events.size == total }
265
- daemon.kill
266
- daemon.join(30)
267
- assert_equal(total, events.size)
268
- end
269
- end
270
-
271
- def test_accepts_payments_on_changing_addresses_on_hardhat
272
- WebMock.enable_net_connect!
273
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
274
- jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
275
- addresses = Primitivo.new([walter])
276
- on_hardhat do |wallet|
277
- active = Primitivo.new([])
278
- event = nil
279
- daemon =
280
- Thread.new do
281
- wallet.accept(addresses, active) do |e|
282
- event = e
283
- end
284
- rescue StandardError => e
285
- fake_loog.error(Backtrace.new(e))
286
- end
287
- wait_for { active.to_a.include?(walter) }
288
- sum1 = 453_000
289
- wallet.pay(JEFF, walter, sum1)
290
- wait_for { !event.nil? }
291
- assert_equal(sum1, event[:amount])
292
- sum2 = 22_000
293
- event = nil
294
- addresses.append(jeff)
295
- wait_for { active.to_a.include?(jeff) }
296
- wallet.pay(WALTER, jeff, sum2)
297
- wait_for { !event.nil? }
298
- assert_equal(sum2, event[:amount])
299
- daemon.kill
300
- daemon.join(30)
301
- end
302
- end
303
-
304
- def test_accepts_payments_on_hardhat_via_proxy
305
- WebMock.enable_net_connect!
306
- via_proxy do |proxy|
307
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
308
- jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
309
- on_hardhat do |w|
310
- wallet = through_proxy(w, proxy)
311
- active = []
312
- event = nil
313
- daemon =
314
- Thread.new do
315
- wallet.accept([walter, jeff], active) do |e|
316
- event = e
317
- end
318
- rescue StandardError => e
319
- fake_loog.error(Backtrace.new(e))
320
- end
321
- wait_for { !active.empty? }
322
- sum = 55_000
323
- wallet.pay(JEFF, walter, sum)
324
- wait_for { !event.nil? }
325
- daemon.kill
326
- daemon.join(30)
327
- assert_equal(sum, event[:amount])
328
- end
329
- end
330
- end
331
-
332
- def test_checks_balance_via_proxy
333
- WebMock.enable_net_connect!
334
- b = nil
335
- via_proxy do |proxy|
336
- on_hardhat do |w|
337
- wallet = through_proxy(w, proxy)
338
- b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
339
- end
340
- end
341
- assert_equal(123_000_100_000, b)
342
- end
343
- end
@@ -1,126 +0,0 @@
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 DELETED
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
4
- # SPDX-License-Identifier: MIT
5
-
6
- $stdout.sync = true
7
-
8
- require 'simplecov'
9
- require 'simplecov-cobertura'
10
- unless SimpleCov.running
11
- SimpleCov.command_name('test')
12
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
13
- [
14
- SimpleCov::Formatter::HTMLFormatter,
15
- SimpleCov::Formatter::CoberturaFormatter
16
- ]
17
- )
18
- SimpleCov.minimum_coverage 90
19
- SimpleCov.minimum_coverage_by_file 90
20
- SimpleCov.start do
21
- add_filter 'test/'
22
- add_filter 'vendor/'
23
- add_filter 'target/'
24
- track_files 'lib/**/*.rb'
25
- track_files '*.rb'
26
- end
27
- end
28
-
29
- require 'minitest/autorun'
30
- require 'minitest/reporters'
31
- Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
32
-
33
- # To make tests retry on failure:
34
- if ENV['RAKE']
35
- require 'minitest/retry'
36
- Minitest::Retry.use!
37
- end
38
-
39
- # Primitive array.
40
- class Primitivo
41
- def initialize(array)
42
- @array = array
43
- end
44
-
45
- def clear
46
- @array.clear
47
- end
48
-
49
- def to_a
50
- @array.to_a
51
- end
52
-
53
- def append(item)
54
- @array.append(item)
55
- end
56
- end
57
-
58
- require 'webmock/minitest'
59
-
60
- # Test.
61
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
62
- # Copyright:: Copyright (c) 2025 Yegor Bugayenko
63
- # License:: MIT
64
- class ERC20::Test < Minitest::Test
65
- def fake_loog
66
- ENV['RAKE'] ? Loog::ERRORS : Loog::VERBOSE
67
- end
68
-
69
- def wait_for(seconds = 30)
70
- start = Time.now
71
- loop do
72
- sleep(0.1)
73
- break if yield
74
- passed = Time.now - start
75
- raise "Giving up after #{passed} seconds of waiting" if passed > seconds
76
- rescue Errno::ECONNREFUSED
77
- retry
78
- end
79
- end
80
-
81
- def wait_for_port(port)
82
- wait_for { Typhoeus::Request.get("http://localhost:#{port}").code == 200 }
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
206
- end