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