zold 0.5 → 0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -2
  3. data/bin/zold +34 -48
  4. data/features/cli.feature +1 -1
  5. data/features/step_definitions/steps.rb +1 -3
  6. data/features/support/env.rb +2 -0
  7. data/fixtures/scripts/push-and-pull.sh +6 -4
  8. data/lib/zold/amount.rb +17 -3
  9. data/lib/zold/commands/create.rb +9 -6
  10. data/lib/zold/commands/fetch.rb +11 -21
  11. data/lib/zold/commands/node.rb +7 -9
  12. data/lib/zold/commands/pay.rb +9 -6
  13. data/lib/zold/commands/propagate.rb +4 -5
  14. data/lib/zold/commands/push.rb +10 -5
  15. data/lib/zold/commands/remote.rb +22 -24
  16. data/lib/zold/commands/show.rb +1 -2
  17. data/lib/zold/commands/taxes.rb +154 -0
  18. data/lib/zold/http.rb +1 -3
  19. data/lib/zold/key.rb +1 -3
  20. data/lib/zold/node/entrance.rb +8 -3
  21. data/lib/zold/node/farm.rb +8 -6
  22. data/lib/zold/node/front.rb +0 -1
  23. data/lib/zold/patch.rb +1 -1
  24. data/lib/zold/remotes.rb +4 -0
  25. data/lib/zold/score.rb +85 -10
  26. data/lib/zold/signature.rb +7 -7
  27. data/lib/zold/tax.rb +79 -0
  28. data/lib/zold/txn.rb +12 -7
  29. data/lib/zold/version.rb +1 -1
  30. data/lib/zold/wallet.rb +2 -2
  31. data/test/commands/test_create.rb +3 -4
  32. data/test/commands/test_diff.rb +2 -3
  33. data/test/commands/test_merge.rb +4 -6
  34. data/test/commands/test_pay.rb +7 -5
  35. data/test/commands/test_remote.rb +5 -3
  36. data/test/commands/test_taxes.rb +66 -0
  37. data/test/node/fake_node.rb +1 -0
  38. data/test/node/test_farm.rb +2 -1
  39. data/test/node/test_front.rb +1 -0
  40. data/test/test__helper.rb +2 -0
  41. data/test/test_remotes.rb +0 -1
  42. data/test/test_score.rb +40 -21
  43. data/test/test_signature.rb +6 -3
  44. data/test/test_tax.rb +53 -0
  45. data/test/test_txn.rb +46 -0
  46. data/test/test_wallet.rb +2 -2
  47. data/test/test_zold.rb +1 -1
  48. data/wp/.gitignore +6 -0
  49. data/wp/wp.tex +38 -0
  50. data/zold.gemspec +1 -3
  51. metadata +12 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 28d39fb05a13cc3dc2a6856289b717c137e751dc
4
- data.tar.gz: 42b2b142eb06e8f360b4d2f311bcf8c86aec7395
3
+ metadata.gz: 01a2323174e903dabb35708bfd1d0f756e930035
4
+ data.tar.gz: a4f6582b8e645cd480ab74e374cc5cefe3698034
5
5
  SHA512:
6
- metadata.gz: 63424ca129a20ac442879e53255572e885c952c32d10bc5b5cddaaca33a04d3698cc0c8114f8b79479988295df8c5bbe6f4bbd65a7c7752d1cbc031768272e68
7
- data.tar.gz: f91e1c18246b1dd7f6fe790ac71a43e4cbdf0c2eae9a718229e7cc4004aea419352986f3b1036ff750200ed40be1aed1a21e9c82aba40de67529d091bbf2dbdd
6
+ metadata.gz: 8e6c51f38a8f4d44c86c6cf1d2c02d3db2a1495504613c29676922a91bc03b325524b87d592dabdeb406fc9ca6906725de8666b805ea98731a7fe4271c950123
7
+ data.tar.gz: '0906447919a3f02950152613a65b19d648ca46cf480405659751ab6ab4eee1d08b01f8f1dfef2a60de4c5e55b17433913637daf37c0d7451f13e8320aa33c572'
data/.rubocop.yml CHANGED
@@ -14,7 +14,7 @@ Layout/MultilineMethodCallIndentation:
14
14
  Metrics/AbcSize:
