erc20 0.0.4 → 0.0.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: 6b5bccf0ea259136bad39ab36f01240da8a029958b627ed337cd20477a7a13c9
4
- data.tar.gz: 4c5d605e086d38755d25d8153c0bf6b220460f5b984607fd304706fe0b3d942f
3
+ metadata.gz: ddbc3966b9eec2d455e33dde71df0427a7a99e4d59f0df69191d8f617434c8bf
4
+ data.tar.gz: 0d577a397eee74c5dae388ca620f8a681c4bec2af8d9b6cf5514ca32cbebc992
5
5
  SHA512:
6
- metadata.gz: 0adbab22a3b752f63e1e001eac554ba8bcc8a40a2fea7234b9d674608eb281c912fb2e21931497006509b8cf2ae80e82d9de8dd49b7727f48759796da375d43b
7
- data.tar.gz: ae3387a6c71c5ba367c8bd5814d2eebced21d6de6087dc214b083659811e2ed60efb5d0d981b7ba57b10030435566645ddd8328ac99ef4f9574b534192385383
6
+ metadata.gz: 1f87ab9c39b5476b9b90bd44020f21970edc8830826129b0a2c6386d562887627e346c55a1e8bb1fcf3e9983d1977e6e0851b7e2fdc162217898951988b2173f
7
+ data.tar.gz: 02d6b7e7058dd6c6f3a74b81ece853e674020a4df84b6fa439b74438ff8227616a76cf3abd1a6f82fdfb7f34391c4e541f96791e404e8e84cb3f364d72e92771
data/Gemfile CHANGED
@@ -25,6 +25,7 @@ gemspec
25
25
 
26
26
  gem 'backtrace', '>0', require: false
27
27
  gem 'donce', '>0', require: false
28
+ gem 'faraday', '>0', require: false
28
29
  gem 'loog', '>0', require: false
29
30
  gem 'minitest', '5.25.4', require: false
30
31
  gem 'minitest-reporters', '1.7.1', require: false
data/Gemfile.lock CHANGED
@@ -2,11 +2,11 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  erc20 (0.0.0)
5
- backtrace (> 0)
6
- eth (~> 0.4)
7
- jsonrpc-client (> 0)
5
+ eth (>= 0.5.13)
6
+ faye-websocket (>= 0.11.3)
7
+ json (>= 2.10.1)
8
+ jsonrpc-client (>= 0.1.4)
8
9
  loog (> 0)
9
- websocket-client-simple (> 0)
10
10
 
11
11
  GEM
12
12
  remote: https://rubygems.org/
@@ -71,13 +71,16 @@ GEM
71
71
  scrypt (~> 3.0)
72
72
  ethon (0.16.0)
73
73
  ffi (>= 1.15.0)
74
- event_emitter (0.2.6)
74
+ eventmachine (1.2.7)
75
75
  faraday (2.12.2)
76
76
  faraday-net_http (>= 2.0, < 3.5)
77
77
  json
78
78
  logger
79
79
  faraday-net_http (3.4.0)
80
80
  net-http (>= 0.5.0)
81
+ faye-websocket (0.11.3)
82
+ eventmachine (>= 0.12.0)
83
+ websocket-driver (>= 0.5.1)
81
84
  ffi (1.17.1-arm64-darwin)
82
85
  ffi (1.17.1-x64-mingw-ucrt)
83
86
  ffi (1.17.1-x86_64-darwin)
@@ -93,7 +96,7 @@ GEM
93
96
  pp (>= 0.6.0)
94
97
  rdoc (>= 4.0.0)
95
98
  reline (>= 0.4.2)
96
- json (2.10.0)
99
+ json (2.10.1)
97
100
  jsonrpc-client (0.1.4)
98
101
  faraday
99
102
  multi_json (>= 1.1.0)
@@ -115,7 +118,6 @@ GEM
115
118
  minitest-retry (0.2.5)
116
119
  minitest (>= 5.0)
117
120
  multi_json (1.15.0)
118
- mutex_m (0.3.0)
119
121
  net-http (0.6.0)
120
122
  uri
121
123
  nokogiri (1.18.2-arm64-darwin)
@@ -251,12 +253,10 @@ GEM
251
253
  unicode-emoji (4.0.4)
252
254
  uri (1.0.2)
253
255
  useragent (0.16.11)
254
- websocket (1.2.11)
255
- websocket-client-simple (0.9.0)
256
+ websocket-driver (0.7.7)
256
257
  base64
257
- event_emitter
258
- mutex_m
259
- websocket
258
+ websocket-extensions (>= 0.1.0)
259
+ websocket-extensions (0.1.5)
260
260
  yard (0.9.37)
261
261
  zeitwerk (2.7.1)
262
262
 
@@ -273,6 +273,7 @@ DEPENDENCIES
273
273
  backtrace (> 0)
