erc20 0.2.3 → 0.2.4
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/Gemfile.lock +2 -1
- data/bin/erc20 +23 -6
- data/erc20.gemspec +1 -0
- data/features/dry.feature +21 -0
- data/lib/erc20/erc20.rb +1 -1
- data/lib/erc20/wallet.rb +43 -21
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5b9458add234faedc7e3827c363cf16b083b2c409b13577a7b30ac601d0ddef
|
|
4
|
+
data.tar.gz: 4593c178c97685ec74c1c2bc0855daddf19082c6b9eead27254d79d644c2a0e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c2459cfdc15924c674852a078116ad91e0c78fa38e522f4bf1075fb409a8756c7f73f9931206c6f3e16c98119ca278c04a6cd0f2ed7470cb765ab1a35284a87
|
|
7
|
+
data.tar.gz: 8ad83464049ed10ec9d261b8ffbd2b1a889d56fc376c2c79fa159fc95f4bba48786fbbab54ab855461f81e10bc551c494a61cd1907f3b5b1636084e9bd04b34e
|
data/Gemfile.lock
CHANGED
|
@@ -2,6 +2,7 @@ PATH
|
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
4
|
erc20 (0.0.0)
|
|
5
|
+
elapsed (~> 0.2)
|
|
5
6
|
eth (~> 0.5)
|
|
6
7
|
faye-websocket (~> 0.11)
|
|
7
8
|
json (~> 2.10)
|
|
@@ -56,7 +57,7 @@ GEM
|
|
|
56
57
|
backtrace (~> 0.3)
|
|
57
58
|
os (~> 1.1)
|
|
58
59
|
qbash (~> 0.3)
|
|
59
|
-
elapsed (0.2.
|
|
60
|
+
elapsed (0.2.1)
|
|
60
61
|
loog (~> 0.6)
|
|
61
62
|
tago (~> 0.1)
|
|
62
63
|
erb (6.0.1)
|
data/bin/erc20
CHANGED
|
@@ -36,6 +36,11 @@ Options are:"
|
|
|
36
36
|
'Ethereum chain ID',
|
|
37
37
|
default: 1
|
|
38
38
|
)
|
|
39
|
+
o.bool(
|
|
40
|
+
'--ssl',
|
|
41
|
+
'Use SSL for HTTP connections',
|
|
42
|
+
default: true
|
|
43
|
+
)
|
|
39
44
|
o.string(
|
|
40
45
|
'--host',
|
|
41
46
|
'Host name of the provider',
|
|
@@ -80,12 +85,18 @@ Options are:"
|
|
|
80
85
|
end
|
|
81
86
|
raise 'Try --help' if opts.arguments.empty?
|
|
82
87
|
log = opts[:verbose] ? Loog::VERBOSE : Loog::REGULAR
|
|
83
|
-
wallet =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
wallet =
|
|
89
|
+
if opts[:dry]
|
|
90
|
+
ERC20::FakeWallet.new
|
|
91
|
+
else
|
|
92
|
+
ERC20::Wallet.new(
|
|
93
|
+
contract: opts[:contract],
|
|
94
|
+
host: opts[:host], port: opts[:port].to_i,
|
|
95
|
+
http_path: opts[:http_path], ws_path: opts[:ws_path],
|
|
96
|
+
ssl: opts[:ssl],
|
|
97
|
+
log:
|
|
98
|
+
)
|
|
99
|
+
end
|
|
89
100
|
case opts.arguments[0]
|
|
90
101
|
when 'key'
|
|
91
102
|
puts Eth::Key.new.private_hex
|
|
@@ -119,14 +130,20 @@ Options are:"
|
|
|
119
130
|
raise 'Amount argument is required (for example, "19.9usdt" or "$19.9")' if amount.nil?
|
|
120
131
|
if /^[0-9]+$/.match?(amount)
|
|
121
132
|
amount = amount.to_i
|
|
133
|
+
log.debug("The token amount equals to #{amount} ERC20 tokens (as provided)")
|
|
122
134
|
elsif /^[0-9]+(\.[0-9]+)?usdt$/.match?(amount)
|
|
123
135
|
amount = (amount.gsub(/usdt$/, '').to_f * 1_000_000).to_i
|
|
136
|
+
log.debug("The USDT amount equals to #{amount} ERC20 tokens")
|
|
124
137
|
elsif /^\$[0-9]+(\.[0-9]+)?$/.match?(amount)
|
|
125
138
|
amount = (amount.gsub(/^\$/, '').to_f * 1_000_000).to_i
|
|
139
|
+
log.debug("The dollar amount equals to #{amount} ERC20 tokens")
|
|
126
140
|
else
|
|
127
141
|
raise "Can't understand amount: #{amount.inspect}"
|
|
128
142
|
end
|
|
129
143
|
log.debug("Sending #{amount} ERC20 tokens")
|
|
144
|
+
if wallet.eth_balance(priv.address.to_s).zero?
|
|
145
|
+
log.debug("ETH balance of #{priv.address} is zero, the txn most definitely will be rejected by the server!")
|
|
146
|
+
end
|
|
130
147
|
puts wallet.pay(priv.private_hex, address, amount)
|
|
131
148
|
when 'eth_pay'
|
|
132
149
|
pkey = opts.arguments[1]
|
data/erc20.gemspec
CHANGED
|
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
|
|
|
25
25
|
s.rdoc_options = ['--charset=UTF-8']
|
|
26
26
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
27
27
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
|
28
|
+
s.add_dependency 'elapsed', '~>0.2'
|
|
28
29
|
s.add_dependency 'eth', '~>0.5'
|
|
29
30
|
s.add_dependency 'faye-websocket', '~>0.11'
|
|
30
31
|
s.add_dependency 'json', '~>2.10'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
Feature: Command Line Processing, in Dry Mode
|
|
5
|
+
As a simple ETH/ERC20 user I want to test it in dry mode
|
|
6
|
+
|
|
7
|
+
Scenario: Gas price price can be retrieved
|
|
8
|
+
When I run bin/erc20 with "price --attempts=4 --dry"
|
|
9
|
+
Then Exit code is zero
|
|
10
|
+
|
|
11
|
+
Scenario: ERC20 balance can be checked
|
|
12
|
+
When I run bin/erc20 with "balance 0x7232148927F8a580053792f44D4d59d40Fd00ABD --verbose --dry"
|
|
13
|
+
Then Exit code is zero
|
|
14
|
+
|
|
15
|
+
Scenario: ETH balance can be checked
|
|
16
|
+
When I run bin/erc20 with "eth_balance 0x7232148927F8a580053792f44D4d59d40Fd00ABD --verbose --dry"
|
|
17
|
+
Then Exit code is zero
|
|
18
|
+
|
|
19
|
+
Scenario: ERC20 payment can be sent
|
|
20
|
+
When I run bin/erc20 with "pay --dry --verbose 46feba063e9b59a8ae0dba68abd39a3cb8f52089e776576d6eb1bb5bfec123d1 0x7232148927F8a580053792f44D4d59d40Fd00ABD $10"
|
|
21
|
+
Then Exit code is zero
|
data/lib/erc20/erc20.rb
CHANGED
data/lib/erc20/wallet.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# SPDX-FileCopyrightText: Copyright (c) 2025 Yegor Bugayenko
|
|
4
4
|
# SPDX-License-Identifier: MIT
|
|
5
5
|
|
|
6
|
+
require 'elapsed'
|
|
6
7
|
require 'eth'
|
|
7
8
|
require 'eventmachine'
|
|
8
9
|
require 'faye/websocket'
|
|
@@ -119,7 +120,10 @@ class ERC20::Wallet
|
|
|
119
120
|
raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
|
|
120
121
|
func = '70a08231' # balanceOf
|
|
121
122
|
data = "0x#{func}000000000000000000000000#{address[2..].downcase}"
|
|
122
|
-
r =
|
|
123
|
+
r =
|
|
124
|
+
with_jsonrpc do |jr|
|
|
125
|
+
jr.eth_call({ to: @contract, data: data }, 'latest')
|
|
126
|
+
end
|
|
123
127
|
b = r[2..].to_i(16)
|
|
124
128
|
log_it(:debug, "The balance of #{address} is #{b} ERC20 tokens")
|
|
125
129
|
b
|
|
@@ -136,7 +140,10 @@ class ERC20::Wallet
|
|
|
136
140
|
raise 'Address can\'t be nil' unless address
|
|
137
141
|
raise 'Address must be a String' unless address.is_a?(String)
|
|
138
142
|
raise 'Invalid format of the address' unless /^0x[0-9a-fA-F]{40}$/.match?(address)
|
|
139
|
-
r =
|
|
143
|
+
r =
|
|
144
|
+
with_jsonrpc do |jr|
|
|
145
|
+
jr.eth_getBalance(address, 'latest')
|
|
146
|
+
end
|
|
140
147
|
b = r[2..].to_i(16)
|
|
141
148
|
log_it(:debug, "The balance of #{address} is #{b} ETHs")
|
|
142
149
|
b
|
|
@@ -150,7 +157,10 @@ class ERC20::Wallet
|
|
|
150
157
|
raise 'Transaction hash can\'t be nil' unless txn
|
|
151
158
|
raise 'Transaction hash must be a String' unless txn.is_a?(String)
|
|
152
159
|
raise 'Invalid format of the transaction hash' unless /^0x[0-9a-fA-F]{64}$/.match?(txn)
|
|
153
|
-
receipt =
|
|
160
|
+
receipt =
|
|
161
|
+
with_jsonrpc do |jr|
|
|
162
|
+
jr.eth_getTransactionReceipt(txn)
|
|
163
|
+
end
|
|
154
164
|
raise "Transaction not found: #{txn}" if receipt.nil?
|
|
155
165
|
logs = receipt['logs'] || []
|
|
156
166
|
transfer_event = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
|
|
@@ -180,7 +190,10 @@ class ERC20::Wallet
|
|
|
180
190
|
raise 'Amount can\'t be nil' unless amount
|
|
181
191
|
raise "Amount (#{amount}) must be an Integer" unless amount.is_a?(Integer)
|
|
182
192
|
raise "Amount (#{amount}) must be a positive Integer" unless amount.positive?
|
|
183
|
-
gas =
|
|
193
|
+
gas =
|
|
194
|
+
with_jsonrpc do |jr|
|
|
195
|
+
jr.eth_estimateGas({ from:, to: @contract, data: to_pay_data(to, amount) }, 'latest').to_i(16)
|
|
196
|
+
end
|
|
184
197
|
log_it(:debug, "It would take #{gas} gas units to send #{amount} tokens from #{from} to #{to}")
|
|
185
198
|
gas
|
|
186
199
|
end
|
|
@@ -196,7 +209,10 @@ class ERC20::Wallet
|
|
|
196
209
|
#
|
|
197
210
|
# @return [Integer] Price of gas unit, in gwei (0.000000001 ETH)
|
|
198
211
|
def gas_price
|
|
199
|
-
block =
|
|
212
|
+
block =
|
|
213
|
+
with_jsonrpc do |jr|
|
|
214
|
+
jr.eth_getBlockByNumber('latest', false)
|
|
215
|
+
end
|
|
200
216
|
raise "Can't get gas price, try again later" if block.nil?
|
|
201
217
|
gwei = block['baseFeePerGas'].to_i(16)
|
|
202
218
|
log_it(:debug, "The cost of one gas unit is #{gwei} gwei")
|
|
@@ -241,9 +257,9 @@ class ERC20::Wallet
|
|
|
241
257
|
from = key.address.to_s
|
|
242
258
|
tnx =
|
|
243
259
|
@mutex.synchronize do
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{
|
|
260
|
+
with_jsonrpc do |jr|
|
|
261
|
+
nonce = jr.eth_getTransactionCount(from, 'pending').to_i(16)
|
|
262
|
+
h = {
|
|
247
263
|
nonce:,
|
|
248
264
|
gas_price: price,
|
|
249
265
|
gas_limit: limit || gas_estimate(from, address, amount),
|
|
@@ -252,10 +268,12 @@ class ERC20::Wallet
|
|
|
252
268
|
data: to_pay_data(address, amount),
|
|
253
269
|
chain_id: @chain
|
|
254
270
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
271
|
+
tx = Eth::Tx.new(h)
|
|
272
|
+
tx.sign(key)
|
|
273
|
+
hex = "0x#{tx.hex}"
|
|
274
|
+
log_it(:debug, "Sending ERC20 transaction #{hex}")
|
|
275
|
+
jr.eth_sendRawTransaction(hex)
|
|
276
|
+
end
|
|
259
277
|
end
|
|
260
278
|
log_it(:debug, "Sent #{amount} ERC20 tokens from #{from} to #{address}: #{tnx}")
|
|
261
279
|
tnx.downcase
|
|
@@ -286,9 +304,9 @@ class ERC20::Wallet
|
|
|
286
304
|
from = key.address.to_s
|
|
287
305
|
tnx =
|
|
288
306
|
@mutex.synchronize do
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
{
|
|
307
|
+
with_jsonrpc do |jr|
|
|
308
|
+
nonce = jr.eth_getTransactionCount(from, 'pending').to_i(16)
|
|
309
|
+
h = {
|
|
292
310
|
chain_id: @chain,
|
|
293
311
|
nonce:,
|
|
294
312
|
gas_price: price,
|
|
@@ -296,10 +314,12 @@ class ERC20::Wallet
|
|
|
296
314
|
to: address,
|
|
297
315
|
value: amount
|
|
298
316
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
317
|
+
tx = Eth::Tx.new(h)
|
|
318
|
+
tx.sign(key)
|
|
319
|
+
hex = "0x#{tx.hex}"
|
|
320
|
+
log_it(:debug, "Sending ETH transaction #{hex}")
|
|
321
|
+
jr.eth_sendRawTransaction(hex)
|
|
322
|
+
end
|
|
303
323
|
end
|
|
304
324
|
log_it(:debug, "Sent #{amount} ETHs from #{from} to #{address}: #{tnx}")
|
|
305
325
|
tnx.downcase
|
|
@@ -474,7 +494,7 @@ class ERC20::Wallet
|
|
|
474
494
|
URI.parse("#{http ? 'http' : 'ws'}#{'s' if @ssl}://#{@host}:#{@port}#{http ? @http_path : @ws_path}")
|
|
475
495
|
end
|
|
476
496
|
|
|
477
|
-
def
|
|
497
|
+
def with_jsonrpc
|
|
478
498
|
JSONRPC.logger = Loog::NULL
|
|
479
499
|
opts = {}
|
|
480
500
|
if @proxy
|
|
@@ -489,7 +509,9 @@ class ERC20::Wallet
|
|
|
489
509
|
}
|
|
490
510
|
end
|
|
491
511
|
end
|
|
492
|
-
|
|
512
|
+
elapsed(@log, good: "Talked to #{url.host}:#{url.port}") do
|
|
513
|
+
yield JSONRPC::Client.new(url.to_s, opts)
|
|
514
|
+
end
|
|
493
515
|
end
|
|
494
516
|
|
|
495
517
|
def to_pay_data(address, amount)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: erc20
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: elapsed
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: eth
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -115,6 +129,7 @@ files:
|
|
|
115
129
|
- bin/erc20
|
|
116
130
|
- erc20.gemspec
|
|
117
131
|
- features/cli.feature
|
|
132
|
+
- features/dry.feature
|
|
118
133
|
- features/gem_package.feature
|
|
119
134
|
- features/step_definitions/steps.rb
|
|
120
135
|
- features/support/env.rb
|