erc20 0.1.1 → 0.1.3

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: fbce7bc57948f9c8acfeec7c0af2cf67e70e2447e064040485e6868d466828dc
4
- data.tar.gz: cc521c55db79dca85e58235c886172a7ca81cf0f59ee61f5c134cc888eb3db4c
3
+ metadata.gz: 429ad43c63c2126e8f57d5adcbe391c573f34f0a4d1174d19427f7f0b121aab2
4
+ data.tar.gz: e354d3bc37396d2c1b35da5ca1800ce01ce90905b9ad9bd64e0617c71e1350b1
5
5
  SHA512:
6
- metadata.gz: c712231ba46caabca78b45e9be366c120e2f5275bc734c874f69c7306d1f961c7b7c3a5b8ae58341cb4d48776a91c5a8bc8ec2360dfcbc294d46ae329d9bf8a4
7
- data.tar.gz: 7a074720cfec0773a6e4e0cc70fe07fde201ae9897cb395a2b91a84429683354b822bef0be10650290b554ffe59b150034a1ea320eed34a396d574267954c130
6
+ metadata.gz: fc5eb0f95f5269cd94b848a05839ac4939b60efef2b0036a2ab693d6f16cc6a1c0d025d5ba580c2f5ce47232c494a64de224b66a47a155464b1401ac022d82f3
7
+ data.tar.gz: 83b0d56783372f8a90eb0e9d89f6eed15101eb7eb0787882833baa73b1d97163b08b175f80c350d1f236e74e363ee9c1ffd4412437baa9630bfa4ee87196d8e2
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ source 'https://rubygems.org'
7
7
  gemspec
8
8
 
9
9
  gem 'backtrace', '>0', require: false
10
+ gem 'concurrent-ruby', '~>1.2', require: false
10
11
  gem 'cucumber', '~>9.2', require: false
11
12
  gem 'donce', '>0', require: false
12
13
  gem 'faraday', '>0', require: false
data/Gemfile.lock CHANGED
@@ -46,14 +46,15 @@ GEM
46
46
  cucumber-tag-expressions (6.1.2)
47
47
  diff-lcs (1.6.1)
48
48
  docile (1.4.1)
49
- donce (0.1.0)
50
- backtrace (> 0)
51
- os (> 0)
52
- qbash (> 0)
49
+ donce (0.2.0)
50
+ backtrace (~> 0.3)
51
+ os (~> 1.1)
52
+ qbash (~> 0.3)
53
53
  elapsed (0.0.1)
54
54
  loog (> 0)
55
55
  tago (> 0)
56
- eth (0.5.13)
56
+ eth (0.5.14)
57
+ bigdecimal (~> 3.1)
57
58
  forwardable (~> 1.3)
58
59
  keccak (~> 1.3)
59
60
  konstructor (~> 1.0)
@@ -63,7 +64,7 @@ GEM
63
64
  ethon (0.16.0)
64
65
  ffi (>= 1.15.0)
65
66
  eventmachine (1.2.7)
66
- faraday (2.12.2)
67
+ faraday (2.13.0)
67
68
  faraday-net_http (>= 2.0, < 3.5)
68
69
  json
69
70
  logger
@@ -72,10 +73,10 @@ GEM
72
73
  faye-websocket (0.11.3)
73
74
  eventmachine (>= 0.12.0)
74
75
  websocket-driver (>= 0.5.1)
75
- ffi (1.17.1-arm64-darwin)
76
- ffi (1.17.1-x64-mingw-ucrt)
77
- ffi (1.17.1-x86_64-darwin)
78
- ffi (1.17.1-x86_64-linux-gnu)
76
+ ffi (1.17.2-arm64-darwin)
77
+ ffi (1.17.2-x64-mingw-ucrt)
78
+ ffi (1.17.2-x86_64-darwin)
79
+ ffi (1.17.2-x86_64-linux-gnu)
79
80
  ffi-compiler (1.3.2)
80
81
  ffi (>= 1.15.5)
81
82
  rake
@@ -106,11 +107,11 @@ GEM
106
107
  uri
107
108
  openssl (3.3.0)
108
109
  os (1.1.4)