274
274
  donce (> 0)
275
275
  erc20!
276
+ faraday (> 0)
276
277
  loog (> 0)
277
278
  minitest (= 5.25.4)
278
279
  minitest-reporters (= 1.7.1)
data/README.md CHANGED
@@ -23,18 +23,19 @@ require 'erc20'
23
23
  w = ERC20::Wallet.new(
24
24
  contract: ERC20::Wallet.USDT, # hex of it
25
25
  host: 'mainnet.infura.io',
26
- path: '/v3/<your-key>',
26
+ http_path: '/v3/<your-infura-key>',
27
+ ws_path: '/ws/v3/<your-infura-key>',
27
28
  log: $stdout
28
29
  )
29
30
 
30
- # Check balance on the address:
31
+ # Check how many ERC20 tokens are on the given address:
31
32
  usdt = w.balance(address)
32
33
 
33
- # Send a few tokens to someone and get transaction hash:
34
- txn = w.pay(private_key, to_address, amount)
34
+ # Send a few ERC20 tokens to someone and get transaction hash:
35
+ hex = w.pay(private_key, to_address, amount)
35
36
 
36
- # Stay waiting, and trigger the block when transactions arrive:
37
- addresses = ['0x...', '0x...']
37
+ # Stay waiting, and trigger the block when new ERC20 payments show up:
38
+ addresses = ['0x...', '0x...'] # only wait for payments to these addresses
38
39
  w.accept(addresses) do |event|
39
40
  puts event[:amount] # how much
40
41
  puts event[:from] # who sent the payment
@@ -55,6 +56,22 @@ To get address from private one:
55
56
  public_hex = Eth::Key.new(priv: key).address
