zold 0.14.28 → 0.14.29

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/appveyor.yml +9 -8
  4. data/bin/zold +8 -4
  5. data/fixtures/scripts/distribute-wallet.sh +4 -3
  6. data/lib/zold/commands/create.rb +6 -5
  7. data/lib/zold/commands/diff.rb +5 -7
  8. data/lib/zold/commands/fetch.rb +3 -2
  9. data/lib/zold/commands/invoice.rb +4 -3
  10. data/lib/zold/commands/merge.rb +14 -13
  11. data/lib/zold/commands/node.rb +40 -17
  12. data/lib/zold/commands/pay.rb +16 -9
  13. data/lib/zold/commands/propagate.rb +24 -21
  14. data/lib/zold/commands/push.rb +23 -22
  15. data/lib/zold/commands/remote.rb +16 -23
  16. data/lib/zold/commands/routines/reconnect.rb +5 -1
  17. data/lib/zold/commands/show.rb +3 -1
  18. data/lib/zold/commands/taxes.rb +9 -3
  19. data/lib/zold/copies.rb +2 -2
  20. data/lib/zold/hungry_wallets.rb +21 -1
  21. data/lib/zold/node/async_entrance.rb +5 -6
  22. data/lib/zold/node/entrance.rb +3 -2
  23. data/lib/zold/node/front.rb +119 -88
  24. data/lib/zold/node/nodup_entrance.rb +3 -2
  25. data/lib/zold/node/sync_entrance.rb +81 -0
  26. data/lib/zold/node/trace.rb +75 -0
  27. data/lib/zold/patch.rb +2 -3
  28. data/lib/zold/remotes.rb +1 -1
  29. data/lib/zold/sync_wallets.rb +78 -0
  30. data/lib/zold/version.rb +1 -1
  31. data/lib/zold/wallet.rb +1 -0
  32. data/lib/zold/wallets.rb +5 -5
  33. data/test/commands/test_create.rb +9 -6
  34. data/test/commands/test_diff.rb +1 -1
  35. data/test/commands/test_invoice.rb +7 -6
  36. data/test/commands/test_list.rb +4 -3
  37. data/test/commands/test_merge.rb +2 -2
  38. data/test/commands/test_pull.rb +3 -1
  39. data/test/commands/test_remote.rb +4 -0
  40. data/test/commands/test_show.rb +5 -4
  41. data/test/fake_home.rb +2 -1
  42. data/test/node/test_async_entrance.rb +1 -1
  43. data/test/node/test_farm.rb +2 -2
  44. data/test/node/test_front.rb +63 -17
  45. data/test/node/test_sync_entrance.rb +41 -0
  46. data/test/node/test_trace.rb +36 -0
  47. data/test/test__helper.rb +18 -0
  48. data/test/test_copies.rb +1 -1
  49. data/test/test_metronome.rb +2 -3
  50. data/test/test_patch.rb +1 -1
  51. data/test/test_remotes.rb +3 -3
  52. data/test/test_sync_wallets.rb +69 -0
  53. data/test/test_upgrades.rb +0 -0
  54. data/test/test_wallet.rb +1 -1
  55. data/test/test_wallets.rb +14 -10
  56. data/test/test_zold.rb +2 -2
  57. data/test/upgrades/test_protocol_up.rb +3 -2
  58. data/zold.gemspec +1 -0
  59. metadata +25 -2
@@ -59,50 +59,51 @@ Available options:"
59
59
  mine = Args.new(opts, @log).take || return
60
60
  mine = @wallets.all if mine.empty?
61
61
  mine.map { |i| Id.new(i) }.each do |id|
62
- wallet = @wallets.find(id)
63
- raise "The wallet #{id} is absent" unless wallet.exists?
64
- push(wallet, opts)
62
+ push(id, opts)
65
63
  end
66
64
  end
67
65
 
68
66
  private
69
67
 
70
- def push(wallet, opts)
68
+ def push(id, opts)
71
69
  total = 0
72
70
  nodes = 0
73
71
  done = 0
74
72
  @remotes.iterate(@log) do |r|
