xrbp 0.2.1 → 0.2.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/examples/nodestore1.rb +12 -7
  3. data/lib/xrbp/core_ext.rb +27 -0
  4. data/lib/xrbp/crypto/account.rb +28 -3
  5. data/lib/xrbp/nodestore.rb +6 -0
  6. data/lib/xrbp/nodestore/amendments.rb +13 -0
  7. data/lib/xrbp/nodestore/backends/decompressor.rb +8 -6
  8. data/lib/xrbp/nodestore/backends/nudb.rb +2 -0
  9. data/lib/xrbp/nodestore/backends/rocksdb.rb +1 -0
  10. data/lib/xrbp/nodestore/db.rb +5 -387
  11. data/lib/xrbp/nodestore/fees.rb +19 -0
  12. data/lib/xrbp/nodestore/format.rb +72 -1
  13. data/lib/xrbp/nodestore/ledger.rb +272 -0
  14. data/lib/xrbp/nodestore/parser.rb +407 -0
  15. data/lib/xrbp/nodestore/protocol.rb +5 -0
  16. data/lib/xrbp/nodestore/protocol/currency.rb +11 -0
  17. data/lib/xrbp/nodestore/protocol/indexes.rb +109 -0
  18. data/lib/xrbp/nodestore/protocol/issue.rb +26 -0
  19. data/lib/xrbp/nodestore/protocol/quality.rb +10 -0
  20. data/lib/xrbp/nodestore/protocol/rate.rb +21 -0
  21. data/lib/xrbp/nodestore/shamap.rb +447 -0
  22. data/lib/xrbp/nodestore/shamap/errors.rb +8 -0
  23. data/lib/xrbp/nodestore/shamap/inner_node.rb +98 -0
  24. data/lib/xrbp/nodestore/shamap/item.rb +14 -0
  25. data/lib/xrbp/nodestore/shamap/node.rb +49 -0
  26. data/lib/xrbp/nodestore/shamap/node_factory.rb +120 -0
  27. data/lib/xrbp/nodestore/shamap/node_id.rb +83 -0
  28. data/lib/xrbp/nodestore/shamap/tagged_cache.rb +20 -0
  29. data/lib/xrbp/nodestore/shamap/tree_node.rb +21 -0
  30. data/lib/xrbp/nodestore/sle.rb +4 -0
  31. data/lib/xrbp/nodestore/sle/st_account.rb +8 -0
  32. data/lib/xrbp/nodestore/sle/st_amount.rb +226 -0
  33. data/lib/xrbp/nodestore/sle/st_ledger_entry.rb +24 -0
  34. data/lib/xrbp/nodestore/sle/st_object.rb +46 -0
  35. data/lib/xrbp/nodestore/sqldb.rb +23 -0
  36. data/lib/xrbp/nodestore/uint.rb +7 -0
  37. data/lib/xrbp/version.rb +1 -1
  38. data/spec/xrbp/nodestore/backends/nudb_spec.rb +3 -1
  39. data/spec/xrbp/nodestore/backends/rocksdb_spec.rb +1 -1
  40. data/spec/xrbp/nodestore/{backends/db_parser.rb → db_parser.rb} +2 -2
  41. data/spec/xrbp/nodestore/ledger_access.rb +17 -0
  42. metadata +30 -3
@@ -0,0 +1,19 @@
1
+ module XRBP
2
+ module NodeStore
3
+ class Fees
4
+ # FIXME where do these get updated in rippled?
5
+ attr_reader :base, :units, :reserve, :increment
6
+
7
+ def initialize
8
+ @base = 0
9
+ @units = 0
10
+ @reserve = 0
11
+ @increment = 0
12
+ end
13
+
14
+ def account_reserve(owner_count)
15
+ STAmount.new :mantissa => reserve + owner_count + increment
16
+ end
17
+ end # class Fees
18
+ end # module NodeStore
19
+ end # module XRBP
@@ -13,7 +13,7 @@ module XRBP
13
13
 
14
14
  NODE_TYPE_CODES = NODE_TYPES.invert
15
15
 
