diff --git a/libraries/app/database_api.cpp b/libraries/app/database_api.cpp index 3f95a8c1..4fce2d6d 100644 --- a/libraries/app/database_api.cpp +++ b/libraries/app/database_api.cpp @@ -113,8 +113,15 @@ class database_api_impl : public std::enable_shared_from_this vector get_unmatched_bets_for_bettor(betting_market_id_type, account_id_type) const; vector get_all_unmatched_bets_for_bettor(account_id_type) const; + const account_object* get_account_from_string( const std::string& name_or_id, bool throw_if_not_found = true ) const; + // Markets / feeds vector get_limit_orders(asset_id_type a, asset_id_type b, uint32_t limit)const; + vector get_account_limit_orders( const string& account_name_or_id, + const string &base, + const string "e, uint32_t limit, + optional ostart_id, + optional ostart_price ); vector get_call_orders(asset_id_type a, uint32_t limit)const; vector get_settle_orders(asset_id_type a, uint32_t limit)const; vector get_margin_positions( const account_id_type& id )const; @@ -623,6 +630,92 @@ vector> database_api_impl::get_accounts(const vector database_api::get_account_limit_orders( const string& account_name_or_id, const string &base, + const string "e, uint32_t limit, optional ostart_id, optional ostart_price) +{ + return my->get_account_limit_orders( account_name_or_id, base, quote, limit, ostart_id, ostart_price ); +} + +vector database_api_impl::get_account_limit_orders( const string& account_name_or_id, const string &base, + const string "e, uint32_t limit, optional ostart_id, optional ostart_price) +{ + FC_ASSERT( limit <= 101 ); + + vector results; + uint32_t count = 0; + + const account_object* account = get_account_from_string(account_name_or_id); + if (account == nullptr) + return results; + + auto assets = lookup_asset_symbols( {base, quote} ); + FC_ASSERT( assets[0], "Invalid base asset symbol: ${s}", ("s",base) ); + FC_ASSERT( assets[1], "Invalid quote asset symbol: ${s}", ("s",quote) ); + + auto base_id = assets[0]->id; + auto quote_id = assets[1]->id; + + if (ostart_price.valid()) { + FC_ASSERT(ostart_price->base.asset_id == base_id, "Base asset inconsistent with start price"); + FC_ASSERT(ostart_price->quote.asset_id == quote_id, "Quote asset inconsistent with start price"); + } + + const auto& index_by_account = _db.get_index_type().indices().get(); + limit_order_multi_index_type::index::type::const_iterator lower_itr; + limit_order_multi_index_type::index::type::const_iterator upper_itr; + + // if both order_id and price are invalid, query the first page + if ( !ostart_id.valid() && !ostart_price.valid() ) + { + lower_itr = index_by_account.lower_bound(std::make_tuple(account->id, price::max(base_id, quote_id))); + } + else if ( ostart_id.valid() ) + { + // in case of the order been deleted during page querying + const limit_order_object *p_loo = _db.find(*ostart_id); + + if ( !p_loo ) + { + if ( ostart_price.valid() ) + { + lower_itr = index_by_account.lower_bound(std::make_tuple(account->id, *ostart_price, *ostart_id)); + } + else + { + // start order id been deleted, yet not provided price either + FC_THROW("Order id invalid (maybe just been canceled?), and start price not provided"); + } + } + else + { + const limit_order_object &loo = *p_loo; + + // in case of the order not belongs to specified account or market + FC_ASSERT(loo.sell_price.base.asset_id == base_id, "Order base asset inconsistent"); + FC_ASSERT(loo.sell_price.quote.asset_id == quote_id, "Order quote asset inconsistent with order"); + FC_ASSERT(loo.seller == account->get_id(), "Order not owned by specified account"); + + lower_itr = index_by_account.lower_bound(std::make_tuple(account->id, loo.sell_price, *ostart_id)); + } + } + else + { + // if reach here start_price must be valid + lower_itr = index_by_account.lower_bound(std::make_tuple(account->id, *ostart_price)); + } + + upper_itr = index_by_account.upper_bound(std::make_tuple(account->id, price::min(base_id, quote_id))); + + // Add the account's orders + for ( ; lower_itr != upper_itr && count < limit; ++lower_itr, ++count) + { + const limit_order_object &order = *lower_itr; + results.emplace_back(order); + } + + return results; +} + std::map database_api::get_full_accounts( const vector& names_or_ids, bool subscribe ) { return my->get_full_accounts( names_or_ids, subscribe ); @@ -1094,6 +1187,25 @@ vector database_api_impl::get_all_unmatched_bets_for_bettor(account_ return boost::copy_range >(bet_idx.equal_range(std::make_tuple(bettor_id))); } +const account_object* database_api_impl::get_account_from_string( const std::string& name_or_id, bool throw_if_not_found) const +{ + // TODO cache the result to avoid repeatly fetching from db + FC_ASSERT( name_or_id.size() > 0); + const account_object* account = nullptr; + if (std::isdigit(name_or_id[0])) + account = _db.find(fc::variant(name_or_id).as()); + else + { + const auto& idx = _db.get_index_type().indices().get(); + auto itr = idx.find(name_or_id); + if (itr != idx.end()) + account = &*itr; + } + if(throw_if_not_found) + FC_ASSERT( account, "no such account" ); + return account; +} + ////////////////////////////////////////////////////////////////////// // // // Markets / feeds // diff --git a/libraries/app/include/graphene/app/database_api.hpp b/libraries/app/include/graphene/app/database_api.hpp index 3fac4b5f..53366e67 100644 --- a/libraries/app/include/graphene/app/database_api.hpp +++ b/libraries/app/include/graphene/app/database_api.hpp @@ -255,6 +255,36 @@ class database_api */ vector> get_accounts(const vector& account_ids)const; + /** + * @brief Fetch all orders relevant to the specified account and specified market, result orders + * are sorted descendingly by price + * + * @param account_name_or_id The name or ID of an account to retrieve + * @param base Base asset + * @param quote Quote asset + * @param limit The limitation of items each query can fetch, not greater than 101 + * @param start_id Start order id, fetch orders which price lower than this order, or price equal to this order + * but order ID greater than this order + * @param start_price Fetch orders with price lower than or equal to this price + * + * @return List of orders from @ref account_name_or_id to the corresponding account + * + * @note + * 1. if @ref account_name_or_id cannot be tied to an account, empty result will be returned + * 2. @ref start_id and @ref start_price can be empty, if so the api will return the "first page" of orders; + * if start_id is specified, its price will be used to do page query preferentially, otherwise the start_price + * will be used; start_id and start_price may be used cooperatively in case of the order specified by start_id + * was just canceled accidentally, in such case, the result orders' price may lower or equal to start_price, + * but orders' id greater than start_id + */ + + vector get_account_limit_orders( const string& account_name_or_id, + const string &base, + const string "e, + uint32_t limit = 101, + optional ostart_id = optional(), + optional ostart_price = optional()); + /** * @brief Fetch all objects relevant to the specified accounts and subscribe to updates * @param callback Function to call with updates @@ -737,6 +767,7 @@ FC_API(graphene::app::database_api, // Markets / feeds (get_order_book) (get_limit_orders) + (get_account_limit_orders) (get_call_orders) (get_settle_orders) (get_margin_positions) diff --git a/libraries/chain/include/graphene/chain/market_object.hpp b/libraries/chain/include/graphene/chain/market_object.hpp index b56f4e9c..185b7359 100644 --- a/libraries/chain/include/graphene/chain/market_object.hpp +++ b/libraries/chain/include/graphene/chain/market_object.hpp @@ -89,6 +89,7 @@ typedef multi_index_container< ordered_unique< tag, composite_key< limit_order_object, member, + member, member > > diff --git a/libraries/wallet/include/graphene/wallet/wallet.hpp b/libraries/wallet/include/graphene/wallet/wallet.hpp index a7189138..7366207c 100644 --- a/libraries/wallet/include/graphene/wallet/wallet.hpp +++ b/libraries/wallet/include/graphene/wallet/wallet.hpp @@ -372,6 +372,30 @@ class wallet_api vector list_core_accounts()const; vector get_market_history(string symbol, string symbol2, uint32_t bucket, fc::time_point_sec start, fc::time_point_sec end)const; + /** + * @brief Fetch all orders relevant to the specified account sorted descendingly by price + * + * @param name_or_id The name or ID of an account to retrieve + * @param base Base asset + * @param quote Quote asset + * @param limit The limitation of items each query can fetch (max: 101) + * @param ostart_id Start order id, fetch orders which price are lower than or equal to this order + * @param ostart_price Fetch orders with price lower than or equal to this price + * + * @return List of orders from \c name_or_id to the corresponding account + * + * @note + * 1. if \c name_or_id cannot be tied to an account, empty result will be returned + * 2. \c ostart_id and \c ostart_price can be \c null, if so the api will return the "first page" of orders; + * if \c ostart_id is specified and valid, its price will be used to do page query preferentially, + * otherwise the \c ostart_price will be used + */ + vector get_account_limit_orders( const string& name_or_id, + const string &base, + const string "e, + uint32_t limit = 101, + optional ostart_id = optional(), + optional ostart_price = optional()); vector get_limit_orders(string a, string b, uint32_t limit)const; vector get_call_orders(string a, uint32_t limit)const; vector get_settle_orders(string a, uint32_t limit)const; @@ -1984,6 +2008,7 @@ FC_API( graphene::wallet::wallet_api, (get_private_key) (load_wallet_file) (normalize_brain_key) + (get_account_limit_orders) (get_limit_orders) (get_call_orders) (get_settle_orders) diff --git a/libraries/wallet/wallet.cpp b/libraries/wallet/wallet.cpp index 812740e6..aaa3db26 100644 --- a/libraries/wallet/wallet.cpp +++ b/libraries/wallet/wallet.cpp @@ -3522,6 +3522,17 @@ vector wallet_api::get_market_history( string symbol1, string sym return my->_remote_hist->get_market_history( get_asset_id(symbol1), get_asset_id(symbol2), bucket, start, end ); } +vector wallet_api::get_account_limit_orders( + const string& name_or_id, + const string &base, + const string "e, + uint32_t limit, + optional ostart_id, + optional ostart_price) +{ + return my->_remote_db->get_account_limit_orders(name_or_id, base, quote, limit, ostart_id, ostart_price); +} + vector wallet_api::get_limit_orders(string a, string b, uint32_t limit)const { return my->_remote_db->get_limit_orders(get_asset(a).id, get_asset(b).id, limit); diff --git a/tests/tests/database_api_tests.cpp b/tests/tests/database_api_tests.cpp index e2453176..cfb50edb 100644 --- a/tests/tests/database_api_tests.cpp +++ b/tests/tests/database_api_tests.cpp @@ -68,4 +68,85 @@ BOOST_FIXTURE_TEST_SUITE(database_api_tests, database_fixture) } FC_LOG_AND_RETHROW() } +BOOST_AUTO_TEST_CASE(get_account_limit_orders) +{ try { + + ACTORS((seller)); + + const auto& bitcny = create_bitasset("CNY"); + const auto& core = asset_id_type()(db); + + int64_t init_balance(10000000); + transfer(committee_account, seller_id, asset(init_balance)); + BOOST_CHECK_EQUAL( 10000000, get_balance(seller, core) ); + + /// Create 250 versatile orders + for (size_t i = 0 ; i < 50 ; ++i) { + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250))); + } + + for (size_t i = 1 ; i < 101 ; ++i) { + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250 + i))); + BOOST_CHECK(create_sell_order(seller, core.amount(100), bitcny.amount(250 - i))); + } + + graphene::app::database_api db_api(db); + std::vector results; + limit_order_object o; + + // query with no constraint, expected: + // 1. up to 101 orders returned + // 2. orders were sorted by price desendingly + results = db_api.get_account_limit_orders(seller.name, "BTS", "CNY"); + BOOST_CHECK(results.size() == 101); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) { + BOOST_CHECK(results[i].sell_price >= results[i+1].sell_price); + } + results.clear(); + + // query with specified limit, expected: + // 1. up to specified amount of orders returned + // 2. orders were sorted by price desendingly + results = db_api.get_account_limit_orders(seller.name, "BTS", "CNY", 50); + results = db_api.get_account_limit_orders(seller.name, "BTS", "CNY", 50); + BOOST_CHECK(results.size() == 50); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) { + BOOST_CHECK(results[i].sell_price >= results[i+1].sell_price); + } + + o = results.back(); + results.clear(); + + // query with specified order id and limit, expected: + // same as before, but also the first order's id equal to specified + results = db_api.get_account_limit_orders(seller.name, "BTS", "CNY", 100, + limit_order_id_type(o.id)); + BOOST_CHECK(results.size() == 100); + BOOST_CHECK(results.front().id == o.id); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) { + BOOST_CHECK(results[i].sell_price >= results[i+1].sell_price); + } + + o = results.back(); + results.clear(); + + // query with specified price and an not exists order id, expected: + // 1. the canceled order should not exists in returned orders and first order's + // id should greater than specified + // 2. returned orders sorted by price desendingly + // 3. the first order's sell price equal to specified + cancel_limit_order(o); // NOTE 1: this canceled order was in scope of the + // first created 50 orders, so with price 2.5 BTS/CNY + results = db_api.get_account_limit_orders(seller.name, "BTS", "CNY", 101, + limit_order_id_type(o.id), o.sell_price); + BOOST_CHECK(results.size() <= 101); + BOOST_CHECK(results.front().id > o.id); + // NOTE 2: because of NOTE 1, here should be equal + BOOST_CHECK(results.front().sell_price == o.sell_price); + for (size_t i = 0 ; i < results.size() - 1 ; ++i) { + BOOST_CHECK(results[i].sell_price >= results[i+1].sell_price); + } + +} FC_LOG_AND_RETHROW() } + BOOST_AUTO_TEST_SUITE_END()