erc20 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|