109
- parallel (1.26.3)
110
- parser (3.3.7.4)
110
+ parallel (1.27.0)
111
+ parser (3.3.8.0)
111
112
  ast (~> 2.4.1)
112
113
  racc
113
- pkg-config (1.6.0)
114
+ pkg-config (1.6.1)
114
115
  prism (1.4.0)
115
116
  qbash (0.4.0)
116
117
  backtrace (> 0)
@@ -139,7 +140,7 @@ GEM
139
140
  rubocop-ast (>= 1.44.0, < 2.0)
140
141
  ruby-progressbar (~> 1.7)
141
142
  unicode-display_width (>= 2.4.0, < 4.0)
142
- rubocop-ast (1.44.0)
143
+ rubocop-ast (1.44.1)
143
144
  parser (>= 3.3.7.2)
144
145
  prism (~> 1.4)
145
146
  rubocop-minitest (0.38.0)
@@ -153,7 +154,7 @@ GEM
153
154
  rubocop-rake (0.7.1)
154
155
  lint_roller (~> 1.1)
155
156
  rubocop (>= 1.72.1)
156
- rubocop-rspec (3.5.0)
157
+ rubocop-rspec (3.6.0)
157
158
  lint_roller (~> 1.1)
158
159
  rubocop (~> 1.72, >= 1.72.1)
159
160
  ruby-progressbar (1.13.0)
@@ -200,6 +201,7 @@ PLATFORMS
200
201
 
201
202
  DEPENDENCIES
202
203
  backtrace (> 0)
204
+ concurrent-ruby (~> 1.2)
203
205
  cucumber (~> 9.2)
204
206
  donce (> 0)
205
207
  erc20!
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Ethereum ERC20 Manipulations in Ruby
2
2
 