75
73
  nodes += 1
76
- total += push_one(wallet, r, opts)
74
+ total += push_one(id, r, opts)
77
75
  done += 1
78
76
  end
79
77
  raise "There are no remote nodes, run 'zold remote reset'" if nodes.zero?
80
- raise "No nodes out of #{nodes} accepted the wallet #{wallet.id}" if done.zero?
81
- @log.info("Push finished to #{done} nodes out of #{nodes}, total score for #{wallet.id} is #{total}")
78
+ raise "No nodes out of #{nodes} accepted the wallet #{id}" if done.zero?
79
+ @log.info("Push finished to #{done} nodes out of #{nodes}, total score for #{id} is #{total}")
82
80
  end
83
81
 
84
- def push_one(wallet, r, opts)
82
+ def push_one(id, r, opts)
85
83
  if opts['ignore-node'].include?(r.to_s)
86
84
  @log.debug("#{r} ignored because of --ignore-node")
87
85
  return 0
88
86
  end
89
87
  start = Time.now
90
- content = AtomicFile.new(wallet.path).read
91
- response = r.http("/wallet/#{wallet.id}#{opts['sync'] ? '?sync=true' : ''}").put(content)
92
- if response.code == '304'
93
- @log.info("#{r}: same version #{content.length}b/#{wallet.txns.count}t \
88
+ @wallets.find(id) do |wallet|
89
+ raise "The wallet #{id} is absent" unless wallet.exists?
90
+ content = AtomicFile.new(wallet.path).read
91
+ response = r.http("/wallet/#{wallet.id}#{opts['sync'] ? '?sync=true' : ''}").put(content)
92
+ if response.code == '304'
93
+ @log.info("#{r}: same version #{content.length}b/#{wallet.txns.count}t \
94
94
  of #{wallet.id} there, in #{(Time.now - start).round(2)}s")
95
- return 0
96
- end
97
- r.assert_code(200, response)
98
- json = JsonPage.new(response.body).to_hash
99
- score = Score.parse_json(json['score'])
100
- r.assert_valid_score(score)
101
- r.assert_score_ownership(score)
102
- r.assert_score_strength(score) unless opts['ignore-score-weakness']
103
- @log.info("#{r} accepted #{content.length}b/#{wallet.digest[0, 6]}/#{wallet.txns.count}t of #{wallet.id} \
95
+ return 0
96
+ end
97
+ r.assert_code(200, response)
98
+ json = JsonPage.new(response.body).to_hash
99
+ score = Score.parse_json(json['score'])
100
+ r.assert_valid_score(score)
101
+ r.assert_score_ownership(score)
102
+ r.assert_score_strength(score) unless opts['ignore-score-weakness']
103
+ @log.info("#{r} accepted #{content.length}b/#{wallet.digest[0, 6]}/#{wallet.txns.count}t of #{wallet.id} \
104
104
  in #{(Time.now - start).round(2)}s: #{Rainbow(score.value).green} (#{json['version']})")
105
- score.value
105
+ score.value
106
+ end
106
107
  end
107
108
  end
108
109
  end
@@ -80,15 +80,15 @@ Available options:"
80
80
  o.bool '--ignore-score-value',
81
81
  'Don\'t complain when their score is too small',
82
82
  default: false
83
+ o.array '--ignore-node',
84
+ 'Ignore this node and never add it to the list',
85
+ default: []
83
86
  o.integer '--min-score',
84
87
  "The minimum score required for winning the election (default: #{Tax::EXACT_SCORE})",
85
88
  default: Tax::EXACT_SCORE
86
89
  o.integer '--max-winners',
87
90
  'The maximum amount of election winners the election (default: 1)',
88
91
  default: 1
89
- o.bool '--force',
90
- 'Add/remove if if this operation is not possible',
91
- default: false
92
92
  o.bool '--skip-ping',
93
93
  'Don\'t ping back the node when adding it (not recommended)',
94
94
  default: false
@@ -161,29 +161,21 @@ Available options:"
161
161
  end
162
162
 
163
163
  def add(host, port, opts)
164
+ if opts['ignore-node'].include?("#{host}:#{port}")
165
+ @log.info("#{host}:#{port} won't be added since it's in the --ignore-node list")
166
+ return
167
+ end
164
168
  unless opts['skip-ping']
165
169
  res = Http.new(uri: "http://#{host}:#{port}/version", score: nil, network: opts['network']).get
166
170
  raise "The node #{host}:#{port} is not responding (code is #{res.code})" unless res.code == '200'
167
171
  end
168
- if @remotes.exists?(host, port)
169
- raise "#{host}:#{port} already exists in the list" unless opts['force']
170
- @log.debug("#{host}:#{port} already exists in the list")
171
- else
172
- @remotes.add(host, port)
173
- @log.info("#{host}:#{port} added to the list, #{@remotes.all.count} total")
174
- end
175
- @log.debug("There are #{@remotes.all.count} remote nodes in the list")
172
+ @remotes.add(host, port)
173
+ @log.info("#{host}:#{port} added to the list, #{@remotes.all.count} total")
176
174
  end
177
175
 
178
- def remove(host, port, opts)
179
- if @remotes.exists?(host, port)
180
- @remotes.remove(host, port)
181
- @log.info("#{host}:#{port} removed from the list")
182
- else
183
- raise "#{host}:#{port} is not in the list" unless opts['force']
184
- @log.debug("#{host}:#{port} is not in the list")
185
- end
186
- @log.debug("There are #{@remotes.all.count} remote nodes in the list")
176
+ def remove(host, port, _)
177
+ @remotes.remove(host, port)
178
+ @log.info("#{host}:#{port} removed from the list, #{@remotes.all.count} total")
187
179
  end
188
180
 
189
181
  # Returns an array of Zold::Score
@@ -242,7 +234,8 @@ it's recommended to reboot, but I don't do it because of --never-reboot")
242
234
  end
243
235
  if deep
244
236
  json['all'].each do |s|
245
- @remotes.add(s['host'], s['port'])
237
+ add(s['host'], s['port'], opts)
238
+ @log.info("#{s['host']}:#{s['port']} found at #{r} and added")
246
239
  end
247
240
  end
248
241
  capacity << { host: score.host, port: score.port, count: json['all'].count }
@@ -255,9 +248,9 @@ in #{(Time.now - start).round(2)}s")
255
248
  end
256
249
  total = @remotes.all.size
257
250
  if total.zero?
258
- @log.debug("The list of remotes is #{Rainbow('empty').red}, run 'zold remote reset'!")
251
+ @log.info("The list of remotes is #{Rainbow('empty').red}, run 'zold remote reset'!")
259
252
  else
260
- @log.debug("There are #{total} known remotes")
253
+ @log.info("There are #{total} known remotes")
261
254
  end
262
255
  end
263
256
 
@@ -44,10 +44,14 @@ module Zold
44
44
  sleep(60) unless @opts['routine-immediately']
45
45
  cmd = Remote.new(remotes: @remotes, log: @log, farm: @farm)
46
46
  args = ['remote', "--network=#{@opts['network']}"]
47
- cmd.run(args + ['add', 'b1.zold.io', '80', '--force']) unless @opts['routine-immediately']
47
+ score = @farm.best[0]
48
+ args << "--ignore-node=#{score.host}:#{score.port}" if score
49
+ cmd.run(args + ['add', 'b1.zold.io', '80']) unless @opts['routine-immediately']
48
50
  cmd.run(args + ['trim'])
49
51
  cmd.run(args + ['select'])
50
52
  cmd.run(args + ['update'] + (@opts['never-reboot'] ? [] : ['--reboot']))
53
+ @log.info("Reconnected, there are #{@remotes.all.count} remote notes: \
54
+ #{@remotes.all.map { |r| "#{r[:host]}:#{r[:port]}" }.join(', ')}")
51
55
  end
52
56
  end
53
57
  end
@@ -53,7 +53,9 @@ Available options:"
53
53
  else
54
54
  total = Amount::ZERO
55
55
  mine.map { |i| Id.new(i) }.each do |id|
56
- total += show(@wallets.find(id), opts)
56
+ @wallets.find(id) do |w|
57
+ total += show(w, opts)
58
+ end
57
59
  end
58
60
  total
59
61
  end
@@ -80,17 +80,23 @@ Available options:"
80
80
  when 'show'
81
81
  raise 'At least one wallet ID is required' unless mine[1]
82
82
  mine[1..-1].each do |id|
83
- show(@wallets.find(Id.new(id)), opts)
83
+ @wallets.find(Id.new(id)) do |w|
84
+ show(w, opts)
85
+ end
84
86
  end
85
87
  when 'debt'
86
88
  raise 'At least one wallet ID is required' unless mine[1]
87
89
  mine[1..-1].each do |id|
88
- debt(@wallets.find(Id.new(id)), opts)
90
+ @wallets.find(Id.new(id)) do |w|
91
+ debt(w, opts)
92
+ end
89
93
  end
90
94
  when 'pay'
91
95
  raise 'At least one wallet ID is required' unless mine[1]
92
96
  mine[1..-1].each do |id|
93
- pay(@wallets.find(Id.new(id)), opts)
97
+ @wallets.find(Id.new(id)) do |w|
98
+ pay(w, opts)
99
+ end
94
100
  end
95
101
  else
96
102
  @log.info(opts.to_s)
@@ -140,8 +140,6 @@ module Zold
140
140
  end
141
141
  end
142
142
 
143
- private
144
-
145
143
  def load
146
144
  FileUtils.mkdir_p(File.dirname(file))
147
145
  FileUtils.touch(file)
@@ -156,6 +154,8 @@ module Zold
156
154
  end
157
155
  end
158
156
 
157
+ private
158
+
159
159
  def save(list)
160
160
  AtomicFile.new(file).write(
161
161
  list.map do |r|
@@ -1,5 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright (c) 2018 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
3
23
  require 'delegate'
4
24
 
5
25
  module Zold
@@ -8,7 +28,7 @@ module Zold
8
28
  # @todo #280:30min Add to the queue. Once in there, try
9
29
  # to pull it as soon as possible as is described in #280.
10
30
  def find(id)
11
- super(id)
31
+ yield super(id)
12
32
  end
13
33
  end
14
34
  end
@@ -67,14 +67,13 @@ module Zold
67
67
  @pool = Concurrent::FixedThreadPool.new(
68
68
  AsyncEntrance::THREADS, max_queue: AsyncEntrance::THREADS, fallback_policy: :abort
69
69
  )
70
- AsyncEntrance::THREADS.times do
70
+ AsyncEntrance::THREADS.times do |t|
71
71
  @pool.post do
72
+ Thread.current.name = "async-#{t}"
72
73
  loop do
73
- VerboseThread.new(@log).run(true) do
74
- take
75
- break if @pool.shuttingdown?
76
- sleep Random.rand(100) / 100
77
- end
74
+ VerboseThread.new(@log).run(true) { take }
75
+ break if @pool.shuttingdown?
76
+ sleep Random.rand(100) / 100
78
77
  end
79
78
  end
80
79
  end
@@ -97,8 +97,9 @@ module Zold
97
97
  @mutex.synchronize do
98
98
  @history.shift if @history.length >= 16
99
99
  @speed.shift if @speed.length >= 64
100
- wallet = @wallets.find(id)
101
- @history << "#{id}/#{sec}/#{modified.count}/#{wallet.balance.to_zld}/#{wallet.txns.count}t"
100
+ @wallets.find(id) do |wallet|
101
+ @history << "#{id}/#{sec}/#{modified.count}/#{wallet.balance.to_zld}/#{wallet.txns.count}t"
102
+ end
102
103
  @speed << sec
103
104
  end
104
105
  modified
@@ -45,12 +45,15 @@ module Zold
45
45
  # Web front
46
46
  class Front < Sinatra::Base
47
47
  configure do
48
+ Thread.current.name = 'sinatra'
48
49
  set :bind, '0.0.0.0'
49
50
  set :suppress_messages, true
50
51
  set :start, Time.now
51
52
  set :lock, false
52
53
  set :show_exceptions, false
53
54
  set :server, 'webrick'
55
+ set :log, nil? # to be injected at node.rb
56
+ set :trace, nil? # to be injected at node.rb
54
57
  set :halt, '' # to be injected at node.rb
55
58
  set :dump_errors, false # to be injected at node.rb
56
59
  set :version, VERSION # to be injected at node.rb
@@ -59,7 +62,6 @@ module Zold
59
62
  set :reboot, false # to be injected at node.rb
60
63
  set :home, nil? # to be injected at node.rb
61
64
  set :logging, true # to be injected at node.rb
62
- set :log, nil? # to be injected at node.rb
63
65
  set :address, nil? # to be injected at node.rb
64
66
  set :farm, nil? # to be injected at node.rb
65
67
  set :metronome, nil? # to be injected at node.rb
@@ -95,15 +97,14 @@ while #{settings.address} is in '#{settings.network}'"
95
97
  s = Score.parse_text(header)
96
98
  error(400, 'The score is invalid') unless s.valid?
97
99
  error(400, 'The score is weak') if s.strength < Score::STRENGTH && !settings.ignore_score_weakness
98
- if s.value > 3
99
- require_relative '../commands/remote'
100
- cmd = Remote.new(remotes: settings.remotes, log: settings.log)
101
- cmd.run(['remote', 'add', s.host, s.port.to_s, '--force', "--network=#{settings.network}"])
102
- cmd.run(%w[remote trim])
103
- cmd.run(%w[remote select])
104
- else
105
- settings.log.debug("#{request.url}: the score is too weak: #{s}")
100
+ if settings.address == "#{s.host}:#{s.port}" && !settings.ignore_score_weakness
101
+ error(400, 'Self-requests are prohibited')
106
102
  end
103
+ require_relative '../commands/remote'
104
+ cmd = Remote.new(remotes: settings.remotes, log: settings.log)
105
+ cmd.run(['remote', 'add', s.host, s.port.to_s, "--network=#{settings.network}"])
106
+ cmd.run(%w[remote trim])
107
+ cmd.run(%w[remote select])
107
108
  end
108
109
  end
109
110
 
@@ -138,6 +139,11 @@ while #{settings.address} is in '#{settings.network}'"
138
139
  score.to_s
139
140
  end
140
141
 
142
+ get '/trace' do
143
+ content_type 'text/plain'
144
+ settings.trace.to_s
145
+ end
146
+
141
147
  get '/favicon.ico' do
142
148
  if score.value >= 16
143
149
  redirect 'https://www.zold.io/images/logo-green.png'
@@ -175,116 +181,141 @@ while #{settings.address} is in '#{settings.network}'"
175
181
 
176
182
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})} do
177
183
  id = Id.new(params[:id])
178
- wallet = settings.wallets.find(id)
179
- error 404 unless wallet.exists?
180
- content_type 'application/json'
181
- {
182
- version: settings.version,
183
- alias: settings.node_alias,
184
- protocol: settings.protocol,
185
- id: wallet.id.to_s,
186
- score: score.to_h,
187
- wallets: settings.wallets.all.count,
188
- mtime: wallet.mtime.utc.iso8601,
189
- size: File.size(wallet.path),
190
- digest: wallet.digest,
191
- balance: wallet.balance.to_i,
192
- body: AtomicFile.new(wallet.path).read
193
- }.to_json
184
+ settings.wallets.find(id) do |wallet|
185
+ error 404 unless wallet.exists?
186
+ content_type 'application/json'
187
+ {
188
+ version: settings.version,
189
+ alias: settings.node_alias,
190
+ protocol: settings.protocol,
191
+ id: wallet.id.to_s,
192
+ score: score.to_h,
193
+ wallets: settings.wallets.all.count,
194
+ mtime: wallet.mtime.utc.iso8601,
195
+ size: File.size(wallet.path),
196
+ digest: wallet.digest,
197
+ copies: Copies.new(File.join(settings.copies, id)).all.count,
198
+ balance: wallet.balance.to_i,
199
+ body: AtomicFile.new(wallet.path).read
200
+ }.to_json
201
+ end
194
202
  end
195
203
 
196
204
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16}).json} do
197
205
  id = Id.new(params[:id])