15
15
  Enabled: false
16
16
  Metrics/BlockLength:
17
- Max: 50
17
+ Max: 100
18
18
  Metrics/ClassLength:
19
19
  Max: 200
20
20
  Style/EndOfLine:
@@ -24,4 +24,6 @@ Metrics/ParameterLists:
24
24
  Layout/AlignParameters:
25
25
  Enabled: false
26
26
  Metrics/PerceivedComplexity:
27
- Max: 12
27
+ Max: 15
28
+ Metrics/LineLength:
29
+ Max: 120
data/bin/zold CHANGED
@@ -41,14 +41,16 @@ Encoding.default_internal = Encoding::UTF_8
41
41
  log = Zold::Log.new
42
42
 
43
43
  args = []
44
- config = File.expand_path('~/.zold')
45
- if File.exist?(config)
46
- body = File.read(config)
47
- extra = body.split(/[\r\n]+/).map(&:strip)
48
- args += extra
49
- log.debug("Found #{body.split(/\n/).length} lines in #{config}")
50
- else
51
- log.debug("Default config file #{config} not found")
44
+ unless ENV['RACK_ENV'] == 'test'
45
+ config = File.expand_path('~/.zold')
46
+ if File.exist?(config)
47
+ body = File.read(config)
48
+ extra = body.split(/[\r\n]+/).map(&:strip)
49
+ args += extra
50
+ log.debug("Found #{body.split(/\n/).length} lines in #{config}")
51
+ else
52
+ log.debug("Default config file #{config} not found")
53
+ end
52
54
  end
53
55
  args += ARGV
54
56
 
@@ -56,44 +58,38 @@ begin
56
58
  opts = Slop.parse(args, strict: false, suppress_errors: true) do |o|
57
59
  o.banner = "Usage: zold [options] command [arguments]
58
60
  Available commands:
59
- #{Rainbow('remote').green}
61
+ #{Rainbow('remote').green} command [options]
60
62
  Manage remote nodes
61
- #{Rainbow('create').green}
63
+ #{Rainbow('create').green} [options]
62
64
  Creates a new wallet with a random ID
63
- #{Rainbow('fetch').green} [ID...]
65
+ #{Rainbow('fetch').green} [ID...] [options]
64
66
  Fetch wallet copies from remote nodes
65
- #{Rainbow('clean').green} [ID...]
67
+ #{Rainbow('clean').green} [ID...] [options]
66
68
  Remove expired local copies
67
- #{Rainbow('merge').green} [ID...]
69
+ #{Rainbow('merge').green} [ID...] [options]
68
70
  Merge remote copies with the HEAD
69
- #{Rainbow('propagate').green} [ID...]
71
+ #{Rainbow('propagate').green} [ID...] [options]
70
72
  Propagate transactions to receiving wallets
71
- #{Rainbow('pull').green} [ID...]
73
+ #{Rainbow('pull').green} [ID...] [options]
72
74
  Fetch and then merge
73
- #{Rainbow('show').green} [ID...]
75
+ #{Rainbow('show').green} [ID...] [options]
74
76
  Show all available information about the wallet
75
- #{Rainbow('pay').green} from to amount details
77
+ #{Rainbow('pay').green} from to amount details [options]
76
78
  Pay ZOLD from one wallet to another
77
- #{Rainbow('invoice').green} ID
79
+ #{Rainbow('invoice').green} ID [options]
78
80
  Generate invoice unique ID for a payment
79
- #{Rainbow('status').green}
80
- Show status of local copies
81
- #{Rainbow('push').green} [ID...]
81
+ #{Rainbow('push').green} [ID...] [options]
82
82
  Push all/some local wallets or the ones required
83
- #{Rainbow('node').green} port
83
+ #{Rainbow('taxes').green} command [ID...] [options]
84
+ Pay taxes, check their status
85
+ #{Rainbow('node').green} [options]
84
86
  Run node at the given TCP port
85
- #{Rainbow('score').green} host port strength
87
+ #{Rainbow('score').green} host port strength [options]
86
88
  Generate score for the given host and port
87
89
  Available options:"
88
90
  o.string '-d', '--dir',