56
57
  ```
57
58
 
59
+ To connect to the server via [HTTP proxy] with [basic authentication]:
60
+
61
+ ```ruby
62
+ w = ERC20::Wallet.new(
63
+ host: 'go.getblock.io',
64
+ http_path: '/<your-rpc-getblock-key>',
65
+ ws_path: '/<your-ws-getblock-key>',
66
+ proxy: 'http://jeffrey:swordfish@example.com:3128' # here!
67
+ )
68
+ ```
69
+
70
+ You can use [squid-proxy] [Docker] image to set up your own [HTTP proxy] server.
71
+
72
+ Of course, this library works with [Polygon], [Optimism],
73
+ and other forks of [Etherium].
74
+
58
75
  ## How to contribute
59
76
 
60
77
  Read
@@ -78,3 +95,9 @@ If it's clean and you don't see any error messages, submit your pull request.
78
95
  [Infura]: https://infura.io/
79
96
  [Alchemy]: https://alchemy.com/
80
97
  [GetBlock]: https://getblock.io/
98
+ [basic authentication]: https://en.wikipedia.org/wiki/Basic_access_authentication
99
+ [HTTP proxy]: https://en.wikipedia.org/wiki/Proxy_server
100
+ [squid-proxy]: https://github.com/yegor256/squid-proxy
101
+ [Docker]: https://www.docker.com/
102
+ [Polygon]: https://polygon.technology/
103
+ [Optimism]: https://www.optimism.io/
data/Rakefile CHANGED
@@ -24,6 +24,8 @@ require 'rubygems'
24
24
  require 'rake'
25
25
  require 'rake/clean'
26
26
 
27
+ ENV['RAKE'] = 'true'
28
+
27
29
  def name
28
30
  @name ||= File.basename(Dir['*.gemspec'].first, '.*')
29
31
  end
data/erc20.gemspec CHANGED
@@ -31,18 +31,20 @@ Gem::Specification.new do |s|
31
31
  s.license = 'MIT'
32
32
  s.summary = 'Sending and receiving ERC20 tokens in Etherium network'
33
33
  s.description =
34
- 'A simple library for making ERC20 manipulations as easy as they' \
35
- 'can be for cryptocurrency newbies'
34
+ 'A simple library for making ERC20 manipulations as easy as they ' \
35
+ 'can be for cryptocurrency newbies: checking balance, sending payments, ' \
36
+ 'and monitoring addresses for incoming payments. The library expects ' \
37
+ 'Etherium node to provide JSON RPC and Websockets API.'
36
38
  s.authors = ['Yegor Bugayenko']
37
39
  s.email = 'yegor256@gmail.com'
38
40
  s.homepage = 'http://github.com/yegor256/erc20.rb'
39
41
  s.files = `git ls-files`.split($RS)
40
42
  s.rdoc_options = ['--charset=UTF-8']
41
43
  s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
42
- s.add_dependency 'backtrace', '>0'
43
- s.add_dependency 'eth', '~>0.4'
44
- s.add_dependency 'jsonrpc-client', '>0'
44
+ s.add_dependency 'eth', '>=0.5.13'
45
+ s.add_dependency 'faye-websocket', '>=0.11.3'
46
+ s.add_dependency 'json', '>=2.10.1'
47
+ s.add_dependency 'jsonrpc-client', '>=0.1.4'
45
48
  s.add_dependency 'loog', '>0'
46
- s.add_dependency 'websocket-client-simple', '>0'
47
49
  s.metadata['rubygems_mfa_required'] = 'true'
48
50
  end
data/lib/erc20/wallet.rb CHANGED
@@ -21,14 +21,17 @@
21
21
  # SOFTWARE.
22
22
 
23
23
  require 'eth'
24
+ require 'eventmachine'
25
+ require 'faye/websocket'
26
+ require 'json'
24
27
  require 'jsonrpc/client'
25
28
  require 'loog'
26
- require 'websocket-client-simple'
29
+ require 'uri'
27
30
  require_relative '../erc20'
28
31
 
29
- # A wallet.
32
+ # A wallet with ERC20 tokens on Etherium.
30
33
  #
31
- # It is NOT thread-safe!
34
+ # Objects of this class are thread-safe.
32
35
  #
33
36
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
34
37
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
@@ -37,34 +40,32 @@ class ERC20::Wallet
37
40
  # Address of USDT contract.
38
41
  USDT = '0xdac17f958d2ee523a2206206994597c13d831ec7'
39
42
 
43
+ # These properties are read-only:
44
+ attr_reader :host, :port, :ssl, :chain, :contract, :ws_path, :http_path
45
+
40
46
  # Constructor.
41
47
  # @param [String] contract Hex of the contract in Etherium
42
- # @param [String] rpc The URL of Etherium JSON-RPC provider
43
48
  # @param [Integer] chain The ID of the chain (1 for mainnet)
44
49
  # @param [String] host The host to connect to
45
50
  # @param [Integer] port TCP port to use
46
- # @param [String] path The path in the connection URL
51
+ # @param [String] http_path The path in the connection URL, for HTTP RPC
52
+ # @param [String] ws_path The path in the connection URL, for Websockets
47
53
  # @param [Boolean] ssl Should we use SSL (for https and wss)
54
+ # @param [String] proxy The URL of the proxy to use
48
55
  # @param [Object] log The destination for logs
49
- def initialize(contract: USDT, rpc: nil, wss: nil, chain: 1, log: $stdout,
50
- host: nil, port: 443, path: '/', ssl: true)
56
+ def initialize(contract: USDT, chain: 1, log: $stdout,
57
+ host: nil, port: 443, http_path: '/', ws_path: '/',
58
+ ssl: true, proxy: nil)
51
59
  @contract = contract
52
- raise 'Use either host or rpc' if rpc && host
53
- raise 'Use either host or wss' if wss && host
54
- if rpc
55
- @rpc = rpc
56
- else
57
- raise 'Either rpc or host+port+path are required' unless host && port && path
58
- @rpc = "http#{ssl ? 's' : ''}://#{host}:#{port}#{path}"
59
- end
60
- if wss
61
- @wss = wss
62
- else
63
- raise 'Either wss or host+port+path are required' unless host && port && path
64
- @wss = "http#{ssl ? 's' : ''}://#{host}:#{port}#{path}"
65
- end
60
+ @host = host
61
+ @port = port
62
+ @ssl = ssl
63
+ @http_path = http_path
64
+ @ws_path = ws_path
66
65
  @log = log
67
66
  @chain = chain
67
+ @proxy = proxy
68
+ @mutex = Mutex.new
68
69
  end
69
70
 
70
71
  # Get balance of a public address.
@@ -86,7 +87,7 @@ class ERC20::Wallet
86
87
  # @param [String] priv Private key, in hex
87
88
  # @param [String] address Public key, in hex
88
89
  # @param [Integer] amount The amount of ERC20 tokens to send
89
- # @param [Integer] gas_limit How much gas you are ready to spend
90
+ # @param [Integer] gas_limit How much gas you're ready to spend
90
91
  # @param [Integer] gas_price How much gas you pay per computation unit
91
92
  # @return [String] Transaction hash
92
93
  def pay(priv, address, amount, gas_limit: nil, gas_price: nil)
@@ -98,93 +99,160 @@ class ERC20::Wallet
98
99
  data = "0x#{func}#{to_padded}#{amt_padded}"
99
100
  key = Eth::Key.new(priv: priv)
100
101
  from = key.address
101
- nonce = jsonrpc.eth_getTransactionCount(from, 'pending').to_i(16)
102
- tx = Eth::Tx.new(
103
- {
104
- nonce:,
105
- gas_price: gas_price || gas_best_price,
106
- gas_limit: gas_limit || gas_estimate(from, data),
107
- to: @contract,
108
- value: 0,
109
- data: data,
110
- chain_id: @chain
111
- }
112
- )
113
- tx.sign(key)
114
- hex = "0x#{tx.hex}"
115
- jsonrpc.eth_sendRawTransaction(hex)
116
- @log.debug("Sent #{amount} from #{from} to #{address}: #{hex}")
117
- hex
102
+ tnx =
103
+ @mutex.synchronize do
104
+ nonce = jsonrpc.eth_getTransactionCount(from, 'pending').to_i(16)
105
+ tx = Eth::Tx.new(
106
+ {
107
+ nonce:,
108
+ gas_price: gas_price || gas_best_price,
109
+ gas_limit: gas_limit || gas_estimate(from, data),
110
+ to: @contract,
111
+ value: 0,
112
+ data: data,
113
+ chain_id: @chain
114
+ }
115
+ )
116
+ tx.sign(key)
117
+ hex = "0x#{tx.hex}"
118
+ jsonrpc.eth_sendRawTransaction(hex)
119
+ end
120
+ @log.debug("Sent #{amount} from #{from} to #{address}: #{tnx}")
121
+ tnx
118
122
  end
119
123
 
120
124
  # Wait for incoming transactions and let the block know when they
121
125
  # arrive. It's a blocking call, it's better to run it in a separate
122
- # thread.
126
+ # thread. It will never finish. In order to stop it, you should do
127
+ # +Thread.kill+.
128
+ #
129
+ # The array with the list of addresses (+addresses+) may change its
130
+ # content on-fly. The +accept()+ method will +eht_subscribe+ to the addresses
131
+ # that are added and will +eth_unsubscribe+ from those that are removed.
132
+ # Once we actually start listening, the +active+ array will be updated
133
+ # with the list of addresses.
134
+ #
135
+ # Both +addresses+ and +active+ must have two methods implemented: +to_a()+
136
+ # and +append()+. Only these methods will be called.
123
137
  #
124
138
  # @param [Array<String>] addresses Addresses to monitor
125
- # @param [Array] ready When connected, TRUE will be added to this array
139
+ # @param [Array] active List of addresses that we are actually listening to
126
140
  # @param [Boolean] raw TRUE if you need to get JSON events as they arrive from Websockets
127
- def accept(addresses, connected: [], raw: false)
128
- WebSocket::Client::Simple.connect(@wss) do |ws|
141
+ # @param [Integer] delay How many seconds to wait between +eth_subscribe+ calls
142
+ def accept(addresses, active = [], raw: false, delay: 1)
143
+ EventMachine.run do
144
+ u = url(http: false)
145
+ @log.debug("Connecting to #{u.hostname}:#{u.port}...")
146
+ ws = Faye::WebSocket::Client.new(u.to_s, [], proxy: @proxy ? { origin: @proxy } : {})
129
147
  log = @log
130
148
  contract = @contract
131
- wss = @wss
149
+ id = rand(99_999)
150
+ attempt = []
132
151
  ws.on(:open) do
133
- log.debug("Connected to #{wss}")
134
- msg = {
135
- jsonrpc: '2.0',
136
- id: 1,
137
- method: 'eth_subscribe',
138
- params: [
139
- 'logs',
140
- {
141
- address: contract,
142
- topics: [
143
- '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
144
- nil,
145
- addresses.map { |a| "0x000000000000000000000000#{a[2..]}" }
146
- ]
147
- }
148
- ]
149
- }
150
- ws.send(msg.to_json)
151
- connected.append(1)
152
- log.debug("Subscribed to #{addresses.count} addresses")
152
+ verbose do
153
+ log.debug("Connected to ws://#{u.hostname}:#{u.port}")
154
+ end
153
155
  end
154
156
  ws.on(:message) do |msg|
155
- data =
156
- begin
157
- JSON.parse(msg.data)
158
- rescue StandardError
159
- {}
157
+ verbose do
158
+ data =
159
+ begin
160
+ JSON.parse(msg.data)
161
+ rescue StandardError
162
+ {}
163
+ end
164
+ if data['id']
165
+ before = active.to_a
166
+ attempt.each do |a|
167
+ active.append(a) unless before.include?(a)
168
+ end
169
+ log.debug(
170
+ "Subscribed ##{id} to #{active.to_a.size} addresses: " \
171
+ "#{active.to_a.map { |a| a[0..6] }.join(', ')}"
172
+ )
173
+ elsif data['method'] == 'eth_subscription' && data.dig('params', 'result')
174
+ event = data['params']['result']
175
+ if raw
176
+ log.debug("New event arrived from #{event['address']}")
177
+ else
178
+ event = {
179
+ amount: event['data'].to_i(16),
180
+ from: "0x#{event['topics'][1][26..].downcase}",
181
+ to: "0x#{event['topics'][2][26..].downcase}"
182
+ }
183
+ log.debug("Payment of #{event[:amount]} tokens arrived from #{event[:from]} to #{event[:to]}")
184
+ end
185
+ yield event
160
186
  end
161
- if data['method'] == 'eth_subscription' && data.dig('params', 'result')
162
- event = data['params']['result']
163
- unless raw
164
- event = {
165
- amount: event['data'].to_i(16),
166
- from: "0x#{event['topics'][1][26..].downcase}",
167
- to: "0x#{event['topics'][2][26..].downcase}"
168
- }
169
- end
170
- log.debug("New event arrived from #{event['address']}")
171
- yield event
172
187
  end
173
188
  end
174
- ws.on(:close) do |_e|
175
- log.debug("Disconnected from #{wss}")
189
+ ws.on(:close) do
190
+ verbose do
191
+ log.debug("Disconnected from ws://#{u.hostname}:#{u.port}")
192
+ end
176
193
  end
177
194
  ws.on(:error) do |e|
178
- log.debug("Error at #{wss}: #{e}")
195
+ verbose do
196
+ log.debug("Error at #{u.hostname}: #{e.message}")
197
+ end
198
+ end
199
+ EventMachine.add_periodic_timer(delay) do
200
+ next if active.to_a.sort == addresses.to_a.sort
201
+ attempt = addresses.to_a
202
+ ws.send(
203
+ {
204
+ jsonrpc: '2.0',
205
+ id:,
206
+ method: 'eth_subscribe',
207
+ params: [
208
+ 'logs',
209
+ {
210
+ address: contract,
211
+ topics: [
212
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
213
+ nil,
214
+ addresses.to_a.map { |a| "0x000000000000000000000000#{a[2..]}" }
215
+ ]
216
+ }
217
+ ]
218
+ }.to_json
219
+ )
220
+ log.debug(
221
+ "Requested to subscribe ##{id} to #{addresses.to_a.size} addresses: " \
222
+ "#{addresses.to_a.map { |a| a[0..6] }.join(', ')}"
223
+ )
179
224
  end
180
225
  end
181
226
  end
182
227
 
183
228
  private
184
229
 
230
+ def verbose
231
+ yield
232
+ rescue StandardError => e
233
+ @log.error(Backtrace.new(e).to_s)
234
+ raise e
235
+ end
236
+
237
+ def url(http: true)
238
+ URI.parse("#{http ? 'http' : 'ws'}#{@ssl ? 's' : ''}://#{@host}:#{@port}#{http ? @http_path : @ws_path}")
239
+ end
240
+
185
241
  def jsonrpc
186
242
  JSONRPC.logger = Loog::NULL
187
- JSONRPC::Client.new(@rpc)
243
+ connection =
244
+ if @proxy
245
+ uri = URI.parse(@proxy)
246
+ Faraday.new do |f|
247
+ f.adapter(Faraday.default_adapter)
248
+ f.proxy = {
249
+ uri: "#{uri.scheme}://#{uri.hostname}:#{uri.port}",
250
+ user: uri.user,
251
+ password: uri.password
252
+ }
253
+ end
254
+ end
255
+ JSONRPC::Client.new(url, connection:)
188
256
  end
189
257
 
190
258
  def gas_estimate(from, data)
data/lib/erc20.rb CHANGED
@@ -27,5 +27,5 @@
27
27
  # License:: MIT
28
28
  module ERC20
29
29
  # Current version of the gem (changed by .rultor.yml on every release)
30
- VERSION = '0.0.4'
30
+ VERSION = '0.0.6'
31
31
  end
@@ -23,22 +23,25 @@
23
23
  require 'backtrace'
24
24
  require 'donce'
25
25
  require 'eth'
26
+ require 'faraday'
26
27
  require 'loog'
28
+ require 'minitest/autorun'
27
29
  require 'random-port'
28
30
  require 'shellwords'
31
+ require 'threads'
29
32
  require 'typhoeus'
30
- require 'minitest/autorun'
31
33
  require_relative '../../lib/erc20'
32
34
  require_relative '../../lib/erc20/wallet'
35
+ require_relative '../test__helper'
33
36
 
34
37
  # Test.
35
38
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
36
39
  # Copyright:: Copyright (c) 2025 Yegor Bugayenko
37
40
  # License:: MIT
38
41
  class TestWallet < Minitest::Test
39
- # At this address, in the mainnet, there are a few USDT tokens. I won't
42
+ # At this address, in Etherium mainnet, there are a few USDT tokens. I won't
40
43
  # move them anyway, that's why tests can use this address forever.
41
- STABLE_ADDRESS = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
44
+ STABLE = '0xEB2fE8872A6f1eDb70a2632EA1f869AB131532f6'
42
45
 
43
46
  # One guy private hex.
44
47
  JEFF = '81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a'
@@ -47,7 +50,7 @@ class TestWallet < Minitest::Test
47
50
  WALTER = '91f9111b1744d55361e632771a4e53839e9442a9fef45febc0a5c838c686a15b'
48
51
 
49
52
  def test_checks_balance_on_mainnet
50
- b = mainnet.balance(STABLE_ADDRESS)
53
+ b = mainnet.balance(STABLE)
51
54
  refute_nil(b)
52
55
  assert_equal(27_258_889, b)
53
56
  end
@@ -61,18 +64,28 @@ class TestWallet < Minitest::Test
61
64
 
62
65
  def test_fails_with_invalid_infura_key
63
66
  w = ERC20::Wallet.new(
64
- rpc: 'https://mainnet.infura.io/v3/invalid-key-here',
65
- wss: 'https://mainnet.infura.io/v3/another-invalid-key-here',
66
- log: Loog::NULL
67
+ host: 'mainnet.infura.io',
68
+ http_path: '/v3/invalid-key-here',
69
+ log: loog
67
70
  )
68
- assert_raises(StandardError) { w.balance(STABLE_ADDRESS) }
71
+ assert_raises(StandardError) { w.balance(STABLE) }
69
72
  end
70
73
 
71
74
  def test_checks_balance_on_testnet
72
- skip('does not work')
73
- b = testnet.balance(STABLE_ADDRESS)
75
+ b = testnet.balance(STABLE)
76
+ refute_nil(b)
77
+ assert_predicate(b, :zero?)
78
+ end
79
+
80
+ def test_checks_balance_on_polygon
81
+ w = ERC20::Wallet.new(
82
+ contract: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
83
+ host: 'polygon-mainnet.infura.io', http_path: "/v3/#{env('INFURA_KEY')}",
84
+ log: loog
85
+ )
86
+ b = w.balance(STABLE)
74
87
  refute_nil(b)
75
- assert_predicate(b, :positive?)
88
+ assert_predicate(b, :zero?)
76
89
  end
77
90
 
78
91
  def test_checks_balance_on_hardhat
@@ -82,6 +95,15 @@ class TestWallet < Minitest::Test
82
95
  end
83
96
  end
84
97
 
98
+ def test_checks_balance_on_hardhat_in_threads
99
+ on_hardhat do |wallet|
100
+ Threads.new.assert do
101
+ b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
102
+ assert_equal(123_000_100_000, b)
103
+ end
104
+ end
105
+ end
106
+
85
107
  def test_pays_on_hardhat
86
108
  on_hardhat do |wallet|
87
109
  to = Eth::Key.new(priv: WALTER).address.to_s
@@ -94,21 +116,34 @@ class TestWallet < Minitest::Test
94
116
  end
95
117
  end
96
118
 
119
+ def test_pays_on_hardhat_in_threads
120
+ on_hardhat do |wallet|
121
+ to = Eth::Key.new(priv: WALTER).address.to_s
122
+ before = wallet.balance(to)
123
+ sum = 42_000
124
+ mul = 10
125
+ Threads.new(mul).assert do
126
+ wallet.pay(JEFF, to, sum)
127
+ end
128
+ assert_equal(before + (sum * mul), wallet.balance(to))
129
+ end
130
+ end
131
+
97
132
  def test_accepts_payments_on_hardhat
98
133
  walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
99
134
  jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
100
135
  on_hardhat do |wallet|
101
- connected = []
136
+ active = []
102
137
  event = nil
103
138
  daemon =
104
139
  Thread.new do
105
- wallet.accept([walter, jeff], connected:) do |e|
140
+ wallet.accept([walter, jeff], active) do |e|
106
141
  event = e
107
142
  end
108
143
  rescue StandardError => e
109
- puts Backtrace.new(e)
144
+ loog.error(Backtrace.new(e))
110
145
  end
111
- wait_for { !connected.empty? }
146
+ wait_for { !active.empty? }
112
147
  sum = 77_000
113
148
  wallet.pay(JEFF, walter, sum)
114
149
  wait_for { !event.nil? }
@@ -120,8 +155,112 @@ class TestWallet < Minitest::Test
120
155
  end
121
156
  end
122
157
 
158
+ def test_accepts_payments_on_changing_addresses_on_hardhat
159
+ walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
160
+ jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
161
+ addresses = Primitivo.new([walter])
162
+ on_hardhat do |wallet|
163
+ active = Primitivo.new([])
164
+ event = nil
165
+ daemon =
166
+ Thread.new do
167
+ wallet.accept(addresses, active) do |e|
168
+ event = e
169
+ end
170
+ rescue StandardError => e
171
+ loog.error(Backtrace.new(e))
172
+ end
173
+ wait_for { active.to_a.include?(walter) }
174
+ sum1 = 453_000
175
+ wallet.pay(JEFF, walter, sum1)
176
+ wait_for { !event.nil? }
177
+ assert_equal(sum1, event[:amount])
178
+ sum2 = 22_000
179
+ event = nil
180
+ addresses.append(jeff)
181
+ wait_for { active.to_a.include?(jeff) }
182
+ wallet.pay(WALTER, jeff, sum2)
183
+ wait_for { !event.nil? }
184
+ assert_equal(sum2, event[:amount])
185
+ daemon.kill
186
+ daemon.join(30)
187
+ end
188
+ end
189
+
190
+ def test_accepts_payments_on_hardhat_via_proxy
191
+ via_proxy do |proxy|
192
+ walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
193
+ jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
194
+ on_hardhat do |w|
195
+ wallet = through_proxy(w, proxy)
196
+ active = []
197
+ event = nil
198
+ daemon =
199
+ Thread.new do
200
+ wallet.accept([walter, jeff], active) do |e|
201
+ event = e
202
+ end
203
+ rescue StandardError => e
204
+ loog.error(Backtrace.new(e))
205
+ end
206
+ wait_for { !active.empty? }
207
+ sum = 55_000
208
+ wallet.pay(JEFF, walter, sum)
209
+ wait_for { !event.nil? }
210
+ daemon.kill
211
+ daemon.join(30)
212
+ assert_equal(sum, event[:amount])
213
+ end
214
+ end
215
+ end
216
+
217
+ def test_accepts_payments_on_mainnet
218
+ active = []
219
+ failed = false
220
+ net = mainnet
221
+ daemon =
222
+ Thread.new do
223
+ net.accept([STABLE], active) do |_|
224
+ # ignore it
225
+ end
226
+ rescue StandardError => e
227
+ failed = true
228
+ loog.error(Backtrace.new(e))
229
+ end
230
+ wait_for { !active.empty? }
231
+ daemon.kill
232
+ daemon.join(30)
233
+ refute(failed)
234
+ end
235
+
236
+ def test_checks_balance_via_proxy
237
+ via_proxy do |proxy|
238
+ on_hardhat do |w|
239
+ wallet = through_proxy(w, proxy)
240
+ b = wallet.balance(Eth::Key.new(priv: JEFF).address.to_s)
241
+ assert_equal(123_000_100_000, b)
242
+ end
243
+ end
244
+ end
245
+
246
+ def test_checks_balance_via_proxy_on_mainnet
247
+ via_proxy do |proxy|
248
+ on_hardhat do
249
+ w = ERC20::Wallet.new(
250
+ host: 'mainnet.infura.io', http_path: "/v3/#{env('INFURA_KEY')}",
251
+ proxy:, log: loog
252
+ )
253
+ assert_equal(27_258_889, w.balance(STABLE))
254
+ end
255
+ end
256
+ end
257
+
123
258
  private
124
259
 
260
+ def loog
261
+ ENV['RAKE'] ? Loog::ERRORS : Loog::VERBOSE
262
+ end
263
+
125
264
  def wait_for
126
265
  start = Time.now
127
266
  loop do
@@ -145,29 +284,66 @@ class TestWallet < Minitest::Test
145
284
 
146
285
  def mainnet
147
286
  [
148
- "https://mainnet.infura.io/v3/#{env('INFURA_KEY')}",
149
- "https://go.getblock.io/#{env('GETBLOCK_KEY')}"
150
- ].map do |url|
151
- ERC20::Wallet.new(rpc: url, wss: url, log: Loog::NULL)
287
+ {
288
+ host: 'mainnet.infura.io',
289
+ http_path: "/v3/#{env('INFURA_KEY')}",
290
+ ws_path: "/ws/v3/#{env('INFURA_KEY')}"
291
+ },
292
+ {
293
+ host: 'go.getblock.io',
294
+ http_path: "/#{env('GETBLOCK_KEY')}",
295
+ ws_path: "/#{env('GETBLOCK_WS_KEY')}"
296
+ }
297
+ ].map do |server|
298
+ ERC20::Wallet.new(host: server[:host], http_path: server[:http_path], ws_path: server[:ws_path], log: loog)
152
299
  end.sample
153
300
  end
154
301
 
155
302
  def testnet
156
303
  [
157
- "https://sepolia.infura.io/v3/#{env('INFURA_KEY')}",
158
- "https://go.getblock.io/#{env('GETBLOCK_SEPOILA_KEY')}"
159
- ].map do |url|
160
- ERC20::Wallet.new(rpc: url, wss: url, log: Loog::NULL)
304
+ {
305
+ host: 'sepolia.infura.io',
306
+ http_path: "/v3/#{env('INFURA_KEY')}",
307
+ ws_path: "/ws/v3/#{env('INFURA_KEY')}"
308
+ },
309
+ {
310
+ host: 'go.getblock.io',
311
+ http_path: "/#{env('GETBLOCK_SEPOILA_KEY')}",
312
+ ws_path: "/#{env('GETBLOCK_SEPOILA_KEY')}"
313
+ }
314
+ ].map do |server|
315
+ ERC20::Wallet.new(host: server[:host], http_path: server[:http_path], ws_path: server[:ws_path], log: loog)
161
316
  end.sample
162
317
  end
163
318
 
319
+ def through_proxy(wallet, proxy)
320
+ ERC20::Wallet.new(
321
+ contract: wallet.contract, chain: wallet.chain,
322
+ host: donce_host, port: wallet.port, http_path: wallet.http_path, ws_path: wallet.ws_path,
323
+ ssl: wallet.ssl, proxy:, log: loog
324
+ )
325
+ end
326
+
327
+ def via_proxy
328
+ RandomPort::Pool::SINGLETON.acquire do |port|
329
+ donce(
330
+ image: 'yegor256/squid-proxy:latest',
331
+ ports: { port => 3128 },
332
+ env: { 'USERNAME' => 'jeffrey', 'PASSWORD' => 'swordfish' },
333
+ root: true, log: loog
334
+ ) do
335
+ yield "http://jeffrey:swordfish@localhost:#{port}"
336
+ end
337
+ end
338
+ end
339
+
164
340
  def on_hardhat
165
341
  RandomPort::Pool::SINGLETON.acquire do |port|
166
342
  donce(
167
343
  home: File.join(__dir__, '../../hardhat'),
168
344
  ports: { port => 8545 },
169
345
  command: 'npx hardhat node',
170
- log: Loog::NULL
346
+ log: loog
171
347
  ) do
172
348
  wait_for_port(port)
173
349
  cmd = [
@@ -180,16 +356,30 @@ class TestWallet < Minitest::Test
180
356
  home: File.join(__dir__, '../../hardhat'),
181
357
  command: "/bin/bash -c #{Shellwords.escape(cmd)}",
182
358
  build_args: { 'HOST' => donce_host, 'PORT' => port },
183
- log: Loog::NULL,
359
+ log: loog,
184
360
  root: true
185
361
  ).split("\n").last
186
362
  wallet = ERC20::Wallet.new(
187
363
  contract:, chain: 4242,
188
- host: 'localhost', port:, path: '/', ssl: false,
189
- log: Loog::NULL
364
+ host: 'localhost', port:, http_path: '/', ws_path: '/', ssl: false,
365
+ log: loog
190
366
  )
191
367
  yield wallet
192
368
  end
193
369
  end
194
370
  end
371
+
372
+ class Primitivo
373
+ def initialize(array)
374
+ @array = array
375
+ end
376
+
377
+ def to_a
378
+ @array.to_a
379
+ end
380
+
381
+ def append(item)
382
+ @array.append(item)
383
+ end
384
+ end
195
385
  end
data/test/test__helper.rb CHANGED
@@ -35,5 +35,7 @@ require 'minitest/reporters'
35
35
  Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new]
36
36
 
37
37
  # To make tests retry on failure:
38
- require 'minitest/retry'
39
- Minitest::Retry.use!(methods_to_skip: [])
38
+ if ENV['RAKE']
39
+ require 'minitest/retry'
40
+ Minitest::Retry.use!(methods_to_skip: [])
41
+ end
metadata CHANGED
@@ -1,73 +1,73 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: erc20
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-10 00:00:00.000000000 Z
11
+ date: 2025-02-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: backtrace
14
+ name: eth
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.5.13
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.5.13
27
27
  - !ruby/object:Gem::Dependency
28
- name: eth
28
+ name: faye-websocket
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0.4'
33
+ version: 0.11.3
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0.4'
40
+ version: 0.11.3
41
41
  - !ruby/object:Gem::Dependency
42
- name: jsonrpc-client
42
+ name: json
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: 2.10.1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: 2.10.1
55
55
  - !ruby/object:Gem::Dependency
56
- name: loog
56
+ name: jsonrpc-client
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ">"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: 0.1.4
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ">"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: 0.1.4
69
69
  - !ruby/object:Gem::Dependency
70
- name: websocket-client-simple
70
+ name: loog
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">"
@@ -80,8 +80,10 @@ dependencies:
80
80
  - - ">"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- description: A simple library for making ERC20 manipulations as easy as theycan be
84
- for cryptocurrency newbies
83
+ description: 'A simple library for making ERC20 manipulations as easy as they can
84
+ be for cryptocurrency newbies: checking balance, sending payments, and monitoring
85
+ addresses for incoming payments. The library expects Etherium node to provide JSON
86
+ RPC and Websockets API.'
85
87
  email: yegor256@gmail.com
86
88
  executables: []
87
89
  extensions: []