198
- wallet = settings.wallets.find(id)
199
- error 404 unless wallet.exists?
200
- content_type 'application/json'
201
- {
202
- version: settings.version,
203
- alias: settings.node_alias,
204
- protocol: settings.protocol,
205
- id: wallet.id.to_s,
206
- score: score.to_h,
207
- wallets: settings.wallets.all.count,
208
- key: wallet.key.to_pub,
209
- mtime: wallet.mtime.utc.iso8601,
210
- digest: wallet.digest,
211
- balance: wallet.balance.to_i,
212
- txns: wallet.txns.count
213
- }.to_json
206
+ settings.wallets.find(id) do |wallet|
207
+ error 404 unless wallet.exists?
208
+ content_type 'application/json'
209
+ {
210
+ version: settings.version,
211
+ alias: settings.node_alias,
212
+ protocol: settings.protocol,
213
+ id: wallet.id.to_s,
214
+ score: score.to_h,
215
+ wallets: settings.wallets.all.count,
216
+ key: wallet.key.to_pub,
217
+ mtime: wallet.mtime.utc.iso8601,
218
+ digest: wallet.digest,
219
+ balance: wallet.balance.to_i,
220
+ txns: wallet.txns.count
221
+ }.to_json
222
+ end
214
223
  end
215
224
 
