diff --git a/src/cpp/ripple/HashedObject.cpp b/src/cpp/ripple/HashedObject.cpp index 3515b5f55..91f98d751 100644 --- a/src/cpp/ripple/HashedObject.cpp +++ b/src/cpp/ripple/HashedObject.cpp @@ -183,6 +183,7 @@ HashedObject::pointer HashedObjectStore::retrieve(const uint256& hash) #ifndef NO_SQLITE3_PREPARE { + LoadEvent::autoptr event(theApp->getJobQueue().getLoadEventAP(jtDISK, "HOS::retrieve")); ScopedLock sl(theApp->getHashNodeDB()->getDBLock()); static SqliteStatement pSt(theApp->getHashNodeDB()->getDB()->getSqliteDB(), "SELECT ObjType,LedgerIndex,Object FROM CommittedObjects WHERE Hash = ?;"); diff --git a/src/cpp/ripple/Ledger.cpp b/src/cpp/ripple/Ledger.cpp index 90593d45f..c73a077ff 100644 --- a/src/cpp/ripple/Ledger.cpp +++ b/src/cpp/ripple/Ledger.cpp @@ -405,7 +405,9 @@ void Ledger::saveAcceptedLedger(Job&, bool fromConsensus) cLog(lsTRACE) << "saveAcceptedLedger " << (fromConsensus ? "fromConsensus " : "fromAcquire ") << getLedgerSeq(); static boost::format ledgerExists("SELECT LedgerSeq FROM Ledgers INDEXED BY SeqLedger where LedgerSeq = %u;"); static boost::format deleteLedger("DELETE FROM Ledgers WHERE LedgerSeq = %u;"); - static boost::format AcctTransExists("SELECT LedgerSeq FROM AccountTransactions WHERE TransID = '%s';"); + static boost::format deleteTrans1("DELETE FROM Transactions WHERE LedgerSeq = %u;"); + static boost::format deleteTrans2("DELETE FROM AccountTransactions WHERE LedgerSeq = %u;"); + static boost::format deleteAcctTrans("DELETE FROM AccountTransactions WHERE TransID = '%s';"); static boost::format transExists("SELECT Status FROM Transactions WHERE TransID = '%s';"); static boost::format updateTx("UPDATE Transactions SET LedgerSeq = %u, Status = '%c', TxnMeta = %s WHERE TransID = '%s';"); @@ -431,88 +433,80 @@ void Ledger::saveAcceptedLedger(Job&, bool fromConsensus) AcceptedLedger::pointer aLedger = AcceptedLedger::makeAcceptedLedger(shared_from_this()); { - { - ScopedLock sl(theApp->getLedgerDB()->getDBLock()); - if (SQL_EXISTS(theApp->getLedgerDB()->getDB(), boost::str(ledgerExists % mLedgerSeq))) - theApp->getLedgerDB()->getDB()->executeSQL(boost::str(deleteLedger % mLedgerSeq)); - } + ScopedLock sl(theApp->getLedgerDB()->getDBLock()); + theApp->getLedgerDB()->getDB()->executeSQL(boost::str(deleteLedger % mLedgerSeq)); + } + { Database *db = theApp->getTxnDB()->getDB(); - { - ScopedLock dbLock(theApp->getTxnDB()->getDBLock()); - db->executeSQL("BEGIN TRANSACTION;"); + ScopedLock dbLock(theApp->getTxnDB()->getDBLock()); + db->executeSQL("BEGIN TRANSACTION;"); - BOOST_FOREACH(const AcceptedLedger::value_type& vt, aLedger->getMap()) + db->executeSQL(boost::str(deleteTrans1 % mLedgerSeq)); + db->executeSQL(boost::str(deleteTrans2 % mLedgerSeq)); + + BOOST_FOREACH(const AcceptedLedger::value_type& vt, aLedger->getMap()) + { + uint256 txID = vt.second.getTransactionID(); + theApp->getMasterTransaction().inLedger(txID, mLedgerSeq); + + db->executeSQL(boost::str(deleteAcctTrans % txID.GetHex())); + + const std::vector& accts = vt.second.getAffected(); + if (!accts.empty()) { - cLog(lsTRACE) << "Saving: " << vt.second.getJson(0); - uint256 txID = vt.second.getTransactionID(); - theApp->getMasterTransaction().inLedger(txID, mLedgerSeq); - - // Make sure transaction is in AccountTransactions. - if (!SQL_EXISTS(db, boost::str(AcctTransExists % txID.GetHex()))) + std::string sql = "INSERT OR REPLACE INTO AccountTransactions (TransID, Account, LedgerSeq) VALUES "; + bool first = true; + for (std::vector::const_iterator it = accts.begin(), end = accts.end(); it != end; ++it) { - // Transaction not in AccountTransactions - const std::vector& accts = vt.second.getAffected(); - if (!accts.empty()) - { - - std::string sql = "INSERT OR REPLACE INTO AccountTransactions (TransID, Account, LedgerSeq) VALUES "; - bool first = true; - for (std::vector::const_iterator it = accts.begin(), end = accts.end(); it != end; ++it) - { - if (!first) - sql += ", ('"; - else - { - sql += "('"; - first = false; - } - sql += txID.GetHex(); - sql += "','"; - sql += it->humanAccountID(); - sql += "',"; - sql += boost::lexical_cast(getLedgerSeq()); - sql += ")"; - } - sql += ";"; - Log(lsTRACE) << "ActTx: " << sql; - db->executeSQL(sql); // may already be in there - } + if (!first) + sql += ", ('"; else - cLog(lsWARNING) << "Transaction in ledger " << mLedgerSeq << " affects no accounts"; - } - - if (SQL_EXISTS(db, boost::str(transExists % txID.GetHex()))) - { - // In Transactions, update LedgerSeq, metadata and Status. - db->executeSQL(boost::str(updateTx - % getLedgerSeq() - % TXN_SQL_VALIDATED - % vt.second.getEscMeta() - % txID.GetHex())); - } - else - { - // Not in Transactions, insert the whole thing.. - db->executeSQL( - SerializedTransaction::getMetaSQLInsertHeader() + - vt.second.getTxn()->getMetaSQL(getLedgerSeq(), vt.second.getEscMeta()) + ";"); + { + sql += "('"; + first = false; + } + sql += txID.GetHex(); + sql += "','"; + sql += it->humanAccountID(); + sql += "',"; + sql += boost::lexical_cast(getLedgerSeq()); + sql += ")"; } + sql += ";"; + Log(lsTRACE) << "ActTx: " << sql; + db->executeSQL(sql); // may already be in there } - db->executeSQL("COMMIT TRANSACTION;"); - } + else + cLog(lsWARNING) << "Transaction in ledger " << mLedgerSeq << " affects no accounts"; - if (!theConfig.RUN_STANDALONE) - theApp->getHashedObjectStore().waitWrite(); // wait until all nodes are written - - { - ScopedLock sl(theApp->getLedgerDB()->getDBLock()); - theApp->getLedgerDB()->getDB()->executeSQL(boost::str(addLedger % - getHash().GetHex() % mLedgerSeq % mParentHash.GetHex() % - boost::lexical_cast(mTotCoins) % mCloseTime % mParentCloseTime % - mCloseResolution % mCloseFlags % - mAccountHash.GetHex() % mTransHash.GetHex())); + if (SQL_EXISTS(db, boost::str(transExists % txID.GetHex()))) + { + // In Transactions, update LedgerSeq, metadata and Status. + db->executeSQL(boost::str(updateTx + % getLedgerSeq() + % TXN_SQL_VALIDATED + % vt.second.getEscMeta() + % txID.GetHex())); + } + else + { + // Not in Transactions, insert the whole thing.. + db->executeSQL( + SerializedTransaction::getMetaSQLInsertHeader() + + vt.second.getTxn()->getMetaSQL(getLedgerSeq(), vt.second.getEscMeta()) + ";"); + } } + db->executeSQL("COMMIT TRANSACTION;"); + } + + { + ScopedLock sl(theApp->getLedgerDB()->getDBLock()); + theApp->getLedgerDB()->getDB()->executeSQL(boost::str(addLedger % + getHash().GetHex() % mLedgerSeq % mParentHash.GetHex() % + boost::lexical_cast(mTotCoins) % mCloseTime % mParentCloseTime % + mCloseResolution % mCloseFlags % + mAccountHash.GetHex() % mTransHash.GetHex())); } if (!fromConsensus) @@ -1313,6 +1307,42 @@ std::vector< std::pair > Ledger::getLedgerHashes() return ret; } +bool Ledger::isValidBook(const uint160& uTakerPaysCurrency, const uint160& uTakerPaysIssuerID, + const uint160& uTakerGetsCurrency, const uint160& uTakerGetsIssuerID) +{ + if (uTakerPaysCurrency.isZero()) + { // XRP in + + if (uTakerPaysIssuerID.isNonZero()) // XRP cannot have an issuer + return false; + + if (uTakerGetsCurrency.isZero()) // XRP to XRP not allowed + return false; + + if (uTakerGetsIssuerID.isZero()) // non-XRP must have issuer + return false; + + return true; + } + + // non-XRP in + if (uTakerPaysIssuerID.isZero()) // non-XRP must have issuer + return false; + + if (uTakerGetsCurrency.isZero()) // non-XRP to XRP + { + if (uTakerGetsIssuerID.isNonZero()) // XRP cannot have issuer + return false; + } + else // non-XRP to non-XRP + { + if ((uTakerPaysCurrency == uTakerGetsCurrency) && (uTakerGetsIssuerID == uTakerGetsIssuerID)) + return false; // Input and output cannot be identical + } + + return true; +} + uint256 Ledger::getBookBase(const uint160& uTakerPaysCurrency, const uint160& uTakerPaysIssuerID, const uint160& uTakerGetsCurrency, const uint160& uTakerGetsIssuerID) { diff --git a/src/cpp/ripple/Ledger.h b/src/cpp/ripple/Ledger.h index e674bb8ac..c260a0443 100644 --- a/src/cpp/ripple/Ledger.h +++ b/src/cpp/ripple/Ledger.h @@ -257,6 +257,9 @@ public: // // Order book dirs have a base so we can use next to step through them in quality order. + static bool isValidBook(const uint160& uTakerPaysCurrency, const uint160& uTakerPaysIssuerID, + const uint160& uTakerGetsCurrency, const uint160& uTakerGetsIssuerID); + static uint256 getBookBase(const uint160& uTakerPaysCurrency, const uint160& uTakerPaysIssuerID, const uint160& uTakerGetsCurrency, const uint160& uTakerGetsIssuerID); diff --git a/src/cpp/ripple/NetworkOPs.cpp b/src/cpp/ripple/NetworkOPs.cpp index 44bee343e..55d65dc96 100644 --- a/src/cpp/ripple/NetworkOPs.cpp +++ b/src/cpp/ripple/NetworkOPs.cpp @@ -1598,21 +1598,21 @@ void NetworkOPs::unsubAccount(uint64 uSeq, const boost::unordered_setgetOrderBookDB().makeBookListeners(currencyIn, currencyOut, issuerIn, issuerOut); + theApp->getOrderBookDB().makeBookListeners(currencyPays, currencyGets, issuerPays, issuerGets); if (listeners) listeners->addSubscriber(isrListener); return true; } bool NetworkOPs::unsubBook(uint64 uSeq, - const uint160& currencyIn, const uint160& currencyOut, const uint160& issuerIn, const uint160& issuerOut) + const uint160& currencyPays, const uint160& currencyGets, const uint160& issuerPays, const uint160& issuerGets) { BookListeners::pointer listeners = - theApp->getOrderBookDB().getBookListeners(currencyIn, currencyOut, issuerIn, issuerOut); + theApp->getOrderBookDB().getBookListeners(currencyPays, currencyGets, issuerPays, issuerGets); if (listeners) listeners->removeSubscriber(uSeq); return true; diff --git a/src/cpp/ripple/NetworkOPs.h b/src/cpp/ripple/NetworkOPs.h index af7a30218..1fa99064b 100644 --- a/src/cpp/ripple/NetworkOPs.h +++ b/src/cpp/ripple/NetworkOPs.h @@ -322,10 +322,10 @@ public: bool subServer(InfoSub::ref ispListener, Json::Value& jvResult); bool unsubServer(uint64 uListener); - bool subBook(InfoSub::ref ispListener, const uint160& currencyIn, const uint160& currencyOut, - const uint160& issuerIn, const uint160& issuerOut); - bool unsubBook(uint64 uListener, const uint160& currencyIn, const uint160& currencyOut, - const uint160& issuerIn, const uint160& issuerOut); + bool subBook(InfoSub::ref ispListener, const uint160& currencyPays, const uint160& currencyGets, + const uint160& issuerPays, const uint160& issuerGets); + bool unsubBook(uint64 uListener, const uint160& currencyPays, const uint160& currencyGets, + const uint160& issuerPays, const uint160& issuerGets); bool subTransactions(InfoSub::ref ispListener); bool unsubTransactions(uint64 uListener); diff --git a/src/cpp/ripple/OrderBookDB.cpp b/src/cpp/ripple/OrderBookDB.cpp index 8cdaa42bd..8e76b7579 100644 --- a/src/cpp/ripple/OrderBookDB.cpp +++ b/src/cpp/ripple/OrderBookDB.cpp @@ -93,40 +93,40 @@ void OrderBookDB::getBooks(const uint160& issuerID, const uint160& currencyID, s } } -BookListeners::pointer OrderBookDB::makeBookListeners(const uint160& currencyIn, const uint160& currencyOut, - const uint160& issuerIn, const uint160& issuerOut) +BookListeners::pointer OrderBookDB::makeBookListeners(const uint160& currencyPays, const uint160& currencyGets, + const uint160& issuerPays, const uint160& issuerGets) { boost::recursive_mutex::scoped_lock sl(mLock); - BookListeners::pointer ret = getBookListeners(currencyIn, currencyOut, issuerIn, issuerOut); + BookListeners::pointer ret = getBookListeners(currencyPays, currencyGets, issuerPays, issuerGets); if (!ret) { ret = boost::make_shared(); - mListeners[issuerIn][issuerOut][currencyIn][currencyOut] = ret; + mListeners[issuerPays][issuerGets][currencyPays][currencyGets] = ret; } return ret; } -BookListeners::pointer OrderBookDB::getBookListeners(const uint160& currencyIn, const uint160& currencyOut, - const uint160& issuerIn, const uint160& issuerOut) +BookListeners::pointer OrderBookDB::getBookListeners(const uint160& currencyPays, const uint160& currencyGets, + const uint160& issuerPays, const uint160& issuerGets) { BookListeners::pointer ret; boost::recursive_mutex::scoped_lock sl(mLock); std::map > > >::iterator - it0 = mListeners.find(issuerIn); + it0 = mListeners.find(issuerPays); if(it0 == mListeners.end()) return ret; std::map > >::iterator - it1 = (*it0).second.find(issuerOut); + it1 = (*it0).second.find(issuerGets); if(it1 == (*it0).second.end()) return ret; - std::map >::iterator it2 = (*it1).second.find(currencyIn); + std::map >::iterator it2 = (*it1).second.find(currencyPays); if(it2 == (*it1).second.end()) return ret; - std::map::iterator it3 = (*it2).second.find(currencyOut); + std::map::iterator it3 = (*it2).second.find(currencyGets); if(it3 == (*it2).second.end()) return ret; @@ -216,16 +216,16 @@ void OrderBookDB::processTxn(Ledger::ref ledger, const ALTransaction& alTx, Json if (data) { STAmount takerGets = data->getFieldAmount(sfTakerGets); - uint160 currencyOut = takerGets.getCurrency(); - uint160 issuerOut = takerGets.getIssuer(); + uint160 currencyGets = takerGets.getCurrency(); + uint160 issuerGets = takerGets.getIssuer(); STAmount takerPays = data->getFieldAmount(sfTakerPays); - uint160 currencyIn = takerPays.getCurrency(); - uint160 issuerIn = takerPays.getIssuer(); + uint160 currencyPays = takerPays.getCurrency(); + uint160 issuerPays = takerPays.getIssuer(); // determine the OrderBook BookListeners::pointer book = - getBookListeners(currencyIn, currencyOut, issuerIn, issuerOut); + getBookListeners(currencyPays, currencyGets, issuerPays, issuerGets); if (book) book->publish(jvObj); } diff --git a/src/cpp/ripple/OrderBookDB.h b/src/cpp/ripple/OrderBookDB.h index b5a95e43b..15ebd24d0 100644 --- a/src/cpp/ripple/OrderBookDB.h +++ b/src/cpp/ripple/OrderBookDB.h @@ -35,8 +35,8 @@ class OrderBookDB boost::unordered_map > mIssuerMap; //std::vector mAllOrderBooks; - // issuerIn, issuerOut, currencyIn, currencyOut - std::map > > > mListeners; + // issuerPays, issuerGets, currencyPays, currencyGets + std::map > > > mListeners; uint32 mSeq; boost::recursive_mutex mLock; @@ -56,13 +56,13 @@ public: void getBooks(const uint160& issuerID, const uint160& currencyID, std::vector& bookRet); // returns the best rate we can find - float getPrice(uint160& currencyIn,uint160& currencyOut); + float getPrice(uint160& currencyPays,uint160& currencyGets); - BookListeners::pointer getBookListeners(const uint160& currencyIn, const uint160& currencyOut, - const uint160& issuerIn, const uint160& issuerOut); - BookListeners::pointer makeBookListeners(const uint160& currencyIn, const uint160& currencyOut, - const uint160& issuerIn, const uint160& issuerOut); + BookListeners::pointer getBookListeners(const uint160& currencyPays, const uint160& currencyGets, + const uint160& issuerPays, const uint160& issuerGets); + BookListeners::pointer makeBookListeners(const uint160& currencyPays, const uint160& currencyGets, + const uint160& issuerPays, const uint160& issuerGets); // see if this txn effects any orderbook void processTxn(Ledger::ref ledger, const ALTransaction& alTx, Json::Value& jvObj); diff --git a/src/cpp/ripple/Peer.cpp b/src/cpp/ripple/Peer.cpp index 991afb52d..a96da59c0 100644 --- a/src/cpp/ripple/Peer.cpp +++ b/src/cpp/ripple/Peer.cpp @@ -168,9 +168,6 @@ void Peer::handleVerifyTimer(const boost::system::error_code& ecResult) else if (ecResult) { cLog(lsINFO) << "Peer verify timer error"; - - // Can't do anything sound. - abort(); } else { @@ -222,6 +219,7 @@ void Peer::connect(const std::string& strIp, int iPort) { cLog(lsINFO) << "Peer: Connect: Outbound: " << ADDRESS(this) << ": " << mIpPort.first << " " << mIpPort.second; + boost::recursive_mutex::scoped_lock sl(ioMutex); boost::asio::async_connect( getSocket(), itrEndpoint, diff --git a/src/cpp/ripple/RPCHandler.cpp b/src/cpp/ripple/RPCHandler.cpp index b543f6072..39b5cdc11 100644 --- a/src/cpp/ripple/RPCHandler.cpp +++ b/src/cpp/ripple/RPCHandler.cpp @@ -2753,37 +2753,101 @@ Json::Value RPCHandler::doSubscribe(Json::Value jvRequest, int& cost) } if (jvRequest.isMember("books")) - { + { // FIXME: This can crash the server if the parameters to things like getBookPage are invalid for (Json::Value::iterator it = jvRequest["books"].begin(); it != jvRequest["books"].end(); it++) { - uint160 currencyOut; - STAmount::currencyFromString(currencyOut,(*it)["CurrencyOut"].asString()); - uint160 issuerOut,issuerIn; - if(currencyOut.isNonZero()) - STAmount::issuerFromString(issuerOut,(*it)["IssuerOut"].asString()); - uint160 currencyIn; - STAmount::currencyFromString(currencyIn,(*it)["CurrencyIn"].asString()); - if(currencyIn.isNonZero()) - STAmount::issuerFromString(issuerIn,(*it)["IssuerIn"].asString()); + uint160 uTakerPaysCurrencyID; + uint160 uTakerPaysIssuerID; + Json::Value jvTakerPays = (*it)["taker_pays"]; - bool bothSides=false; - if((*it).isMember("BothSides") && (*it)["BothSides"].asBool()) bothSides=true; + // Parse mandatory currency. + if (!jvTakerPays.isMember("currency") + || !STAmount::currencyFromString(uTakerPaysCurrencyID, jvTakerPays["currency"].asString())) + { + cLog(lsINFO) << "Bad taker_pays currency."; - mNetOps->subBook(ispSub, currencyIn, currencyOut, issuerIn, issuerOut); - if(bothSides) mNetOps->subBook(ispSub, currencyOut, currencyIn, issuerOut, issuerIn); - if((*it)["StateNow"].asBool()) + return rpcError(rpcSRC_CUR_MALFORMED); + } + // Parse optional issuer. + else if (((jvTakerPays.isMember("issuer")) + && (!jvTakerPays["issuer"].isString() + || !STAmount::issuerFromString(uTakerPaysIssuerID, jvTakerPays["issuer"].asString()))) + // Don't allow illegal issuers. + || (!uTakerPaysCurrencyID != !uTakerPaysIssuerID) + || ACCOUNT_ONE == uTakerPaysIssuerID) + { + cLog(lsINFO) << "Bad taker_pays issuer."; + + return rpcError(rpcSRC_ISR_MALFORMED); + } + + uint160 uTakerGetsCurrencyID; + uint160 uTakerGetsIssuerID; + Json::Value jvTakerGets = (*it)["taker_gets"]; + + // Parse mandatory currency. + if (!jvTakerGets.isMember("currency") + || !STAmount::currencyFromString(uTakerGetsCurrencyID, jvTakerGets["currency"].asString())) + { + cLog(lsINFO) << "Bad taker_pays currency."; + + return rpcError(rpcSRC_CUR_MALFORMED); + } + // Parse optional issuer. + else if (((jvTakerGets.isMember("issuer")) + && (!jvTakerGets["issuer"].isString() + || !STAmount::issuerFromString(uTakerGetsIssuerID, jvTakerGets["issuer"].asString()))) + // Don't allow illegal issuers. + || (!uTakerGetsCurrencyID != !uTakerGetsIssuerID) + || ACCOUNT_ONE == uTakerGetsIssuerID) + { + cLog(lsINFO) << "Bad taker_gets issuer."; + + return rpcError(rpcDST_ISR_MALFORMED); + } + + if (uTakerPaysCurrencyID == uTakerGetsCurrencyID + && uTakerPaysIssuerID == uTakerGetsIssuerID) + { + cLog(lsINFO) << "taker_gets same as taker_pays."; + + return rpcError(rpcBAD_MARKET); + } + + RippleAddress raTakerID; + + if (!(*it).isMember("taker")) + { + raTakerID.setAccountID(ACCOUNT_ONE); + } + else if (!raTakerID.setAccountID((*it)["taker"].asString())) + { + return rpcError(rpcBAD_ISSUER); + } + + bool bothSides = (*it)["both_sides"].asBool(); + + if (!Ledger::isValidBook(uTakerPaysCurrencyID, uTakerPaysIssuerID, uTakerGetsCurrencyID, uTakerGetsIssuerID)) + { + cLog(lsWARNING) << "Bad market: " << + uTakerPaysCurrencyID << ":" << uTakerPaysIssuerID << " -> " << + uTakerGetsCurrencyID << ":" << uTakerGetsIssuerID; + return rpcError(rpcBAD_MARKET); + } + + mNetOps->subBook(ispSub, uTakerPaysCurrencyID, uTakerGetsCurrencyID, uTakerPaysIssuerID, uTakerGetsIssuerID); + if (bothSides) mNetOps->subBook(ispSub, uTakerGetsCurrencyID, uTakerPaysCurrencyID, uTakerGetsIssuerID, uTakerPaysIssuerID); + if ((*it)["state_now"].asBool()) { Ledger::pointer ledger= theApp->getLedgerMaster().getClosedLedger(); - RippleAddress raTakerID; - raTakerID.setAccountID(ACCOUNT_ONE); const Json::Value jvMarker = Json::Value(Json::nullValue); - mNetOps->getBookPage(ledger, currencyOut, issuerOut, currencyIn, issuerIn, raTakerID.getAccountID(), false, 0, jvMarker, jvResult); - if(bothSides) + mNetOps->getBookPage(ledger, uTakerPaysCurrencyID, uTakerPaysIssuerID, uTakerGetsCurrencyID, uTakerGetsIssuerID, raTakerID.getAccountID(), false, 0, jvMarker, jvResult); + if (bothSides) { Json::Value tempJson(Json::objectValue); - if(jvResult.isMember("offers")) jvResult["bids"]=jvResult["offers"]; - mNetOps->getBookPage(ledger, currencyIn, issuerIn, currencyOut, issuerOut, raTakerID.getAccountID(), false, 0, jvMarker, tempJson); - if(tempJson.isMember("offers")) jvResult["asks"]=tempJson["offers"]; + if (jvResult.isMember("offers")) jvResult["bids"]=jvResult["offers"]; + mNetOps->getBookPage(ledger, uTakerGetsCurrencyID, uTakerGetsIssuerID, uTakerPaysCurrencyID, uTakerPaysIssuerID, raTakerID.getAccountID(), false, 0, jvMarker, tempJson); + if (tempJson.isMember("offers")) jvResult["asks"]=tempJson["offers"]; } } } @@ -2888,14 +2952,68 @@ Json::Value RPCHandler::doUnsubscribe(Json::Value jvRequest, int& cost) { for (Json::Value::iterator it = jvRequest["books"].begin(); it != jvRequest["books"].end(); it++) { - uint160 currencyOut; - STAmount::issuerFromString(currencyOut,(*it)["CurrencyOut"].asString()); - uint160 issuerOut = RippleAddress::createNodePublic( (*it)["IssuerOut"].asString() ).getAccountID(); - uint160 currencyIn; - STAmount::issuerFromString(currencyOut,(*it)["CurrencyIn"].asString()); - uint160 issuerIn = RippleAddress::createNodePublic( (*it)["IssuerIn"].asString() ).getAccountID(); + uint160 uTakerPaysCurrencyID; + uint160 uTakerPaysIssuerID; + Json::Value jvTakerPays = (*it)["taker_pays"]; - mNetOps->unsubBook(ispSub->getSeq(), currencyIn, currencyOut, issuerIn, issuerOut); + // Parse mandatory currency. + if (!jvTakerPays.isMember("currency") + || !STAmount::currencyFromString(uTakerPaysCurrencyID, jvTakerPays["currency"].asString())) + { + cLog(lsINFO) << "Bad taker_pays currency."; + + return rpcError(rpcSRC_CUR_MALFORMED); + } + // Parse optional issuer. + else if (((jvTakerPays.isMember("issuer")) + && (!jvTakerPays["issuer"].isString() + || !STAmount::issuerFromString(uTakerPaysIssuerID, jvTakerPays["issuer"].asString()))) + // Don't allow illegal issuers. + || (!uTakerPaysCurrencyID != !uTakerPaysIssuerID) + || ACCOUNT_ONE == uTakerPaysIssuerID) + { + cLog(lsINFO) << "Bad taker_pays issuer."; + + return rpcError(rpcSRC_ISR_MALFORMED); + } + + uint160 uTakerGetsCurrencyID; + uint160 uTakerGetsIssuerID; + Json::Value jvTakerGets = (*it)["taker_gets"]; + + // Parse mandatory currency. + if (!jvTakerGets.isMember("currency") + || !STAmount::currencyFromString(uTakerGetsCurrencyID, jvTakerGets["currency"].asString())) + { + cLog(lsINFO) << "Bad taker_pays currency."; + + return rpcError(rpcSRC_CUR_MALFORMED); + } + // Parse optional issuer. + else if (((jvTakerGets.isMember("issuer")) + && (!jvTakerGets["issuer"].isString() + || !STAmount::issuerFromString(uTakerGetsIssuerID, jvTakerGets["issuer"].asString()))) + // Don't allow illegal issuers. + || (!uTakerGetsCurrencyID != !uTakerGetsIssuerID) + || ACCOUNT_ONE == uTakerGetsIssuerID) + { + cLog(lsINFO) << "Bad taker_gets issuer."; + + return rpcError(rpcDST_ISR_MALFORMED); + } + + if (uTakerPaysCurrencyID == uTakerGetsCurrencyID + && uTakerPaysIssuerID == uTakerGetsIssuerID) + { + cLog(lsINFO) << "taker_gets same as taker_pays."; + + return rpcError(rpcBAD_MARKET); + } + + bool bothSides = (*it)["both_sides"].asBool(); + + mNetOps->unsubBook(ispSub->getSeq(), uTakerPaysCurrencyID, uTakerGetsCurrencyID, uTakerPaysIssuerID, uTakerGetsIssuerID); + if (bothSides) mNetOps->unsubBook(ispSub->getSeq(), uTakerGetsCurrencyID, uTakerPaysCurrencyID, uTakerGetsIssuerID, uTakerPaysIssuerID); } } diff --git a/src/cpp/ripple/SerializedTransaction.cpp b/src/cpp/ripple/SerializedTransaction.cpp index 0aec1aacb..7486e0329 100644 --- a/src/cpp/ripple/SerializedTransaction.cpp +++ b/src/cpp/ripple/SerializedTransaction.cpp @@ -233,7 +233,7 @@ std::string SerializedTransaction::getSQLInsertReplaceHeader() std::string SerializedTransaction::getMetaSQLInsertHeader() { - return "INSERT OR REPLACE INTO Transactions " + getMetaSQLValueHeader() + " VALUES "; + return "INSERT INTO Transactions " + getMetaSQLValueHeader() + " VALUES "; } std::string SerializedTransaction::getSQL(uint32 inLedger, char status) const diff --git a/src/js/account.js b/src/js/account.js index 2e1121e5d..dcc14c67b 100644 --- a/src/js/account.js +++ b/src/js/account.js @@ -128,6 +128,22 @@ Account.prototype.entry = function (callback) return this; }; +/** + * Notify object of a relevant transaction. + * + * This is only meant to be called by the Remote class. You should never have to + * call this yourself. + */ +Account.prototype.notifyTx = function (message) +{ + // Only trigger the event if the account object is actually + // subscribed - this prevents some weird phantom events from + // occurring. + if (this._subs) { + this.emit('transaction', message); + } +}; + exports.Account = Account; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/amount.js b/src/js/amount.js index 5a121d4f4..6302c39b0 100644 --- a/src/js/amount.js +++ b/src/js/amount.js @@ -98,6 +98,8 @@ Amount.prototype.abs = function () { Amount.prototype.add = function (v) { var result; + v = Amount.from_json(v); + if (!this.is_comparable(v)) { result = Amount.NaN(); } @@ -779,7 +781,7 @@ Amount.prototype.set_issuer = function (issuer) { // Result in terms of this' currency and issuer. Amount.prototype.subtract = function (v) { // Correctness over speed, less code has less bugs, reuse add code. - return this.add(v.negate()); + return this.add(Amount.from_json(v).negate()); }; Amount.prototype.to_number = function (allow_nan) { diff --git a/src/js/currency.js b/src/js/currency.js index e4a4e305c..22bd112fc 100644 --- a/src/js/currency.js +++ b/src/js/currency.js @@ -22,9 +22,9 @@ Currency.json_rewrite = function (j) { }; Currency.from_json = function (j) { - return 'string' === typeof j - ? (new Currency()).parse_json(j) - : j.clone(); + if (j instanceof Currency) return j.clone(); + else if ('string' === typeof j) return (new Currency()).parse_json(j); + else return new Currency(); // NaN }; Currency.is_valid = function (j) { @@ -67,7 +67,7 @@ Currency.prototype.is_native = function () { }; Currency.prototype.is_valid = function () { - return !isNaN(this._value); + return 'string' === typeof this._value || !isNaN(this._value); }; Currency.prototype.to_json = function () { diff --git a/src/js/index.js b/src/js/index.js index b603d6a53..05ec1f842 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,5 +1,6 @@ exports.Remote = require('./remote').Remote; exports.Amount = require('./amount').Amount; +exports.Currency = require('./currency').Currency; exports.UInt160 = require('./amount').UInt160; exports.Seed = require('./amount').Seed; diff --git a/src/js/meta.js b/src/js/meta.js index 46c0e46cb..23ba81521 100644 --- a/src/js/meta.js +++ b/src/js/meta.js @@ -1,4 +1,5 @@ var extend = require('extend'); +var utils = require('./utils'); var UInt160 = require('./uint160').UInt160; var Amount = require('./amount').Amount; @@ -102,7 +103,35 @@ Meta.prototype.getAffectedAccounts = function () } }); + accounts = utils.arrayUnique(accounts); + return accounts; }; +Meta.prototype.getAffectedBooks = function () +{ + var books = []; + + this.each(function (an) { + if (an.entryType !== 'Offer') return; + + var gets = Amount.from_json(an.fields.TakerGets); + var pays = Amount.from_json(an.fields.TakerPays); + + var getsKey = gets.currency().to_json(); + if (getsKey !== 'XRP') getsKey += '/' + gets.issuer().to_json(); + + var paysKey = pays.currency().to_json(); + if (paysKey !== 'XRP') paysKey += '/' + pays.issuer().to_json(); + + var key = getsKey + ":" + paysKey; + + books.push(key); + }); + + books = utils.arrayUnique(books); + + return books; +}; + exports.Meta = Meta; diff --git a/src/js/orderbook.js b/src/js/orderbook.js index 00ac29b23..150743b46 100644 --- a/src/js/orderbook.js +++ b/src/js/orderbook.js @@ -1,6 +1,10 @@ // Routines for working with an orderbook. // +// One OrderBook object represents one half of an order book. (i.e. bids OR +// asks) Which one depends on the ordering of the parameters. +// // Events: +// - transaction A transaction that affects the order book. // var network = require("./network.js"); @@ -12,28 +16,29 @@ var Currency = require('./currency').Currency; var extend = require('extend'); var OrderBook = function (remote, - currency_out, issuer_out, - currency_in, issuer_in) { + currency_gets, issuer_gets, + currency_pays, issuer_pays) { var self = this; this._remote = remote; - this._currency_out = currency_out; - this._issuer_out = issuer_out; - this._currency_in = currency_in; - this._issuer_in = issuer_in; + this._currency_gets = currency_gets; + this._issuer_gets = issuer_gets; + this._currency_pays = currency_pays; + this._issuer_pays = issuer_pays; this._subs = 0; - // Ledger entry object - // Important: This must never be overwritten, only extend()-ed - this._entry = {}; + // We consider ourselves synchronized if we have a current copy of the offers, + // we are online and subscribed to updates. + this._sync = false; + + // Offers + this._offers = []; this.on('newListener', function (type, listener) { if (OrderBook.subscribe_events.indexOf(type) !== -1) { if (!self._subs && 'open' === self._remote._online_state) { - self._remote.request_subscribe() - .books([self.to_json()], true) - .request(); + self._subscribe(); } self._subs += 1; } @@ -44,6 +49,7 @@ var OrderBook = function (remote, self._subs -= 1; if (!self._subs && 'open' === self._remote._online_state) { + self._sync = false; self._remote.request_unsubscribe() .books([self.to_json()]) .request(); @@ -59,6 +65,10 @@ var OrderBook = function (remote, } }); + this._remote.on('disconnect', function () { + self._sync = false; + }); + return this; }; @@ -67,17 +77,42 @@ OrderBook.prototype = new EventEmitter; /** * List of events that require a remote subscription to the orderbook. */ -OrderBook.subscribe_events = ['transaction']; +OrderBook.subscribe_events = ['transaction', 'model', 'trade']; + +/** + * Subscribes to orderbook. + * + * @private + */ +OrderBook.prototype._subscribe = function () +{ + var self = this; + self._remote.request_subscribe() + .books([self.to_json()], true) + .on('error', function () { + // XXX What now? + }) + .on('success', function (res) { + self._sync = true; + self._offers = res.offers; + self.emit('model', self._offers); + }) + .request(); +}; OrderBook.prototype.to_json = function () { var json = { - "CurrencyOut": this._currency_out, - "CurrencyIn": this._currency_in + "taker_gets": { + "currency": this._currency_gets + }, + "taker_pays": { + "currency": this._currency_pays + } }; - if (json["CurrencyOut"] !== "XRP") json["IssuerOut"] = this._issuer_out; - if (json["CurrencyIn"] !== "XRP") json["IssuerIn"] = this._issuer_in; + if (this._currency_gets !== "XRP") json["taker_gets"]["issuer"] = this._issuer_gets; + if (this._currency_pays !== "XRP") json["taker_pays"]["issuer"] = this._issuer_pays; return json; }; @@ -90,15 +125,121 @@ OrderBook.prototype.to_json = function () */ OrderBook.prototype.is_valid = function () { + // XXX Should check for same currency (non-native) && same issuer return ( - Currency.is_valid(this._currency_in) && - (this._currency_in !== "XRP" && UInt160.is_valid(this._issuer_in)) && - Currency.is_valid(this._currency_out) && - (this._currency_out !== "XRP" && UInt160.is_valid(this._issuer_out)) && - !(this._currency_in === "XRP" && this._currency_out === "XRP") + Currency.is_valid(this._currency_pays) && + (this._currency_pays === "XRP" || UInt160.is_valid(this._issuer_pays)) && + Currency.is_valid(this._currency_gets) && + (this._currency_gets === "XRP" || UInt160.is_valid(this._issuer_gets)) && + !(this._currency_pays === "XRP" && this._currency_gets === "XRP") ); }; +/** + * Notify object of a relevant transaction. + * + * This is only meant to be called by the Remote class. You should never have to + * call this yourself. + */ +OrderBook.prototype.notifyTx = function (message) +{ + var self = this; + + var changed = false; + + var trade_gets = Amount.from_json("0" + ((this._currency_gets === 'XRP') ? "" : + "/" + this._currency_gets + + "/" + this._issuer_gets)); + var trade_pays = Amount.from_json("0" + ((this._currency_pays === 'XRP') ? "" : + "/" + this._currency_pays + + "/" + this._issuer_pays)); + + message.mmeta.each(function (an) { + if (an.entryType !== 'Offer') return; + + var i, l, offer; + if (an.diffType === 'DeletedNode' || + an.diffType === 'ModifiedNode') { + for (i = 0, l = self._offers.length; i < l; i++) { + offer = self._offers[i]; + if (offer.index === an.ledgerIndex) { + if (an.diffType === 'DeletedNode') { + self._offers.splice(i, 1); + } + else extend(offer, an.fieldsFinal); + changed = true; + break; + } + } + + trade_gets = trade_gets.add(an.fieldsPrev.TakerGets); + trade_pays = trade_pays.add(an.fieldsPrev.TakerPays); + if (an.diffType === 'ModifiedNode') { + trade_gets = trade_gets.subtract(an.fieldsFinal.TakerGets); + trade_pays = trade_pays.subtract(an.fieldsFinal.TakerPays); + } + } else if (an.diffType === 'CreatedNode') { + var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets); + for (i = 0, l = self._offers.length; i < l; i++) { + offer = self._offers[i]; + var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets); + + if (price.compareTo(priceItem) <= 0) { + var obj = an.fields; + obj.index = an.ledgerIndex; + self._offers.splice(i, 0, an.fields); + changed = true; + break; + } + } + } + }); + + // Only trigger the event if the account object is actually + // subscribed - this prevents some weird phantom events from + // occurring. + if (this._subs) { + this.emit('transaction', message); + if (changed) this.emit('model', this._offers); + if (!trade_gets.is_zero()) this.emit('trade', trade_pays, trade_gets); + } +}; + +/** + * Get offers model asynchronously. + * + * This function takes a callback and calls it with an array containing the + * current set of offers in this order book. + * + * If the data is available immediately, the callback may be called synchronously. + */ +OrderBook.prototype.offers = function (callback) +{ + var self = this; + + if ("function" === typeof callback) { + if (this._sync) { + callback(this._offers); + } else { + this.once('model', function (offers) { + callback(offers); + }); + } + } + return this; +}; + +/** + * Return latest known offers. + * + * Usually, this will just be an empty array if the order book hasn't been + * loaded yet. But this accessor may be convenient in some circumstances. + */ +OrderBook.prototype.offersSync = function () +{ + return this._offers; +}; + exports.OrderBook = OrderBook; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/remote.js b/src/js/remote.js index fed025256..25c6d7a29 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -208,20 +208,25 @@ Request.prototype.books = function (books, state) { for (var i = 0, l = books.length; i < l; i++) { var book = books[i]; + var json = {}; - var json = { - "CurrencyOut": Currency.json_rewrite(book["CurrencyOut"]), - "CurrencyIn": Currency.json_rewrite(book["CurrencyIn"]) - }; + function process(side) { + if (!book[side]) throw new Error("Missing "+side); - if (json["CurrencyOut"] !== "XRP") { - json["IssuerOut"] = UInt160.json_rewrite(book["IssuerOut"]); - } - if (json["CurrencyIn"] !== "XRP") { - json["IssuerIn"] = UInt160.json_rewrite(book["IssuerIn"]); + var obj = {}; + obj["currency"] = Currency.json_rewrite(book[side]["currency"]); + if (obj["currency"] !== "XRP") { + obj.issuer = UInt160.json_rewrite(book[side]["issuer"]); + } + + json[side] = obj; } - if (state || book["StateNow"]) json["StateNow"] = true; + process("taker_gets"); + process("taker_pays"); + + if (state || book["state_now"]) json["state_now"] = true; + if (book["both_sides"]) json["both_sides"] = true; procBooks.push(json); } @@ -280,6 +285,7 @@ var Remote = function (opts, trace) { this._reserve_base = undefined; this._reserve_inc = undefined; this._server_status = undefined; + this._last_tx = null; // Local signing implies local fees and sequences if (this.local_signing) { @@ -300,6 +306,9 @@ var Remote = function (opts, trace) { // Hash map of Account objects by AccountId. this._accounts = {}; + // Hash map of OrderBook objects + this._books = {}; + // List of secrets that we know about. this.secrets = { // Secrets can be set by calling set_secret(account, secret). @@ -629,7 +638,10 @@ Remote.prototype._connect_message = function (ws, json) { // unsubscribes will be added as needed. // XXX If not trusted, need proof. - // XXX Should de-duplicate transaction events + // De-duplicate transactions that are immediately following each other + // XXX Should have a cache of n txs so we can dedup out of order txs + if (this._last_tx === message.transaction.hash) break; + this._last_tx = message.transaction.hash; if (this.trace) utils.logObject("remote: tx: %s", message); @@ -641,12 +653,15 @@ Remote.prototype._connect_message = function (ws, json) { for (var i = 0, l = affected.length; i < l; i++) { var account = self._accounts[affected[i]]; - // Only trigger the event if the account object is actually - // subscribed - this prevents some weird phantom events from - // occurring. - if (account && account._subs) { - account.emit('transaction', message); - } + if (account) account.notifyTx(message); + } + + // Pass the event on to any related OrderBooks + affected = message.mmeta.getAffectedBooks(); + for (i = 0, l = affected.length; i < l; i++) { + var book = self._books[affected[i]]; + + if (book) book.notifyTx(message); } this.emit('transaction', message); @@ -1143,13 +1158,26 @@ Remote.prototype.account = function (accountId) { return this._accounts[accountId]; }; -Remote.prototype.book = function (currency_out, issuer_out, - currency_in, issuer_in) { - var book = new OrderBook(this, - currency_out, issuer_out, - currency_in, issuer_in); +Remote.prototype.book = function (currency_gets, issuer_gets, + currency_pays, issuer_pays) { + var gets = currency_gets; + if (gets !== 'XRP') gets += '/' + issuer_gets; + var pays = currency_pays; + if (pays !== 'XRP') pays += '/' + issuer_pays; - return book; + var key = gets + ":" + pays; + + if (!this._books[key]) { + var book = new OrderBook(this, + currency_gets, issuer_gets, + currency_pays, issuer_pays); + + if (!book.is_valid()) return book; + + this._books[key] = book; + } + + return this._books[key]; } // Return the next account sequence if possible. diff --git a/src/js/utils.js b/src/js/utils.js index 462f3f286..eafa8bea7 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -97,6 +97,21 @@ var assert = function (assertion, msg) { } }; +/** + * Return unique values in array. + */ +var arrayUnique = function (arr) { + var u = {}, a = []; + for (var i = 0, l = arr.length; i < l; ++i){ + if (u.hasOwnProperty(arr[i])) { + continue; + } + a.push(arr[i]); + u[arr[i]] = 1; + } + return a; +}; + /** * Convert a ripple epoch to a JavaScript timestamp. * @@ -115,6 +130,7 @@ exports.stringToHex = stringToHex; exports.chunkString = chunkString; exports.logObject = logObject; exports.assert = assert; +exports.arrayUnique = arrayUnique; exports.toTimestamp = toTimestamp; // vim:sw=2:sts=2:ts=8:et