mini-wallet 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5363b3d1d2d3dd287d1ab89d3e8ac183fca8280570498f599ff78baede7b8aa5
4
+ data.tar.gz: 8b4e707db9b3448700536cb60d02ebfca86eee76ad4aa716fe0c0283958c6ca2
5
+ SHA512:
6
+ metadata.gz: 1e0880a384d54de15753097b62d3a7929a297c3e66082615fac1345621c96974e0312073b16c2225e9a655c075bdebae62b00df82a6686920503522a4724fe4c
7
+ data.tar.gz: bd02c9181483b9f46ea7cada3bc4c036792a43508a383a39712ae4b26b1d05319c5f46eaf036e2fbcfb472a3486cfb7c2d26a14e3c083be6f3016451edf850ad
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+
2
+ # Минималистичный Bitcoin-кошелёк (Signet)
3
+
4
+ ---
5
+
6
+ ## Общее описание
7
+
8
+ - Генерация и сохранение приватного ключа в `.wallet/signet_private_key`
9
+ - Показ баланса на адресе
10
+ - Отправка BTC на адрес Signet
11
+ - Динамические комиссии за транзакции
12
+
13
+ ---
14
+
15
+ ## Запуск
16
+
17
+ ### 1. Сборка
18
+
19
+ **Docker:**
20
+
21
+ ```bash
22
+ docker compose build
23
+ ```
24
+
25
+ **Makefile:**
26
+
27
+ ```bash
28
+ make build
29
+ ```
30
+
31
+ ---
32
+
33
+ ### 2. Инициализация кошелька
34
+
35
+ **Docker:**
36
+
37
+ ```bash
38
+ docker compose run --rm mini_wallet init
39
+ ```
40
+
41
+ **Makefile:**
42
+
43
+ ```bash
44
+ make init
45
+ ```
46
+
47
+ > Выведет адрес, на который можно получaть Signet-BTC.
48
+
49
+ При тестировании кошелька тестовые суммы запрашивались с адреса:
50
+
51
+ - [https://signetfaucet.bublina.eu.org/](https://signetfaucet.bublina.eu.org/)
52
+
53
+ ---
54
+
55
+ ### 3. Проверка баланса
56
+
57
+ **Docker:**
58
+
59
+ ```bash
60
+ docker compose run --rm mini_wallet balance
61
+ ```
62
+
63
+ **Makefile:**
64
+
65
+ ```bash
66
+ make balance
67
+ ```
68
+
69
+ ---
70
+
71
+ ### 4. Отправка средств
72
+
73
+ **Docker:**
74
+
75
+ ```bash
76
+ docker compose run --rm mini_wallet send <адрес> <сумма_в_BTC>
77
+ ```
78
+
79
+ **Makefile:**
80
+
81
+ ```bash
82
+ make send TO=<адрес> AMOUNT=<сумма>
83
+ ```
84
+
85
+ Пример:
86
+
87
+ ```bash
88
+ make send TO=tb1q... AMOUNT=0.00002
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Тестирование
94
+
95
+ ### RSpec
96
+
97
+ **Docker:**
98
+
99
+ ```bash
100
+ docker compose run --rm --entrypoint rspec mini_wallet
101
+ ```
102
+
103
+ **Makefile:**
104
+
105
+ ```bash
106
+ make test
107
+ ```
108
+
109
+ ### Проверка команд с помощью bash-скрипта (опционально)
110
+
111
+ Данный пункт требует наличия баланса на адресе созданного (командoй `make init`) кошелька.
112
+
113
+ Перед началом выполните команду:
114
+
115
+ ```bash
116
+ cp .env.example .env
117
+ ```
118
+
119
+ Далее укажите в файле `.env` значение вашего текущего адреса кошелька в переменной `SENDER_ADDR`.
120
+ Узнать адрес своего кошелька можно командой `make balance`.
121
+ Также нужно указать signet-адрес куда будут отправляться средства - `RECIPIENT_ADDR`.
122
+
123
+ После можно запустить скрипт проверки работоспособности команд кошелька.
124
+
125
+ ```bash
126
+ make
127
+ ```
data/bin/mini_wallet ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'dry/cli'
3
+ require 'zeitwerk'
4
+ require 'bitcoin'
5
+ require 'colorize'
6
+ require 'whirly'
7
+
8
+ loader = Zeitwerk::Loader.new
9
+ loader.push_dir(File.expand_path('../lib', __dir__))
10
+ loader.setup
11
+
12
+ Dry::CLI.new(MiniWallet::Cli).call
@@ -0,0 +1,42 @@
1
+ require 'faraday'
2
+
3
+ module Blockstream
4
+ class Client
5
+ BASE_URL = 'https://blockstream.info/signet/api/'.freeze
6
+
7
+ def fetch_address_info(address)
8
+ response = conn.get("address/#{address}")
9
+ handle_response(response) { |body| JSON.parse(body) }
10
+ end
11
+
12
+ def fetch_transaction_hex(txid)
13
+ response = conn.get("tx/#{txid}/hex")
14
+ handle_response(response) { |body| body }
15
+ end
16
+
17
+ def fetch_utxos(address)
18
+ response = conn.get("address/#{address}/utxo")
19
+ handle_response(response) { |body| JSON.parse(body) }
20
+ end
21
+
22
+ def fetch_fee_rate
23
+ response = conn.get('fee-estimates')
24
+ body = handle_response(response) { |body| JSON.parse(body) }
25
+ rate = body['1']
26
+
27
+ [rate.to_i, 1].max
28
+ end
29
+
30
+ private
31
+
32
+ def conn
33
+ @conn ||= Faraday.new(url: BASE_URL)
34
+ end
35
+
36
+ def handle_response(response)
37
+ raise "HTTP error #{response.status}: #{response.body}" unless response.success?
38
+
39
+ yield(response.body)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ require 'faraday'
2
+
3
+ module Mempool
4
+ class Client
5
+ BASE_URL = 'https://mempool.space/signet/api/'.freeze
6
+
7
+ def broadcast_transaction(tx_hex)
8
+ response = conn.post('tx') do |req|
9
+ req.headers['Content-Type'] = 'text/plain'
10
+ req.body = tx_hex
11
+ end
12
+
13
+ response.body
14
+ end
15
+
16
+ private
17
+
18
+ def conn
19
+ @conn ||= Faraday.new(url: BASE_URL)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ require 'dry/initializer'
2
+ require 'bigdecimal'
3
+
4
+ module MiniWallet
5
+ module Balance
6
+ class Fetch
7
+ extend Dry::Initializer
8
+
9
+ option :client
10
+ option :address
11
+
12
+ def call
13
+ response = client.fetch_address_info(address)
14
+
15
+ funded = response.dig('chain_stats', 'funded_txo_sum') || 0
16
+ spent = response.dig('chain_stats', 'spent_txo_sum') || 0
17
+
18
+ (BigDecimal(funded - spent) / 100_000_000).to_f
19
+ rescue Faraday::Error, JSON::ParserError => e
20
+ puts "Error fetching balance: #{e.message}"
21
+ 0.0
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module MiniWallet
2
+ module Cli
3
+ extend Dry::CLI::Registry
4
+
5
+ register 'init', Commands::Init
6
+ register 'balance', Commands::Balance
7
+ register 'send', Commands::Send
8
+ end
9
+ end
@@ -0,0 +1,20 @@
1
+ module MiniWallet
2
+ module Commands
3
+ class Balance < Base
4
+ desc 'Show BTC balance for wallet'
5
+
6
+ def call(*)
7
+ key = ensure_wallet_exists
8
+ address = key.to_p2wpkh
9
+
10
+ with_spinner do
11
+ client = Blockstream::Client.new
12
+ balance_value = MiniWallet::Balance::Fetch.new(address:, client:).call
13
+ formatted_balance = format('%.8f', balance_value)
14
+ balance = "#{formatted_balance} BTC".green.bold
15
+ puts "Balance for #{address}: #{balance}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ require 'dry/cli'
2
+ require 'whirly'
3
+
4
+ module MiniWallet
5
+ module Commands
6
+ class Base < Dry::CLI::Command
7
+ private
8
+
9
+ def ensure_wallet_exists
10
+ MiniWallet::KeyManager.load_key
11
+ rescue MiniWallet::KeyManager::WalletNotFoundError
12
+ puts 'Wallet not found.'.yellow
13
+ print 'Would you like to create a new wallet now? (y/n): '
14
+ response = STDIN.gets&.strip&.downcase
15
+
16
+ raise Dry::CLI::Error, 'Wallet not initialized. Run `mini_wallet init`.' unless %w[y
17
+ yes].include?(response)
18
+
19
+ MiniWallet::Commands::Init.new.call
20
+ MiniWallet::KeyManager.load_key
21
+ end
22
+
23
+ def with_spinner(status: nil, &block)
24
+ if ENV['TEST'] == 'true'
25
+ block.call
26
+ else
27
+ Whirly.start(spinner: 'dots7', status:) { block.call }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,21 @@
1
+ module MiniWallet
2
+ module Commands
3
+ class Init < Base
4
+ desc 'Generates a new Signet wallet (private key + address)'
5
+
6
+ def call(*)
7
+ if MiniWallet::KeyManager.key_file_exists?
8
+ puts "Wallet already exists at: #{MiniWallet::KeyManager::KEY_FILE}".yellow
9
+ address = MiniWallet::KeyManager.load_key.to_addr
10
+ puts "Address: #{address}"
11
+ return
12
+ end
13
+
14
+ key = MiniWallet::KeyManager.generate_key
15
+ puts 'New wallet created.'.green.bold
16
+ puts "Address: #{key.to_addr}".bold
17
+ puts "Private key saved to: #{MiniWallet::KeyManager::KEY_FILE}"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ module MiniWallet
2
+ module Commands
3
+ class Send < Base
4
+ desc 'Send amount (BTC) to address on Signet'
5
+
6
+ argument :to_address, required: true, desc: 'Recipient Bitcoin address'
7
+ argument :send_amount_btc, required: true, desc: 'Amount BTC to send'
8
+
9
+ def call(to_address:, send_amount_btc:)
10
+ unless Bitcoin.valid_address?(to_address)
11
+ puts 'Invalid Bitcoin address'.red
12
+ return
13
+ end
14
+
15
+ key = ensure_wallet_exists
16
+
17
+ with_spinner do
18
+ result = MiniWallet::Tx::Build.new(
19
+ to_address:,
20
+ send_amount_btc:,
21
+ key:,
22
+ client: Blockstream::Client.new
23
+ ).call
24
+
25
+ if result.failure?
26
+ puts result.failure.red
27
+ return
28
+ end
29
+
30
+ tx_hex = result.value!
31
+ tx_id = Mempool::Client.new.broadcast_transaction(tx_hex)
32
+
33
+ puts 'Transaction broadcasted successfully.'
34
+ puts "Transaction id: #{tx_id.green.bold}..."
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,42 @@
1
+ require 'fileutils'
2
+
3
+ module MiniWallet
4
+ class KeyManager
5
+ KEY_FILE = ENV.fetch('WALLET_KEY_FILE', '.wallet/priv_key.wif').freeze
6
+ KEY_TYPE = Bitcoin::Key::TYPES[:p2wpkh]
7
+
8
+ class WalletNotFoundError < StandardError; end
9
+
10
+ class << self
11
+ def generate_key
12
+ key = Bitcoin::Key.generate(KEY_TYPE)
13
+ save_key(key.to_wif)
14
+ key
15
+ end
16
+
17
+ def load_key
18
+ raise WalletNotFoundError, 'Init new wallet first.' unless key_file_exists?
19
+
20
+ load_key_from_file
21
+ end
22
+
23
+ def key_file_exists?
24
+ File.exist?(KEY_FILE)
25
+ end
26
+
27
+ private
28
+
29
+ def load_key_from_file
30
+ file = File.read(KEY_FILE).strip
31
+ Bitcoin::Key.from_wif(file)
32
+ end
33
+
34
+ def save_key(priv_key)
35
+ dir = File.dirname(KEY_FILE)
36
+ FileUtils.mkdir_p(dir, mode: 0o700) unless Dir.exist?(dir)
37
+ File.write(KEY_FILE, priv_key)
38
+ File.chmod(0o600, KEY_FILE)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,147 @@
1
+ require 'dry/operation'
2
+ require 'dry/initializer'
3
+ require 'bigdecimal'
4
+
5
+ # Builds and signs Bitcoin transactions using UTXOs from Signet.
6
+ # Handles fee estimation, change output, and SegWit signing.
7
+ module MiniWallet
8
+ module Tx
9
+ class Build < Dry::Operation
10
+ extend Dry::Initializer
11
+
12
+ MIN_FEE_SAT = 150
13
+
14
+ option :to_address
15
+ option :send_amount_btc
16
+ option :key
17
+ option :client
18
+
19
+ def call # rubocop:disable Metrics/AbcSize
20
+ send_amount_sat = step parse_amount_btc(send_amount_btc)
21
+ fee_rate = step fetch_fee_rate
22
+ utxos = step fetch_utxos(key.to_p2wpkh)
23
+ probe_tx = build_probe_tx_raw(utxos:, to_address:, send_amount_sat:, key:)
24
+ fee_satoshi = calculate_fee(fee_rate:, vsize: probe_tx.vsize)
25
+ total_value = step check_balance(utxos:, send_amount_sat:, fee_satoshi:)
26
+ tx = build_final_tx(utxos:, to_address:, send_amount_sat:, total_value:, fee_satoshi:, key:)
27
+ signed_tx = step sign_tx_inputs(tx:, utxos:, key:)
28
+
29
+ signed_tx.to_hex
30
+ end
31
+
32
+ private
33
+
34
+ def parse_amount_btc(input)
35
+ amount_sat = (BigDecimal(input) * 100_000_000).to_i
36
+ return Failure('Amount must be positive') if amount_sat <= 0
37
+
38
+ Success(amount_sat)
39
+ rescue ArgumentError
40
+ Failure('Invalid BTC amount format')
41
+ end
42
+
43
+ def fetch_fee_rate
44
+ rate = client.fetch_fee_rate
45
+
46
+ if rate.is_a?(Numeric) && rate.positive?
47
+ Success(rate.to_i)
48
+ else
49
+ Success(1)
50
+ end
51
+ rescue StandardError => e
52
+ warn "Failed to fetch fee rate: #{e.message}. Using fallback rate 1 sat/vB."
53
+ Success(1)
54
+ end
55
+
56
+ def fetch_utxos(address)
57
+ utxos = client.fetch_utxos(address)
58
+ return Failure('No UTXOs available') if utxos.empty?
59
+
60
+ Success(utxos)
61
+ rescue StandardError => e
62
+ Failure("Failed to fetch UTXOs: #{e.message}")
63
+ end
64
+
65
+ def calculate_fee(fee_rate:, vsize:)
66
+ dynamic = fee_rate * vsize
67
+ [dynamic, MIN_FEE_SAT].max
68
+ end
69
+
70
+ def build_probe_tx_raw(utxos:, to_address:, send_amount_sat:, key:)
71
+ tx = Bitcoin::Tx.new
72
+ tx = add_inputs_raw(tx:, utxos:)
73
+ tx = add_output_raw(tx:, to_address:, value: send_amount_sat)
74
+
75
+ change_script = Bitcoin::Script.parse_from_addr(key.to_p2wpkh)
76
+ tx.out << Bitcoin::TxOut.new(value: 1, script_pubkey: change_script)
77
+ tx
78
+ end
79
+
80
+ def check_balance(utxos:, send_amount_sat:, fee_satoshi:)
81
+ total = utxos.sum { _1['value'] }
82
+ required = send_amount_sat + fee_satoshi
83
+
84
+ if total >= required
85
+ Success(total)
86
+ else
87
+ balance_btc = BigDecimal(total) / 100_000_000
88
+ required_btc = BigDecimal(required) / 100_000_000
89
+ Failure(
90
+ "Insufficient funds: balance #{'%.8f' % balance_btc} BTC, " \
91
+ "required #{'%.8f' % required_btc} BTC (fee: #{fee_satoshi} sat)"
92
+ )
93
+ end
94
+ end
95
+
96
+ def build_final_tx(utxos:, to_address:, send_amount_sat:, total_value:, fee_satoshi:, key:)
97
+ tx = Bitcoin::Tx.new
98
+ add_inputs_raw(tx:, utxos: utxos)
99
+ add_output_raw(tx:, to_address:, value: send_amount_sat)
100
+
101
+ change_sat = total_value - (send_amount_sat + fee_satoshi)
102
+ add_output_raw(tx:, to_address: key.to_p2wpkh, value: change_sat) if change_sat.positive?
103
+
104
+ tx
105
+ end
106
+
107
+ def add_inputs_raw(tx:, utxos:)
108
+ utxos.each do |utxo|
109
+ out_point = Bitcoin::OutPoint.from_txid(utxo['txid'], utxo['vout'])
110
+ tx.in << Bitcoin::TxIn.new(out_point: out_point)
111
+ end
112
+ tx
113
+ end
114
+
115
+ def add_output_raw(tx:, to_address:, value:)
116
+ script = Bitcoin::Script.parse_from_addr(to_address)
117
+ tx.out << Bitcoin::TxOut.new(value:, script_pubkey: script)
118
+ tx
119
+ end
120
+
121
+ def fetch_prev_script_pub_key(txid:, vout:)
122
+ tx_hex = client.fetch_transaction_hex(txid)
123
+ script = Bitcoin::Tx
124
+ .parse_from_payload([tx_hex].pack('H*'))
125
+ .out[vout]
126
+ .script_pubkey
127
+ Success(script)
128
+ end
129
+
130
+ def sign_tx_inputs(tx:, utxos:, key:)
131
+ utxos.each_with_index do |utxo, index|
132
+ script_pubkey = step fetch_prev_script_pub_key(txid: utxo['txid'], vout: utxo['vout'])
133
+
134
+ sig_hash = tx.sighash_for_input(index, script_pubkey, sig_version: :witness_v0, amount: utxo['value'])
135
+ signature = key.sign(sig_hash) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
136
+
137
+ tx.in[index].script_witness.stack.concat([signature, key.pubkey.htb])
138
+
139
+ unless tx.verify_input_sig(index, script_pubkey, amount: utxo['value'])
140
+ return Failure("ScriptPubkey verification failed for txid #{utxo['txid']}")
141
+ end
142
+ end
143
+ Success(tx)
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,3 @@
1
+ module MiniWallet
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'bitcoin'
2
+
3
+ Bitcoin.chain_params = :signet
4
+
5
+ module MiniWallet
6
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mini-wallet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ilmir
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bitcoin-ruby
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: colorize
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.1.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 1.1.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-cli
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 1.3.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 1.3.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: dry-initializer
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 3.2.0
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 3.2.0
68
+ - !ruby/object:Gem::Dependency
69
+ name: dry-operation
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 1.0.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 1.0.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: faraday
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: whirly
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.3.0
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.3.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: zeitwerk
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '2.0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '2.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: rspec
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '3.13'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '3.13'
138
+ - !ruby/object:Gem::Dependency
139
+ name: webmock
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 3.25.1
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 3.25.1
152
+ description: Generate keys, check balance, and send transactions on Bitcoin Signet.
153
+ email:
154
+ - code.for.func@gmail.com
155
+ executables:
156
+ - mini_wallet
157
+ extensions: []
158
+ extra_rdoc_files: []
159
+ files:
160
+ - README.md
161
+ - bin/mini_wallet
162
+ - lib/blockstream/client.rb
163
+ - lib/mempool/client.rb
164
+ - lib/mini_wallet.rb
165
+ - lib/mini_wallet/balance/fetch.rb
166
+ - lib/mini_wallet/cli.rb
167
+ - lib/mini_wallet/commands/balance.rb
168
+ - lib/mini_wallet/commands/base.rb
169
+ - lib/mini_wallet/commands/init.rb
170
+ - lib/mini_wallet/commands/send.rb
171
+ - lib/mini_wallet/key_manager.rb
172
+ - lib/mini_wallet/tx/build.rb
173
+ - lib/mini_wallet/version.rb
174
+ homepage: https://gitlab.com/i-karimov/mini_wallet
175
+ licenses:
176
+ - MIT
177
+ metadata: {}
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - ">="
184
+ - !ruby/object:Gem::Version
185
+ version: 2.7.0
186
+ required_rubygems_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ requirements: []
192
+ rubygems_version: 3.7.2
193
+ specification_version: 4
194
+ summary: A minimal Bitcoin wallet CLI for Testnet
195
+ test_files: []