216
225
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/balance} do
217
226
  id = Id.new(params[:id])
218
- wallet = settings.wallets.find(id)
219
- error 404 unless wallet.exists?
220
- content_type 'text/plain'
221
- wallet.balance.to_i.to_s
227
+ settings.wallets.find(id) do |wallet|
228
+ error 404 unless wallet.exists?
229
+ content_type 'text/plain'
230
+ wallet.balance.to_i.to_s
231
+ end
222
232
  end
223
233
 
224
234
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/key} do
225
235
  id = Id.new(params[:id])
226
- wallet = settings.wallets.find(id)
227
- error 404 unless wallet.exists?
228
- content_type 'text/plain'
229
- wallet.key.to_pub
236
+ settings.wallets.find(id) do |wallet|
237
+ error 404 unless wallet.exists?
238
+ content_type 'text/plain'
239
+ wallet.key.to_pub
240
+ end
230
241
  end
231
242
 
232
243
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/mtime} do
233
244
  id = Id.new(params[:id])
234
- wallet = settings.wallets.find(id)
235
- error 404 unless wallet.exists?
236
- content_type 'text/plain'
237
- wallet.mtime.utc.iso8601.to_s
245
+ settings.wallets.find(id) do |wallet|
246
+ error 404 unless wallet.exists?
247
+ content_type 'text/plain'
248
+ wallet.mtime.utc.iso8601.to_s
249
+ end
238
250
  end
