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.
- checksums.yaml +4 -4
- data/examples/nodestore1.rb +12 -7
- data/lib/xrbp/core_ext.rb +27 -0
- data/lib/xrbp/crypto/account.rb +28 -3
- data/lib/xrbp/nodestore.rb +6 -0
- data/lib/xrbp/nodestore/amendments.rb +13 -0
- data/lib/xrbp/nodestore/backends/decompressor.rb +8 -6
- data/lib/xrbp/nodestore/backends/nudb.rb +2 -0
- data/lib/xrbp/nodestore/backends/rocksdb.rb +1 -0
- data/lib/xrbp/nodestore/db.rb +5 -387
- data/lib/xrbp/nodestore/fees.rb +19 -0
- data/lib/xrbp/nodestore/format.rb +72 -1
- data/lib/xrbp/nodestore/ledger.rb +272 -0
- data/lib/xrbp/nodestore/parser.rb +407 -0
- data/lib/xrbp/nodestore/protocol.rb +5 -0
- data/lib/xrbp/nodestore/protocol/currency.rb +11 -0
- data/lib/xrbp/nodestore/protocol/indexes.rb +109 -0
- data/lib/xrbp/nodestore/protocol/issue.rb +26 -0
- data/lib/xrbp/nodestore/protocol/quality.rb +10 -0
- data/lib/xrbp/nodestore/protocol/rate.rb +21 -0
- data/lib/xrbp/nodestore/shamap.rb +447 -0
- data/lib/xrbp/nodestore/shamap/errors.rb +8 -0
- data/lib/xrbp/nodestore/shamap/inner_node.rb +98 -0
- data/lib/xrbp/nodestore/shamap/item.rb +14 -0
- data/lib/xrbp/nodestore/shamap/node.rb +49 -0
- data/lib/xrbp/nodestore/shamap/node_factory.rb +120 -0
- data/lib/xrbp/nodestore/shamap/node_id.rb +83 -0
- data/lib/xrbp/nodestore/shamap/tagged_cache.rb +20 -0
- data/lib/xrbp/nodestore/shamap/tree_node.rb +21 -0
- data/lib/xrbp/nodestore/sle.rb +4 -0
- data/lib/xrbp/nodestore/sle/st_account.rb +8 -0
- data/lib/xrbp/nodestore/sle/st_amount.rb +226 -0
- data/lib/xrbp/nodestore/sle/st_ledger_entry.rb +24 -0
- data/lib/xrbp/nodestore/sle/st_object.rb +46 -0
- data/lib/xrbp/nodestore/sqldb.rb +23 -0
- data/lib/xrbp/nodestore/uint.rb +7 -0
- data/lib/xrbp/version.rb +1 -1
- data/spec/xrbp/nodestore/backends/nudb_spec.rb +3 -1
- data/spec/xrbp/nodestore/backends/rocksdb_spec.rb +1 -1
- data/spec/xrbp/nodestore/{backends/db_parser.rb → db_parser.rb} +2 -2
- data/spec/xrbp/nodestore/ledger_access.rb +17 -0
- 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
|
-
|
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
|