89
91
  'The directory where wallets are stored (default: .)',
90
92
  default: Dir.pwd
91
- o.string '--private-key',
92
- 'The location of RSA private key (default: ~/.ssh/id_rsa)',
93
- default: '~/.ssh/id_rsa'
94
- o.string '--public-key',
95
- 'The location of RSA public key (default: ~/.ssh/id_rsa.pub)',
96
- default: '~/.ssh/id_rsa.pub'
97
93
  o.bool '-h', '--help', 'Show these instructions'
98
94
  o.bool '--trace', 'Show full stack trace in case of a problem'
99
95
  o.on '--no-colors', 'Disable colors in the ouput' do
@@ -129,11 +125,7 @@ Available options:"
129
125
  Zold::Node.new(log: log).run(args)
130
126
  when 'create'
131
127
  require_relative '../lib/zold/commands/create'
132
- Zold::Create.new(
133
- wallets: wallets,
134
- pubkey: Zold::Key.new(file: opts['public-key']),
135
- log: log
136
- ).run(args)
128
+ Zold::Create.new(wallets: wallets, log: log).run(args)
137
129
  when 'remote'
138
130
  require_relative '../lib/zold/commands/remote'
139
131
  Zold::Remote.new(remotes: remotes, log: log).run(args)
@@ -142,11 +134,7 @@ Available options:"
142
134
  Zold::Invoice.new(wallets: wallets, log: log).run(args)
143
135
  when 'pay'
144
136
  require_relative '../lib/zold/commands/pay'
145
- Zold::Pay.new(
146
- wallets: wallets,
147
- pvtkey: Zold::Key.new(file: opts['private-key']),
148
- log: log
149
- ).run(args)
137
+ Zold::Pay.new(wallets: wallets, log: log).run(args)
150
138
  when 'show'
151
139
  require_relative '../lib/zold/commands/show'
152
140
  Zold::Show.new(wallets: wallets, log: log).run(args)
@@ -170,23 +158,21 @@ Available options:"
170
158
  Zold::Fetch.new(remotes: remotes, copies: copies, log: log).run(args)
171
159
  require_relative '../lib/zold/commands/merge'
172
160
  Zold::Merge.new(wallets: wallets, copies: copies, log: log).run(args)
161
+ when 'taxes'
162
+ require_relative '../lib/zold/commands/taxes'
163
+ Zold::Taxes.new(wallets: wallets, log: log).run(args)
173
164
  when 'push'
174
165
  require_relative '../lib/zold/commands/push'
175
166
  Zold::Push.new(wallets: wallets, remotes: remotes, log: log).run(args)
176
167
  when 'score'
177
168
  require_relative '../lib/zold/score'
178
- if args.length != 3
179
- raise 'Exactly three args required: host, port, strength'
180
- end
169
+ raise 'Exactly four args required: host, port, invoice, strength' if args.length != 4
181
170
  host = args[0]
182
- raise "Invalid host name: #{host}" unless host =~ /[a-z0-9\.-]+/
183
171
  port = args[1].to_i
184
- raise "Invalid TCP port: #{port}" if port <= 0 || port > 65535
185
- strength = args[2].to_i
172
+ invoice = args[2]
173
+ strength = args[3].to_i
186
174
  raise "Invalid strength: #{strength}" if strength <= 0 || strength > 8
187
- score = Zold::Score.new(
188
- Time.now, host, port, strength: strength
189
- )
175
+ score = Zold::Score.new(Time.now, host, port, invoice, strength: strength)
190
176
  loop do
191
177
  log.info(score.to_s)
192
178
  score = score.next
data/features/cli.feature CHANGED
@@ -12,6 +12,6 @@ Feature: Command Line Processing
12
12
  Then Exit code is zero
13
13
 
14
14
  Scenario: Wallet can be created
15
- When I run bin/zold with " --private-key id_rsa --public-key id_rsa.pub --trace create"
15
+ When I run bin/zold with "--trace create --public-key=id_rsa.pub"
16
16
  Then Exit code is zero
17
17
 
@@ -44,9 +44,7 @@ When(%r{^I run bin/zold with "([^"]*)"$}) do |arg|
44
44
  end
45
45
 