239
251
 
240
252
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/digest} do
241
253
  id = Id.new(params[:id])
242
- wallet = settings.wallets.find(id)
243
- error 404 unless wallet.exists?
244
- content_type 'text/plain'
245
- wallet.digest
254
+ settings.wallets.find(id) do |wallet|
255
+ error 404 unless wallet.exists?
256
+ content_type 'text/plain'
257
+ wallet.digest
258
+ end
246
259
  end
247
260
 
248
261
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})\.txt} do
249
262
  id = Id.new(params[:id])
250
- wallet = settings.wallets.find(id)
251
- error 404 unless wallet.exists?
252
- content_type 'text/plain'
253
- [
254
- wallet.network,
255
- wallet.protocol,
256
- wallet.id.to_s,
257
- wallet.key.to_pub,
258
- '',
259
- wallet.txns.map(&:to_text).join("\n"),
260
- '',
261
- '--',
262
- "Balance: #{wallet.balance.to_zld}",
263
- "Transactions: #{wallet.txns.count}",
264
- "Wallet size: #{File.size(wallet.path)} bytes",
265
- "Modified: #{wallet.mtime.utc.iso8601}",
266
- "Digest: #{wallet.digest}"
267
- ].join("\n")
263
+ settings.wallets.find(id) do |wallet|
264
+ error 404 unless wallet.exists?
265
+ content_type 'text/plain'
266
+ [
267
+ wallet.network,
268
+ wallet.protocol,
269
+ wallet.id.to_s,
270
+ wallet.key.to_pub,
271
+ '',
272
+ wallet.txns.map(&:to_text).join("\n"),
273
+ '',
274
+ '--',
275
+ "Balance: #{wallet.balance.to_zld} ZLD (#{wallet.balance.to_i} zents)",
276
+ "Transactions: #{wallet.txns.count}",
277
+ "File size: #{File.size(wallet.path)} bytes (#{Copies.new(File.join(settings.copies, id)).all.count} copies)",
278
+ "Modified: #{wallet.mtime.utc.iso8601}",
279
+ "Digest: #{wallet.digest}"
280
+ ].join("\n")
281
+ end
268
282
  end
