xrbp 0.2.2 → 0.2.3
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 +3 -1
- data/lib/xrbp/common.rb +6 -0
- data/lib/xrbp/core_ext.rb +21 -0
- data/lib/xrbp/nodestore/format.rb +76 -26
- data/lib/xrbp/nodestore/ledger.rb +46 -14
- data/lib/xrbp/nodestore/parser.rb +47 -14
- data/lib/xrbp/nodestore/protocol/indexes.rb +11 -8
- data/lib/xrbp/nodestore/protocol/issue.rb +15 -0
- data/lib/xrbp/nodestore/protocol/rate.rb +13 -1
- data/lib/xrbp/nodestore/shamap/node_factory.rb +6 -1
- data/lib/xrbp/nodestore/shamap/node_id.rb +2 -2
- data/lib/xrbp/nodestore/sle/st_amount.rb +46 -181
- data/lib/xrbp/nodestore/sle/st_amount_arithmatic.rb +126 -0
- data/lib/xrbp/nodestore/sle/st_amount_comparison.rb +49 -0
- data/lib/xrbp/nodestore/sle/st_amount_conversion.rb +203 -0
- data/lib/xrbp/nodestore/sqldb.rb +69 -4
- data/lib/xrbp/overlay/handshake.rb +1 -1
- data/lib/xrbp/version.rb +1 -1
- data/spec/xrbp/crypto/account_spec.rb +7 -2
- data/spec/xrbp/nodestore/amendments_spec.rb +11 -0
- data/spec/xrbp/nodestore/db_parser.rb +64 -1
- data/spec/xrbp/nodestore/fees_spec.rb +3 -0
- data/spec/xrbp/nodestore/ledger_access.rb +87 -2
- data/spec/xrbp/nodestore/protocol/indexes_spec.rb +43 -0
- data/spec/xrbp/nodestore/protocol/rate_spec.rb +12 -0
- data/spec/xrbp/nodestore/shamap/inner_node_spec.rb +44 -0
- data/spec/xrbp/nodestore/shamap/node_factory_spec.rb +9 -0
- data/spec/xrbp/nodestore/shamap/node_id_spec.rb +6 -0
- data/spec/xrbp/nodestore/shamap/node_spec.rb +25 -0
- data/spec/xrbp/nodestore/shamap_spec.rb +144 -0
- data/spec/xrbp/nodestore/sle/st_amount_arithmatic_spec.rb +7 -0
- data/spec/xrbp/nodestore/sle/st_amount_comparison_spec.rb +11 -0
- data/spec/xrbp/nodestore/sle/st_amount_conversion_spec.rb +64 -0
- data/spec/xrbp/nodestore/sle/st_amount_spec.rb +47 -0
- data/spec/xrbp/nodestore/sle/st_ledger_entry_spec.rb +5 -0
- data/spec/xrbp/nodestore/sle/st_object_spec.rb +29 -0
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90ae30e5619f27c7a1f40b891a0e84b12f2de86bec369391d6e6f39f9a02bc89
|
4
|
+
data.tar.gz: 68365c498a34d0e4c7c3c805b59718f867fa46a6ab2784eab012b22726361212
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e0660fab2c5f8248577df45b795d5d6794b03dbb33e88e566ec2e08f477147e74dc92d1703155dc152e103956ac586c38d6e1ad4b02fcd52594f9307f4d97c13
|
7
|
+
data.tar.gz: 3d49ec2789f104a194d6f5b6471cf40c3d0fc51695694157f2c79d189e21fe38ec3a94b2d8cbda9594a87f611f7b3440c5c3f0f9987d12a67782cf22f82c7d0e
|
data/examples/nodestore1.rb
CHANGED
@@ -21,4 +21,6 @@ puts nledger.order_book iou1, iou2
|
|
21
21
|
puts nledger.txs
|
22
22
|
|
23
23
|
require 'xrbp/nodestore/sqldb'
|
24
|
-
|
24
|
+
sql = XRBP::NodeStore::SQLDB.new("/var/lib/rippled/nudb")
|
25
|
+
puts sql.ledgers.hash_for_seq(49340234)
|
26
|
+
puts sql.ledgers.count
|
data/lib/xrbp/common.rb
CHANGED
data/lib/xrbp/core_ext.rb
CHANGED
@@ -59,6 +59,20 @@ class Integer
|
|
59
59
|
end
|
60
60
|
b
|
61
61
|
end
|
62
|
+
|
63
|
+
def byte_string
|
64
|
+
bytes.reverse.pack("C*")
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_int32
|
68
|
+
self & (2**32-1)
|
69
|
+
end
|
70
|
+
|
71
|
+
alias :to_int :to_int32
|
72
|
+
|
73
|
+
def from_xrp_time
|
74
|
+
XRBP.from_xrp_time(self)
|
75
|
+
end
|
62
76
|
end
|
63
77
|
|
64
78
|
# @private
|
@@ -71,3 +85,10 @@ class Array
|
|
71
85
|
fill(x, length...n)
|
72
86
|
end
|
73
87
|
end
|
88
|
+
|
89
|
+
# @private
|
90
|
+
class Time
|
91
|
+
def to_xrp_time
|
92
|
+
XRBP.to_xrp_time(self)
|
93
|
+
end
|
94
|
+
end
|
@@ -383,32 +383,82 @@ module XRBP
|
|
383
383
|
###
|
384
384
|
|
385
385
|
TX_TYPES = {
|
386
|
-
-1 => :
|
387
|
-
0 => :
|
388
|
-
1 => :
|
389
|
-
2 => :
|
390
|
-
3 => :
|
391
|
-
4 => :
|
392
|
-
5 => :
|
393
|
-
6 => :
|
394
|
-
7 => :
|
395
|
-
8 => :
|
396
|
-
|
397
|
-
9 => :
|
398
|
-
|
399
|
-
11 => :
|
400
|
-
12 => :
|
401
|
-
13 => :
|
402
|
-
14 => :
|
403
|
-
15 => :
|
404
|
-
16 => :
|
405
|
-
17 => :
|
406
|
-
18 => :
|
407
|
-
19 => :
|
408
|
-
20 => :
|
409
|
-
|
410
|
-
100 => :
|
411
|
-
101 => :
|
386
|
+
-1 => :Invalid,
|
387
|
+
0 => :Payment,
|
388
|
+
1 => :EscrowCreate,
|
389
|
+
2 => :EscrowFinish,
|
390
|
+
3 => :AccountSet,
|
391
|
+
4 => :EscrowCancel,
|
392
|
+
5 => :SetRegularKey,
|
393
|
+
6 => :NickNameSet, #open
|
394
|
+
7 => :OfferCreate,
|
395
|
+
8 => :OfferCancel,
|
396
|
+
|
397
|
+
9 => :unused,
|
398
|
+
|
399
|
+
11 => :TicketCreate,
|
400
|
+
12 => :TicketCancel,
|
401
|
+
13 => :SignerListSet,
|
402
|
+
14 => :PaymentChannelCreate,
|
403
|
+
15 => :PaymentChannelFund,
|
404
|
+
16 => :PaymentChannelClaim,
|
405
|
+
17 => :CheckCreate,
|
406
|
+
18 => :CheckCash,
|
407
|
+
19 => :CheckCancel,
|
408
|
+
20 => :DepositPreauth,
|
409
|
+
|
410
|
+
100 => :EnableAmendment,
|
411
|
+
101 => :SetFee
|
412
|
+
}
|
413
|
+
|
414
|
+
# https://xrpl.org/transaction-results.html
|
415
|
+
TX_RESULTS = {
|
416
|
+
# tec
|
417
|
+
# https://xrpl.org/tec-codes.html
|
418
|
+
# "Transaction failed, but it was applied to a ledger to apply the transaction cost.
|
419
|
+
# They have numerical values in the range 100 to 199"
|
420
|
+
100 => :tecCLAIM,
|
421
|
+
146 => :tecCRYPTOCONDITION_ERROR,
|
422
|
+
121 => :tecDIR_FULL,
|
423
|
+
149 => :tecDUPLICATE,
|
424
|
+
143 => :tecDST_TAG_NEEDED,
|
425
|
+
148 => :tecEXPIRED,
|
426
|
+
105 => :tecFAILED_PROCESSING,
|
427
|
+
137 => :tecFROZEN,
|
428
|
+
122 => :tecINSUF_RESERVE_LINE,
|
429
|
+
123 => :tecINSUF_RESERVE_OFFER,
|
430
|
+
141 => :tecINSUFFICIENT_RESERVE,
|
431
|
+
144 => :tecINTERNAL,
|
432
|
+
147 => :tecINVARIANT_FAILED,
|
433
|
+
142 => :tecNEED_MASTER_KEY,
|
434
|
+
130 => :tecNO_ALTERNATIVE_KEY,
|
435
|
+
134 => :tecNO_AUTH,
|
436
|
+
124 => :tecNO_DST,
|
437
|
+
125 => :tecNO_DST_INSUF_XRP,
|
438
|
+
140 => :tecNO_ENTRY,
|
439
|
+
133 => :tecNO_ISSUER,
|
440
|
+
150 => :tecKILLED,
|
441
|
+
135 => :tecNO_LINE,
|
442
|
+
126 => :tecNO_LINE_INSUF_RESERVE,
|
443
|
+
127 => :tecNO_LINE_REDUNDANT,
|
444
|
+
139 => :tecNO_PERMISSION,
|
445
|
+
131 => :tecNO_REGULAR_KEY,
|
446
|
+
138 => :tecNO_TARGET,
|
447
|
+
145 => :tecOVERSIZE,
|
448
|
+
132 => :tecOWNERS,
|
449
|
+
128 => :tecPATH_DRY,
|
450
|
+
101 => :tecPATH_PARTIAL,
|
451
|
+
129 => :tecUNFUNDED,
|
452
|
+
102 => :tecUNFUNDED_ADD,
|
453
|
+
104 => :tecUNFUNDED_PAYMENT,
|
454
|
+
103 => :tecUNFUNDED_OFFER,
|
455
|
+
|
456
|
+
# tef, tel, tem, ter transactions _not_
|
457
|
+
# applied to ledger
|
458
|
+
|
459
|
+
# tes
|
460
|
+
# https://xrpl.org/tes-success.html
|
461
|
+
0 => :tesSUCCESS
|
412
462
|
}
|
413
463
|
|
414
464
|
###
|
@@ -43,12 +43,16 @@ module XRBP
|
|
43
43
|
@fees ||= Fees.new
|
44
44
|
end
|
45
45
|
|
46
|
+
# Returns boolean indicating if specified account
|
47
|
+
# is flagged as globally frozen
|
46
48
|
def global_frozen?(account)
|
47
49
|
return false if account == Crypto.xrp_account
|
48
50
|
sle = state_map.read(Indexes::account(account))
|
49
51
|
return sle && sle.flag?(:global_freeze)
|
50
52
|
end
|
51
53
|
|
54
|
+
# Returns boolean indicating if specific account
|
55
|
+
# has frozen trust-line for specified IOU
|
52
56
|
def frozen?(account, iou)
|
53
57
|
return false if iou[:currency] == 'XRP'
|
54
58
|
|
@@ -62,16 +66,19 @@ module XRBP
|
|
62
66
|
:low_freeze)
|
63
67
|
end
|
64
68
|
|
69
|
+
# Return IOU balance which owner account holds
|
65
70
|
def account_holds(owner_id, iou)
|
66
71
|
return xrp_liquid(owner_id, 0) if iou[:currency] == 'XRP'
|
67
72
|
sle = state_map.read(Indexes::line(owner_id, iou))
|
68
73
|
return STAmount.zero if !sle || frozen?(owner_id, iou)
|
69
74
|
|
70
75
|
amount = sle.amount(:balance)
|
71
|
-
amount.negate! if owner_id >
|
76
|
+
amount.negate! if Crypto.account_id(owner_id).to_bn >
|
77
|
+
Crypto.account_id(iou[:account]).to_bn
|
72
78
|
balance_hook(amount)
|
73
79
|
end
|
74
80
|
|
81
|
+
# Returns available (liquid) XRP account holds
|
75
82
|
def xrp_liquid(account, owner_count_adj)
|
76
83
|
sle = state_map.read(Indexes::account(account))
|
77
84
|
return STAmount.zero unless sle
|
@@ -122,8 +129,12 @@ module XRBP
|
|
122
129
|
count
|
123
130
|
end
|
124
131
|
|
132
|
+
# Return TransferRate configured for IOU
|
133
|
+
#
|
134
|
+
# @see {Rate}
|
125
135
|
def transfer_rate(issuer)
|
126
136
|
sle = state_map.read(Indexes::account(issuer))
|
137
|
+
|
127
138
|
return Rate.new sle.field(:uint32,
|
128
139
|
:transfer_rate) if sle &&
|
129
140
|
sle.field?(:transfer_rate)
|
@@ -134,6 +145,10 @@ module XRBP
|
|
134
145
|
|
135
146
|
public
|
136
147
|
|
148
|
+
# TODO: helper method to get first funded high quailty
|
149
|
+
# offer from order book. Also market depth helper
|
150
|
+
|
151
|
+
# Return all offers for the given input/output currency pair
|
137
152
|
def order_book(input, output)
|
138
153
|
offers = []
|
139
154
|
|
@@ -145,6 +160,7 @@ module XRBP
|
|
145
160
|
global_freeze = global_frozen?(output[:account]) ||
|
146
161
|
global_frozen?(input[:account])
|
147
162
|
|
163
|
+
# transfer rate multipled to offer output to pay issuer
|
148
164
|
rate = transfer_rate(output[:account])
|
149
165
|
|
150
166
|
balances = {}
|
@@ -180,28 +196,32 @@ module XRBP
|
|
180
196
|
# Read offer from db and process
|
181
197
|
sle_offer = state_map.read(offer_index)
|
182
198
|
if sle_offer
|
199
|
+
# Direct info from nodestore offer
|
183
200
|
owner_id = sle_offer.account_id(:account)
|
184
201
|
taker_gets = sle_offer.amount(:taker_gets)
|
185
202
|
taker_pays = sle_offer.amount(:taker_pays)
|
186
203
|
|
187
|
-
|
188
|
-
|
204
|
+
# Owner / Output Calculation
|
205
|
+
owner_funds = nil # how much of offer output the owner has
|
206
|
+
first_owner_offer = true # owner_funds returned w/ first owner offer
|
189
207
|
|
208
|
+
# issuer is offering it's own IOU, fully funded
|
190
209
|
if output[:account] == owner_id
|
191
|
-
# issuer is offering it's own IOU, fully funded
|
192
210
|
owner_funds = taker_gets
|
193
211
|
|
212
|
+
# all offers not ours are unfunded
|
194
213
|
elsif global_freeze
|
195
|
-
# all offers not ours are unfunded
|
196
214
|
owner_funds.clear(output)
|
197
215
|
|
198
216
|
else
|
217
|
+
# if we have owner funds cached
|
199
218
|
if balances[owner_id]
|
200
219
|
owner_funds = balances[owner_id]
|
201
220
|
first_owner_offer = false
|
202
221
|
|
222
|
+
# did not find balance in cache
|
203
223
|
else
|
204
|
-
#
|
224
|
+
# lookup from nodestore
|
205
225
|
owner_funds = account_holds(owner_id, output)
|
206
226
|
|
207
227
|
# treat negative funds as zero
|
@@ -209,29 +229,33 @@ module XRBP
|
|
209
229
|
end
|
210
230
|
end
|
211
231
|
|
212
|
-
offer = Hash[sle_offer.fields]
|
213
|
-
taker_gets_funded = nil
|
214
|
-
owner_funds_limit = owner_funds
|
215
|
-
offer_rate = Rate.parity
|
232
|
+
offer = Hash[sle_offer.fields] # copy the offer fields to return
|
233
|
+
taker_gets_funded = nil # how much offer owner will actually be able to fund
|
234
|
+
owner_funds_limit = owner_funds # how much the offer owner has limited by the output transfer fee
|
235
|
+
offer_rate = Rate.parity # offer base output transfer rate
|
216
236
|
|
237
|
+
# Check if transfer fee applies,
|
217
238
|
if rate != Rate.parity && # transfer fee
|
218
239
|
# TODO: provide support for 'taker_id' rpc param:
|
219
240
|
#taker_id != output[:account] && # not taking offers of own IOUs
|
220
241
|
output[:account] != owner_id # offer owner not issuing own funds
|
221
242
|
# Need to charge a transfer fee to offer owner.
|
222
243
|
offer_rate = rate
|
223
|
-
owner_funds_limit = owner_funds / offer_rate.
|
244
|
+
owner_funds_limit = owner_funds / offer_rate.to_amount
|
224
245
|
end
|
225
246
|
|
226
|
-
|
247
|
+
# Check if owner has enough funds to pay it all
|
227
248
|
if owner_funds_limit >= taker_gets
|
228
249
|
# Sufficient funds no shenanigans.
|
229
250
|
taker_gets_funded = taker_gets
|
230
251
|
|
231
252
|
else
|
232
|
-
# Only
|
253
|
+
# Only set these fields, if not fully funded.
|
233
254
|
taker_gets_funded = owner_funds_limit
|
234
255
|
offer[:taker_gets_funded] = taker_gets_funded
|
256
|
+
|
257
|
+
# the account that takes the offer will need to
|
258
|
+
# pay the 'gets' amount actually funded times the dir_rate (quality)
|
235
259
|
offer[:taker_pays_funded] = [taker_pays,
|
236
260
|
taker_gets_funded *
|
237
261
|
dir_rate].min
|
@@ -240,13 +264,21 @@ module XRBP
|
|
240
264
|
offer[:taker_pays_funded].issue = taker_pays.issue
|
241
265
|
end
|
242
266
|
|
267
|
+
# Calculate how much owner will pay after this offer,
|
268
|
+
# if no transfer fee, then the amount funded,
|
269
|
+
# else the minimum of what the owner has or the
|
270
|
+
# amount funded w/ transfer fee
|
243
271
|
owner_pays = (Rate.parity == offer_rate) ?
|
244
272
|
taker_gets_funded :
|
245
273
|
[owner_funds,
|
246
|
-
|
274
|
+
taker_gets_funded *
|
275
|
+
offer_rate.to_amount].min
|
247
276
|
|
277
|
+
# Update balance cache w/ new owner balance
|
248
278
|
balances[owner_id] = owner_funds - owner_pays
|
249
279
|
|
280
|
+
# Set additional params and store the offer
|
281
|
+
|
250
282
|
# include all offers funded and unfunded
|
251
283
|
offer[:quality] = dir_rate
|
252
284
|
offer[:owner_funds] = owner_funds if first_owner_offer
|
@@ -157,6 +157,9 @@ module XRBP
|
|
157
157
|
elsif e == :transaction_type
|
158
158
|
return Format::TX_TYPES[value]
|
159
159
|
|
160
|
+
elsif e == :transaction_result
|
161
|
+
return Format::TX_RESULTS[value]
|
162
|
+
|
160
163
|
elsif e == :ledger_entry_type
|
161
164
|
return Format::LEDGER_ENTRY_TYPE_CODES[value.chr]
|
162
165
|
end
|
@@ -189,22 +192,39 @@ module XRBP
|
|
189
192
|
|
190
193
|
# Parse 'Amount' data type from binary data.
|
191
194
|
#
|
195
|
+
# Stored internally in 64 bits:
|
196
|
+
# - Bit 1 = 0 for XRP, 1 for IOU
|
197
|
+
# - Bit 2 = sign, 0 for negative, 1 for positive
|
198
|
+
#
|
199
|
+
# For XRP:
|
200
|
+
# - Remaining bits = value in drops
|
201
|
+
#
|
202
|
+
# For IOU:
|
203
|
+
# - Next 8 bits = Exponent
|
204
|
+
# - Next 54 bits = Mantissa
|
205
|
+
# Where value = mantissa * (10 ^ exponent)
|
206
|
+
# Note: 97 is added to exponent when serializing and subtracted when parsing so
|
207
|
+
# that effective nominal exponent is always in range of -96 to 80
|
208
|
+
#
|
192
209
|
# @see https://developers.ripple.com/currency-formats.html
|
210
|
+
# @see STAmount(SerialIter& sit, SField const& name);
|
211
|
+
# @see {STAmount#from_wire}
|
193
212
|
#
|
194
213
|
# @protected
|
195
214
|
def parse_amount(data)
|
196
215
|
amount = data[0..7].unpack("Q>").first
|
197
|
-
xrp = amount < 0x8000000000000000
|
198
|
-
|
199
|
-
# FIXME : is sign/neg right (?)
|
200
216
|
|
201
|
-
|
202
|
-
|
217
|
+
# native eg, xrp
|
218
|
+
is_xrp = (amount & STAmount::NOT_NATIVE) == 0
|
219
|
+
if is_xrp
|
220
|
+
is_pos = (amount & STAmount::POS_NATIVE) != 0
|
221
|
+
return STAmount.new(:issue => NodeStore.xrp_issue,
|
222
|
+
:mantissa => amount & ~STAmount::POS_NATIVE), data[8..-1] if is_pos
|
203
223
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
224
|
+
return STAmount.new(:issue => NodeStore.xrp_issue,
|
225
|
+
:mantissa => amount,
|
226
|
+
:neg => true), data[8..-1]
|
227
|
+
end
|
208
228
|
|
209
229
|
data = data[8..-1]
|
210
230
|
currency = Format::CURRENCY_CODE.decode(data)
|
@@ -213,12 +233,25 @@ module XRBP
|
|
213
233
|
data = data[Format::CURRENCY_CODE.size..-1]
|
214
234
|
issuer, data = parse_account(data, 20)
|
215
235
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
236
|
+
issue = Issue.new(currency, issuer)
|
237
|
+
|
238
|
+
exp = (amount >> (64 - 10)).to_int
|
239
|
+
mant = (amount & ~(1023 << (64 - 10)))
|
240
|
+
|
241
|
+
if mant
|
242
|
+
neg = (exp & 256) == 0
|
243
|
+
exp = (exp & 255) - 97
|
244
|
+
|
245
|
+
sle = STAmount.new(:issue => issue,
|
246
|
+
:neg => neg,
|
247
|
+
:mantissa => mant,
|
248
|
+
:exponent => exp)
|
249
|
+
|
250
|
+
return sle, data
|
251
|
+
end
|
220
252
|
|
221
|
-
|
253
|
+
raise unless exp == 512
|
254
|
+
return STAmount.new(:issue => issue)
|
222
255
|
end
|
223
256
|
|
224
257
|
# Parse 'Account' data type from binary data.
|
@@ -4,15 +4,14 @@ module XRBP
|
|
4
4
|
module Indexes
|
5
5
|
|
6
6
|
def self.get_quality(base)
|
7
|
-
# FIXME:
|
7
|
+
# FIXME: assuming native platform is big endian,
|
8
8
|
# need to account for all platforms
|
9
|
-
base[-8..-1].
|
9
|
+
base[-8..-1].to_bn
|
10
10
|
end
|
11
11
|
|
12
12
|
def self.get_quality_next(base)
|
13
13
|
nxt = "10000000000000000".to_i(16)
|
14
|
-
(base.to_bn + nxt).
|
15
|
-
.reverse.pack("C*")
|
14
|
+
(base.to_bn + nxt).byte_string
|
16
15
|
end
|
17
16
|
|
18
17
|
###
|
@@ -45,6 +44,14 @@ module XRBP
|
|
45
44
|
sha512.digest[0..31]
|
46
45
|
end
|
47
46
|
|
47
|
+
# TODO: Account Owner Dir from id
|
48
|
+
def self.owner_dir(id)
|
49
|
+
end
|
50
|
+
|
51
|
+
# TODO: Offer Index for account id and seq
|
52
|
+
def self.offer_index(id, seq)
|
53
|
+
end
|
54
|
+
|
48
55
|
# Trust line for account/iou
|
49
56
|
def self.line(account, iou)
|
50
57
|
account = Crypto.account_id(account)
|
@@ -68,10 +75,6 @@ module XRBP
|
|
68
75
|
sha512.digest[0..31]
|
69
76
|
end
|
70
77
|
|
71
|
-
# TODO: order book dir hash for ledger
|
72
|
-
def self.order_book_dir()
|
73
|
-
end
|
74
|
-
|
75
78
|
# Order book index for given input/output
|
76
79
|
def self.order_book(input, output)
|
77
80
|
input = Hash[input]
|