diff --git a/test/ledger-state.coffee b/test/ledger-state.coffee new file mode 100644 index 000000000..544202289 --- /dev/null +++ b/test/ledger-state.coffee @@ -0,0 +1,389 @@ +################################### REQUIRES ################################### + +# This gives coffee-script proper file/lines in the exceptions + +async = require("async") +assert = require 'assert' +Amount = require("ripple-lib").Amount +Remote = require("ripple-lib").Remote +Seed = require("ripple-lib").Seed +Base = require("ripple-lib").Base +Transaction = require("ripple-lib").Transaction +sjcl = require("ripple-lib").sjcl +Server = require("./server").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) + + get_address: (passphrase) -> + key_pair = Seed.from_json(passphrase).get_key() + pubKey = sjcl.codec.hex.toBits key_pair.to_hex_pub() + Base.encode_check(0,sjcl.codec.bytes.fromBits(@SHA256_RIPEMD160 pubKey)) + + constructor: (passphrase) -> + @passphrase = passphrase + @address = @get_address(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) -> + try + amt = Amount.from_json(amt_val) + if not amt.is_valid() + throw new Error() + catch e + try + amt = Amount.from_human(amt_val) + if not amt.is_valid() + throw new Error() + catch e + amt = null + amt + + amount_key: (amt) -> + currency = amt.currency().to_json() + issuer = @realias_issuer amt.issuer().to_json() + key = "#{currency}/#{issuer}" + key.issuer = issuer + key + + 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) + + 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}" + + 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" + + a_offers = @ensure(account_id, @offers_by_ci) + a_offers = @ensure(account_id, @offers_by_ci) + offers_all = @ensure('offers', a_offers, []) + + if gets_amt.is_native() + total = a_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 + key_offers = @ensure(key, a_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 + + base_reserve: -> + @declaration.reserve?.base ? "50.0" + + incr_reserve: -> + @declaration.reserve?.base ? "12.5" + + 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 + + a_offers = @offers_by_ci[account_id] + if a_offers? + if a_offers.xrp_total? + total_needed = total_needed.add a_offers.xrp_total + if a_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] + + format_trusts: -> + for account_id, trusts of @trusts_by_ci + for curr_issuer, amt of trusts + @trusts.push [account_id, amt] + + 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() + on_each?(args..., tx) + fn.apply(tx, args).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) -> + 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() + ).on("error", (m) => + @assert false, pretty_json m + ).request() + ), -> 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.passphrase + # 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 in @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) -> + 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_info, accounts_apply_arguments, + ((acc) -> + LOG("Checking account_info for #{acc}")), + cb) + ], (error) -> + assert !error, + "There was an error @ #{self.what}" + done() \ No newline at end of file diff --git a/test/mocha.opts b/test/mocha.opts index bc72ee58e..43cc8b14d 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1 @@ ---reporter spec --compilers coffee:coffee-script --ui tdd --timeout 10000 --slow 600 \ No newline at end of file +--reporter spec --compilers coffee:coffee-script --ui tdd --timeout 3000 --slow 600 \ No newline at end of file diff --git a/test/new-path-test.coffee b/test/new-path-test.coffee new file mode 100644 index 000000000..407d4cdb1 --- /dev/null +++ b/test/new-path-test.coffee @@ -0,0 +1,677 @@ +################################### REQUIRES ################################### + +extend = require 'extend' +fs = require 'fs' +async = require 'async' +deep_eq = require 'deep-equal' + +{Amount + Remote + Seed + Base + Transaction + PathFind + sjcl + UInt160} = require 'ripple-lib' + +testutils = require './testutils' +{Server} = require './server' +{LedgerState, TestAccount} = require './ledger-state' +{test_accounts} = require './random-test-addresses' + +simple_assert = require 'assert' + +#################################### README #################################### +""" +The tests are written in a declarative style: + + Each case has an entry in the `path_finding_cases` object + The key translates to a `suite(key, {...})` + The `{...}` passed in is compiled into a setup/teardown for the `ledger` and + into a bunch of `test` invokations for the `paths_expected` + + - aliases are used throughout for easier reading + + - test account addresses will be created `on the fly` + + no need to declare in testconfig.js + debugged responses from the server substitute addresses for aliases + + - The fixtures are setup just once for each ledger, multiple path finding + tests can be executed + + - `paths_expected` top level keys are group names + 2nd level keys are path test declarations + + test declaration keys can be suffixed meaningfully with + + `_skip` + `_only` + + test declaration values can set + + debug: true + + Will dump the path declaration and + translated request and subsequent response + + - hops in `alternatives[*][paths][*]` can be written in shorthand + eg. + ABC/G3|G3 + get `ABC/G3` through `G3` + + ABC/M1|M1 + get `ABC/M1` through `M1` + + XRP|$ + get `XRP` through `$` + $ signifies an order book rather than account + + ------------------------------------------------------------------------------ + Tests can be written in the 'path-tests.json' file in same directory # <-- + ------------------------------------------------------------------------------ +""" +#################################### HELPERS ################################### + +assert = simple_assert +refute = (cond, msg) -> assert(!cond, msg) +prettyj = pretty_json = (v) -> JSON.stringify(v, undefined, 2) + +propagater = (done) -> + (f) -> + -> + return if done.aborted + try + f(arguments...) + catch e + done.aborted = true + throw e + +assert_match = (o, key_vals, message) -> + """ + assert_match path[i], matcher, + "alternative[#{ai}].paths[#{pi}]" + """ + + for k,v of key_vals + assert.equal o[k], v, message + +#################################### CONFIG #################################### + +config = testutils.init_config() + +############################### ALTERNATIVES TEST ############################## + +expand_alternative = (alt) -> + """ + + Make explicit the currency and issuer in each hop in paths_computed + + """ + amt = Amount.from_json(alt.source_amount) + + for path in alt.paths_computed + prev_issuer = amt.issuer().to_json() + prev_currency = amt.currency().to_json() + + for hop, hop_i in path + if not hop.currency? + hop.currency = prev_currency + + if not hop.issuer? and hop.currency != 'XRP' + if hop.account? + hop.issuer = hop.account + else + hop.issuer = prev_issuer + + if hop.type & 0x10 + prev_currency = hop.currency + + if hop.type & 0x20 + prev_issuer = hop.issuer + else if hop.account? + prev_issuer = hop.account + + return alt + +create_shorthand = (alternatives) -> + """ + + Convert explicit paths_computed into the format used by `paths_expected` + These can be pasted in as the basis of tests. + + """ + shorthand = [] + + for alt in alternatives + short_alt = {} + shorthand.push short_alt + + amt = Amount.from_json alt.source_amount + if amt.is_native() + short_alt.amount = amt.to_human() + if not (~short_alt.amount.search('.')) + short_alt.amount = short_alt.amount + '.0' + else + short_alt.amount = amt.to_text_full() + + short_alt.paths = [] + + for path in alt.paths_computed + short_path = [] + short_alt.paths.push short_path + + for node in path + hop = node.currency + hop = "#{hop}/#{node.issuer}" if node.issuer? + hop = "#{hop}|#{if node.account? then node.account else "$"}" + short_path.push hop + + return shorthand + +ensure_list = (v) -> + if Array.isArray(v) + v + else + [v] + +test_alternatives_factory = (realias_pp, realias_text) -> + """ + + We are using a factory to create `test_alternatives` because it needs the + per ledger `realias_*` functions + + """ + hop_matcher = (decl_hop) -> + [ci, f] = decl_hop.split('|') + if not f? + throw new Error("No `|` in #{decl_hop}") + + [c, i] = ci.split('/') + is_account = if f == '$' then false else true + matcher = currency: c + matcher.issuer = i if i? + matcher.account = f if is_account + matcher + + match_path = (test, path, ai, pi) -> + test = (hop_matcher(hop) for hop in test) + assert.equal path.length, test.length, + "alternative[#{ai}] path[#{pi}] expecting #{test.length} hops" + + for matcher, i in test + assert_match path[i], matcher, + "alternative[#{ai}].paths[#{pi}]" + return + + simple_match_path = (test, path, ai, pi) -> + """ + + Params + @test + + A shorthand specified path + + @path + + A path as returned by the server with `expand_alternative` done + so issuer and currency are always stated. + + """ + test = (hop_matcher(hop) for hop in test) + return false if not test.length == path.length + + for matcher, i in test + for k, v of matcher + return false if not path[i]? + if path[i][k] != v + return false + true + + amounts = -> + (Amount.from_json a for a in arguments) + + amounts_text = -> + (realias_text a.to_text_full() for a in arguments) + + check_for_no_redundant_paths = (alternatives) -> + for alt, i in alternatives + existing_paths = [] + for path in alt.paths_computed + for existing in existing_paths + assert !(deep_eq path, existing), + "Duplicate path in alternatives[#{i}]\n"+ + "#{realias_pp alternatives[0]}" + + existing_paths.push path + return + + test_alternatives = (test, actual, error_context) -> + """ + + Params: + @test + alternatives in shorthand format + + @actual + alternatives as returned in a `path_find` response + + @error_context + + a function providing a string with extra context to provide to assertion + messages + + """ + check_for_no_redundant_paths actual + + for t, ti in ensure_list(test) + a = actual[ti] + [t_amt, a_amt] = amounts(t.amount, a.source_amount) + [t_amt_txt, a_amt_txt] = amounts_text(t_amt, a_amt) + + # console.log typeof t_amt + + assert t_amt.equals(a_amt), + "Expecting alternative[#{ti}].amount: "+ + "#{t_amt_txt} == #{a_amt_txt}" + + t_paths = ensure_list(t.paths) + + tn = t_paths.length + an = a.paths_computed.length + assert.equal tn, an, "Different number of paths specified for alternative[#{ti}]"+ + ", expected: #{prettyj t_paths}, "+ + "actual(shorthand): #{prettyj create_shorthand actual}"+ + "actual(verbose): #{prettyj a.paths_computed}"+ + error_context() + + for p, i in t_paths + matched = false + + for m in a.paths_computed + if simple_match_path(p, m, ti, i) + matched = true + break + + assert matched, "Can't find a match for path[#{i}]: #{prettyj p} "+ + "amongst #{prettyj create_shorthand [a]}"+ + error_context() + return + +################################################################################ + +create_path_test = (pth) -> + return (done) -> + propagates = propagater done + + self = this + WHAT = self.log_what + ledger = self.ledger + test_alternatives = test_alternatives_factory ledger.pretty_json.bind(ledger), + ledger.realias_issuer + + + WHAT "#{pth.title}: #{pth.src} sending #{pth.dst}, "+ + "#{pth.send}, via #{pth.via}" + + one_message = (f) -> + self.remote._servers[0].once 'before_send_message_for_non_mutators', f + + sent = "TODO: need to patch ripple-lib" + one_message (m) -> sent = m + + error_info = (m, more) -> + info = path_expected: pth, path_find_request: sent, path_find_updates: m + extend(info, more) if more? + ledger.pretty_json(info) + + assert Amount.from_json(pth.send).is_valid(), + "#{pth.send} is not valid Amount" + + _src = UInt160.json_rewrite(pth.src) + _dst = UInt160.json_rewrite(pth.dst) + _amt = Amount.from_json(pth.send) + + # self.server.clear_logs() "TODO: need to patch ripple-lib" + pf = self.remote.path_find(_src, _dst, _amt, [{currency: pth.via}]) + + updates = 0 + max_seen = 0 + messages = {} + + pf.on "error", propagates (m) -> # <-- + assert false, "fail (error): #{error_info(m)}" + done() + + pf.on "update", propagates (m) -> # <-- + # TODO:hack: + expand_alternative alt for alt in m.alternatives + + messages[if updates then "update-#{updates}" else 'initial-response'] = m + updates++ + + assert m.alternatives.length >= max_seen, + "Subsequent path_find update' should never have less " + + "alternatives:\n#{ledger.pretty_json messages}" + + max_seen = m.alternatives.length + + if updates == 2 + testutils.ledger_close(self.remote, -> ) + + if updates == 3 + # "TODO: need to patch ripple-lib" + # self.log_pre(self.server.get_logs(), "Server Logs") + + if pth.do_send? + do_send( (ledger.pretty_json.bind ledger), WHAT, self.remote, pth, + messages['update-2'], done ) + + if pth.debug + console.log ledger.pretty_json(messages) + console.log error_info(m) + console.log ledger.pretty_json create_shorthand(m.alternatives) + + if pth.alternatives? + # We realias before doing any comparisons + alts = ledger.realias_issuer(JSON.stringify(m.alternatives)) + alts = JSON.parse(alts) + test = pth.alternatives + + assert test.length == alts.length, + "Number of `alternatives` specified is different: "+ + "#{error_info(m)}" + + if test.length == alts.length + test_alternatives(pth.alternatives, alts, -> error_info(m)) + + if pth.n_alternatives? + assert pth.n_alternatives == m.alternatives.length, + "fail (wrong n_alternatives): #{error_info(m)}" + + done() if not pth.do_send? + +################################ SUITE CREATION ################################ + +skip_or_only = (title, test_or_suite) -> + endsWith = (s, suffix) -> + ~s.indexOf(suffix, s.length - suffix.length) + + if endsWith title, '_only' + test_or_suite.only + else if endsWith title, '_skip' + test_or_suite.skip + else + test_or_suite + +gather_path_definers = (path_expected) -> + tests = [] + + for group, subgroup of path_expected + for title, path of subgroup + definer_factory = (group, title, path) -> + path.title = "#{[group, title].join('.')}" + test_func = skip_or_only path.title, test + -> + test_func(path.title, create_path_test(path) ) + + tests.push definer_factory(group, title, path) + tests + +suite_factory = (declaration) -> + -> + context = null + + suiteSetup (done) -> + context = @ + @log_what = -> + + @uniquely_gifted = 'yes' + + testutils.build_setup().call @, -> + context.ledger = new LedgerState(declaration.ledger, + assert, + context.remote, + config) + + context.ledger.setup(context.log_what, done) + + suiteTeardown (done) -> + testutils.build_teardown().call context, done + + for definer in gather_path_definers(declaration.paths_expected) + definer() + +define_suites = (path_finding_cases) -> + for case_name, declaration of path_finding_cases + suite_func = skip_or_only case_name, suite + suite_func case_name, suite_factory(declaration) + +############################## PATH FINDING CASES ############################## +# Later we reference A0, the `unknown account`, directly embedding the full +# address. +A0 = (new TestAccount('A0')).address +assert A0 == 'rBmhuVAvi372AerwzwERGjhLjqkMmAwxX' + +path_finding_cases_string = fs.readFileSync(__dirname + "/path-tests.json") +path_finding_cases = JSON.parse path_finding_cases_string + +# You need two gateways, same currency. A market maker. A source that trusts one +# gateway and holds its currency, and a destination that trusts the other. + +extend path_finding_cases, + "Path Tests (Bitstamp + SnapSwap account holders | liquidity provider with no offers)": + ledger: + accounts: + G1BS: + balance: ["1000.0"] + G2SW: + balance: ["1000.0"] + A1: + balance: ["1000.0", "1000/HKD/G1BS"] + trusts: ["2000/HKD/G1BS"] + A2: + balance: ["1000.0", "1000/HKD/G2SW"] + trusts: ["2000/HKD/G2SW"] + M1: + # SnapSwap wants to be able to set trust line quality settings so they + # can charge a fee when transactions ripple across. Liquitidy + # provider, via trusting/holding both accounts + balance: ["11000.0", + "1200/HKD/G1BS", + "5000/HKD/G2SW" + ] + trusts: ["100000/HKD/G1BS", "100000/HKD/G2SW"] + # We haven't got ANY offers + + paths_expected: { + BS: + P1: + debug: false + src: "A1", dst: "A2", send: "10/HKD/A2", via: "HKD" + n_alternatives: 1 + P2: + debug: false + src: "A2", dst: "A1", send: "10/HKD/A1", via: "HKD" + n_alternatives: 1 + P3: + debug: false + src: "G1BS", dst: "A2", send: "10/HKD/A2", via: "HKD" + alternatives: [ + amount: "10/HKD/G1BS", + paths: [["HKD/M1|M1", "HKD/G2SW|G2SW"]] + ] + P5: + debug: false + src: "M1", + send: "10/HKD/M1", + dst: "G1BS", + via: "HKD" + P4: + debug: false + src: "G2SW", send: "10/HKD/A1", dst: "A1", via: "HKD" + alternatives: [ + amount: "10/HKD/G2SW", + paths: [["HKD/M1|M1", "HKD/G1BS|G1BS"]] + ] + } + "Path Tests #4 (non-XRP to non-XRP, same currency)": { + ledger: + accounts: + G1: balance: ["1000.0"] + G2: balance: ["1000.0"] + G3: balance: ["1000.0"] + G4: balance: ["1000.0"] + A1: + balance: ["1000.0", "1000/HKD/G1"] + trusts: ["2000/HKD/G1"] + A2: + balance: ["1000.0", "1000/HKD/G2"] + trusts: ["2000/HKD/G2"] + A3: + balance: ["1000.0", "1000/HKD/G1"] + trusts: ["2000/HKD/G1"] + A4: + balance: ["10000.0"] + M1: + balance: ["11000.0", "1200/HKD/G1", "5000/HKD/G2"] + trusts: ["100000/HKD/G1", "100000/HKD/G2"] + offers: [ + ["1000/HKD/G1", "1000/HKD/G2"] + ] + M2: + balance: ["11000.0", "1200/HKD/G1", "5000/HKD/G2"] + trusts: ["100000/HKD/G1", "100000/HKD/G2"] + offers: [ + ["10000.0", "1000/HKD/G2"] + ["1000/HKD/G1", "10000.0"] + ] + + paths_expected: { + T4: + "A) Borrow or repay": + comment: 'Source -> Destination (repay source issuer)' + src: "A1", send: "10/HKD/G1", dst: "G1", via: "HKD" + alternatives: [amount: "10/HKD/A1", paths: []] + + "A2) Borrow or repay": + comment: 'Source -> Destination (repay destination issuer)' + src: "A1", send: "10/HKD/A1", dst: "G1", via: "HKD" + alternatives: [amount: "10/HKD/A1", paths: []] + + "B) Common gateway": + comment: 'Source -> AC -> Destination' + src: "A1", send: "10/HKD/A3", dst: "A3", via: "HKD" + alternatives: [amount: "10/HKD/A1", paths: [["HKD/G1|G1"]]] + + "C) Gateway to gateway": + comment: 'Source -> OB -> Destination' + src: "G1", send: "10/HKD/G2", dst: "G2", via: "HKD" + debug: false + alternatives: [ + amount: "10/HKD/G1" + paths: [["HKD/M2|M2"], + ["HKD/M1|M1"], + ["HKD/G2|$"] + ["XRP|$", "HKD/G2|$"] + ] + ] + + "D) User to unlinked gateway via order book": + comment: 'Source -> AC -> OB -> Destination' + src: "A1", send: "10/HKD/G2", dst: "G2", via: "HKD" + debug: false + alternatives: [ + amount: "10/HKD/A1" + paths: [ + ["HKD/G1|G1", "HKD/G2|$"], # <-- + ["HKD/G1|G1", "HKD/M2|M2"], + ["HKD/G1|G1", "HKD/M1|M1"], + ["HKD/G1|G1", "XRP|$", "HKD/G2|$"] + ] + ] + + "I4) XRP bridge": + comment: 'Source -> AC -> OB to XRP -> OB from XRP -> AC -> Destination' + src: "A1", send: "10/HKD/A2", dst: "A2", via: "HKD" + debug: false + alternatives: [ + amount: "10/HKD/A1", + paths: [ + # Focus + ["HKD/G1|G1", "HKD/G2|$", "HKD/G2|G2" ], + ["HKD/G1|G1", "XRP|$", "HKD/G2|$", "HKD/G2|G2"], # <-- + # Incidental + ["HKD/G1|G1", "HKD/M1|M1", "HKD/G2|G2"], + ["HKD/G1|G1", "HKD/M2|M2", "HKD/G2|G2"] + ] + ] + + } + }, + "Path Tests #2 (non-XRP to non-XRP, same currency)": { + ledger: + accounts: + G1: balance: ["1000.0"] + G2: balance: ["1000.0"] + A1: + balance: ["1000.0", "1000/HKD/G1"] + trusts: ["2000/HKD/G1"] + A2: + balance: ["1000.0", "1000/HKD/G2"] + trusts: ["2000/HKD/G2"] + A3: + balance: ["1000.0"] + trusts: ["2000/HKD/A2"] + M1: + balance: ["11000.0", "5000/HKD/G1", "5000/HKD/G2"] + trusts: ["100000/HKD/G1", "100000/HKD/G2"] + offers: [ + ["1000/HKD/G1", "1000/HKD/G2"] + # ["2000/HKD/G2", "2000/HKD/G1"] + # ["2000/HKD/M1", "2000/HKD/G1"] + # ["100.0", "1000/HKD/G1"] + # ["1000/HKD/G1", "100.0"] + ] + + paths_expected: { + T4: + "E) Gateway to user": + ledger: false + comment: 'Source -> OB -> AC -> Destination' + # comment: 'Gateway -> OB -> Gateway 2 -> User' + src: "G1", send: "10/HKD/A2", dst: "A2", via: "HKD" + debug: false + alternatives: [ + amount: "10/HKD/G1" + paths: [ + ["HKD/G2|$", "HKD/G2|G2"], + ["HKD/M1|M1", "HKD/G2|G2"] + ] + ] + + "F) Different gateways, ripple _skip": + comment: 'Source -> AC -> AC -> Destination' + + "G) Different users of different gateways, ripple _skip": + comment: 'Source -> AC -> AC -> AC -> Destination' + + "H) Different gateways, order book _skip": + comment: 'Source -> AC -> OB -> AC -> Destination' + + "I1) XRP bridge _skip": + comment: 'Source -> OB to XRP -> OB from XRP -> Destination' + src: "A4", send: "10/HKD/G2", dst: "G2", via: "XRP" + debug: true + + "I2) XRP bridge _skip": + comment: 'Source -> AC -> OB to XRP -> OB from XRP -> Destination' + + "I3) XRP bridge _skip": + comment: 'Source -> OB to XRP -> OB from XRP -> AC -> Destination' + } + } + +################################# DEFINE SUITES ################################ + +define_suites(path_finding_cases) \ No newline at end of file diff --git a/test/path-tests.json b/test/path-tests.json new file mode 100644 index 000000000..765b0a815 --- /dev/null +++ b/test/path-tests.json @@ -0,0 +1,88 @@ +{ + "Path Tests #1 (XRP -> XRP) and #2 (XRP -> IOU)": { + + "ledger": {"accounts": {"A1": {"balance": ["100000.0", + "3500/XYZ/G1", + "1200/ABC/G3"], + "trusts": ["5000/XYZ/G1", + "5000/ABC/G3"]}, + "A2": {"balance": ["10000.0"], + "trusts": ["5000/XYZ/G2", + "5000/ABC/G3"]}, + "A3": {"balance": ["1000.0"], + "trusts": ["1000/ABC/A2"]}, + "G1": {"balance": ["1000.0"]}, + "G2": {"balance": ["1000.0"]}, + "G3": {"balance": ["1000.0"]}, + "M1": {"balance": ["1000.0", + "25000/XYZ/G2", + "25000/ABC/G3"], + "offers": [["1000/XYZ/G1", + "1000/XYZ/G2"], + ["10000.0", + "1000/ABC/G3"]], + "trusts": ["100000/XYZ/G1", + "100000/ABC/G3", + "100000/XYZ/G2"]}}}, + + "paths_expected": {"T1": {"A1": {"n_alternatives": 0, + "src": "A1", + "send": "10.0", + "dst": "A2", + "via": "XRP"}, + "A2": {"comment": "Send to non existing account", + "src": "A1", + "send_comment": "malformed error not great for 10.0 amount", + "send": "200.0", + "dst": "rBmhuVAvi372AerwzwERGjhLjqkMmAwxX", + "via": "XRP", + "n_alternatives": 0}}, + "T2": {"A": {"alternatives": [{"amount": "100.0", + "paths": [ + ["ABC/G3|$"] + ]}], + "src": "A2", + "send": "10/ABC/G3", + "dst": "G3", + "via": "XRP", + "debug": 0, + "n_alternatives": 1}, + "B": {"alternatives": [{"amount": "10.0", + "paths": [["ABC/G3|$", + "ABC/G3|G3"]]}], + "src": "A1", + "send": "1/ABC/A2", + "dst": "A2", + "via": "XRP", + "n_alternatives": 1}, + "C": {"alternatives": [{"amount": "10.0", + "paths": [["ABC/G3|$", + "ABC/G3|G3", + "ABC/A2|A2"]]}], + "src": "A1", + "send": "1/ABC/A3", + "dst": "A3", + "via": "XRP", + "n_alternatives": 1}}}}, + "Path Tests #3 (non-XRP to XRP)": { + + "ledger": {"accounts": {"A1": {"balance": ["1000.0", + "1000/ABC/G3"]}, + "A2": {"balance": ["1000.0", + "1000/ABC/G3"]}, + "G3": {"balance": ["1000.0"]}, + "M1": {"balance": ["11000.0", + "1200/ABC/G3"], + "offers": [["1000/ABC/G3", + "10000.0"]], + "trusts": ["100000/ABC/G3"]}}}, + + "paths_expected": {"T3": {"A": {"alternatives": [{"amount": "1/ABC/A1", + "paths": [["ABC/G3|G3", + "XRP|$"]]}], + "src": "A1", + "dst": "A2", + "debug":false, + "send": "10.0", + "via": "ABC"}}}} +} diff --git a/test/random-test-addresses.coffee b/test/random-test-addresses.coffee new file mode 100644 index 000000000..7d58fbf75 --- /dev/null +++ b/test/random-test-addresses.coffee @@ -0,0 +1,153 @@ +exports.test_accounts = [ + 'rBmhuVAvi372AerwzwERGjhLjqkMmAwxX', + 'r4nmQNH4Fhjfh6cHDbvVSsBv7KySbj4cBf', + 'rGpeQzUWFu4fMhJHZ1Via5aqFC3A5twZUD', + 'rrnsYgWn13Z28GtRgznrSUsLfMkvsXCZSu', + 'rJsaPnGdeo7BhMnHjuc3n44Mf7Ra1qkSVJ', + 'rnYDWQaRdMb5neCGgvFfhw3MBoxmv5LtfH', + 'r31PEiKfa3y6xTi7uBcSp7F3nDLvVMmqyi', + 'rfM5xD2CY6XB8o1WsWoJ3ZHGkbHU4NYXr', + 'r9MB1RNWZChfV3YdLrB3Rm5AoMULewDtiu', + 'rH15iZg9KFSi7d1usvcsPerUtg7dhpMbk4', + 'rGWYwGaczQWiduWkccFZKXfp5nDRPqNBNS', + 'rBU1EP5oMwKxWr1gZnNe7K8GouQTBhzUKs', + 'rwiTxuknPNeLDYHHLgajRVetKEEwkYhTaQ', + 'rEbq9pWn2knXFTjjuoNNrKgQeGxhmispMi', + 'rJfBCsnwSHXjTJ4GH5Ax6Kyw48X977hqyq', + 'rado7qRcvPpS8ZL8SNg4SG8kBNksHyqoRa', + 'rNgfurDhqvfsVzLr5ZGB3dJysJhRkvJ79F', + 'rBtVTnNgX3uR3kyfVyaQ6hjZTdk42ay9Z3', + 'rND1XyLAU9G2ydhUgmRo4i2kdrSKgYZc31', + 'rHd21p9Gb834Ri4pzRzGFJ7PjRzymWuBWu', + 'rhfvFTgpPzCvFPgbA9Nvz42mX92U5ak92m', + 'rByT8y4BUX1Kcc6xotEabCgwc5PGsbTfSv', + 'rPBrMbL7uGuteU49b2ibcEBSztoWPX4srr', + 'rQGRcZn2RyXJL3s7Dfqqzc96Juc7j9j6i9', + 'rwZrLewGghBMj29kFq7TPw9h9p5eAQ1LUp', + 'rM9qHXk5uifboWrpu9Wte6Gjzbe974nZ4z', + 'rBt69hdwMeBgmAtM9YwuFAKxjMqgaBLf3F', + 'rHpcrpggafr5NNn7mafPhaeB8PMYKXGahp', + 'rsMKP5MSoyve54o7LgwnZzFfGKzAK8SE5F', + 'rfN3ccsNxPt41MjHNZRk7ek7q4TpPLqUzL', + 'rDRWocPjBdhKZSWZezbsnwNpALcJ6GqSGf', + 'rsmJ4tEMWpcK2qcEMp9uoUv4Ht5Nd6cGTV', + 'rsxWsnMVRnFozrKJV2VZ1SG6UNbEeHYu16', + 'r4KoiD6MpaQNPzBka3FRLREkx6EZFwynY4', + 'rUdovjorVqxyemu5jbpfqA6DYDLdD4eYcj', + 'r4MrNttmbdiJ7DjWh1MCKW3Kh7kfML46TA', + 'rndYw73Btcm9Pv9gssZY1S9UcDUPLnpip7', + 'rh8MnoZmAeWyLx7X8bJZqyjZ48mv1og5PS', + 'rBoJvU7pcvoy5hjDMMTDNVG4YG85Ed3MEq', + 'rs4f1BwdNgXAHWLT8rZgW2T1RKSBNY4iDz', + 'rEmhxShqw42EPm7bY7df5uQySZBkQWnqae', + 'rNerRdGnbZP6wej22zBdoTUfQKWoMDTH7d', + 'rDyXvd2WFALJovh76uLe5kUrJ7QLpgmQYE', + 'rUVi1L28AsCvieXP5pMqPHA9WAfsvCDUjU', + 'rscuoJN9um2VM4xVv386X5T9APtExFKsbB', + 'raeeyPs6g5xQn5jyNQbCZ6QeLrqu3FrFvb', + 'r9UqovJD979WTfNEWXxDU2CVj3K1yo2mqG', + 'rfRjsAqM1MEuSbWzuLcD6EhSZazgqvZSjy', + 'rUL4CAxmfpNqDXsQTPCK9ZJ8zHqhUvDWfw', + 'rP6ZRDFZxjQqeAgdBh1YQSQjWNSASpCL7N', + 'rsV4AtAqsdyRyZ8s4kaWbM21EPwY5fonx5', + 'rHaKEMyJErGY6VaKuTj16fSheTp4BRpWG1', + 'rELHJtahsRpSiSj1nfkY5yKRHCCyRgynw4', + 'rLYtaGnw4xK86J6mTsLfayyREoYaPPr8Cj', + 'rD5pAYUfZypmJRSrJnBy3pYo5ApHqw5Jt5', + 'rfYQrqwNXoA8e2gBDmiHAJAMYrASdQvqDm', + 'rESt1CB9Sqaj8PYj8SV9x76iwMGBFPzLHb', + 'rHZWAXh3NdQbyksKzDRLeP9ui32TcqssHZ', + 'rK9iNjw5SozqKj5zNervwQQTLAgu8V813j', + 'rUjpFBSmZ8F6cP16VxqpdAXCVCW3rBSZyn', + 'raPib2vNQAjhh47fVQ7PswKaX1daNBSs2G', + 'rwhuqz7FppLNvLWdxs7TLLW9UDVztFbw9z', + 'rJYRe27KXWTjs4P3uu1d4x58Pk5Y13DbUg', + 'rLFxCuE2GHq38wFUHpswgHJAcz6EUhPimC', + 'rAaQrzi5satsth174EogwdtdxLZRW5n1h', + 'rB18Rxdv1aPYtf9nDFpNPJ2HA5BBAqmyoG', + 'rDSaTM6nCSrc1vH8pPcTAwQpmvb9Y6M2gw', + 'rpmeCBJUpp9ij1nRM23tRGesWjY7chSHqs', + 'rwQz7ZkCGdQt7iiuWC3EpbbKwWdL7uLT9C', + 'rULwRizwxjBwDjcaA44Tbh9MjM5TZFTcLj', + 'rGMZBEGHbSoegfvqXC2ajXe7RM2Z1LK65N', + 'rGyFSppE8G7cELAdBU5yL7kWodbw29YdpN', + 'rJYN3qZsjWhH6bzXX6ZHMZewkMPHeEyGNb', + 'rEgZVpVSs75eEh6KM4HvcvG64p2TZBL4DC', + 'rBRfZaqSAkXZYQWBfoy4sN2Q7zZHVGiGwU', + 'rwg1DRGXLTzQpoWtS35mDowN4PSFQ732eQ', + 'rKZvkY3T6ahhqkWLTQDSdB1MTKFbbnkqBX', + 'rGXgpwvaAZ7rBmoSKFUrd83N7WN3Lm4vuX', + 'rhCMsQ3SJa5Wb4AvBe27hxBQaQQjDG1LG4', + 'rNtvcU3ePYpZnuYKG77pjRxtKJJ1yrutbm', + 'rsfK2cTikveAeyvSG8F62c4VFUvZBsH5Rf', + 'rJT2LrXe7hH1pMEnhEkCMznwtgKuYJS7uz', + 'rE4Fi4GVjo9NY2g6MtMbitjenUZ21zFoSG', + 'rp39zV6AFPRJ7yrrL1PSPC1s3oKM2uv2iW', + 'raCoTW3mhdK6WGUZUSEuvbFM34CSGRRHut', + 'rKZD9yCV7XAgKq3Rj3a5DmqHHkHBd3ZYao', + 'rfKtpLEQz8bGCVtQsEzD8cJKeo6AW6J2pD', + 'rJqNyWJ3rovwkWFdwPhCc6m3jpFogGzRr9', + 'r41fiShunXNNgJjTjqU9whsjnSYU1BXsjY', + 'r3uHcWgsNwowCBGF5rhCP5dfdjpYmByBbJ', + 'r4GZm8WnwX5E9cr8uGiLX42y7KN2NaLqYn', + 'rKBVsMWErdH443FUkaT799CRVyY9XnVyCK', + 'razu52accAWWHjxhWEHXNLHWCDhhs1L79p', + 'rHaL5e3niiikv6KJG4UARqpcjyD5tKNgyV', + 'rMfcPdGcB5y9zEYw91t3QG2Qj1Z7tqms3S', + 'r3mqtPNiwkLKisvbnFjM9eC8Rm9JUEXLMD', + 'rKJGLaJr5SFjZ43BkDeqWKWAtGy1qAAkPf', + 'rM2zQeTDrt6sMM936Gxh263t1qx3hEb7XK', + 'rGtqoQJGe4zWenC3yWH9pByTAsGPcjmTU2', + 'raNUsR5m8jsmb9o9mpNZGGyV86aZUYPunr', + 'rManQrKu85ezooZ11UmXbxejw5EYHqiUZm', + 'rKrjCBtwnhQeYkJKrVjDA5CaR5W9NPN2Bb', + 'rcwyaWqsvGXMRyVW2KHnGUV2MhJVhVx2p', + 'rLozhAmzcwtEgr1bA28GUh2kbf5kFBMhph', + 'rJqt8XVcGdpfjZmujoTc6ArHQdZZVhADgM', + 'rPthXZ93cGAtHJrnAF58vZz6E1js8o6fVf', + 'rL1LH68PG93BuAeLsiM4cxeBtFmxsoGhRB', + 'rndqHX6xLknzbgmQPXL4DvhNLRANYf8cDA', + 'rNEnd2tV3Z1aL5kQmShmHGM3uciSJ4jakF', + 'rGqPPWnLVWDwWXf495qdxp4um1BTw8TBh5', + 'rhEDG4mzWGXjjfQp4WMCpZPBDyug5ays9e', + 'raR6MJ2dYxj1PECUKnLbeamM1k8FnYsY34', + 'rDcC1S6TRNgTMTgpuSzhAZdTdQJ5EHVf8X', + 'rLHtg3bWtMKNDYXydd7frGB5pvFbtqzTjg', + 'rnHhh7qXu2trXuC8aD3Zm7gvM9YetRkEDB', + 'rfwwUrJztrR7PeKzELEoC1cjcBpsqHmxws', + 'rw4KzQgZwPJDYX4wHc3NgsLjdR12JGAKsF', + 'rPCbN5kfEjNgP7TBN3VDJf8eUhv1zCreRL', + 'r32W6FTsguE3hMv3h79eoNVSJutsDDw6rH', + 'rBSDvMzyxsea8Zgy8EfryZ7cc1QGrVp9Do', + 'rfz1TPVJPQYbpSxi6oTiWyhNqy4J1Vz7VL', + 'rKCyiG5sKxqmKoT2NGT9zS86gS26m9HrQa', + 'rK7WRdHd6aNqCu2tNbebxYrQkP5u2DouS3', + 'rLC3xWV4N3ohGxxzcueDrxPLHNqbXrmpUK', + 'rDE2MwprS1vFvknSKLZJfmxKmbVuE2F85', + 'r91o1Huejw2qqz37b22Ev8igM4V2Rog1jv', + 'rEhGRU4P7pVjuxco5BgoXz1974QESKQ1Wy', + 'raVZ3Uk3qmKqe5aV1ifQsRFL6gU7m25Y2L', + 'r9p5buUwdMmZ5Um5uK5gC9piRd1fBzMPBN', + 'r4ruPrbfwnkp5aDSNzQXnWcEK64hJGRnDG', + 'rwsHCfEwi3PzsfcpcaWLbHXZ2zG5FtNX1J', + 'r4DkAh7hbTX4E8JPp4kAWkQYazW6236dBB', + 'rGZSqs5KHQKEJMCaDS3iyWagymixa9zuCv', + 'r9Nn5Le1bourefaZJ79K2h5VnREFpTqjUw', + 'rh7NSNpXR9mwWFF8uXvaWjcLVRPfxqQfM9', + 'rpdxPDQ8mV8gadRcDBh7kNFj1mxYjfddh5', + 'rMHxgnXgo68vDLUj327YtsHjQyiHGjeccj', + 'raDd7xZ3RYvKcGiojy9h5UqFV7WULv626i', + 'rEHzCw3nAuHj66C3xKnGFxV6zDBAssNdSY', + 'rfvWW9qgyNKYVuJ212himJ7mt6fvkUaS7E', + 'r9ctoegJfquoj1RHo1bxuW9MjWhbjHyZSA', + 'rsNPHLq4CgxGoenX67jpuabKe8hx1QvLZk', + 'rLZouhBz2CpT3ULfJjt193gsLNR4TxjdUS', + 'rKuUjCThS3DBqfDAupxTvyhkWaskxHoWSP', + 'rKwSstBmYbKFM3CFw8hvBqsU8pfcZMgNwA', + 'rBN8iGgpbCc6KYNE36D67TeS6t1YXwmTQc', + 'rGvBvCyqwn5kPuRUErGD7idtKRb7LtptrS', + 'r3h89HbdaYJBVd1wfHV6GxDj6Pf2s5iXnF', + 'rfbUVWFke9JzbK6BhkuayCgX2MVoJvgxBk', + 'rwTXRBQvPKPMskjp9By1kLJ1pPdRAg77Rz', + 'rw4ve76xnh8cPwVpE58i46s39MbQ7x1m5W', + 'r32XzGdUP3GuNJXDGxFM2cYkw7K1KrwXdt' ] \ No newline at end of file