269
283
 
270
284
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})\.bin} do
271
285
  id = Id.new(params[:id])
272
- wallet = settings.wallets.find(id)
273
- error 404 unless wallet.exists?
274
- content_type 'text/plain'
275
- AtomicFile.new(wallet.path).read
286
+ settings.wallets.find(id) do |wallet|
287
+ error 404 unless wallet.exists?
288
+ content_type 'text/plain'
289
+ AtomicFile.new(wallet.path).read
290
+ end
276
291
  end
277
292
 
278
293
  get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/copies} do
279
294
  id = Id.new(params[:id])
280
- wallet = settings.wallets.find(id)
281
- error 404 unless wallet.exists?
282
- content_type 'text/plain'
283
- Copies.new(File.join(settings.copies, id)).all.map do |c|
284
- wallet = Wallet.new(c[:path])
285
- "#{c[:name]}: #{c[:score]} #{wallet.balance}/#{wallet.txns.count}t/\
286
- #{wallet.digest[0, 6]}/#{File.size(c[:path])}b/#{Age.new(File.mtime(c[:path]))}"
287
- end.join("\n")
295
+ settings.wallets.find(id) do |wallet|
296
+ error 404 unless wallet.exists?
297
+ content_type 'text/plain'
298
+ copies = Copies.new(File.join(settings.copies, id))
299
+ copies.load.map { |c| "#{c[:name]}: #{c[:host]}:#{c[:port]} #{c[:score]} #{c[:time]}" }.join("\n") +
300
+ "\n\n" +
301
+ copies.all.map do |c|
302
+ w = Wallet.new(c[:path])
303
+ "#{c[:name]}: #{c[:score]} #{w.balance}/#{w.txns.count}t/\
304
+ #{w.digest[0, 6]}/#{File.size(c[:path])}b/#{Age.new(File.mtime(c[:path]))}"
305
+ end.join("\n")
306
+ end
307
+ end
308
+
309
+ get %r{/wallet/(?<id>[A-Fa-f0-9]{16})/copy/(?<name>[0-9]+)} do
310
+ id = Id.new(params[:id])
311
+ name = params[:name]
312
+ settings.wallets.find(id) do |wallet|
313
+ error 404 unless wallet.exists?
314
+ copy = Copies.new(File.join(settings.copies, id)).all.find { |c| c[:name] == name }
315
+ error 404 if copy.nil?
316
+ content_type 'text/plain'
317
+ File.read(copy[:path])
318
+ end
288
319
  end
289
320
 
290
321
  put %r{/wallet/(?<id>[A-Fa-f0-9]{16})/?} do
@@ -331,7 +362,7 @@ while #{settings.address} is in '#{settings.network}'"
331
362
  error 400 do
332
363
  status 400
333
364
  content_type 'text/plain'
334
- env['sinatra.error'].message
365
+ env['sinatra.error'] ? env['sinatra.error'].message : 'Invalid request'
335
366
  end
336
367
 
337
368
  error do