################################### 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` invocations 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.js' 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) -> """ @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) -> """ @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) -> 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: messages 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" options = { src_account: _src dst_account: _dst dst_amount: _amt, src_currencies: [{currency: pth.via}] } pf = self.remote.path_find(options) updates = 0 max_seen = 0 messages = {} propagates = propagater done 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++ # console.log 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 == 2 # "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)}" pf.close() 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 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) ) gather_path_definers = (path_expected) -> tests = [] for group, subgroup of path_expected for title, path of subgroup tests.push definer_factory(group, title, path) tests suite_factory = (declaration) -> -> context = null suiteSetup (done) -> context = @ @log_what = -> 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' try path_finding_cases = require('./path-tests-json') catch e console.log e # 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, "CNY test": paths_expected: BS: P101: src: "SRC", dst: "GATEWAY_DST", send: "10.1/CNY/GATEWAY_DST", via: "XRP", n_alternatives: 1 ledger: accounts: SRC: balance: ["4999.999898"] trusts: [] offers: [] GATEWAY_DST: balance: ["10846.168060"] trusts: [] offers: [] MONEY_MAKER_1: balance: ["4291.430036"] trusts: [] offers: [] MONEY_MAKER_2: balance: [ "106839375770" "0.0000000003599/CNY/MONEY_MAKER_1" "137.6852546843001/CNY/GATEWAY_DST" ] trusts: [ "1001/CNY/MONEY_MAKER_1" "1001/CNY/GATEWAY_DST" ] offers: [ [ "1000000" "1/CNY/GATEWAY_DST" # [] ] [ "1/CNY/GATEWAY_DST" "1000000" # [] ] [ "318000/CNY/GATEWAY_DST" "53000000000" # ["Sell"] ] [ "209000000" "4.18/CNY/MONEY_MAKER_2" # [] ] [ "990000/CNY/MONEY_MAKER_1" "10000000000" # ["Sell"] ] [ "9990000/CNY/MONEY_MAKER_1" "10000000000" # ["Sell"] ] [ "8870000/CNY/GATEWAY_DST" "10000000000" # ["Sell"] ] [ "232000000" "5.568/CNY/MONEY_MAKER_2" # [] ] ] A1: balance: [ # "240.997150" "1240.997150" "0.0000000119761/CNY/MONEY_MAKER_1" "33.047994/CNY/GATEWAY_DST" ] trusts: [ "1000000/CNY/MONEY_MAKER_1" "100000/USD/MONEY_MAKER_1" "10000/BTC/MONEY_MAKER_1" "1000/USD/GATEWAY_DST" "1000/CNY/GATEWAY_DST" ] offers: [] A2: balance: [ "14115.046893" "209.3081873019994/CNY/MONEY_MAKER_1" "694.6251706504019/CNY/GATEWAY_DST" ] trusts: [ "3000/CNY/MONEY_MAKER_1" "3000/CNY/GATEWAY_DST" ] offers: [ [ "2000000000" "66.8/CNY/MONEY_MAKER_1" # [] ] [ "1200000000" "42/CNY/GATEWAY_DST" # [] ] [ "43.2/CNY/MONEY_MAKER_1" "900000000" # ["Sell"] ] ] A3: balance: [ "512087.883181" "23.617050013581/CNY/MONEY_MAKER_1" "70.999614649799/CNY/GATEWAY_DST" ] trusts: [ "10000/CNY/MONEY_MAKER_1" "10000/CNY/GATEWAY_DST" ] offers: [[ "2240/CNY/MONEY_MAKER_1" "50000000000" # ["Sell"] ]] "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)