mirror of
https://github.com/Xahau/xahaud.git
synced 2025-12-06 17:27:52 +00:00
431 lines
14 KiB
CoffeeScript
431 lines
14 KiB
CoffeeScript
################################### REQUIRES ###################################
|
|
|
|
# This gives coffee-script proper file/lines in the exceptions
|
|
|
|
async = require 'async'
|
|
assert = require 'assert'
|
|
{
|
|
Amount
|
|
Remote
|
|
Seed
|
|
Base
|
|
Transaction
|
|
sjcl
|
|
} = require 'ripple-lib'
|
|
{Server} = require './server'
|
|
testutils = require './testutils'
|
|
|
|
#################################### HELPERS ###################################
|
|
|
|
pretty_json = (v) -> JSON.stringify(v, undefined, 2)
|
|
|
|
exports.TestAccount = class TestAccount
|
|
SHA256_RIPEMD160: (bits) ->
|
|
sjcl.hash.ripemd160.hash sjcl.hash.sha256.hash(bits)
|
|
|
|
derive_pair: (passphrase) ->
|
|
seed = Seed.from_json(passphrase)
|
|
master_seed = seed.to_json()
|
|
key_pair = seed.get_key()
|
|
pubKey = sjcl.codec.hex.toBits key_pair.to_hex_pub()
|
|
address = Base.encode_check(0,
|
|
sjcl.codec.bytes.fromBits(@SHA256_RIPEMD160 pubKey))
|
|
[address, master_seed, key_pair]
|
|
|
|
constructor: (passphrase) ->
|
|
@passphrase = passphrase
|
|
[@address, @master_seed, @key_pair] = @derive_pair(passphrase)
|
|
|
|
############################# LEDGER STATE COMPILER ############################
|
|
|
|
exports.LedgerState = class LedgerState
|
|
setup_issuer_realiaser: ->
|
|
users = @config.accounts
|
|
lookup = {}
|
|
accounts = []
|
|
|
|
for name, user of users
|
|
accounts.push user.account
|
|
lookup[user.account] = name
|
|
|
|
realias = new RegExp(accounts.join("|"), "g")
|
|
@realias_issuer = (str) -> str.replace(realias, (match) ->lookup[match])
|
|
|
|
parse_amount: (amt_val) ->
|
|
amt = Amount.from_json(amt_val)
|
|
if not amt.is_valid()
|
|
amt = Amount.from_human(amt_val)
|
|
if not amt.is_valid()
|
|
amt = null
|
|
amt
|
|
|
|
amount_key: (amt) ->
|
|
currency = amt.currency().to_json()
|
|
issuer = @realias_issuer amt.issuer().to_json()
|
|
"#{currency}/#{issuer}"
|
|
|
|
apply: (context)->
|
|
@create_accounts_by_issuing_xrp_from_root(context)
|
|
@create_trust_limits(context)
|
|
@deliver_ious(context)
|
|
|
|
record_iou: (account_id, amt)->
|
|
key = @amount_key amt
|
|
@assert @declaration.accounts[key.split('/')[1]]?,
|
|
"Account for #{key} does not exist"
|
|
|
|
a_ious = @ensure account_id, @ious
|
|
@assert !a_ious[key]?,
|
|
"Account #{account_id} has more than one amount for #{key}"
|
|
a_ious[key] = amt
|
|
|
|
ensure: (account_id, obj, val) ->
|
|
if not obj[account_id]?
|
|
obj[account_id] = val ? {}
|
|
obj[account_id]
|
|
|
|
record_xrp: (account_id, amt)->
|
|
@assert !@accounts[account_id]?,
|
|
"Already declared XRP for #{account_id}"
|
|
@accounts[account_id] = amt
|
|
|
|
record_trust: (account_id, amt, is_balance) ->
|
|
key = @amount_key amt
|
|
a_trusts = @ensure account_id, @trusts_by_ci
|
|
|
|
if a_trusts[key]? and !is_balance
|
|
cmp = amt.compareTo a_trusts[key]
|
|
@assert cmp != - 1,
|
|
"Account #{account_id} trust is less than balance for #{key}"
|
|
a_trusts[key] = amt
|
|
|
|
compile_explicit_trusts: ->
|
|
for account_id, account of @declaration.accounts
|
|
if not account.trusts?
|
|
continue
|
|
|
|
for amt_val in account.trusts
|
|
amt = @parse_amount amt_val
|
|
@assert amt != null and !amt.is_native(),
|
|
"Trust amount #{amt_val} specified for #{account_id} is not valid"
|
|
@record_trust(account_id, amt, false)
|
|
|
|
undefined
|
|
|
|
compile_accounts_balances_and_implicit_trusts: ->
|
|
for account_id, account of @declaration.accounts
|
|
xrp_balance = null
|
|
|
|
@assert account.balance?,
|
|
"No balance declared for #{account_id}"
|
|
|
|
for amt_val in account.balance
|
|
amt = @parse_amount amt_val
|
|
@assert amt != null,
|
|
"Balance amount #{amt_val} specified for #{account_id} is not valid"
|
|
|
|
if amt.is_native()
|
|
xrp_balance = @record_xrp(account_id, amt)
|
|
else
|
|
@record_iou(account_id, amt)
|
|
@record_trust(account_id, amt, true)
|
|
|
|
@assert xrp_balance,
|
|
"No XRP balanced declared for #{account_id}"
|
|
|
|
undefined
|
|
|
|
compile_offers: ->
|
|
for account_id, account of @declaration.accounts
|
|
if not account.offers?
|
|
continue
|
|
for offer in account.offers
|
|
[pays, gets, splat...] = offer
|
|
gets_amt = @parse_amount gets
|
|
@assert gets_amt != null,
|
|
"For account #{account_id} taker_gets amount #{gets} is invalid"
|
|
|
|
pays_amt = @parse_amount pays
|
|
@assert pays_amt != null,
|
|
"For account #{account_id} taker_pays amount #{pays} is invalid"
|
|
|
|
offers = @ensure(account_id, @offers_by_ci)
|
|
offers = @ensure(account_id, @offers_by_ci)
|
|
offers_all = @ensure('offers', offers, [])
|
|
|
|
if gets_amt.is_native()
|
|
total = offers.xrp_total ?= new Amount.from_json('0')
|
|
new_total = total.add(gets_amt)
|
|
@assert @accounts[account_id].compareTo(new_total) != - 1,
|
|
"Account #{account_id}s doesn't have enough xrp to place #{offer}"
|
|
else
|
|
key = @amount_key gets_amt
|
|
|
|
if key.split('/')[1] != account_id
|
|
key_offers = @ensure(key, offers, {})
|
|
|
|
total = key_offers.total ?= Amount.from_json("0/#{key}")
|
|
new_total = total.add(gets_amt)
|
|
a_ious = @ensure(account_id, @ious)
|
|
@assert a_ious[key]?,
|
|
"Account #{account_id} doesn't hold any #{key}"
|
|
@assert a_ious[key].compareTo(new_total) != - 1,
|
|
"Account #{account_id} doesn't have enough #{key}"
|
|
" to place #{offer}"
|
|
|
|
key_offers.total = new_total
|
|
|
|
offers_all.push [pays_amt, gets_amt, splat...]
|
|
|
|
@offers = []
|
|
for account_id, obj of @offers_by_ci
|
|
for offer in obj.offers
|
|
sliced = offer[0..]
|
|
sliced.unshift account_id
|
|
@offers.push sliced
|
|
# @offers[account_id] = obj.offers
|
|
|
|
@offers
|
|
|
|
base_reserve: ->
|
|
@declaration.reserve?.base ? "200.0"
|
|
|
|
incr_reserve: ->
|
|
@declaration.reserve?.base ? "50.0"
|
|
|
|
check_reserves: ->
|
|
base_reserve_amt = @base_reserve()
|
|
incr_reserve_amt = @incr_reserve()
|
|
|
|
base_reserve = @parse_amount base_reserve_amt
|
|
inc_reserve = @parse_amount incr_reserve_amt
|
|
|
|
@assert base_reserve != null,
|
|
"Base reserve amount #{base_reserve_amt} is invalid"
|
|
|
|
@assert base_reserve != null,
|
|
"incremental amount #{incr_reserve_amt} is invalid"
|
|
|
|
for account_id, account of @declaration.accounts
|
|
total_needed = base_reserve.clone()
|
|
owner_count = 0
|
|
|
|
offers = @offers_by_ci[account_id]
|
|
if offers?
|
|
if offers.xrp_total?
|
|
total_needed = total_needed.add offers.xrp_total
|
|
if offers.offers?
|
|
owner_count += @offers_by_ci[account_id].offers.length
|
|
|
|
if @trusts_by_ci[account_id]?
|
|
owner_count += Object.keys(@trusts_by_ci[account_id]).length
|
|
|
|
owner_count_amount = Amount.from_json(String(owner_count))
|
|
inc_reserve_n = owner_count_amount.multiply(inc_reserve)
|
|
total_needed = total_needed.add(inc_reserve_n)
|
|
|
|
@assert @accounts[account_id].compareTo total_needed != - 1,
|
|
"Account #{account_id} needs more XRP for reserve"
|
|
|
|
@reserves[account_id] = total_needed
|
|
|
|
format_payments: ->
|
|
# We do these first as the following @ious need xrp to issue ious ;0
|
|
for account_id, xrps of @accounts
|
|
@xrp_payments.push ['root', account_id, xrps]
|
|
|
|
for account_id, ious of @ious
|
|
for curr_issuer, amt of ious
|
|
src = @realias_issuer amt.issuer().to_json()
|
|
dst = account_id
|
|
@iou_payments.push [src, dst, amt]
|
|
|
|
undefined
|
|
|
|
format_trusts: ->
|
|
for account_id, trusts of @trusts_by_ci
|
|
for curr_issuer, amt of trusts
|
|
@trusts.push [account_id, amt]
|
|
|
|
undefined
|
|
|
|
transactor: (fn, args_list, on_each, callback) ->
|
|
if args_list.length == 0
|
|
return callback()
|
|
|
|
if not callback?
|
|
callback = on_each
|
|
on_each = null
|
|
|
|
@assert callback?, "Must supply a callback"
|
|
finalized = {
|
|
n: args_list.length
|
|
one: ->
|
|
if --finalized.n <= 0
|
|
callback()
|
|
}
|
|
|
|
async.concatSeries(args_list, ((args, callback) =>
|
|
tx = @remote.transaction()
|
|
fn.apply(tx, args)
|
|
on_each?(args..., tx) # after payment() offer_create() etc so set_flags works
|
|
tx.on("proposed", (m) =>
|
|
@assert m.engine_result is "tesSUCCESS", "Transactor failure: #{@pretty_json m}"
|
|
callback()
|
|
).on('final', (m) =>
|
|
finalized.one()
|
|
)
|
|
.on("error", (m) =>
|
|
assert false, pretty_json m
|
|
).submit()
|
|
),
|
|
=> testutils.ledger_close @remote, ->
|
|
)
|
|
|
|
requester: (fn, args_list, on_each, callback, on_results) ->
|
|
if not callback?
|
|
callback = on_each
|
|
on_each = null
|
|
|
|
@assert callback?, "Must supply a callback"
|
|
|
|
async.concatSeries(args_list, ((args, callback) =>
|
|
req = fn.apply @remote, args
|
|
on_each?(args..., req)
|
|
req.on("success", (m) =>
|
|
if m.status?
|
|
@assert m.status is "success", "requester failure: #{@pretty_json m}"
|
|
callback(null, m)
|
|
).on("error", (m) =>
|
|
@assert false, @pretty_json m
|
|
).request()
|
|
),
|
|
(error, results_list) ->
|
|
on_results?(results_list);
|
|
callback()
|
|
)
|
|
|
|
ensure_config_has_test_accounts: ->
|
|
for account of @declaration.accounts
|
|
if not @config.accounts[account]?
|
|
acc = @config.accounts[account] = {}
|
|
user = new TestAccount(account)
|
|
acc.account = user.address
|
|
acc.secret = user.master_seed
|
|
# Index by nickname ...
|
|
@remote.set_secret account, acc.secret
|
|
# ... and by account ID
|
|
@remote.set_secret acc.account, acc.secret
|
|
@setup_issuer_realiaser()
|
|
|
|
pretty_json: (v) ->
|
|
@realias_issuer pretty_json(v)
|
|
|
|
constructor: (declaration, @assert, @remote, @config) ->
|
|
@declaration = declaration
|
|
@accounts = {} # {$account_id : $xrp_amt}
|
|
@trusts_by_ci = {} # {$account_id : {$currency/$issuer : $iou_amt}}
|
|
@ious = {} # {$account_id : {$currency/$issuer : $iou_amt}}
|
|
@offers_by_ci = {} # {$account_id : {offers: [], $currency/$issuer : {total: $iou_amt}}}
|
|
@reserves = {}
|
|
|
|
@xrp_payments = [] # {$account_id: []}
|
|
@trusts = [] # {$account_id: []}
|
|
@iou_payments = [] # {$account_id: []}
|
|
@offers = [] # {$account_id: []}
|
|
|
|
@ensure_config_has_test_accounts()
|
|
@compile_accounts_balances_and_implicit_trusts()
|
|
@compile_explicit_trusts()
|
|
@compile_offers()
|
|
@check_reserves()
|
|
@format_payments()
|
|
@format_trusts()
|
|
|
|
setup: (log, done) ->
|
|
LOG = (m) ->
|
|
self.what = m
|
|
log(m)
|
|
|
|
accounts = (k for k,ac of @accounts).sort()
|
|
@remote.set_account_seq(seq, 1) for seq in accounts.concat 'root' # <--
|
|
accounts_apply_arguments = ([ac] for ac, _ of @accounts)
|
|
self = this
|
|
|
|
async.waterfall [
|
|
(cb) ->
|
|
self.transactor(
|
|
Transaction::payment,
|
|
self.xrp_payments,
|
|
((src, dest, amt) ->
|
|
LOG("Account `#{src}` creating account `#{dest}` by
|
|
making payment of #{amt.to_text_full()}") ),
|
|
cb)
|
|
(cb) ->
|
|
self.transactor(
|
|
Transaction::ripple_line_set,
|
|
self.trusts,
|
|
((src, amt) ->
|
|
issuer = self.realias_issuer amt.issuer().to_json()
|
|
currency = amt.currency().to_json()
|
|
LOG("Account `#{src}` trusts account `#{issuer}` for
|
|
#{amt.to_text()} #{currency}") ),
|
|
cb)
|
|
(cb) ->
|
|
self.transactor(
|
|
Transaction::payment,
|
|
self.iou_payments,
|
|
((src, dest, amt, tx) ->
|
|
LOG("Account `#{src}` is making a payment of #{amt.to_text_full()}
|
|
to `#{dest}`") ),
|
|
cb)
|
|
(cb) ->
|
|
self.transactor(
|
|
Transaction::offer_create,
|
|
self.offers,
|
|
((src, pays, gets, tx) ->
|
|
tx.set_flags('Passive')
|
|
LOG("Account `#{src}` is selling #{gets.to_text_full()}
|
|
for #{pays.to_text_full()}")),
|
|
cb)
|
|
(cb) ->
|
|
testutils.ledger_close self.remote, cb
|
|
(cb) ->
|
|
self.requester(Remote::request_account_lines, accounts_apply_arguments,
|
|
((acc) ->
|
|
LOG("Checking account_lines for #{acc}")),
|
|
cb)
|
|
(cb) ->
|
|
self.requester(Remote::request_account_offers, accounts_apply_arguments,
|
|
((acc) ->
|
|
LOG("Checking account_offers for #{acc}")),
|
|
cb, (results) ->
|
|
|
|
for [ac], ix in accounts_apply_arguments
|
|
account = self.declaration.accounts[ac]
|
|
offers_declared = (account.offers ? []).length
|
|
actual = results[ix].offers
|
|
offers_made = actual.length
|
|
if offers_made != offers_declared
|
|
shortened = []
|
|
for offer in actual
|
|
keys = ['taker_pays', 'taker_gets']
|
|
pair = (Amount.from_json(offer[k]).to_text_full() for k in keys)
|
|
shortened.push pair
|
|
|
|
shortened_text = self.pretty_json shortened
|
|
self.assert offers_made == offers_declared,
|
|
"Account #{ac} has failed offer\n"+
|
|
"Declared: #{pretty_json account.offers}\n"+
|
|
"Actual: #{shortened_text}"
|
|
|
|
)
|
|
(cb) ->
|
|
self.requester(Remote::request_account_info, accounts_apply_arguments,
|
|
((acc) ->
|
|
LOG("Checking account_info for #{acc}")),
|
|
cb)
|
|
], (error) ->
|
|
assert !error,
|
|
"There was an error @ #{self.what}"
|
|
done() |