46
46
  Then(/^Stdout contains "([^"]*)"$/) do |txt|
47
- unless @stdout.include?(txt)
48
- raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}"
49
- end
47
+ raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
50
48
  end
51
49
 
52
50
  Then(/^Stdout is empty$/) do
@@ -18,5 +18,7 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
19
  # SOFTWARE.
20
20
 
21
+ ENV['RACK_ENV'] = 'test'
22
+
21
23
  require 'simplecov'
22
24
  require_relative '../../lib/zold'
@@ -9,7 +9,9 @@ port=`python -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.gets
9
9
 
10
10
  mkdir server
11
11
  cd server
12
- zold --trace node --host=localhost --port=${port} --bind-port=${port} --threads=0 --standalone &
12
+ zold --trace node --invoice=NOPREFIX@ffffffffffffffff \
13
+ --host=localhost --port=${port} --bind-port=${port} \
14
+ --threads=0 --standalone &
13
15
  pid=$!
14
16
  trap "kill -9 $pid" EXIT
15
17
  cd ..
@@ -23,10 +25,10 @@ zold --trace remote clean
23
25
  zold --trace remote add localhost ${port}
24
26
  zold --trace remote show
25
27
 
26
- zold --trace --public-key id_rsa.pub create 0000000000000000
27
- target=`zold --public-key id_rsa.pub create`
28
+ zold --trace create --public-key=id_rsa.pub 0000000000000000
29
+ target=`zold create --public-key=id_rsa.pub`
28
30
  invoice=`zold invoice ${target}`
29
- zold --trace --private-key id_rsa pay 0000000000000000 ${invoice} 14.99 'To save the world!'
31
+ zold --trace pay --private-key=id_rsa 0000000000000000 ${invoice} 14.99 'To save the world!'
30
32
  zold --trace propagate 0000000000000000
31
33
  zold --trace show
32
34
  zold --trace show 0000000000000000
data/lib/zold/amount.rb CHANGED
@@ -29,9 +29,9 @@ module Zold
29
29
  class Amount
30
30
  def initialize(coins: nil, zld: nil)
31
31
  raise 'You can\'t specify both coints and zld' if !coins.nil? && !zld.nil?
32
+ raise "Integer is required, while #{coins.class} provided: #{coins}" unless coins.nil? || coins.is_a?(Integer)
32
33
  @coins = coins unless coins.nil?
33
34
  @coins = (zld * 2**24).to_i unless zld.nil?
34
- raise "Integer is required: #{@coins.class}" unless @coins.is_a?(Integer)
35
35
  end
36
36
 
37
37
  ZERO = Amount.new(coins: 0)
@@ -54,21 +54,35 @@ module Zold
54
54
  end
55
55
 
56
56
  def ==(other)
57
+ raise '== may only work with Amount' unless other.is_a?(Amount)
57
58
  @coins == other.to_i
58
59
  end
59
60
 
60
61
  def >(other)
62
+ raise '> may only work with Amount' unless other.is_a?(Amount)
61
63
  @coins > other.to_i
62
64
  end
63
65
 
64
66
  def <(other)
67
+ raise '< may only work with Amount' unless other.is_a?(Amount)
65
68
  @coins < other.to_i
66
69
  end
67
70
 
71
+ def <=(other)
72
+ raise '<= may only work with Amount' unless other.is_a?(Amount)
73
+ @coins <= other.to_i
74
+ end
75
+
68
76
  def +(other)
77
+ raise '+ may only work with Amount' unless other.is_a?(Amount)
69
78
  Amount.new(coins: @coins + other.to_i)
70
79
  end
71
80
 
81
+ def -(other)
82
+ raise '- may only work with Amount' unless other.is_a?(Amount)
83
+ Amount.new(coins: @coins - other.to_i)
84
+ end
85
+
72
86
  def zero?
73
87
  @coins.zero?
74
88
  end
@@ -77,8 +91,8 @@ module Zold
77
91
  @coins < 0
78
92
  end
79
93
 
80
- def mul(m)
81
- c = @coins * m
94
+ def *(other)
95
+ c = (@coins * other).to_i
82
96
  raise "Overflow, can't multiply #{@coins} by #{m}" if c > 2**63
83
97
  Amount.new(coins: c)
84
98
  end
@@ -29,9 +29,8 @@ require_relative '../id'
29
29
  module Zold
30
30
  # Create command
31
31
  class Create
32
- def initialize(wallets:, pubkey:, log: Log::Quiet.new)
32
+ def initialize(wallets:, log: Log::Quiet.new)
33
33
  @wallets = wallets
34
- @pubkey = pubkey
35
34
  @log = log
36
35
  end
37
36
 
@@ -39,6 +38,10 @@ module Zold
39
38
  opts = Slop.parse(args, help: true) do |o|
40
39
  o.banner = "Usage: zold create [options]
41
40
  Available options:"
41
+ o.string '--public-key',
42
+ 'The location of RSA public key (default: ~/.ssh/id_rsa.pub)',
43
+ require: true,
44
+ default: '~/.ssh/id_rsa.pub'
42
45
  o.bool '--help', 'Print instructions'
43
46
  end
44
47
  if opts.help?
@@ -48,12 +51,12 @@ Available options:"
48
51
  create(opts.arguments.empty? ? Id.new : Id.new(opts.arguments[0]), opts)
49
52
  end
50
53
 
51
- def create(id, _)
54
+ def create(id, opts)
52
55
  wallet = @wallets.find(id)
53
- wallet.init(id, @pubkey)
56
+ key = Zold::Key.new(file: opts['public-key'])
57
+ wallet.init(id, key)
54
58
  @log.info(wallet.id)
55
- @log.debug("Wallet #{Rainbow(wallet).green} \
56
- created at #{@wallets.path}")
59
+ @log.debug("Wallet #{Rainbow(wallet).green} created at #{@wallets.path}")
57
60
  wallet
58
61
  end
59
62
  end
@@ -67,8 +67,7 @@ Available options:"
67
67
  @remotes.all.each do |r|
68
68
  total += 1 if fetch_one(id, r, cps, opts)
69
69
  end
70
- @log.debug("#{total} copies fetched, \
71
- there are #{cps.all.count} available locally")
70
+ @log.debug("#{total} copies fetched, there are #{cps.all.count} available locally")
72
71
  end
73
72
 
74
73
  def fetch_one(id, r, cps, opts)
@@ -80,34 +79,25 @@ there are #{cps.all.count} available locally")
80
79
  uri = URI("#{r[:home]}wallet/#{id}")
81
80
  res = Http.new(uri).get
82
81
  unless res.code == '200'
83
- @log.error(
84
- "#{address} #{Rainbow(res.code).red}/#{res.message} at #{uri}"
85
- )
82
+ @log.error("#{address} #{Rainbow(res.code).red}/#{res.message} at #{uri}")
86
83
  return false
87
84
  end
88
85
  json = JSON.parse(res.body)
89
- score = Score.new(
90
- Time.parse(json['score']['time']),
91
- r[:host],
92
- r[:port],
93
- json['score']['suffixes']
94
- )
86
+ score = Score.parse_json(json['score'])
95
87
  unless score.valid?
96
- @log.error("#{address} invalid score")
88
+ @log.error("#{address}: invalid score")
89
+ return false
90
+ end
91
+ if score.expired?
92
+ @log.error("#{address}: score expired")
97
93
  return false
98
94
  end
99
95
  if score.strength < Score::STRENGTH && !opts['ignore-score-weakness']
100
- @log.error(
101
- "#{address} score is too weak: #{score.strength} \
102
- (<#{Score::STRENGTH})"
103
- )
96
+ @log.error("#{address} score is too weak: #{score.strength} (<#{Score::STRENGTH})")
104
97
  return false
105
98
  end
106
- cps.add(json['body'], r[:host], r[:port], score.value)
107
- @log.info(
108
- "#{address} #{json['body'].length}b/\
109
- #{Rainbow(score.value).green} (v.#{json['version']})"
110
- )
99
+ cps.add(json['body'], score.host, score.port, score.value)
100
+ @log.info("#{address} #{json['body'].length}b/#{Rainbow(score.value).green} (v.#{json['version']})")
111
101
  true
112
102
  end
113
103
  end
@@ -40,6 +40,8 @@ module Zold
40
40
  def run(args = [])
41
41
  opts = Slop.parse(args, help: true) do |o|
42
42
  o.banner = 'Usage: zold node [options]'
43
+ o.string '--invoice',
44
+ 'The invoice you want to collect money to'
43
45
  o.integer '--port',
44
46
  "TCP port to open for the Net (default: #{Remotes::PORT})",
45
47
  default: Remotes::PORT
@@ -65,6 +67,7 @@ module Zold
65
67
  @log.info(opts.to_s)
66
68
  return
67
69
  end
70
+ raise '--invoice is mandatory' unless opts[:invoice]
68
71
  Zold::Front.set(:log, @log)
69
72
  Zold::Front.set(:logging, @log.debug?)
70
73
  FileUtils.mkdir_p(opts[:home])
@@ -75,18 +78,13 @@ module Zold
75
78
  AccessLog: []
76
79
  )
77
80
  if opts['standalone']
78
- Zold::Front.set(:remotes, Remotes::Empty.new)
81
+ remotes = Remotes::Empty.new
82
+ @log.debug('Running in standalone mode!')
79
83
  else
80
- Zold::Front.set(
81
- :remotes,
82
- Remotes.new(
83
- File.join(opts[:home], '.zoldata/remotes')
84
- )
85
- )
84
+ remotes = Remotes.new(File.join(opts[:home], 'zold-remotes'))
86
85
  end
87
86
  wallets = Wallets.new(File.join(opts[:home], 'zold-wallets'))
88
87
  Zold::Front.set(:wallets, wallets)
89
- remotes = Remotes.new(File.join(opts[:home], 'zold-remotes'))
90
88
  Zold::Front.set(:remotes, remotes)
91
89
  copies = File.join(opts[:home], 'zold-copies')
92
90
  Zold::Front.set(:copies, copies)
@@ -96,7 +94,7 @@ module Zold
96
94
  :entrance, Entrance.new(wallets, remotes, copies, address, log: @log)
97
95
  )
98
96
  Zold::Front.set(:port, opts['bind-port'])
99
- farm = Farm.new(log: @log)
97
+ farm = Farm.new(opts[:invoice], log: @log)
100
98
  farm.start(
101
99
  opts[:host], opts[:port],
102
100
  threads: opts[:threads], strength: opts[:strength]
@@ -28,9 +28,8 @@ require_relative '../log'
28
28
  module Zold
29
29
  # Money sending command
30
30
  class Pay
31
- def initialize(wallets:, pvtkey:, log: Log::Quiet.new)
31
+ def initialize(wallets:, log: Log::Quiet.new)
32
32
  @wallets = wallets
33
- @pvtkey = pvtkey
34
33
  @log = log
35
34
  end
36
35
 
@@ -43,6 +42,10 @@ Where:
43
42
  'amount' is the amount to pay, in ZLD, for example '14.95'
44
43
  'details' is the optional text to attach to the payment
45
44
  Available options:"
45
+ o.string '--private-key',
46
+ 'The location of RSA private key (default: ~/.ssh/id_rsa)',
47
+ require: true,
48
+ default: '~/.ssh/id_rsa'
46
49
  o.bool '--force',
47
50
  'Ignore all validations',
48
51
  default: false
@@ -68,12 +71,12 @@ Available options:"
68
71
  raise 'The amount can\'t be zero' if amount.zero?
69
72
  raise "The amount can't be negative: #{amount}" if amount.negative?
70
73
  if !from.root? && from.balance < @amount
71
- raise "There is not enough funds in #{from} to send #{amount}, \
72
- only #{payer.balance} left"
74
+ raise "There is not enough funds in #{from} to send #{amount}, only #{payer.balance} left"
73
75
  end
74
76
  end
75
- txn = from.sub(amount, invoice, @pvtkey, details)
76
- @log.debug("#{amount} sent from #{from} to #{invoice}: #{details}")
77
+ key = Zold::Key.new(file: opts['private-key'])
78
+ txn = from.sub(amount, invoice, key, details)
79
+ @log.debug("#{amount} sent from #{from} to #{txn.bnf}: #{details}")
77
80
  @log.info(txn.id)
78
81
  txn
79
82
  end