erc20 0.1.6 → 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,334 +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_eth_pays_on_hardhat
107
- WebMock.enable_net_connect!
108
- on_hardhat do |wallet|
109
- to = Eth::Key.new(priv: WALTER).address.to_s
110
- before = wallet.eth_balance(to)
111
- sum = 42_000
112
- from = Eth::Key.new(priv: JEFF).address.to_s
113
- assert_operator(wallet.eth_balance(from), :>, sum * 2)
114
- txn = wallet.eth_pay(JEFF, to, sum)
115
- assert_equal(66, txn.length)
116
- assert_match(/^0x[a-f0-9]{64}$/, txn)
117
- assert_equal(before + sum, wallet.eth_balance(to))
118
- end
119
- end
120
-
121
- def test_pays_on_hardhat_in_threads
122
- WebMock.enable_net_connect!
123
- on_hardhat do |wallet|
124
- to = Eth::Key.new(priv: WALTER).address.to_s
125
- before = wallet.balance(to)
126
- sum = 42_000
127
- mul = 10
128
- Threads.new(mul).assert do
129
- wallet.pay(JEFF, to, sum)
130
- end
131
- assert_equal(before + (sum * mul), wallet.balance(to))
132
- end
133
- end
134
-
135
- def test_pays_eth_on_hardhat_in_threads
136
- WebMock.enable_net_connect!
137
- on_hardhat do |wallet|
138
- to = Eth::Key.new(priv: WALTER).address.to_s
139
- before = wallet.eth_balance(to)
140
- sum = 42_000
141
- mul = 10
142
- Threads.new(mul).assert do
143
- wallet.eth_pay(JEFF, to, sum)
144
- end
145
- assert_equal(before + (sum * mul), wallet.eth_balance(to))
146
- end
147
- end
148
-
149
- def test_accepts_payments_on_hardhat
150
- WebMock.enable_net_connect!
151
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
152
- jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
153
- on_hardhat do |wallet|
154
- active = []
155
- event = nil
156
- daemon =
157
- Thread.new do
158
- wallet.accept([walter, jeff], active) do |e|
159
- event = e
160
- end
161
- rescue StandardError => e
162
- fake_loog.error(Backtrace.new(e))
163
- end
164
- wait_for { !active.empty? }
165
- sum = 77_000
166
- wallet.pay(JEFF, walter, sum)
167
- wait_for { !event.nil? }
168
- daemon.kill
169
- daemon.join(30)
170
- assert_equal(sum, event[:amount])
171
- assert_equal(jeff, event[:from])
172
- assert_equal(walter, event[:to])
173
- assert_equal(66, event[:txn].length)
174
- end
175
- end
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
-
209
- def test_accepts_many_payments_on_hardhat
210
- WebMock.enable_net_connect!
211
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
212
- on_hardhat do |wallet|
213
- active = []
214
- events = Concurrent::Set.new
215
- total = 10
216
- daemon =
217
- Thread.new do
218
- wallet.accept([walter], active) do |e|
219
- events.add(e)
220
- end
221
- rescue StandardError => e
222
- fake_loog.error(Backtrace.new(e))
223
- end
224
- wait_for { !active.empty? }
225
- sum = 1_234
226
- Threads.new(total).assert do
227
- wallet.pay(JEFF, walter, sum)
228
- end
229
- wait_for { events.size == total }
230
- daemon.kill
231
- daemon.join(30)
232
- assert_equal(total, events.size)
233
- end
234
- end
235
-
236
- def test_accepts_payments_with_failures_on_hardhat
237
- WebMock.enable_net_connect!
238
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
239
- on_hardhat do |wallet|
240
- active = []
241
- events = Concurrent::Set.new
242
- total = 10
243
- daemon =
244
- Thread.new do
245
- wallet.accept([walter], active) do |e|
246
- events.add(e)
247
- raise 'intentional'
248
- end
249
- end
250
- wait_for { !active.empty? }
251
- sum = 1_234
252
- Threads.new(total).assert do
253
- wallet.pay(JEFF, walter, sum)
254
- end
255
- wait_for { events.size == total }
256
- daemon.kill
257
- daemon.join(30)
258
- assert_equal(total, events.size)
259
- end
260
- end
261
-
262
- def test_accepts_payments_on_changing_addresses_on_hardhat
263
- WebMock.enable_net_connect!
264
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
265
- jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
266
- addresses = Primitivo.new([walter])
267
- on_hardhat do |wallet|
268
- active = Primitivo.new([])
269
- event = nil
270
- daemon =
271
- Thread.new do
272
- wallet.accept(addresses, active) do |e|
273
- event = e
274
- end
275
- rescue StandardError => e
276
- fake_loog.error(Backtrace.new(e))
277
- end
278
- wait_for { active.to_a.include?(walter) }
279
- sum1 = 453_000
280
- wallet.pay(JEFF, walter, sum1)
281
- wait_for { !event.nil? }
282
- assert_equal(sum1, event[:amount])
283
- sum2 = 22_000
284
- event = nil
285
- addresses.append(jeff)
286
- wait_for { active.to_a.include?(jeff) }
287
- wallet.pay(WALTER, jeff, sum2)
288
- wait_for { !event.nil? }
289
- assert_equal(sum2, event[:amount])
290
- daemon.kill
291
- daemon.join(30)
292
- end
293
- end
294
-
295
- def test_accepts_payments_on_hardhat_via_proxy
296
- WebMock.enable_net_connect!
297
- via_proxy do |proxy|
298
- walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
299
- jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
300
- on_hardhat do |w|
301
- wallet = through_proxy(w, proxy)
302
- active = []
303
- event = nil
304
- daemon =
305
- Thread.new do
306
- wallet.accept([walter, jeff], active) do |e|
307
- event = e
308
- end
309
- rescue StandardError => e
310
- fake_loog.error(Backtrace.new(e))
311
- end
312
- wait_for { !active.empty? }
313
- sum = 55_000
314
- wallet.pay(JEFF, walter, sum)
315
- wait_for { !event.nil? }
316
- daemon.kill
317
- daemon.join(30)
318
- assert_equal(sum, event[:amount])
319
- end
320
- end
321
- end
322
-
323
- def test_checks_balance_via_proxy
324
- WebMock.enable_net_connect!
325
- b = nil
326
- via_proxy do |proxy|
327
- on_hardhat do |w|
328
- wallet = through_proxy(w, proxy)
329
- b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
330
- end
331
- end
332
- assert_equal(123_000_100_000, b)
333
- end
334
- 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