erc20 0.0.15 → 0.0.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/Gemfile +3 -3
- data/Gemfile.lock +4 -4
- data/README.md +12 -2
- data/lib/erc20/erc20.rb +1 -1
- data/lib/erc20/fake_wallet.rb +20 -2
- data/lib/erc20/wallet.rb +85 -13
- data/test/erc20/test_fake_wallet.rb +13 -4
- data/test/erc20/test_wallet.rb +40 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d2dd0547d65ca59e601a28f93a2b35b45ae8b3dc57fe6a09bd296d13fe807fd
|
4
|
+
data.tar.gz: a69c35dfd60d083f891a840285411373e7996937481f7c52a63f2ce9c09e2462
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1ddfb2214d688465488e224c59460e185b8bb361acadfd3b4d215053392f53dcf23367366b29a577b563be982432233efcc72ba5f4e6f6cae6651189b221c53c
|
7
|
+
data.tar.gz: d8f93754ea6baf64d46591aa3ab52559bb601785df08d3071f51d5e283c7d0ad7c2b226bdaec12abbef913750136f5e895a302a451aa4ddac2225b862497631f
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
@@ -35,10 +35,10 @@ gem 'rake', '13.2.1', require: false
|
|
35
35
|
gem 'random-port', '>0', require: false
|
36
36
|
gem 'rspec-rails', '7.1.1', require: false
|
37
37
|
gem 'rubocop', '1.72.1', require: false
|
38
|
-
gem 'rubocop-minitest', '0
|
39
|
-
gem 'rubocop-performance', '
|
38
|
+
gem 'rubocop-minitest', '>0', require: false
|
39
|
+
gem 'rubocop-performance', '>0', require: false
|
40
40
|
gem 'rubocop-rake', '>0', require: false
|
41
|
-
gem 'rubocop-rspec', '
|
41
|
+
gem 'rubocop-rspec', '>0', require: false
|
42
42
|
gem 'simplecov', '0.22.0', require: false
|
43
43
|
gem 'simplecov-cobertura', '2.1.0', require: false
|
44
44
|
gem 'threads', '0.4.1', require: false
|
data/Gemfile.lock
CHANGED
@@ -184,7 +184,7 @@ GEM
|
|
184
184
|
regexp_parser (2.10.0)
|
185
185
|
reline (0.6.0)
|
186
186
|
io-console (~> 0.5)
|
187
|
-
rexml (3.4.
|
187
|
+
rexml (3.4.1)
|
188
188
|
rspec-core (3.13.3)
|
189
189
|
rspec-support (~> 3.13.0)
|
190
190
|
rspec-expectations (3.13.3)
|
@@ -288,10 +288,10 @@ DEPENDENCIES
|
|
288
288
|
random-port (> 0)
|
289
289
|
rspec-rails (= 7.1.1)
|
290
290
|
rubocop (= 1.72.1)
|
291
|
-
rubocop-minitest (
|
292
|
-
rubocop-performance (
|
291
|
+
rubocop-minitest (> 0)
|
292
|
+
rubocop-performance (> 0)
|
293
293
|
rubocop-rake (> 0)
|
294
|
-
rubocop-rspec (
|
294
|
+
rubocop-rspec (> 0)
|
295
295
|
simplecov (= 0.22.0)
|
296
296
|
simplecov-cobertura (= 2.1.0)
|
297
297
|
threads (= 0.4.1)
|
data/README.md
CHANGED
@@ -37,13 +37,23 @@ hex = w.pay(private_key, to_address, amount)
|
|
37
37
|
# Stay waiting, and trigger the block when new ERC20 payments show up:
|
38
38
|
addresses = ['0x...', '0x...'] # only wait for payments to these addresses
|
39
39
|
w.accept(addresses) do |event|
|
40
|
-
puts event[:
|
40
|
+
puts event[:txn] # hash of transaction
|
41
41
|
puts event[:amount] # how much, in tokens (1000000 = $1 USDT)
|
42
42
|
puts event[:from] # who sent the payment
|
43
43
|
puts event[:to] # who was the receiver
|
44
44
|
end
|
45
45
|
```
|
46
46
|
|
47
|
+
It's also possible to check ETH balance and send ETH transaction:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# Check how many ETHs are there on the given address:
|
51
|
+
eth = w.eth_balance(address)
|
52
|
+
|
53
|
+
# Send a few ETHs to someone and get transaction hash:
|
54
|
+
hex = w.eth_pay(private_key, to_address, amount)
|
55
|
+
```
|
56
|
+
|
47
57
|
To generate a new private key, use [eth](https://rubygems.org/gems/eth):
|
48
58
|
|
49
59
|
```ruby
|
@@ -51,7 +61,7 @@ require 'eth'
|
|
51
61
|
key = Eth::Key.new.private_hex
|
52
62
|
```
|
53
63
|
|
54
|
-
To
|
64
|
+
To convert a private key to a public address:
|
55
65
|
|
56
66
|
```ruby
|
57
67
|
public_hex = Eth::Key.new(priv: key).address
|
data/lib/erc20/erc20.rb
CHANGED
data/lib/erc20/fake_wallet.rb
CHANGED
@@ -43,7 +43,7 @@ class ERC20::FakeWallet
|
|
43
43
|
@http_path = '/'
|
44
44
|
end
|
45
45
|
|
46
|
-
# Get balance of a public address.
|
46
|
+
# Get ERC20 balance of a public address.
|
47
47
|
#
|
48
48
|
# @param [String] _hex Public key, in hex, starting from '0x'
|
49
49
|
# @return [Integer] Balance, in tokens
|
@@ -51,7 +51,15 @@ class ERC20::FakeWallet
|
|
51
51
|
42_000_000
|
52
52
|
end
|
53
53
|
|
54
|
-
#
|
54
|
+
# Get ETH balance of a public address.
|
55
|
+
#
|
56
|
+
# @param [String] _hex Public key, in hex, starting from '0x'
|
57
|
+
# @return [Integer] Balance, in tokens
|
58
|
+
def eth_balance(_hex)
|
59
|
+
42_000_000
|
60
|
+
end
|
61
|
+
|
62
|
+
# Send a single ERC20 payment from a private address to a public one.
|
55
63
|
#
|
56
64
|
# @param [String] _priv Private key, in hex
|
57
65
|
# @param [String] _address Public key, in hex
|
@@ -61,6 +69,16 @@ class ERC20::FakeWallet
|
|
61
69
|
'0x172de9cda30537eae68ab4a96163ebbb8f8a85293b8737dd2e5deb4714b14623'
|
62
70
|
end
|
63
71
|
|
72
|
+
# Send a single ETH payment from a private address to a public one.
|
73
|
+
#
|
74
|
+
# @param [String] _priv Private key, in hex
|
75
|
+
# @param [String] _address Public key, in hex
|
76
|
+
# @param [Integer] _amount The amount of ETHs to send
|
77
|
+
# @return [String] Transaction hash
|
78
|
+
def eth_pay(_priv, _address, _amount, *)
|
79
|
+
'0x172de9cda30537eae68ab4a96163ebbb8f8a85293b8737dd2e5deb4714b14623'
|
80
|
+
end
|
81
|
+
|
64
82
|
# Wait and accept.
|
65
83
|
#
|
66
84
|
# @param [Array<String>] addresses Addresses to monitor
|
data/lib/erc20/wallet.rb
CHANGED
@@ -122,23 +122,44 @@ class ERC20::Wallet
|
|
122
122
|
@mutex = Mutex.new
|
123
123
|
end
|
124
124
|
|
125
|
-
# Get balance of a public address.
|
125
|
+
# Get ERC20 balance of a public address (it's not the same as ETH balance!).
|
126
126
|
#
|
127
|
-
#
|
127
|
+
# An address in Etherium may have many balances. One of them is the main
|
128
|
+
# balance in ETH crypto. Another balance is the one kept by ERC20 contract
|
129
|
+
# in its own ledge in root storage. This balance is checked by this method.
|
130
|
+
#
|
131
|
+
# @param [String] address Public key, in hex, starting from '0x'
|
128
132
|
# @return [Integer] Balance, in tokens
|
129
|
-
def balance(
|
130
|
-
raise 'Address can\'t be nil' unless
|
131
|
-
raise 'Address must be a String' unless
|
132
|
-
raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(
|
133
|
+
def balance(address)
|
134
|
+
raise 'Address can\'t be nil' unless address
|
135
|
+
raise 'Address must be a String' unless address.is_a?(String)
|
136
|
+
raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
|
133
137
|
func = '70a08231' # balanceOf
|
134
|
-
data = "0x#{func}000000000000000000000000#{
|
138
|
+
data = "0x#{func}000000000000000000000000#{address[2..].downcase}"
|
135
139
|
r = jsonrpc.eth_call({ to: @contract, data: data }, 'latest')
|
136
140
|
b = r[2..].to_i(16)
|
137
|
-
@log.debug("Balance of #{
|
141
|
+
@log.debug("Balance of #{address} is #{b} ERC20 tokens")
|
142
|
+
b
|
143
|
+
end
|
144
|
+
|
145
|
+
# Get ETH balance of a public address.
|
146
|
+
#
|
147
|
+
# An address in Etherium may have many balances. One of them is the main
|
148
|
+
# balance in ETH crypto. This balance is checked by this method.
|
149
|
+
#
|
150
|
+
# @param [String] hex Public key, in hex, starting from '0x'
|
151
|
+
# @return [Integer] Balance, in ETH
|
152
|
+
def eth_balance(address)
|
153
|
+
raise 'Address can\'t be nil' unless address
|
154
|
+
raise 'Address must be a String' unless address.is_a?(String)
|
155
|
+
raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
|
156
|
+
r = jsonrpc.eth_getBalance(address, 'latest')
|
157
|
+
b = r[2..].to_i(16)
|
158
|
+
@log.debug("Balance of #{address} is #{b} ETHs")
|
138
159
|
b
|
139
160
|
end
|
140
161
|
|
141
|
-
# Send a single payment from a private address to a public one.
|
162
|
+
# Send a single ERC20 payment from a private address to a public one.
|
142
163
|
#
|
143
164
|
# @param [String] priv Private key, in hex
|
144
165
|
# @param [String] address Public key, in hex
|
@@ -179,7 +200,7 @@ class ERC20::Wallet
|
|
179
200
|
{
|
180
201
|
nonce:,
|
181
202
|
gas_price: gas_price || gas_best_price,
|
182
|
-
gas_limit: gas_limit || gas_estimate(from, data),
|
203
|
+
gas_limit: gas_limit || gas_estimate(from, @contract, data),
|
183
204
|
to: @contract,
|
184
205
|
value: 0,
|
185
206
|
data: data,
|
@@ -190,7 +211,58 @@ class ERC20::Wallet
|
|
190
211
|
hex = "0x#{tx.hex}"
|
191
212
|
jsonrpc.eth_sendRawTransaction(hex)
|
192
213
|
end
|
193
|
-
@log.debug("Sent #{amount} from #{from} to #{address}: #{tnx}")
|
214
|
+
@log.debug("Sent #{amount} ERC20 tokens from #{from} to #{address}: #{tnx}")
|
215
|
+
tnx.downcase
|
216
|
+
end
|
217
|
+
|
218
|
+
# Send a single ETH payment from a private address to a public one.
|
219
|
+
#
|
220
|
+
# @param [String] priv Private key, in hex
|
221
|
+
# @param [String] address Public key, in hex
|
222
|
+
# @param [Integer] amount The amount of ERC20 tokens to send
|
223
|
+
# @param [Integer] gas_limit How much gas you're ready to spend
|
224
|
+
# @param [Integer] gas_price How much gas you pay per computation unit
|
225
|
+
# @return [String] Transaction hash
|
226
|
+
def eth_pay(priv, address, amount, gas_limit: nil, gas_price: nil)
|
227
|
+
raise 'Private key can\'t be nil' unless priv
|
228
|
+
raise 'Private key must be a String' unless priv.is_a?(String)
|
229
|
+
raise 'Invalid format of private key' unless /^[0-9a-fA-F]{64}$/.match?(priv)
|
230
|
+
raise 'Address can\'t be nil' unless address
|
231
|
+
raise 'Address must be a String' unless address.is_a?(String)
|
232
|
+
raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
|
233
|
+
raise 'Amount can\'t be nil' unless amount
|
234
|
+
raise 'Amount must be an Integer' unless amount.is_a?(Integer)
|
235
|
+
raise 'Amount must be a positive Integer' unless amount.positive?
|
236
|
+
if gas_limit
|
237
|
+
raise 'Gas limit must be an Integer' unless gas_limit.is_a?(Integer)
|
238
|
+
raise 'Gas limit must be a positive Integer' unless gas_limit.positive?
|
239
|
+
end
|
240
|
+
if gas_price
|
241
|
+
raise 'Gas price must be an Integer' unless gas_price.is_a?(Integer)
|
242
|
+
raise 'Gas price must be a positive Integer' unless gas_price.positive?
|
243
|
+
end
|
244
|
+
key = Eth::Key.new(priv: priv)
|
245
|
+
from = key.address.to_s
|
246
|
+
data = ''
|
247
|
+
tnx =
|
248
|
+
@mutex.synchronize do
|
249
|
+
nonce = jsonrpc.eth_getTransactionCount(from, 'pending').to_i(16)
|
250
|
+
tx = Eth::Tx.new(
|
251
|
+
{
|
252
|
+
chain_id: @chain,
|
253
|
+
nonce:,
|
254
|
+
gas_price: gas_price || gas_best_price,
|
255
|
+
gas_limit: gas_limit || gas_estimate(from, address, data),
|
256
|
+
to: address,
|
257
|
+
value: amount,
|
258
|
+
data:
|
259
|
+
}
|
260
|
+
)
|
261
|
+
tx.sign(key)
|
262
|
+
hex = "0x#{tx.hex}"
|
263
|
+
jsonrpc.eth_sendRawTransaction(hex)
|
264
|
+
end
|
265
|
+
@log.debug("Sent #{amount} ETHs from #{from} to #{address}: #{tnx}")
|
194
266
|
tnx.downcase
|
195
267
|
end
|
196
268
|
|
@@ -343,8 +415,8 @@ class ERC20::Wallet
|
|
343
415
|
JSONRPC::Client.new(url, connection:)
|
344
416
|
end
|
345
417
|
|
346
|
-
def gas_estimate(from, data)
|
347
|
-
jsonrpc.eth_estimateGas({ from:, to
|
418
|
+
def gas_estimate(from, to, data)
|
419
|
+
jsonrpc.eth_estimateGas({ from:, to:, data: }, 'latest').to_i(16)
|
348
420
|
end
|
349
421
|
|
350
422
|
def gas_best_price
|
@@ -33,10 +33,6 @@ require 'typhoeus'
|
|
33
33
|
require_relative '../../lib/erc20/fake_wallet'
|
34
34
|
require_relative '../test__helper'
|
35
35
|
|
36
|
-
k = Eth::Key.new
|
37
|
-
puts k.private_hex
|
38
|
-
puts k.address
|
39
|
-
|
40
36
|
# Test.
|
41
37
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
42
38
|
# Copyright:: Copyright (c) 2025 Yegor Bugayenko
|
@@ -47,6 +43,11 @@ class TestFakeWallet < Minitest::Test
|
|
47
43
|
refute_nil(b)
|
48
44
|
end
|
49
45
|
|
46
|
+
def test_checks_fake_eth_balance
|
47
|
+
b = ERC20::FakeWallet.new.eth_balance('0xEB2fE8872A6f1eDb70a2632Effffffffffffffff')
|
48
|
+
refute_nil(b)
|
49
|
+
end
|
50
|
+
|
50
51
|
def test_returns_host
|
51
52
|
assert_equal('example.com', ERC20::FakeWallet.new.host)
|
52
53
|
end
|
@@ -59,6 +60,14 @@ class TestFakeWallet < Minitest::Test
|
|
59
60
|
assert_match(/^0x[a-f0-9]{64}$/, txn)
|
60
61
|
end
|
61
62
|
|
63
|
+
def test_pays_fake_eths
|
64
|
+
priv = '81a9b2114d53731ecc84b261ef6c0387dde34d5907fe7b441240cc21d61bf80a'
|
65
|
+
to = '0xfadef8ba4a5d709a2bf55b7a8798c9b438c640c1'
|
66
|
+
txn = ERC20::FakeWallet.new.eth_pay(Eth::Key.new(priv:), to, 555)
|
67
|
+
assert_equal(66, txn.length)
|
68
|
+
assert_match(/^0x[a-f0-9]{64}$/, txn)
|
69
|
+
end
|
70
|
+
|
62
71
|
def test_accepts_payments_on_hardhat
|
63
72
|
active = Primitivo.new([])
|
64
73
|
addresses = Primitivo.new(['0xfadef8ba4a5d709a2bf55b7a8798c9b438c640c1'])
|
data/test/erc20/test_wallet.rb
CHANGED
@@ -54,6 +54,12 @@ class TestWallet < Minitest::Test
|
|
54
54
|
assert_equal(8_000_000, b)
|
55
55
|
end
|
56
56
|
|
57
|
+
def test_checks_eth_balance_on_mainnet
|
58
|
+
b = mainnet.eth_balance(STABLE)
|
59
|
+
refute_nil(b)
|
60
|
+
assert_equal(0, b)
|
61
|
+
end
|
62
|
+
|
57
63
|
def test_checks_balance_of_absent_address
|
58
64
|
a = '0xEB2fE8872A6f1eDb70a2632Effffffffffffffff'
|
59
65
|
b = mainnet.balance(a)
|
@@ -95,6 +101,13 @@ class TestWallet < Minitest::Test
|
|
95
101
|
end
|
96
102
|
end
|
97
103
|
|
104
|
+
def test_checks_eth_balance_on_hardhat
|
105
|
+
on_hardhat do |wallet|
|
106
|
+
b = wallet.balance(Eth::Key.new(priv: WALTER).address.to_s)
|
107
|
+
assert_equal(456_000_000_000, b)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
98
111
|
def test_checks_balance_on_hardhat_in_threads
|
99
112
|
on_hardhat do |wallet|
|
100
113
|
Threads.new.assert do
|
@@ -118,6 +131,20 @@ class TestWallet < Minitest::Test
|
|
118
131
|
end
|
119
132
|
end
|
120
133
|
|
134
|
+
def test_eth_pays_on_hardhat
|
135
|
+
on_hardhat do |wallet|
|
136
|
+
to = Eth::Key.new(priv: WALTER).address.to_s
|
137
|
+
before = wallet.eth_balance(to)
|
138
|
+
sum = 42_000
|
139
|
+
from = Eth::Key.new(priv: JEFF).address.to_s
|
140
|
+
assert_operator(wallet.eth_balance(from), :>, sum * 2)
|
141
|
+
txn = wallet.eth_pay(JEFF, to, sum)
|
142
|
+
assert_equal(66, txn.length)
|
143
|
+
assert_match(/^0x[a-f0-9]{64}$/, txn)
|
144
|
+
assert_equal(before + sum, wallet.eth_balance(to))
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
121
148
|
def test_pays_on_hardhat_in_threads
|
122
149
|
on_hardhat do |wallet|
|
123
150
|
to = Eth::Key.new(priv: WALTER).address.to_s
|
@@ -131,6 +158,19 @@ class TestWallet < Minitest::Test
|
|
131
158
|
end
|
132
159
|
end
|
133
160
|
|
161
|
+
def test_pays_eth_on_hardhat_in_threads
|
162
|
+
on_hardhat do |wallet|
|
163
|
+
to = Eth::Key.new(priv: WALTER).address.to_s
|
164
|
+
before = wallet.eth_balance(to)
|
165
|
+
sum = 42_000
|
166
|
+
mul = 10
|
167
|
+
Threads.new(mul).assert do
|
168
|
+
wallet.eth_pay(JEFF, to, sum)
|
169
|
+
end
|
170
|
+
assert_equal(before + (sum * mul), wallet.eth_balance(to))
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
134
174
|
def test_accepts_payments_on_hardhat
|
135
175
|
walter = Eth::Key.new(priv: WALTER).address.to_s.downcase
|
136
176
|
jeff = Eth::Key.new(priv: JEFF).address.to_s.downcase
|