xrbp 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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