erc20 0.0.5 → 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 +4 -4
- data/lib/erc20/wallet.rb +105 -57
- data/lib/erc20.rb +1 -1
- data/test/erc20/test_wallet.rb +79 -10
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddbc3966b9eec2d455e33dde71df0427a7a99e4d59f0df69191d8f617434c8bf
|
4
|
+
data.tar.gz: 0d577a397eee74c5dae388ca620f8a681c4bec2af8d9b6cf5514ca32cbebc992
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f87ab9c39b5476b9b90bd44020f21970edc8830826129b0a2c6386d562887627e346c55a1e8bb1fcf3e9983d1977e6e0851b7e2fdc162217898951988b2173f
|
7
|
+
data.tar.gz: 02d6b7e7058dd6c6f3a74b81ece853e674020a4df84b6fa439b74438ff8227616a76cf3abd1a6f82fdfb7f34391c4e541f96791e404e8e84cb3f364d72e92771
|
data/lib/erc20/wallet.rb
CHANGED
@@ -29,9 +29,9 @@ require 'loog'
|
|
29
29
|
require 'uri'
|
30
30
|
require_relative '../erc20'
|
31
31
|
|
32
|
-
# A wallet.
|
32
|
+
# A wallet with ERC20 tokens on Etherium.
|
33
33
|
#
|
34
|
-
#
|
34
|
+
# Objects of this class are thread-safe.
|
35
35
|
#
|
36
36
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
37
37
|
# Copyright:: Copyright (c) 2025 Yegor Bugayenko
|
@@ -65,6 +65,7 @@ class ERC20::Wallet
|
|
65
65
|
@log = log
|
66
66
|
@chain = chain
|
67
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
|
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,23 +99,26 @@ 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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
@@ -122,22 +126,83 @@ class ERC20::Wallet
|
|
122
126
|
# thread. It will never finish. In order to stop it, you should do
|
123
127
|
# +Thread.kill+.
|
124
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.
|
137
|
+
#
|
125
138
|
# @param [Array<String>] addresses Addresses to monitor
|
126
|
-
# @param [Array]
|
139
|
+
# @param [Array] active List of addresses that we are actually listening to
|
127
140
|
# @param [Boolean] raw TRUE if you need to get JSON events as they arrive from Websockets
|
128
|
-
|
129
|
-
|
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
|
130
144
|
u = url(http: false)
|
131
|
-
@log.debug("Connecting to #{u}...")
|
132
|
-
ws = Faye::WebSocket::Client.new(u, [], proxy: @proxy ? { origin: @proxy } : {})
|
145
|
+
@log.debug("Connecting to #{u.hostname}:#{u.port}...")
|
146
|
+
ws = Faye::WebSocket::Client.new(u.to_s, [], proxy: @proxy ? { origin: @proxy } : {})
|
133
147
|
log = @log
|
134
148
|
contract = @contract
|
149
|
+
id = rand(99_999)
|
150
|
+
attempt = []
|
135
151
|
ws.on(:open) do
|
136
|
-
|
152
|
+
verbose do
|
153
|
+
log.debug("Connected to ws://#{u.hostname}:#{u.port}")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
ws.on(:message) do |msg|
|
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
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
ws.on(:close) do
|
190
|
+
verbose do
|
191
|
+
log.debug("Disconnected from ws://#{u.hostname}:#{u.port}")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
ws.on(:error) do |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
|
137
202
|
ws.send(
|
138
203
|
{
|
139
204
|
jsonrpc: '2.0',
|
140
|
-
id
|
205
|
+
id:,
|
141
206
|
method: 'eth_subscribe',
|
142
207
|
params: [
|
143
208
|
'logs',
|
@@ -146,48 +211,31 @@ class ERC20::Wallet
|
|
146
211
|
topics: [
|
147
212
|
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
|
148
213
|
nil,
|
149
|
-
addresses.map { |a| "0x000000000000000000000000#{a[2..]}" }
|
214
|
+
addresses.to_a.map { |a| "0x000000000000000000000000#{a[2..]}" }
|
150
215
|
]
|
151
216
|
}
|
152
217
|
]
|
153
218
|
}.to_json
|
154
219
|
)
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
data =
|
160
|
-
begin
|
161
|
-
JSON.parse(msg.data)
|
162
|
-
rescue StandardError
|
163
|
-
{}
|
164
|
-
end
|
165
|
-
if data['method'] == 'eth_subscription' && data.dig('params', 'result')
|
166
|
-
event = data['params']['result']
|
167
|
-
unless raw
|
168
|
-
event = {
|
169
|
-
amount: event['data'].to_i(16),
|
170
|
-
from: "0x#{event['topics'][1][26..].downcase}",
|
171
|
-
to: "0x#{event['topics'][2][26..].downcase}"
|
172
|
-
}
|
173
|
-
end
|
174
|
-
log.debug("New event arrived from #{event['address']}")
|
175
|
-
yield event
|
176
|
-
end
|
177
|
-
end
|
178
|
-
ws.on(:close) do |_e|
|
179
|
-
log.debug("Disconnected from #{@host}")
|
180
|
-
end
|
181
|
-
ws.on(:error) do |e|
|
182
|
-
log.debug("Error at #{@host}: #{e.message}")
|
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
|
+
)
|
183
224
|
end
|
184
225
|
end
|
185
226
|
end
|
186
227
|
|
187
228
|
private
|
188
229
|
|
230
|
+
def verbose
|
231
|
+
yield
|
232
|
+
rescue StandardError => e
|
233
|
+
@log.error(Backtrace.new(e).to_s)
|
234
|
+
raise e
|
235
|
+
end
|
236
|
+
|
189
237
|
def url(http: true)
|
190
|
-
"#{http ? 'http' : 'ws'}#{@ssl ? 's' : ''}://#{@host}:#{@port}#{http ? @http_path : @ws_path}"
|
238
|
+
URI.parse("#{http ? 'http' : 'ws'}#{@ssl ? 's' : ''}://#{@host}:#{@port}#{http ? @http_path : @ws_path}")
|
191
239
|
end
|
192
240
|
|
193
241
|
def jsonrpc
|
data/lib/erc20.rb
CHANGED
data/test/erc20/test_wallet.rb
CHANGED
@@ -28,6 +28,7 @@ require 'loog'
|
|
28
28
|
require 'minitest/autorun'
|
29
29
|
require 'random-port'
|
30
30
|
require 'shellwords'
|
31
|
+
require 'threads'
|
31
32
|
require 'typhoeus'
|
32
33
|
require_relative '../../lib/erc20'
|
33
34
|
require_relative '../../lib/erc20/wallet'
|
@@ -94,6 +95,15 @@ class TestWallet < Minitest::Test
|
|
94
95
|
end
|
95
96
|
end
|
96
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
|
+
|
97
107
|
def test_pays_on_hardhat
|
98
108
|
on_hardhat do |wallet|
|
99
109
|
to = Eth::Key.new(priv: WALTER).address.to_s
|
@@ -106,21 +116,34 @@ class TestWallet < Minitest::Test
|
|
106
116
|
end
|
107
117
|
end
|
108
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
|
+
|
109
132
|
def test_accepts_payments_on_hardhat
|
110
133
|
walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
|
111
134
|
jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
|
112
135
|
on_hardhat do |wallet|
|
113
|
-
|
136
|
+
active = []
|
114
137
|
event = nil
|
115
138
|
daemon =
|
116
139
|
Thread.new do
|
117
|
-
wallet.accept([walter, jeff],
|
140
|
+
wallet.accept([walter, jeff], active) do |e|
|
118
141
|
event = e
|
119
142
|
end
|
120
143
|
rescue StandardError => e
|
121
144
|
loog.error(Backtrace.new(e))
|
122
145
|
end
|
123
|
-
wait_for { !
|
146
|
+
wait_for { !active.empty? }
|
124
147
|
sum = 77_000
|
125
148
|
wallet.pay(JEFF, walter, sum)
|
126
149
|
wait_for { !event.nil? }
|
@@ -132,23 +155,55 @@ class TestWallet < Minitest::Test
|
|
132
155
|
end
|
133
156
|
end
|
134
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
|
+
|
135
190
|
def test_accepts_payments_on_hardhat_via_proxy
|
136
191
|
via_proxy do |proxy|
|
137
192
|
walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
|
138
193
|
jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
|
139
194
|
on_hardhat do |w|
|
140
195
|
wallet = through_proxy(w, proxy)
|
141
|
-
|
196
|
+
active = []
|
142
197
|
event = nil
|
143
198
|
daemon =
|
144
199
|
Thread.new do
|
145
|
-
wallet.accept([walter, jeff],
|
200
|
+
wallet.accept([walter, jeff], active) do |e|
|
146
201
|
event = e
|
147
202
|
end
|
148
203
|
rescue StandardError => e
|
149
204
|
loog.error(Backtrace.new(e))
|
150
205
|
end
|
151
|
-
wait_for { !
|
206
|
+
wait_for { !active.empty? }
|
152
207
|
sum = 55_000
|
153
208
|
wallet.pay(JEFF, walter, sum)
|
154
209
|
wait_for { !event.nil? }
|
@@ -160,19 +215,19 @@ class TestWallet < Minitest::Test
|
|
160
215
|
end
|
161
216
|
|
162
217
|
def test_accepts_payments_on_mainnet
|
163
|
-
|
218
|
+
active = []
|
164
219
|
failed = false
|
165
220
|
net = mainnet
|
166
221
|
daemon =
|
167
222
|
Thread.new do
|
168
|
-
net.accept([STABLE],
|
223
|
+
net.accept([STABLE], active) do |_|
|
169
224
|
# ignore it
|
170
225
|
end
|
171
226
|
rescue StandardError => e
|
172
227
|
failed = true
|
173
228
|
loog.error(Backtrace.new(e))
|
174
229
|
end
|
175
|
-
wait_for { !
|
230
|
+
wait_for { !active.empty? }
|
176
231
|
daemon.kill
|
177
232
|
daemon.join(30)
|
178
233
|
refute(failed)
|
@@ -203,7 +258,7 @@ class TestWallet < Minitest::Test
|
|
203
258
|
private
|
204
259
|
|
205
260
|
def loog
|
206
|
-
ENV['RAKE'] ? Loog::
|
261
|
+
ENV['RAKE'] ? Loog::ERRORS : Loog::VERBOSE
|
207
262
|
end
|
208
263
|
|
209
264
|
def wait_for
|
@@ -313,4 +368,18 @@ class TestWallet < Minitest::Test
|
|
313
368
|
end
|
314
369
|
end
|
315
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
|
316
385
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: erc20
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
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-
|
11
|
+
date: 2025-02-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: eth
|