3
- [![DevOps By Rultor.com](http://www.rultor.com/b/yegor256/erc20)](http://www.rultor.com/p/yegor256/erc20)
3
+ [![DevOps By Rultor.com](https://www.rultor.com/b/yegor256/erc20)](https://www.rultor.com/p/yegor256/erc20)
4
4
  [![We recommend RubyMine](https://www.elegantobjects.org/rubymine.svg)](https://www.jetbrains.com/ruby/)
5
5
 
6
6
  [![rake](https://github.com/yegor256/erc20/actions/workflows/rake.yml/badge.svg)](https://github.com/yegor256/erc20/actions/workflows/rake.yml)
7
- [![PDD status](http://www.0pdd.com/svg?name=yegor256/erc20)](http://www.0pdd.com/p?name=yegor256/erc20)
8
- [![Gem Version](https://badge.fury.io/rb/erc20.svg)](http://badge.fury.io/rb/erc20)
7
+ [![PDD status](https://www.0pdd.com/svg?name=yegor256/erc20)](https://www.0pdd.com/p?name=yegor256/erc20)
8
+ [![Gem Version](https://badge.fury.io/rb/erc20.svg)](https://badge.fury.io/rb/erc20)
9
9
  [![Test Coverage](https://img.shields.io/codecov/c/github/yegor256/erc20.svg)](https://codecov.io/github/yegor256/erc20?branch=master)
10
- [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/yegor256/erc20/master/frames)
10
+ [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/yegor256/erc20/master/frames)
11
11
  [![Hits-of-Code](https://hitsofcode.com/github/yegor256/erc20)](https://hitsofcode.com/view/github/yegor256/erc20)
12
12
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/erc20/blob/master/LICENSE.txt)
13
13
 
@@ -15,7 +15,22 @@ This small Ruby [gem](https://rubygems.org/gems/erc20)
15
15
  makes manipulations with [Ethereum] [ERC20] tokens
16
16
  as simple as possible, when you have a provider of
17
17
  [JSON-RPC] and [WebSockets] Ethereum APIs, such as
18
- [Infura], [GetBlock], or [Alchemy]:
18
+ [Infura], [GetBlock], or [Alchemy].
19
+
20
+ Install it like this:
21
+
22
+ ```bash
23
+ gem install erc20
24
+ ```
25
+
26
+ Or simply add this to your Gemfile:
27
+
28
+ ```ruby
29
+ gem 'erc20'
30
+ ```
31
+
32
+ Then, make an instance of the main class and use to read
33
+ balances, send and receive payments:
19
34
 
20
35
  ```ruby
21
36
  # Create a wallet:
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.1'
28
+ VERSION = '0.1.3'
29
29
  end
data/lib/erc20/wallet.rb CHANGED
@@ -121,7 +121,7 @@ class ERC20::Wallet
121
121
  data = "0x#{func}000000000000000000000000#{address[2..].downcase}"
122
122
  r = jsonrpc.eth_call({ to: @contract, data: data }, 'latest')
123
123
  b = r[2..].to_i(16)
124
- @log.debug("Balance of #{address} is #{b} ERC20 tokens")
124
+ @log.debug("The balance of #{address} is #{b} ERC20 tokens")
125
125
  b
126
126
  end
127
127
 
@@ -138,7 +138,7 @@ class ERC20::Wallet
138
138
  raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
139
139
  r = jsonrpc.eth_getBalance(address, 'latest')
140
140
  b = r[2..].to_i(16)
141
- @log.debug("Balance of #{address} is #{b} ETHs")
141
+ @log.debug("The balance of #{address} is #{b} ETHs")
142
142
  b
143
143
  end
144
144
 
@@ -294,9 +294,14 @@ class ERC20::Wallet
294
294
  # Once we actually start listening, the +active+ array will be updated
295
295
  # with the list of addresses.
296
296
  #
297
- # The +addresses+ must have +to_a()+ implemented.
298
- # The +active+ must have +append()+ implemented.
299
- # Only these methods will be called.
297
+ # The +addresses+ must have +to_a()+ implemented. This method will be
298
+ # called every +delay+ seconds. It is expected that it returns the list
299
+ # of Ethereum public addresses that must be monitored.
300
+ #
301
+ # The +active+ must have +append()+ and +to_a()+ implemented. This array
302
+ # maintains the list of addresses that were mentioned in incoming transactions.
303
+ # This array is used mostly for testing. It is suggested to always provide
304
+ # an empty array.
300
305
  #
301
306
  # @param [Array<String>] addresses Addresses to monitor
302
307
  # @param [Array] active List of addresses that we are actually listening to
@@ -321,50 +326,58 @@ class ERC20::Wallet
321
326
  attempt = []
322
327
  log_url = "ws#{@ssl ? 's' : ''}://#{u.hostname}:#{u.port}"
323
328
  ws.on(:open) do
324
- verbose do
325
- log.debug("Connected to #{log_url}")
329
+ safe do
330
+ verbose do
331
+ log.debug("Connected to #{log_url}")
332
+ end
326
333
  end
327
334
  end
328
335
  ws.on(:message) do |msg|
329
- verbose do
330
- data = to_json(msg)
331
- if data['id']
332
- before = active.to_a
333
- attempt.each do |a|
334
- active.append(a) unless before.include?(a)
335
- end
336
- log.debug(
337
- "Subscribed ##{subscription_id} to #{active.to_a.size} addresses at #{log_url}: " \
338
- "#{active.to_a.map { |a| a[0..6] }.join(', ')}"
339
- )
340
- elsif data['method'] == 'eth_subscription' && data.dig('params', 'result')
341
- event = data['params']['result']
342
- if raw
343
- log.debug("New event arrived from #{event['address']}")
344
- else
345
- event = {
346
- amount: event['data'].to_i(16),
347
- from: "0x#{event['topics'][1][26..].downcase}",
348
- to: "0x#{event['topics'][2][26..].downcase}",
349
- txn: event['transactionHash'].downcase
350
- }
336
+ safe do
337
+ verbose do
338
+ data = to_json(msg)
339
+ if data['id']
340
+ before = active.to_a
341
+ attempt.each do |a|
342
+ active.append(a) unless before.include?(a)
343
+ end
351
344
  log.debug(
352
- "Payment of #{event[:amount]} tokens arrived " \
353
- "from #{event[:from]} to #{event[:to]} in #{event[:txn]}"
345
+ "Subscribed ##{subscription_id} to #{active.to_a.size} addresses at #{log_url}: " \
346
+ "#{active.to_a.map { |a| a[0..6] }.join(', ')}"
354
347
  )
348
+ elsif data['method'] == 'eth_subscription' && data.dig('params', 'result')
349
+ event = data['params']['result']
350
+ if raw
351
+ log.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.debug(
360
+ "Payment of #{event[:amount]} tokens arrived " \
361
+ "from #{event[:from]} to #{event[:to]} in #{event[:txn]}"
362
+ )
363
+ end
364
+ yield event
355
365
  end
356
- yield event
357
366
  end
358
367
  end
359
368
  end
360
369
  ws.on(:close) do
361
- verbose do
362
- log.debug("Disconnected from #{log_url}")
370
+ safe do
371
+ verbose do
372
+ log.debug("Disconnected from #{log_url}")
373
+ end
363
374
  end
364
375
  end
365
376
  ws.on(:error) do |e|
366
- verbose do
367
- log.debug("Error at #{log_url}: #{e.message}")
377
+ safe do
378
+ verbose do
379
+ log.debug("Error at #{log_url}: #{e.message}")
380
+ end
368
381
  end
369
382
  end
370
383
  EventMachine.add_periodic_timer(delay) do
@@ -411,15 +424,22 @@ class ERC20::Wallet
411
424
  raise e
412
425
  end
413
426
 
427
+ def safe
428
+ yield
429
+ rescue StandardError
430
+ # ignore it
431
+ end
432
+
414
433
  def url(http: true)
415
434
  URI.parse("#{http ? 'http' : 'ws'}#{@ssl ? 's' : ''}://#{@host}:#{@port}#{http ? @http_path : @ws_path}")
416
435
  end
417
436
 
418
437
  def jsonrpc
419
438
  JSONRPC.logger = Loog::NULL
420
- connection =
421
- if @proxy
422
- uri = URI.parse(@proxy)
439
+ opts = {}
440
+ if @proxy
441
+ uri = URI.parse(@proxy)
442
+ opts[:connection] =
423
443
  Faraday.new do |f|
424
444
  f.adapter(Faraday.default_adapter)
425
445
  f.proxy = {
@@ -428,8 +448,8 @@ class ERC20::Wallet
428
448
  password: uri.password
429
449
  }
430
450
  end
431
- end
432
- JSONRPC::Client.new(url, connection:)
451
+ end
452
+ JSONRPC::Client.new(url.to_s, opts)
433
453
  end
434
454
 
435
455
  def to_pay_data(address, amount)
@@ -8,7 +8,6 @@ require 'donce'
8
8
  require 'eth'
9
9
  require 'faraday'
10
10
  require 'loog'
11
- require 'minitest/autorun'
12
11
  require 'random-port'
13
12
  require 'shellwords'
14
13
  require 'threads'
@@ -20,7 +19,7 @@ require_relative '../../lib/erc20/fake_wallet'
20
19
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
21
20
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
22
21
  # License:: MIT
23
- class TestFakeWallet < Minitest::Test
22
+ class TestFakeWallet < ERC20::Test
24
23
  def test_checks_gas_estimate
25
24
  b = ERC20::FakeWallet.new.gas_estimate(
26
25
  '0xEB2fE8872A6f1eDb70a2632Effffffffffffffff',
@@ -8,7 +8,6 @@ require 'donce'
8
8
  require 'eth'
9
9
  require 'faraday'
10
10
  require 'loog'
11
- require 'minitest/autorun'
12
11
  require 'random-port'
13
12
  require 'shellwords'
14
13
  require 'threads'
@@ -20,7 +19,7 @@ require_relative '../../lib/erc20/wallet'
20
19
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
21
20
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
22
21
  # License:: MIT
23
- class TestWallet < Minitest::Test
22
+ class TestWallet < ERC20::Test
24
23
  # At this address, in Ethereum mainnet, there are $8 USDT and 0.0042 ETH. I won't
25
24
  # move them anyway, that's why tests can use this address forever.
26
25
  STABLE = '0x7232148927F8a580053792f44D4d59d40Fd00ABD'
@@ -60,6 +59,7 @@ class TestWallet < Minitest::Test
60
59
  def test_fails_with_invalid_infura_key
61
60
  skip('Apparently, even with invalid key, Infura returns balance')
62
61
  w = ERC20::Wallet.new(
62
+ contract: ERC20::Wallet.USDT,
63
63
  host: 'mainnet.infura.io',
64
64
  http_path: '/v3/invalid-key-here',
65
65
  log: loog
@@ -201,6 +201,57 @@ class TestWallet < Minitest::Test
201
201
  end
202
202
  end
203
203
 
204
+ def test_accepts_many_payments_on_hardhat
205
+ walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
206
+ on_hardhat do |wallet|
207
+ active = []
208
+ events = Concurrent::Set.new
209
+ total = 10
210
+ daemon =
211
+ Thread.new do
212
+ wallet.accept([walter], active) do |e|
213
+ events.add(e)
214
+ end
215
+ rescue StandardError => e
216
+ loog.error(Backtrace.new(e))
217
+ end
218
+ wait_for { !active.empty? }
219
+ sum = 1_234
220
+ Threads.new(total).assert do
221
+ wallet.pay(JEFF, walter, sum)
222
+ end
223
+ wait_for { events.size == total }
224
+ daemon.kill
225
+ daemon.join(30)
226
+ assert_equal(total, events.size)
227
+ end
228
+ end
229
+
230
+ def test_accepts_payments_with_failures_on_hardhat
231
+ walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
232
+ on_hardhat do |wallet|
233
+ active = []
234
+ events = Concurrent::Set.new
235
+ total = 10
236
+ daemon =
237
+ Thread.new do
238
+ wallet.accept([walter], active) do |e|
239
+ events.add(e)
240
+ raise 'intentional'
241
+ end
242
+ end
243
+ wait_for { !active.empty? }
244
+ sum = 1_234
245
+ Threads.new(total).assert do
246
+ wallet.pay(JEFF, walter, sum)
247
+ end
248
+ wait_for { events.size == total }
249
+ daemon.kill
250
+ daemon.join(30)
251
+ assert_equal(total, events.size)
252
+ end
253
+ end
254
+
204
255
  def test_accepts_payments_on_changing_addresses_on_hardhat
205
256
  walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
206
257
  jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
@@ -280,13 +331,14 @@ class TestWallet < Minitest::Test
280
331
  end
281
332
 
282
333
  def test_checks_balance_via_proxy
334
+ b = nil
283
335
  via_proxy do |proxy|
284
336
  on_hardhat do |w|
285
337
  wallet = through_proxy(w, proxy)
286
338
  b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
287
- assert_equal(123_000_100_000, b)
288
339
  end
289
340
  end
341
+ assert_equal(123_000_100_000, b)
290
342
  end
291
343
 
292
344
  def test_checks_balance_via_proxy_on_mainnet
data/test/test__helper.rb CHANGED
@@ -6,14 +6,27 @@
6
6
  $stdout.sync = true
7
7
 
8
8
  require 'simplecov'
9
- SimpleCov.external_at_exit = true
10
- SimpleCov.start
11
-
12
9
  require 'simplecov-cobertura'
13
- SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
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
14
28
 
15
29
  require 'minitest/autorun'
16
-
17
30
  require 'minitest/reporters'
18
31
  Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
19
32
 
@@ -42,7 +55,7 @@ end
42
55
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
43
56
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
44
57
  # License:: MIT
45
- class Minitest::Test
58
+ class ERC20::Test < Minitest::Test
46
59
  def loog
47
60
  ENV['RAKE'] ? Loog::ERRORS : Loog::VERBOSE
48
61
  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.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-05 00:00:00.000000000 Z
10
+ date: 2025-04-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: eth
@@ -122,7 +122,6 @@ files:
122
122
  - ".pdd"
123
123
  - ".rubocop.yml"
124
124
  - ".rultor.yml"
125
- - ".simplecov"
126
125
  - ".yamllint.yml"
127
126
  - Gemfile
128
127
  - Gemfile.lock
data/.simplecov DELETED
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
5
- # SPDX-License-Identifier: MIT
6
-
7
- if Gem.win_platform?
8
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
9
- SimpleCov::Formatter::HTMLFormatter
10
- ]
11
- SimpleCov.start do
12
- add_filter '/test/'
13
- end
14
- else
15
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
16
- [SimpleCov::Formatter::HTMLFormatter]
17
- )
18
- SimpleCov.start do
19
- add_filter '/test/'
20
- minimum_coverage 20
21
- end
22
- end