16
- HASH_PREFIXES = { # ASCII value:
16
+ HASH_PREFIX_CODES = { # ASCII value:
17
17
  "54584E00" => :tx_id, # TXN
18
18
  "534E4400" => :tx_node, # SND
19
19
  "4D4C4E00" => :leaf_node, # MLN
@@ -29,6 +29,8 @@ module XRBP
29
29
  # :paychan_claim # CLM
30
30
  }
31
31
 
32
+ HASH_PREFIXES = HASH_PREFIX_CODES.invert
33
+
32
34
  ###
33
35
 
34
36
  SERIALIZED_TYPES = {
@@ -48,6 +50,38 @@ module XRBP
48
50
  19 => :vector256
49
51
  }
50
52
 
53
+ SERIALIZED_FLAGS = {
54
+ # ltACCOUNT_ROOT
55
+ :password_spent => 0x00010000, # True, if password set fee is spent.
56
+ :require_dest_tag => 0x00020000, # True, to require a DestinationTag for payments.
57
+ :require_auth => 0x00040000, # True, to require a authorization to hold IOUs.
58
+ :disallow_xrp => 0x00080000, # True, to disallow sending XRP.
59
+ :disable_master => 0x00100000, # True, force regular key
60
+ :no_freeze => 0x00200000, # True, cannot freeze ripple states
61
+ :global_freeze => 0x00400000, # True, all assets frozen
62
+ :default_ripple => 0x00800000, # True, trust lines allow rippling by default
63
+ :deposit_auth => 0x01000000, # True, all deposits require authorization
64
+
65
+ # ltOFFER
66
+ :passive => 0x00010000,
67
+ :sell => 0x00020000, # True, offer was placed as a sell.
68
+
69
+ # ltRIPPLE_STATE
70
+ :low_reserve => 0x00010000, # True, if entry counts toward reserve.
71
+ :high_reserve => 0x00020000,
72
+ :low_auth => 0x00040000,
73
+ :high_auth => 0x00080000,
74
+ :low_no_ripple => 0x00100000,
75
+ :high_no_ripple => 0x00200000,
76
+ :low_freeze => 0x00400000, # True, low side has set freeze flag
77
+ :high_freeze => 0x00800000, # True, high side has set freeze flag
78
+
79
+ # ltSIGNER_LIST
80
+ :one_owner_count => 0x00010000, # True, uses only one OwnerCount
81
+ }
82
+
83
+ ###
84
+
51
85
  ENCODINGS = {
52
86
  # 16-bit unsigned integers (common)
53
87
  [:uint16, 1] => :ledger_entry_type,
@@ -232,6 +266,8 @@ module XRBP
232
266
  [:vector256, 3] => :amendments,
233
267
  }
234
268
 
269
+ ENCODING_TYPES = ENCODINGS.invert
270
+
235
271
  ###
236
272
 
237
273
  TYPE_INFER = Bistro.new([
@@ -319,6 +355,33 @@ module XRBP
319
355
 
320
356
  ###
321
357
 
358
+ LEDGER_NAMESPACE = {
359
+ :account => 'a',
360
+ :dir_node => 'd',
361
+ :generator => 'g',
362
+ :ripple => 'r',
363
+ :offer => 'o',
364
+ :owner_dir => 'O',
365
+ :book_dir => 'B',
366
+ :contract => 'c',
367
+ :skip_list => 's',
368
+ :escrow => 'u',
369
+ :amendment => 'f',
370
+ :fee => 'e',
371
+ :ticket => 'T',
372
+ :signer_list => 'S',
373
+ :xrp_uchannel => 'x',
374
+ :check => 'C',
375
+ :deposit_preauth => 'p',
376
+
377
+ # usused (according to rippled docs)
378
+ :nickname => 'n'
379
+ }
380
+
381
+ LEDGER_NAMESPACE_CODES = LEDGER_NAMESPACE.invert
382
+
383
+ ###
384
+
322
385
  TX_TYPES = {
323
386
  -1 => :invalid,
324
387
  0 => :payment,
@@ -356,6 +419,14 @@ module XRBP
356
419
  'C3', 'iso_code',
357
420
  'C5', 'reserved2'
358
421
  ])
422
+
423
+ def self.encode_currency(iso_code)
424
+ return ([0] * 20).pack("C*") if iso_code == 'XRP'
425
+
426
+ ([0] * 12).pack("C*") +
427
+ iso_code.upcase +
428
+ ([0] * 5).pack("C*")
429
+ end
359
430
  end # module Format
360
431
  end # module NodeStore
361
432
  end # module XRBP
@@ -0,0 +1,272 @@
1
+ require_relative './amendments'
2
+ require_relative './fees'
3
+ require_relative './parser'
4
+
5
+
6
+ module XRBP
7
+ module NodeStore
8
+ class Ledger
9
+ include Amendments
10
+ include Parser
11
+
12
+ def initialize(args={})
13
+ @db = args[:db]
14
+ @hash = args[:hash]
15
+
16
+ if @hash
17
+ state_map.fetch_root [info["account_hash"]].pack("H*")
18
+ tx_map.fetch_root [info["tx_hash"]].pack("H*")
19
+ end
20
+ end
21
+
22
+ def txs
23
+ @txs ||= tx_map.collect { |tx| parse_tx_inner(tx.data) }
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :db, :hash
29
+
30
+ def state_map
31
+ @state_map ||= SHAMap.new :db => db
32
+ end
33
+
34
+ def tx_map
35
+ @tx_map ||= SHAMap.new :db => db
36
+ end
37
+
38
+ def info
39
+ @info ||= db.ledger(hash)
40
+ end
41
+
42
+ def fees
43
+ @fees ||= Fees.new
44
+ end
45
+
46
+ def global_frozen?(account)
47
+ return false if account == Crypto.xrp_account
48
+ sle = state_map.read(Indexes::account(account))
49
+ return sle && sle.flag?(:global_freeze)
50
+ end
51
+
52
+ def frozen?(account, iou)
53
+ return false if iou[:currency] == 'XRP'
54
+
55
+ sle = state_map.read(Indexes::account(iou[:account]))
56
+ return true if sle && sle.flag?(:global_freeze)
57
+
58
+ return false if iou[:account] == account
59
+
60
+ sle = state_map.read(Indexes::line(account, iou))
61
+ sle && sle.flag?(iou[:account] > account ? :high_freeze :
62
+ :low_freeze)
63
+ end
64
+
65
+ def account_holds(owner_id, iou)
66
+ return xrp_liquid(owner_id, 0) if iou[:currency] == 'XRP'
67
+ sle = state_map.read(Indexes::line(owner_id, iou))
68
+ return STAmount.zero if !sle || frozen?(owner_id, iou)
69
+
70
+ amount = sle.amount(:balance)
71
+ amount.negate! if owner_id > iou[:account]
72
+ balance_hook(amount)
73
+ end
74
+
75
+ def xrp_liquid(account, owner_count_adj)
76
+ sle = state_map.read(Indexes::account(account))
77
+ return STAmount.zero unless sle
78
+
79
+ if fix1141? info['parent_close_time']
80
+ owner_count = confine_owner_account(owner_count_hook(
81
+ sle.field(:uint32, :owner_count)),
82
+ owner_count_adj)
83
+
84
+ reserve = fees.account_reserve(owner_count)
85
+ full_balance = sle.amount(:balance)
86
+ balance = balance_hook(full_balance)
87
+ amount = balance - reserve
88
+ return STAmount.zero if balance < reserve
89
+ return amount
90
+
91
+ else
92
+ owner_count = confine_owner_account(sle.field(:uint32, :owner_count),
93
+ owner_count_adj)
94
+ reserve = fees.account_reserve(sle.field(:uint32, :owner_count))
95
+ full_balance = sle.amount(:balance)
96
+ amount = balance - reserve
97
+ return STAmount.zero if balance < reserve
98
+ return balance_hook(amount)
99
+ end
100
+ end
101
+
102
+ def confine_owner_account(current, adjustment)
103
+ adjusted = current + adjustment
104
+ if adjustment > 0
105
+ # XXX: std::numeric_limits<std::uint32_t>::max
106
+ adjusted = 2**32-1 if adjusted < current
107
+ else
108
+ adjusted = 0 if adjusted > current
109
+ end
110
+
111
+ adjusted
112
+ end
113
+
114
+ def balance_hook(amount)
115
+ # TODO currently implementing ReadView::balanceHook,
116
+ # implement PaymentSandbox::balanceHook?
117
+ amount
118
+ end
119
+
120
+ def owner_count_hook(count)
121
+ # Same PaymentSandbox TODO as in balance_hook above
122
+ count
123
+ end
124
+
125
+ def transfer_rate(issuer)
126
+ sle = state_map.read(Indexes::account(issuer))
127
+ return Rate.new sle.field(:uint32,
128
+ :transfer_rate) if sle &&
129
+ sle.field?(:transfer_rate)
130
+ Rate.parity
131
+ end
132
+
133
+ ###
134
+
135
+ public
136
+
137
+ def order_book(input, output)
138
+ offers = []
139
+
140
+ # Start at order book index
141
+ # Stop after max order book quality
142
+ tip_index = Indexes::order_book(input, output)
143
+ book_end = Indexes::get_quality_next(tip_index)
144
+
145
+ global_freeze = global_frozen?(output[:account]) ||
146
+ global_frozen?(input[:account])
147
+
148
+ rate = transfer_rate(output[:account])
149
+
150
+ balances = {}
151
+ done = false # set true when we cannot traverse anymore
152
+ direct = true # set true when we need to find next dir
153
+ offer_dir = nil # current directory being travred
154
+ dir_rate = nil # current directory quality
155
+ offer_index = nil # index of current offer being processed
156
+ book_entry = nil # index of next offer directory record
157
+ until done
158
+ if direct
159
+ direct = false
160
+ # Return first index after tip
161
+ ledger_index = state_map.succ(tip_index, book_end)
162
+ if ledger_index
163
+ # retrieve offer_dir SLE from db
164
+ offer_dir = state_map.read(ledger_index)
165
+ else
166
+ offer_dir = nil
167
+ end
168
+
169
+ if !offer_dir
170
+ done = true
171
+ else
172
+ # Set new tip, get first offer at new tip
173
+ tip_index = offer_dir.key
174
+ dir_rate = STAmount.from_quality(Indexes::get_quality(tip_index))
175
+ offer_index, offer_dir, book_entry = state_map.cdir_first(tip_index)
176
+ end
177
+ end
178
+
179
+ if !done
180
+ # Read offer from db and process
181
+ sle_offer = state_map.read(offer_index)
182
+ if sle_offer
183
+ owner_id = sle_offer.account_id(:account)
184
+ taker_gets = sle_offer.amount(:taker_gets)
185
+ taker_pays = sle_offer.amount(:taker_pays)
186
+
187
+ owner_funds = nil
188
+ first_owner_offer = true
189
+
190
+ if output[:account] == owner_id
191
+ # issuer is offering it's own IOU, fully funded
192
+ owner_funds = taker_gets
193
+
194
+ elsif global_freeze
195
+ # all offers not ours are unfunded
196
+ owner_funds.clear(output)
197
+
198
+ else
199
+ if balances[owner_id]
200
+ owner_funds = balances[owner_id]
201
+ first_owner_offer = false
202
+
203
+ else
204
+ # did not find balance in table
205
+ owner_funds = account_holds(owner_id, output)
206
+
207
+ # treat negative funds as zero
208
+ owner_funds.clear if owner_funds < STAmount.zero
209
+ end
210
+ end
211
+
212
+ offer = Hash[sle_offer.fields]
213
+ taker_gets_funded = nil
214
+ owner_funds_limit = owner_funds
215
+ offer_rate = Rate.parity
216
+
217
+ if rate != Rate.parity && # transfer fee
218
+ # TODO: provide support for 'taker_id' rpc param:
219
+ #taker_id != output[:account] && # not taking offers of own IOUs
220
+ output[:account] != owner_id # offer owner not issuing own funds
221
+ # Need to charge a transfer fee to offer owner.
222
+ offer_rate = rate
223
+ owner_funds_limit = owner_funds / offer_rate.rate
224
+ end
225
+
226
+
227
+ if owner_funds_limit >= taker_gets
228
+ # Sufficient funds no shenanigans.
229
+ taker_gets_funded = taker_gets
230
+
231
+ else
232
+ # Only provide, if not fully funded.
233
+ taker_gets_funded = owner_funds_limit
234
+ offer[:taker_gets_funded] = taker_gets_funded
235
+ offer[:taker_pays_funded] = [taker_pays,
236
+ taker_gets_funded *
237
+ dir_rate].min
238
+
239
+ # XXX: done in multiply operation in rippled
240
+ offer[:taker_pays_funded].issue = taker_pays.issue
241
+ end
242
+
243
+ owner_pays = (Rate.parity == offer_rate) ?
244
+ taker_gets_funded :
245
+ [owner_funds,
246
+ taker_gets_funded * offer_rate].min
247
+
248
+ balances[owner_id] = owner_funds - owner_pays
249
+
250
+ # include all offers funded and unfunded
251
+ offer[:quality] = dir_rate
252
+ offer[:owner_funds] = owner_funds if first_owner_offer
253
+ offers << offer
254
+
255
+ else
256
+ puts "missing offer"
257
+ end
258
+
259
+ # Retrieve next offer in offer_dir,
260
+ # updating offer_index, offer_dir, book_entry appropriately
261
+ offer_index, offer_dir, book_entry = *state_map.cdir_next(tip_index, offer_dir, book_entry)
262
+
263
+ # if next offer not retrieved find next record after tip
264
+ direct = true if !offer_index
265
+ end
266
+ end
267
+
268
+ return offers
269
+ end
270
+ end # class Ledger
271
+ end # module NodeStore
272
+ end # module XRBP
@@ -0,0 +1,407 @@
1
+ module XRBP
2
+ module NodeStore
3
+ module Parser
4
+ protected
5
+
6
+ # Parsers binary ledger representation into structured ledger.
7
+ #
8
+ # @protected
9
+ def parse_ledger(ledger)
10
+ obj = Format::LEDGER.decode(ledger)
11
+ obj['close_time'] = XRBP::from_xrp_time(obj['close_time']).utc
12
+ obj['parent_close_time'] = XRBP::from_xrp_time(obj['parent_close_time']).utc
13
+ obj['parent_hash'].upcase!
14
+ obj['tx_hash'].upcase!
15
+ obj['account_hash'].upcase!
16
+ obj
17
+ end
18
+
19
+ # Certain data types are prefixed with an 'encoding' header
20
+ # consisting of a field and/or type. Field, type, and remaining
21
+ # bytes are returned
22
+ #
23
+ # @protected
24
+ def parse_encoding(encoding)
25
+ enc = encoding.unpack("C").first
26
+ type = enc >> 4
27
+ field = enc & 0xF
28
+ encoding = encoding[1..-1]
29
+
30
+ if type == 0
31
+ type = encoding.unpack("C").first
32
+ encoding = encoding[1..-1]
33
+ end
34
+
35
+ if field == 0
36
+ field = encoding.unpack("C").first
37
+ encoding = encoding[1..-1]
38
+ end
39
+
40
+ type = Format::SERIALIZED_TYPES[type]
41
+ [[type, field], encoding]
42
+ end
43
+
44
+ # Parses binary ledger entry into hash. Data returned
45
+ # in hash includes ledger entry type prefix, index,
46
+ # and array of parsed fields.
47
+ #
48
+ # @protected
49
+ def parse_ledger_entry(ledger_entry)
50
+ # validate parsability
51
+ obj = Format::TYPE_INFER.decode(ledger_entry)
52
+ node_type = Format::NODE_TYPE_CODES[obj["node_type"]]
53
+ hash_prefix = Format::HASH_PREFIX_CODES[obj["hash_prefix"].upcase]
54
+ raise unless node_type == :account_node &&
55
+ hash_prefix == :leaf_node
56
+
57
+ # discard node type, and hash prefix
58
+ ledger_entry = ledger_entry[13..-1]
59
+
60
+ # verify encoding
61
+ encoding, ledger_entry = parse_encoding(ledger_entry)
62
+ raise "Invalid Ledger Entry" unless Format::ENCODINGS[encoding] == :ledger_entry_type
63
+ ledger_entry = ledger_entry.bytes
64
+
65
+ # first byte after encoding is ledger entry type prefix
66
+ prefix = ledger_entry[0..1].pack("C*")
67
+
68
+ # last 32 bytes is entry index
69
+ index = ledger_entry[-32..-1].pack("C*")
70
+ .unpack("H*")
71
+ .first
72
+ .upcase
73
+
74
+ # remaining bytes are serialized object
75
+ fields, remaining = parse_fields(ledger_entry[2...-32].pack("C*"))
76
+ raise unless remaining.empty?
77
+
78
+ # TODO return STLedgerEntry object
79
+
80
+ { :type => Format::LEDGER_ENTRY_TYPE_CODES[prefix[1]],
81
+ :index => index,
82
+ :fields => fields }
83
+ end
84
+
85
+ ###
86
+
87
+ # Parse and return series of fields from binary data.
88
+ #
89
+ # @protected
90
+ def parse_fields(fields)
91
+ parsed = {}
92
+ until fields == "" || fields == "\0" || fields.nil?
93
+ encoding, fields = parse_encoding(fields)
94
+ return parsed if encoding.first.nil?
95
+
96
+ e = Format::ENCODINGS[encoding]
97
+ value, fields = parse_field(fields, encoding)
98
+ break unless value
99
+ parsed[e] = convert_field(encoding, value)
100
+ end
101
+
102
+ return parsed, fields
103
+ end
104
+
105
+ # Parse single field of specified encoding from data.
106
+ # Dispatches to corresponding parsing method when appropriate.
107
+ #
108
+ # @protected
109
+ def parse_field(data, encoding)
110
+ length = encoding.first
111
+
112
+ case length
113
+ when :uint8
114
+ return data.unpack("C").first, data[1..-1]
115
+ when :uint16
116
+ return data.unpack("S>").first, data[2..-1]
117
+ when :uint32
118
+ return data.unpack("L>").first, data[4..-1]
119
+ when :uint64
120
+ return data.unpack("Q>").first, data[8..-1]
121
+ when :hash128
122
+ return data.unpack("H32").first, data[16..-1]
123
+ when :hash160
124
+ return data.unpack("H40").first, data[20..-1]
125
+ when :hash256
126
+ return data.unpack("H64").first, data[32..-1]
127
+ when :amount
128
+ return parse_amount(data)
129
+ when :vl
130
+ vl, offset = parse_vl(data)
131
+ return data[offset..vl+offset-1], data[vl+offset..-1]
132
+ when :account
133
+ return parse_account(data)
134
+ when :array
135
+ return parse_array(data, encoding)
136
+ when :object
137
+ return parse_object(data, encoding)
138
+ when :pathset
139
+ return parse_pathset(data)
140
+ when :vector256
141
+ vl, offset = parse_vl(data)
142
+
143
+ # split into array of 256-bit (= 32 byte = 4 chars) strings
144
+ return data[offset..vl+offset-1].chunk(32),
145
+ data[vl+offset..-1]
146
+ end
147
+
148
+ raise
149
+ end
150
+
151
+ def convert_field(encoding, value)
152
+ e = Format::ENCODINGS[encoding]
153
+
154
+ if encoding.first == :vl
155
+ return value.unpack("H*").first
156
+
157
+ elsif e == :transaction_type
158
+ return Format::TX_TYPES[value]
159
+
160
+ elsif e == :ledger_entry_type
161
+ return Format::LEDGER_ENTRY_TYPE_CODES[value.chr]
162
+ end
163
+
164
+ value
165
+ end
166
+
167
+ # Parse variable length header from data buffer. Returns length
168
+ # extracted from header and the number of bytes in header.
169
+ #
170
+ # @protected
171
+ def parse_vl(data)
172
+ data = data.bytes
173
+ first = data.first.to_i
174
+ return first, 1 if first <= 192
175
+
176
+ data = data[1..-1]
177
+ second = data.first.to_i
178
+ if first <= 240
179
+ return (193+(first-193)*256+second), 2
180
+
181
+ elsif first <= 254
182
+ data = data[1..-1]
183
+ third = data.first.to_i
184
+ return (12481 + (first-241)*65536 + second*256 + third), 3
185
+ end
186
+
187
+ raise
188
+ end
189
+
190
+ # Parse 'Amount' data type from binary data.
191
+ #
192
+ # @see https://developers.ripple.com/currency-formats.html
193
+ #
194
+ # @protected
195
+ def parse_amount(data)
196
+ amount = data[0..7].unpack("Q>").first
197
+ xrp = amount < 0x8000000000000000
198
+
199
+ # FIXME : is sign/neg right (?)
200
+
201
+ return STAmount.new(:issue => NodeStore.xrp_issue,
202
+ :mantissa => amount & 0x3FFFFFFFFFFFFFFF), data[8..-1] if xrp
203
+
204
+ sign = (amount & 0x4000000000000000) >> 62 # 0 = neg / 1 = pos
205
+ neg = (sign == 0)
206
+ exp = (amount & 0x3FC0000000000000) >> 54
207
+ mant = (amount & 0x003FFFFFFFFFFFFF)
208
+
209
+ data = data[8..-1]
210
+ currency = Format::CURRENCY_CODE.decode(data)
211
+ currency = currency["iso_code"].pack("C*")
212
+
213
+ data = data[Format::CURRENCY_CODE.size..-1]
214
+ issuer, data = parse_account(data, 20)
215
+
216
+ sle = STAmount.new(:issue => Issue.new(currency, issuer),
217
+ :neg => neg,
218
+ :mantissa => mant,
219
+ :exponent => exp)
220
+
221
+ return sle, data
222
+ end
223
+
224
+ # Parse 'Account' data type from binary data.
225
+ #
226
+ # @protected
227
+ def parse_account(data, vl=nil)
228
+ unless vl
229
+ vl,offset = parse_vl(data)
230
+ data = data[offset..-1]
231
+ end
232
+
233
+ acct = "\0" + data[0..vl-1]
234
+ sha256 = OpenSSL::Digest::SHA256.new
235
+ digest = sha256.digest(sha256.digest(acct))[0..3]
236
+ acct += digest
237
+ acct.force_encoding(Encoding::BINARY) # required for Base58 gem
238
+
239
+ # TODO return STAccount
240
+ return Base58.binary_to_base58(acct, :ripple), data[vl..-1]
241
+ end
242
+
243
+ # Parse array of fields from binary data.
244
+ #
245
+ # @protected
246
+ def parse_array(data, encoding)
247
+ e = Format::ENCODINGS[encoding]
248
+ return nil, data if e == :end_of_array
249
+
250
+ array = []
251
+ until data == "" || data.nil?
252
+ aencoding, data = parse_encoding(data)
253
+ break if aencoding.first.nil?
254
+
255
+ e = Format::ENCODINGS[aencoding]
256
+ break if e == :end_of_array
257
+
258
+ value, data = parse_field(data, aencoding)
259
+ break unless value
260
+ array << value
261
+ end
262
+
263
+ return array, data
264
+ end
265
+
266
+ # Parse Object consisting of multiple fields from binary data.
267
+ #
268
+ # @protected
269
+ def parse_object(data, encoding)
270
+ e = Format::ENCODINGS[encoding]
271
+ case e
272
+ when :end_of_object
273
+ return nil, data
274
+
275
+ when :signer, :signer_entry,
276
+ :majority, :memo,
277
+ :modified_node, :created_node, :deleted_node,
278
+ :previous_fields, :final_fields, :new_fields
279
+ # TODO return STObject
280
+ return parse_fields(data)
281
+
282
+ #else:
283
+ end
284
+
285
+ raise "unknown object type: #{e}"
286
+ end
287
+
288
+ # Parse PathSet from binary data.
289
+ #
290
+ # @protected
291
+ def parse_pathset(data)
292
+ pathset = [[]]
293
+ until data == "" || data.nil?
294
+ segment = data.unpack("C").first
295
+ data = data[1..-1]
296
+ return pathset, data if segment == 0x00 # end of path
297
+
298
+ if segment == 0xFF # path boundry
299
+ pathset << []
300
+ else
301
+ account, current, issuer = nil
302
+
303
+ path = {}
304
+
305
+ if (segment & 0x01) != 0 # path account
306
+ account, data = parse_account(data, 20)
307
+ path[:account] = account
308
+ end
309
+
310
+ if (segment & 0x10) != 0 # path currency
311
+ currency = Format::CURRENCY_CODE.decode(data)
312
+ currency = currency["iso_code"].pack("C*")
313
+ data = data[Format::CURRENCY_CODE.size..-1]
314
+ path[:currency] = currency
315
+ end
316
+
317
+ if (segment & 0x20) != 0 # path issuer
318
+ issuer, data = parse_account(data, 20)
319
+ path[:issuer] = issuer
320
+ end
321
+
322
+ pathset.last << path
323
+ end
324
+ end
325
+
326
+ # TODO return STPathSet
327
+ return pathset, data
328
+ end
329
+
330
+ ###
331
+
332
+ # Parse Transaction from binary data
333
+ #
334
+ # @protected
335
+ def parse_tx(tx)
336
+ obj = Format::TYPE_INFER.decode(tx)
337
+ node_type = Format::NODE_TYPE_CODES[obj["node_type"]]
338
+ hash_prefix = Format::HASH_PREFIX_CODES[obj["hash_prefix"].upcase]
339
+ raise unless node_type == :tx_node &&
340
+ hash_prefix == :tx_node
341
+
342
+ # discard node type, and hash prefix
343
+ tx = tx[13..-1]
344
+
345
+ parse_tx_inner(tx)
346
+ end
347
+
348
+ # Parse Inner Transaction from binary data
349
+ #
350
+ # @protected
351
+ def parse_tx_inner(tx)
352
+ # get node length
353
+ vl, offset = parse_vl(tx)
354
+ node, _tx = tx.bytes[offset..vl+offset-1], tx.bytes[vl+offset..-1]
355
+ node, _remaining = parse_fields(node.pack("C*"))
356
+
357
+ # get meta length
358
+ vl, offset = parse_vl(_tx.pack("C*"))
359
+ meta, index = _tx[offset..vl+offset-1], _tx[vl+offset..-1]
360
+ meta, _remaining = parse_fields(meta.pack("C*"))
361
+
362
+ # TODO return STTx
363
+ { :node => node,
364
+ :meta => meta,
365
+ :index => index.pack("C*").unpack("H*").first.upcase }
366
+ end
367
+
368
+ # Parse InnerNode from binary data.
369
+ #
370
+ # @protected
371
+ def parse_inner_node(node)
372
+ # verify parsability
373
+ obj = Format::TYPE_INFER.decode(node)
374
+ hash_prefix = Format::HASH_PREFIX_CODES[obj["hash_prefix"].upcase]
375
+ raise unless hash_prefix == :inner_node
376
+
377
+ node = Format::INNER_NODE.decode(node)
378
+ node['node_type'] = Format::NODE_TYPE_CODES[node['node_type']]
379
+ node
380
+ end
381
+
382
+ # Return type and extracted structure from binary data.
383
+ #
384
+ # @protected
385
+ def infer_type(value)
386
+ obj = Format::TYPE_INFER.decode(value)
387
+ node_type = Format::NODE_TYPE_CODES[obj["node_type"]]
388
+ hash_prefix = Format::HASH_PREFIX_CODES[obj["hash_prefix"].upcase]
389
+
390
+ if hash_prefix == :inner_node
391
+ return :inner_node, parse_inner_node(value)
392
+
393
+ elsif node_type == :account_node
394
+ return :ledger_entry, parse_ledger_entry(value)
395
+
396
+ elsif node_type == :tx_node
397
+ return :tx, parse_tx(value)
398
+
399
+ elsif node_type == :ledger
400
+ return :ledger, parse_ledger(value)
401
+ end
402
+
403
+ return nil
404
+ end
405
+ end # module Parser
406
+ end # module NodeStore
407
+ end # module XRBP