diff --git a/package.json b/package.json index 70566db75..f8cee61ad 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "simple-jsonrpc": "~0.0.1" }, "devDependencies": { + "grunt": "~0.3.17", "buster": "~0.6.2", "grunt-webpack": "~0.4.0" }, diff --git a/rippled-example.cfg b/rippled-example.cfg index f682966e7..f744233b2 100644 --- a/rippled-example.cfg +++ b/rippled-example.cfg @@ -235,6 +235,12 @@ # [database_path]: # Full path of database directory. # +# [path_search_size] +# When searching for paths, the maximum number of nodes allowed. This can take +# exponentially more resource as the size is increasded. +# +# The default is: 5 +# # [rpc_startup]: # Specify a list of RPC commands to run at startup. # diff --git a/src/cpp/ripple/Application.cpp b/src/cpp/ripple/Application.cpp index 5eff862f8..20235bc65 100644 --- a/src/cpp/ripple/Application.cpp +++ b/src/cpp/ripple/Application.cpp @@ -25,7 +25,7 @@ Application* theApp = NULL; DatabaseCon::DatabaseCon(const std::string& strName, const char *initStrings[], int initCount) { - boost::filesystem::path pPath = theConfig.DATA_DIR / strName; + boost::filesystem::path pPath = theConfig.RUN_STANDALONE ? "" : theConfig.DATA_DIR / strName; mDatabase = new SqliteDatabase(pPath.string().c_str()); mDatabase->connect(); @@ -49,10 +49,6 @@ Application::Application() : { getRand(mNonce256.begin(), mNonce256.size()); getRand(reinterpret_cast(&mNonceST), sizeof(mNonceST)); - mJobQueue.setThreadCount(); - mSweepTimer.expires_from_now(boost::posix_time::seconds(10)); - mSweepTimer.async_wait(boost::bind(&Application::sweep, this)); - mLoadMgr.init(); } extern const char *RpcDBInit[], *TxnDBInit[], *LedgerDBInit[], *WalletDBInit[], *HashNodeDBInit[], *NetNodeDBInit[]; @@ -88,6 +84,11 @@ void sigIntHandler(int) void Application::setup() { + mJobQueue.setThreadCount(); + mSweepTimer.expires_from_now(boost::posix_time::seconds(10)); + mSweepTimer.async_wait(boost::bind(&Application::sweep, this)); + mLoadMgr.init(); + #ifndef WIN32 #ifdef SIGINT if (!theConfig.RUN_STANDALONE) diff --git a/src/cpp/ripple/CallRPC.cpp b/src/cpp/ripple/CallRPC.cpp index c24f99fbb..f3edd39c9 100644 --- a/src/cpp/ripple/CallRPC.cpp +++ b/src/cpp/ripple/CallRPC.cpp @@ -512,6 +512,7 @@ Json::Value RPCParser::parseCommand(std::string strMethod, Json::Value jvParams) // { "nickname_info", &RPCParser::parseNicknameInfo, 1, 1 }, { "owner_info", &RPCParser::parseOwnerInfo, 1, 2 }, { "peers", &RPCParser::parseAsIs, 0, 0 }, + { "ping", &RPCParser::parseAsIs, 0, 0 }, // { "profile", &RPCParser::parseProfile, 1, 9 }, { "random", &RPCParser::parseAsIs, 0, 0 }, { "ripple_path_find", &RPCParser::parseRipplePathFind, 1, 1 }, diff --git a/src/cpp/ripple/Config.cpp b/src/cpp/ripple/Config.cpp index 7af7c0824..3d1c9f3ac 100644 --- a/src/cpp/ripple/Config.cpp +++ b/src/cpp/ripple/Config.cpp @@ -28,6 +28,7 @@ #define SECTION_NETWORK_QUORUM "network_quorum" #define SECTION_NODE_SEED "node_seed" #define SECTION_NODE_SIZE "node_size" +#define SECTION_PATH_SEARCH_SIZE "path_search_size" #define SECTION_PEER_CONNECT_LOW_WATER "peer_connect_low_water" #define SECTION_PEER_IP "peer_ip" #define SECTION_PEER_PORT "peer_port" @@ -219,6 +220,7 @@ Config::Config() LEDGER_HISTORY = 256; + PATH_SEARCH_SIZE = DEFAULT_PATH_SEARCH_SIZE; ACCOUNT_PROBE_MAX = 10; VALIDATORS_SITE = DEFAULT_VALIDATORS_SITE; @@ -445,6 +447,9 @@ void Config::load() LEDGER_HISTORY = boost::lexical_cast(strTemp); } + if (sectionSingleB(secConfig, SECTION_PATH_SEARCH_SIZE, strTemp)) + PATH_SEARCH_SIZE = boost::lexical_cast(strTemp); + if (sectionSingleB(secConfig, SECTION_ACCOUNT_PROBE_MAX, strTemp)) ACCOUNT_PROBE_MAX = boost::lexical_cast(strTemp); diff --git a/src/cpp/ripple/Config.h b/src/cpp/ripple/Config.h index 741e9a7ef..bff35fcc2 100644 --- a/src/cpp/ripple/Config.h +++ b/src/cpp/ripple/Config.h @@ -50,6 +50,9 @@ const int SYSTEM_WEBSOCKET_PUBLIC_PORT = 6563; // XXX Going away. // Might connect with fewer for testing. #define DEFAULT_PEER_CONNECT_LOW_WATER 4 +// Grows exponentially worse. +#define DEFAULT_PATH_SEARCH_SIZE 5 + enum SizedItemName { siSweepInterval, @@ -142,6 +145,9 @@ public: bool RPC_ALLOW_REMOTE; Json::Value RPC_STARTUP; + // Path searching + int PATH_SEARCH_SIZE; + // Validation RippleAddress VALIDATION_SEED, VALIDATION_PUB, VALIDATION_PRIV; diff --git a/src/cpp/ripple/JobQueue.cpp b/src/cpp/ripple/JobQueue.cpp index 53d98484b..3f3d0a3e2 100644 --- a/src/cpp/ripple/JobQueue.cpp +++ b/src/cpp/ripple/JobQueue.cpp @@ -56,6 +56,8 @@ const char* Job::toString(JobType t) case jtRPC: return "rpc"; case jtACCEPTLEDGER: return "acceptLedger"; case jtTXN_PROC: return "processTransaction"; + case jtOB_SETUP: return "orderBookSetup"; + case jtPATH_FIND: return "pathFind"; default: assert(false); return "unknown"; } } @@ -101,7 +103,9 @@ void JobQueue::addJob(JobType type, const boost::function& jobFunc) assert(type != jtINVALID); boost::mutex::scoped_lock sl(mJobLock); - assert(mThreadCount != 0); // do not add jobs to a queue with no threads + + if (type != jtCLIENT) // FIXME: Workaround incorrect client shutdown ordering + assert(mThreadCount != 0); // do not add jobs to a queue with no threads mJobSet.insert(Job(type, ++mLastJob, mJobLoads[type], jobFunc)); ++mJobCounts[type]; diff --git a/src/cpp/ripple/JobQueue.h b/src/cpp/ripple/JobQueue.h index d16ddec3d..10dfbc7dd 100644 --- a/src/cpp/ripple/JobQueue.h +++ b/src/cpp/ripple/JobQueue.h @@ -37,13 +37,15 @@ enum JobType jtDEATH = 14, // job of death, used internally // special types not dispatched by the job pool - jtPEER = 17, - jtDISK = 18, - jtRPC = 19, - jtACCEPTLEDGER = 20, - jtTXN_PROC = 21, + jtPEER = 24, + jtDISK = 25, + jtRPC = 26, + jtACCEPTLEDGER = 27, + jtTXN_PROC = 28, + jtOB_SETUP = 29, + jtPATH_FIND = 30 }; // CAUTION: If you add new types, add them to JobType.cpp too -#define NUM_JOB_TYPES 24 +#define NUM_JOB_TYPES 32 class Job { diff --git a/src/cpp/ripple/Ledger.cpp b/src/cpp/ripple/Ledger.cpp index 7f8e9e4dd..e0f9589eb 100644 --- a/src/cpp/ripple/Ledger.cpp +++ b/src/cpp/ripple/Ledger.cpp @@ -362,6 +362,23 @@ bool Ledger::getTransaction(const uint256& txID, Transaction::pointer& txn, Tran return true; } +bool Ledger::getTransactionMeta(const uint256& txID, TransactionMetaSet::pointer& meta) +{ + SHAMapTreeNode::TNType type; + SHAMapItem::pointer item = mTransactionMap->peekItem(txID, type); + if (!item) + return false; + + if (type != SHAMapTreeNode::tnTRANSACTION_MD) + return false; + + SerializerIterator it(item->peekSerializer()); + it.getVL(); // skip transaction + meta = boost::make_shared(txID, mLedgerSeq, it.getVL()); + + return true; +} + uint256 Ledger::getHash() { if (!mValidHash) @@ -1231,7 +1248,7 @@ uint256 Ledger::getBookBase(const uint160& uTakerPaysCurrency, const uint160& uT uint256 uBaseIndex = getQualityIndex(s.getSHA512Half()); // Return with quality 0. - cLog(lsDEBUG) << boost::str(boost::format("getBookBase(%s,%s,%s,%s) = %s") + cLog(lsTRACE) << boost::str(boost::format("getBookBase(%s,%s,%s,%s) = %s") % STAmount::createHumanCurrency(uTakerPaysCurrency) % RippleAddress::createHumanAccountID(uTakerPaysIssuerID) % STAmount::createHumanCurrency(uTakerGetsCurrency) diff --git a/src/cpp/ripple/Ledger.h b/src/cpp/ripple/Ledger.h index 97fc9c2b6..cf09383ac 100644 --- a/src/cpp/ripple/Ledger.h +++ b/src/cpp/ripple/Ledger.h @@ -179,6 +179,7 @@ public: bool hasTransaction(const uint256& TransID) const { return mTransactionMap->hasItem(TransID); } Transaction::pointer getTransaction(const uint256& transID) const; bool getTransaction(const uint256& transID, Transaction::pointer& txn, TransactionMetaSet::pointer& txMeta); + bool getTransactionMeta(const uint256& transID, TransactionMetaSet::pointer& txMeta); static SerializedTransaction::pointer getSTransaction(SHAMapItem::ref, SHAMapTreeNode::TNType); SerializedTransaction::pointer getSMTransaction(SHAMapItem::ref, SHAMapTreeNode::TNType, diff --git a/src/cpp/ripple/LedgerConsensus.cpp b/src/cpp/ripple/LedgerConsensus.cpp index 52d9533f3..8cba7d23e 100644 --- a/src/cpp/ripple/LedgerConsensus.cpp +++ b/src/cpp/ripple/LedgerConsensus.cpp @@ -1156,7 +1156,7 @@ int LedgerConsensus::applyTransaction(TransactionEngine& engine, SerializedTrans return LCAT_SUCCESS; } - if (isTefFailure(result) || isTemMalformed(result)) + if (isTefFailure(result) || isTemMalformed(result) || isTelLocal(result)) { // failure cLog(lsDEBUG) << "Transaction failure: " << transHuman(result); return LCAT_FAIL; diff --git a/src/cpp/ripple/LedgerEntrySet.cpp b/src/cpp/ripple/LedgerEntrySet.cpp index 15461e52e..43f0fc451 100644 --- a/src/cpp/ripple/LedgerEntrySet.cpp +++ b/src/cpp/ripple/LedgerEntrySet.cpp @@ -1072,25 +1072,31 @@ STAmount LedgerEntrySet::accountHolds(const uint160& uAccountID, const uint160& SLE::pointer sleAccount = entryCache(ltACCOUNT_ROOT, Ledger::getAccountRootIndex(uAccountID)); uint64 uReserve = mLedger->getReserve(sleAccount->getFieldU32(sfOwnerCount)); - saAmount = sleAccount->getFieldAmount(sfBalance)-uReserve; + STAmount saBalance = sleAccount->getFieldAmount(sfBalance); - if (saAmount < uReserve) + if (saBalance < uReserve) { saAmount.zero(); } else { - saAmount -= uReserve; + saAmount = saBalance-uReserve; } + + cLog(lsINFO) << boost::str(boost::format("accountHolds: uAccountID=%s saAmount=%s saBalance=%s uReserve=%d") + % RippleAddress::createHumanAccountID(uAccountID) + % saAmount.getFullText() + % saBalance.getFullText() + % uReserve); } else { saAmount = rippleHolds(uAccountID, uCurrencyID, uIssuerID); - } - cLog(lsINFO) << boost::str(boost::format("accountHolds: uAccountID=%s saAmount=%s") - % RippleAddress::createHumanAccountID(uAccountID) - % saAmount.getFullText()); + cLog(lsINFO) << boost::str(boost::format("accountHolds: uAccountID=%s saAmount=%s") + % RippleAddress::createHumanAccountID(uAccountID) + % saAmount.getFullText()); + } return saAmount; } @@ -1135,7 +1141,7 @@ STAmount LedgerEntrySet::rippleTransferFee(const uint160& uSenderID, const uint1 if (QUALITY_ONE != uTransitRate) { - STAmount saTransitRate(CURRENCY_ONE, ACCOUNT_ONE, uTransitRate, -9); + STAmount saTransitRate(CURRENCY_ONE, ACCOUNT_ONE, static_cast(uTransitRate), -9); STAmount saTransferTotal = STAmount::multiply(saAmount, saTransitRate, saAmount.getCurrency(), saAmount.getIssuer()); STAmount saTransferFee = saTransferTotal-saAmount; diff --git a/src/cpp/ripple/LedgerMaster.cpp b/src/cpp/ripple/LedgerMaster.cpp index 8ab38341c..e486cd9d9 100644 --- a/src/cpp/ripple/LedgerMaster.cpp +++ b/src/cpp/ripple/LedgerMaster.cpp @@ -29,6 +29,8 @@ void LedgerMaster::pushLedger(Ledger::ref newLedger) // all candidate transactions must already be applied cLog(lsINFO) << "PushLedger: " << newLedger->getHash(); boost::recursive_mutex::scoped_lock ml(mLock); + if (!mPubLedger) + mPubLedger = newLedger; if (!!mFinalizedLedger) { mFinalizedLedger->setClosed(); diff --git a/src/cpp/ripple/LedgerMaster.h b/src/cpp/ripple/LedgerMaster.h index 91fa0e035..458dd3980 100644 --- a/src/cpp/ripple/LedgerMaster.h +++ b/src/cpp/ripple/LedgerMaster.h @@ -93,6 +93,14 @@ public: Ledger::pointer closeLedger(bool recoverHeldTransactions); + uint256 getHashBySeq(uint32 index) + { + uint256 hash = mLedgerHistory.getLedgerHash(index); + if (hash.isNonZero()) + return hash; + return Ledger::getHashByIndex(index); + } + Ledger::pointer getLedgerBySeq(uint32 index) { if (mCurrentLedger && (mCurrentLedger->getLedgerSeq() == index)) diff --git a/src/cpp/ripple/LoadManager.cpp b/src/cpp/ripple/LoadManager.cpp index 54c70bd98..1a03a92bd 100644 --- a/src/cpp/ripple/LoadManager.cpp +++ b/src/cpp/ripple/LoadManager.cpp @@ -242,24 +242,26 @@ void LoadFeeTrack::setRemoteFee(uint32 f) mRemoteTxnLoadFee = f; } -void LoadFeeTrack::raiseLocalFee() +bool LoadFeeTrack::raiseLocalFee() { boost::mutex::scoped_lock sl(mLock); uint32 origFee = mLocalTxnLoadFee; - if (mLocalTxnLoadFee < mLocalTxnLoadFee) // make sure this fee takes effect - mLocalTxnLoadFee = mLocalTxnLoadFee; + if (mLocalTxnLoadFee < mRemoteTxnLoadFee) // make sure this fee takes effect + mLocalTxnLoadFee = mRemoteTxnLoadFee; mLocalTxnLoadFee += (mLocalTxnLoadFee / lftFeeIncFraction); // increment by 1/16th if (mLocalTxnLoadFee > lftFeeMax) mLocalTxnLoadFee = lftFeeMax; - tLog(origFee != mLocalTxnLoadFee, lsDEBUG) << - "Local load fee raised from " << origFee << " to " << mLocalTxnLoadFee; + if (origFee == mLocalTxnLoadFee) + return false; + cLog(lsDEBUG) << "Local load fee raised from " << origFee << " to " << mLocalTxnLoadFee; + return true; } -void LoadFeeTrack::lowerLocalFee() +bool LoadFeeTrack::lowerLocalFee() { boost::mutex::scoped_lock sl(mLock); uint32 origFee = mLocalTxnLoadFee; @@ -269,8 +271,10 @@ void LoadFeeTrack::lowerLocalFee() if (mLocalTxnLoadFee < lftNormalFee) mLocalTxnLoadFee = lftNormalFee; - tLog(origFee != mLocalTxnLoadFee, lsDEBUG) << - "Local load fee lowered from " << origFee << " to " << mLocalTxnLoadFee; + if (origFee == mLocalTxnLoadFee) + return false; + cLog(lsDEBUG) << "Local load fee lowered from " << origFee << " to " << mLocalTxnLoadFee; + return true; } Json::Value LoadFeeTrack::getJson(uint64 baseFee, uint32 referenceFeeUnits) @@ -312,10 +316,13 @@ void LoadManager::threadEntry() ++mUptime; } + bool change; if (theApp->getJobQueue().isOverloaded()) - theApp->getFeeTrack().raiseLocalFee(); + change = theApp->getFeeTrack().raiseLocalFee(); else - theApp->getFeeTrack().lowerLocalFee(); + change = theApp->getFeeTrack().lowerLocalFee(); + if (change) + theApp->getOPs().reportFeeChange(); t += boost::posix_time::seconds(1); boost::posix_time::time_duration when = t - boost::posix_time::microsec_clock::universal_time(); diff --git a/src/cpp/ripple/LoadManager.h b/src/cpp/ripple/LoadManager.h index b64394298..825e8f760 100644 --- a/src/cpp/ripple/LoadManager.h +++ b/src/cpp/ripple/LoadManager.h @@ -164,8 +164,8 @@ public: Json::Value getJson(uint64 baseFee, uint32 referenceFeeUnits); void setRemoteFee(uint32); - void raiseLocalFee(); - void lowerLocalFee(); + bool raiseLocalFee(); + bool lowerLocalFee(); }; diff --git a/src/cpp/ripple/NetworkOPs.cpp b/src/cpp/ripple/NetworkOPs.cpp index bdceb63c4..4c798660b 100644 --- a/src/cpp/ripple/NetworkOPs.cpp +++ b/src/cpp/ripple/NetworkOPs.cpp @@ -35,7 +35,8 @@ void InfoSub::onSendEmpty() NetworkOPs::NetworkOPs(boost::asio::io_service& io_service, LedgerMaster* pLedgerMaster) : mMode(omDISCONNECTED), mNeedNetworkLedger(false), mProposing(false), mValidating(false), mNetTimer(io_service), mLedgerMaster(pLedgerMaster), mCloseTimeOffset(0), mLastCloseProposers(0), - mLastCloseConvergeTime(1000 * LEDGER_IDLE_INTERVAL), mLastValidationTime(0) + mLastCloseConvergeTime(1000 * LEDGER_IDLE_INTERVAL), mLastValidationTime(0), + mLastLoadBase(256), mLastLoadFactor(256) { } @@ -118,6 +119,24 @@ bool NetworkOPs::haveLedger(uint32 seq) return mLedgerMaster->haveLedger(seq); } +uint32 NetworkOPs::getValidatedSeq() +{ + return mLedgerMaster->getValidatedLedger()->getLedgerSeq(); +} + +bool NetworkOPs::isValidated(uint32 seq, const uint256& hash) +{ + if (!isValidated(seq)) + return false; + + return mLedgerMaster->getHashBySeq(seq) == hash; +} + +bool NetworkOPs::isValidated(uint32 seq) +{ // use when ledger was retrieved by seq + return haveLedger(seq) && (seq <= mLedgerMaster->getValidatedLedger()->getLedgerSeq()); +} + bool NetworkOPs::addWantedHash(const uint256& h) { boost::recursive_mutex::scoped_lock sl(mWantedHashLock); @@ -1020,12 +1039,12 @@ void NetworkOPs::pubServer() jvObj["type"] = "serverStatus"; jvObj["server_status"] = strOperatingMode(); - jvObj["load_base"] = theApp->getFeeTrack().getLoadBase(); - jvObj["load_factor"] = theApp->getFeeTrack().getLoadFactor(); + jvObj["load_base"] = (mLastLoadBase = theApp->getFeeTrack().getLoadBase()); + jvObj["load_factor"] = (mLastLoadFactor = theApp->getFeeTrack().getLoadFactor()); BOOST_FOREACH(InfoSub* ispListener, mSubServer) { - ispListener->send(jvObj); + ispListener->send(jvObj, true); } } } @@ -1046,7 +1065,7 @@ void NetworkOPs::setMode(OperatingMode om) std::vector< std::pair > NetworkOPs::getAccountTxs(const RippleAddress& account, uint32 minLedger, uint32 maxLedger) -{ +{ // can be called with no locks std::vector< std::pair > ret; std::string sql = @@ -1073,7 +1092,7 @@ std::vector< std::pair > }else rawMeta.resize(metaSize); TransactionMetaSet::pointer meta= boost::make_shared(txn->getID(), txn->getLedger(), rawMeta.getData()); - ret.push_back(std::pair(txn,meta)); + ret.push_back(std::pair(txn,meta)); } } @@ -1162,7 +1181,7 @@ Json::Value NetworkOPs::getServerInfo(bool human, bool admin) } else info["load_factor"] = - static_cast(theApp->getFeeTrack().getLoadBase()) / theApp->getFeeTrack().getLoadFactor(); + static_cast(theApp->getFeeTrack().getLoadFactor()) / theApp->getFeeTrack().getLoadBase(); bool valid = false; Ledger::pointer lpClosed = getValidatedLedger(); @@ -1227,7 +1246,7 @@ void NetworkOPs::pubProposedTransaction(Ledger::ref lpCurrent, const SerializedT boost::recursive_mutex::scoped_lock sl(mMonitorLock); BOOST_FOREACH(InfoSub* ispListener, mSubRTTransactions) { - ispListener->send(jvObj); + ispListener->send(jvObj, true); } } TransactionMetaSet::pointer ret; @@ -1258,7 +1277,7 @@ void NetworkOPs::pubLedger(Ledger::ref lpAccepted) BOOST_FOREACH(InfoSub* ispListener, mSubLedger) { - ispListener->send(jvObj); + ispListener->send(jvObj, true); } } } @@ -1285,6 +1304,15 @@ void NetworkOPs::pubLedger(Ledger::ref lpAccepted) } } +void NetworkOPs::reportFeeChange() +{ + if ((theApp->getFeeTrack().getLoadBase() == mLastLoadBase) && + (theApp->getFeeTrack().getLoadFactor() == mLastLoadFactor)) + return; + + theApp->getJobQueue().addJob(jtCLIENT, boost::bind(&NetworkOPs::pubServer, this)); +} + Json::Value NetworkOPs::transJson(const SerializedTransaction& stTxn, TER terResult, bool bAccepted, Ledger::ref lpCurrent, const std::string& strType) { Json::Value jvObj(Json::objectValue); @@ -1323,12 +1351,12 @@ void NetworkOPs::pubAcceptedTransaction(Ledger::ref lpCurrent, const SerializedT BOOST_FOREACH(InfoSub* ispListener, mSubTransactions) { - ispListener->send(jvObj); + ispListener->send(jvObj, true); } BOOST_FOREACH(InfoSub* ispListener, mSubRTTransactions) { - ispListener->send(jvObj); + ispListener->send(jvObj, true); } } theApp->getOrderBookDB().processTxn(stTxn, terResult, meta, jvObj); @@ -1339,6 +1367,8 @@ void NetworkOPs::pubAcceptedTransaction(Ledger::ref lpCurrent, const SerializedT void NetworkOPs::pubAccountTransaction(Ledger::ref lpCurrent, const SerializedTransaction& stTxn, TER terResult, bool bAccepted, TransactionMetaSet::pointer& meta) { boost::unordered_set notify; + int iProposed = 0; + int iAccepted = 0; { boost::recursive_mutex::scoped_lock sl(mMonitorLock); @@ -1356,6 +1386,7 @@ void NetworkOPs::pubAccountTransaction(Ledger::ref lpCurrent, const SerializedTr { BOOST_FOREACH(InfoSub* ispListener, simiIt->second) { + ++iProposed; notify.insert(ispListener); } } @@ -1368,6 +1399,7 @@ void NetworkOPs::pubAccountTransaction(Ledger::ref lpCurrent, const SerializedTr { BOOST_FOREACH(InfoSub* ispListener, simiIt->second) { + ++iAccepted; notify.insert(ispListener); } } @@ -1375,6 +1407,7 @@ void NetworkOPs::pubAccountTransaction(Ledger::ref lpCurrent, const SerializedTr } } } + cLog(lsINFO) << boost::str(boost::format("pubAccountTransaction: iProposed=%d iAccepted=%d") % iProposed % iAccepted); // FIXME: This can crash. An InfoSub can go away while we hold a regular pointer to it. if (!notify.empty()) @@ -1385,7 +1418,7 @@ void NetworkOPs::pubAccountTransaction(Ledger::ref lpCurrent, const SerializedTr BOOST_FOREACH(InfoSub* ispListener, notify) { - ispListener->send(jvObj); + ispListener->send(jvObj, true); } } } @@ -1401,6 +1434,8 @@ void NetworkOPs::subAccount(InfoSub* ispListener, const boost::unordered_setinsertSubAccountInfo(naAccountID, uLedgerIndex); } diff --git a/src/cpp/ripple/NetworkOPs.h b/src/cpp/ripple/NetworkOPs.h index 918ea4ccf..8940d55cb 100644 --- a/src/cpp/ripple/NetworkOPs.h +++ b/src/cpp/ripple/NetworkOPs.h @@ -36,7 +36,7 @@ public: virtual ~InfoSub(); - virtual void send(const Json::Value& jvObj) = 0; + virtual void send(const Json::Value& jvObj, bool broadcast) = 0; void onSendEmpty(); @@ -116,6 +116,9 @@ protected: boost::recursive_mutex mWantedHashLock; boost::unordered_set mWantedHashes; + uint32 mLastLoadBase; + uint32 mLastLoadFactor; + void setMode(OperatingMode); Json::Value transJson(const SerializedTransaction& stTxn, TER terResult, bool bAccepted, Ledger::ref lpCurrent, const std::string& strType); @@ -153,6 +156,10 @@ public: // Do we have this inclusive range of ledgers in our database bool haveLedgerRange(uint32 from, uint32 to); bool haveLedger(uint32 seq); + uint32 getValidatedSeq(); + bool isValidated(uint32 seq); + bool isValidated(uint32 seq, const uint256& hash); + bool isValidated(Ledger::ref l) { return isValidated(l->getLedgerSeq(), l->getHash()); } SerializedValidation::ref getLastValidation() { return mLastValidation; } void setLastValidation(SerializedValidation::ref v) { mLastValidation = v; } @@ -257,6 +264,7 @@ public: std::list >& peekStoredProposals() { return mStoredProposals; } void storeProposal(LedgerProposal::ref proposal, const RippleAddress& peerPublic); uint256 getConsensusLCL(); + void reportFeeChange(); bool addWantedHash(const uint256& h); bool isWantedHash(const uint256& h, bool remove); diff --git a/src/cpp/ripple/OfferCreateTransactor.cpp b/src/cpp/ripple/OfferCreateTransactor.cpp index 648872575..76f563660 100644 --- a/src/cpp/ripple/OfferCreateTransactor.cpp +++ b/src/cpp/ripple/OfferCreateTransactor.cpp @@ -56,11 +56,13 @@ TER OfferCreateTransactor::takeOffers( while (temUNCERTAIN == terResult) { SLE::pointer sleOfferDir; - uint64 uTipQuality = 0; + uint64 uTipQuality = 0; + STAmount saTakerFunds = mEngine->getNodes().accountFunds(uTakerAccountID, saTakerPays); // Figure out next offer to take, if needed. - if (saTakerGot < saTakerGets // Have less than wanted. - && saTakerPaid < saTakerPays) // Didn't spend all funds. + if (saTakerFunds // Taker has funds available. + && saTakerGot < saTakerGets // Have less than wanted. + && saTakerPaid < saTakerPays) // Didn't spend all funds allocated. { sleOfferDir = mEngine->entryCache(ltDIR_NODE, mEngine->getLedger()->getNextLedgerIndex(uTipIndex, uBookEnd)); if (sleOfferDir) @@ -82,7 +84,15 @@ TER OfferCreateTransactor::takeOffers( } } - if (!sleOfferDir // No offer directory to take. + if (!saTakerFunds) // Taker has no funds. + { + // Done. Ran out of funds on previous round. As fees aren't calculated directly in this routine, funds are checked here. + cLog(lsINFO) << "takeOffers: done: taker unfunded."; + + bUnfunded = true; // Don't create an order. + terResult = tesSUCCESS; + } + else if (!sleOfferDir // No offer directory to take. || uTakeQuality < uTipQuality // No offers of sufficient quality available. || (bPassive && uTakeQuality == uTipQuality)) { @@ -149,7 +159,6 @@ TER OfferCreateTransactor::takeOffers( cLog(lsINFO) << "takeOffers: saOfferPays=" << saOfferPays.getFullText(); STAmount saOfferFunds = mEngine->getNodes().accountFunds(uOfferOwnerID, saOfferPays); - STAmount saTakerFunds = mEngine->getNodes().accountFunds(uTakerAccountID, saTakerPays); SLE::pointer sleOfferAccount; // Owner of offer. if (!saOfferFunds.isPositive()) // Includes zero. @@ -243,17 +252,13 @@ TER OfferCreateTransactor::takeOffers( if (!bUnfunded) { - terResult = mEngine->getNodes().accountSend(uOfferOwnerID, uTakerAccountID, saSubTakerGot); // Offer owner pays taker. + // Distribute funds. The sends charge appropriate fees which are implied by offer. -// if (tesSUCCESS == terResult) -// terResult = mEngine->getNodes().accountSend(uOfferOwnerID, uTakerGetsAccountID, saOfferIssuerFee); // Offer owner pays issuer transfer fee. + terResult = mEngine->getNodes().accountSend(uOfferOwnerID, uTakerAccountID, saSubTakerGot); // Offer owner pays taker. if (tesSUCCESS == terResult) terResult = mEngine->getNodes().accountSend(uTakerAccountID, uOfferOwnerID, saSubTakerPaid); // Taker pays offer owner. -// if (tesSUCCESS == terResult) -// terResult = mEngine->getNodes().accountSend(uTakerAccountID, uTakerPaysAccountID, saTakerIssuerFee); // Taker pays issuer transfer fee. - // Reduce amount considered paid by taker's rate (not actual cost). STAmount saPay = saTakerPays - saTakerPaid; if (saTakerFunds < saPay) @@ -550,6 +555,9 @@ TER OfferCreateTransactor::doApply() tLog(tesSUCCESS != terResult, lsINFO) << boost::str(boost::format("OfferCreate: final terResult=%s") % transToken(terResult)); + if (isTesSuccess(terResult)) + theApp->getOrderBookDB().invalidate(); + return terResult; } diff --git a/src/cpp/ripple/OrderBook.cpp b/src/cpp/ripple/OrderBook.cpp index 939fefe7f..c56027073 100644 --- a/src/cpp/ripple/OrderBook.cpp +++ b/src/cpp/ripple/OrderBook.cpp @@ -1,14 +1,14 @@ #include "OrderBook.h" #include "Ledger.h" -OrderBook::pointer OrderBook::newOrderBook(SerializedLedgerEntry::pointer ledgerEntry) +OrderBook::pointer OrderBook::newOrderBook(SerializedLedgerEntry::ref ledgerEntry) { if(ledgerEntry->getType() != ltOFFER) return( OrderBook::pointer()); return( OrderBook::pointer(new OrderBook(ledgerEntry))); } -OrderBook::OrderBook(SerializedLedgerEntry::pointer ledgerEntry) +OrderBook::OrderBook(SerializedLedgerEntry::ref ledgerEntry) { const STAmount saTakerGets = ledgerEntry->getFieldAmount(sfTakerGets); const STAmount saTakerPays = ledgerEntry->getFieldAmount(sfTakerPays); diff --git a/src/cpp/ripple/OrderBook.h b/src/cpp/ripple/OrderBook.h index 813709d3b..fa3c0f669 100644 --- a/src/cpp/ripple/OrderBook.h +++ b/src/cpp/ripple/OrderBook.h @@ -1,3 +1,8 @@ + +#ifndef ORDERBOOK_H +#define ORDERBOOK_H + + #include "SerializedLedger.h" #include "NetworkOPs.h" #include @@ -15,14 +20,14 @@ class OrderBook uint160 mIssuerOut; //SerializedLedgerEntry::pointer mLedgerEntry; - OrderBook(SerializedLedgerEntry::pointer ledgerEntry); // For accounts in a ledger + OrderBook(SerializedLedgerEntry::ref ledgerEntry); // For accounts in a ledger public: typedef boost::shared_ptr pointer; typedef const boost::shared_ptr& ref; // returns NULL if ledgerEntry doesn't point to an order // if ledgerEntry is an Order it creates the OrderBook this order would live in - static OrderBook::pointer newOrderBook(SerializedLedgerEntry::pointer ledgerEntry); + static OrderBook::pointer newOrderBook(SerializedLedgerEntry::ref ledgerEntry); uint256& getBookBase(){ return(mBookBase); } uint160& getCurrencyIn(){ return(mCurrencyIn); } @@ -34,4 +39,6 @@ public: STAmount& getTakePrice(STAmount& takeAmount); }; +#endif + // vim:ts=4 diff --git a/src/cpp/ripple/OrderBookDB.cpp b/src/cpp/ripple/OrderBookDB.cpp index ab95082d8..2aed79cf8 100644 --- a/src/cpp/ripple/OrderBookDB.cpp +++ b/src/cpp/ripple/OrderBookDB.cpp @@ -1,18 +1,33 @@ #include +#include "Application.h" #include "OrderBookDB.h" #include "Log.h" SETUP_LOG(); -OrderBookDB::OrderBookDB() +OrderBookDB::OrderBookDB() : mSeq(0) { } -// TODO: this would be way faster if we could just look under the order dirs -void OrderBookDB::setup(Ledger::pointer ledger) +void OrderBookDB::invalidate() { + boost::recursive_mutex::scoped_lock sl(mLock); + mSeq = 0; +} + +// TODO: this would be way faster if we could just look under the order dirs +void OrderBookDB::setup(Ledger::ref ledger) +{ + boost::recursive_mutex::scoped_lock sl(mLock); + + if (ledger->getLedgerSeq() == mSeq) + return; + mSeq = ledger->getLedgerSeq(); + + LoadEvent::autoptr ev = theApp->getJobQueue().getLoadEventAP(jtOB_SETUP); + mXRPOrders.clear(); mIssuerMap.clear(); mKnownMap.clear(); @@ -61,17 +76,21 @@ void OrderBookDB::setup(Ledger::pointer ledger) // return list of all orderbooks that want IssuerID std::vector& OrderBookDB::getBooks(const uint160& issuerID) { - return mIssuerMap.find(issuerID) == mIssuerMap.end() + boost::recursive_mutex::scoped_lock sl(mLock); + std::map< uint160, std::vector >::iterator it = mIssuerMap.find(issuerID); + return (it == mIssuerMap.end()) ? mEmptyVector - : mIssuerMap[issuerID]; + : it->second; } // return list of all orderbooks that want this issuerID and currencyID void OrderBookDB::getBooks(const uint160& issuerID, const uint160& currencyID, std::vector& bookRet) { - if (mIssuerMap.find(issuerID) == mIssuerMap.end()) + boost::recursive_mutex::scoped_lock sl(mLock); + std::map< uint160, std::vector >::iterator it = mIssuerMap.find(issuerID); + if (it != mIssuerMap.end()) { - BOOST_FOREACH(OrderBook::ref book, mIssuerMap[issuerID]) + BOOST_FOREACH(OrderBook::ref book, it->second) { if (book->getCurrencyIn() == currencyID) bookRet.push_back(book); @@ -81,6 +100,7 @@ void OrderBookDB::getBooks(const uint160& issuerID, const uint160& currencyID, s BookListeners::pointer OrderBookDB::makeBookListeners(uint160 currencyIn, uint160 currencyOut, uint160 issuerIn, uint160 issuerOut) { + boost::recursive_mutex::scoped_lock sl(mLock); BookListeners::pointer ret=getBookListeners(currencyIn, currencyOut, issuerIn, issuerOut); if(!ret) { @@ -92,6 +112,7 @@ BookListeners::pointer OrderBookDB::makeBookListeners(uint160 currencyIn, uint16 BookListeners::pointer OrderBookDB::getBookListeners(uint160 currencyIn, uint160 currencyOut, uint160 issuerIn, uint160 issuerOut) { + boost::recursive_mutex::scoped_lock sl(mLock); std::map > > >::iterator it0=mListeners.find(issuerIn); if(it0 != mListeners.end()) { @@ -163,6 +184,7 @@ BookListeners::pointer OrderBookDB::getBookListeners(uint160 currencyIn, uint160 // We need to determine which streams a given meta effects void OrderBookDB::processTxn(const SerializedTransaction& stTxn, TER terResult,TransactionMetaSet::pointer& meta,Json::Value& jvObj) { + boost::recursive_mutex::scoped_lock sl(mLock); if(terResult==tesSUCCESS) { // check if this is an offer or an offer cancel or a payment that consumes an offer @@ -226,7 +248,7 @@ void BookListeners::publish(Json::Value& jvObj) BOOST_FOREACH(InfoSub* sub,mListeners) { - sub->send(jvObj); + sub->send(jvObj, true); } } diff --git a/src/cpp/ripple/OrderBookDB.h b/src/cpp/ripple/OrderBookDB.h index 5651dd5dc..c3b99fda4 100644 --- a/src/cpp/ripple/OrderBookDB.h +++ b/src/cpp/ripple/OrderBookDB.h @@ -1,3 +1,7 @@ + +#ifndef ORDERBOOK_DB_H +#define ORDERBOOK_DB_H + #include "Ledger.h" #include "OrderBook.h" #include @@ -31,9 +35,13 @@ class OrderBookDB std::map mKnownMap; + uint32 mSeq; + boost::recursive_mutex mLock; + public: OrderBookDB(); - void setup(Ledger::pointer ledger); + void setup(Ledger::ref ledger); + void invalidate(); // return list of all orderbooks that want XRP std::vector& getXRPInBooks(){ return mXRPOrders; } @@ -56,4 +64,6 @@ public: }; +#endif + // vim:ts=4 diff --git a/src/cpp/ripple/ParseSection.cpp b/src/cpp/ripple/ParseSection.cpp index 839d3c85e..12bdded4b 100644 --- a/src/cpp/ripple/ParseSection.cpp +++ b/src/cpp/ripple/ParseSection.cpp @@ -1,4 +1,5 @@ #include "ParseSection.h" +#include "Log.h" #include "utils.h" #include @@ -7,6 +8,8 @@ #define SECTION_DEFAULT_NAME "" +SETUP_LOG(); + section ParseSection(const std::string& strInput, const bool bTrim) { std::string strData(strInput); @@ -113,6 +116,12 @@ bool sectionSingleB(section& secSource, const std::string& strSection, std::stri { strValue = (*pmtEntries)[0]; } + else if (pmtEntries) + { + cLog(lsWARNING) << boost::str(boost::format("Section [%s]: requires 1 line not %d lines.") + % strSection + % pmtEntries->size()); + } return bSingle; } diff --git a/src/cpp/ripple/Pathfinder.cpp b/src/cpp/ripple/Pathfinder.cpp index f3654366b..aaf895d31 100644 --- a/src/cpp/ripple/Pathfinder.cpp +++ b/src/cpp/ripple/Pathfinder.cpp @@ -118,8 +118,8 @@ bool Pathfinder::bDefaultPath(const STPath& spPath) // When path is a default (implied). Don't need to add it to return set. bDefault = pspCurrent->vpnNodes == mPsDefault->vpnNodes; - cLog(lsDEBUG) << "findPaths: expanded path: " << pspCurrent->getJson(); - cLog(lsDEBUG) << "findPaths: default path: indirect: " << spPath.getJson(0); + cLog(lsTRACE) << "findPaths: expanded path: " << pspCurrent->getJson(); + cLog(lsTRACE) << "findPaths: default path: indirect: " << spPath.getJson(0); return bDefault; } @@ -127,17 +127,21 @@ bool Pathfinder::bDefaultPath(const STPath& spPath) return false; } -Pathfinder::Pathfinder(const RippleAddress& uSrcAccountID, const RippleAddress& uDstAccountID, const uint160& uSrcCurrencyID, const uint160& uSrcIssuerID, const STAmount& saDstAmount) - : mSrcAccountID(uSrcAccountID.getAccountID()), +Pathfinder::Pathfinder(Ledger::ref ledger, + const RippleAddress& uSrcAccountID, const RippleAddress& uDstAccountID, + const uint160& uSrcCurrencyID, const uint160& uSrcIssuerID, const STAmount& saDstAmount) + : mSrcAccountID(uSrcAccountID.getAccountID()), mDstAccountID(uDstAccountID.getAccountID()), mDstAmount(saDstAmount), mSrcCurrencyID(uSrcCurrencyID), - mSrcIssuerID(uSrcIssuerID) + mSrcIssuerID(uSrcIssuerID), + mSrcAmount(uSrcCurrencyID, uSrcIssuerID, 1u, 0, true), + mLedger(ledger) { - mLedger = theApp->getLedgerMaster().getCurrentLedger(); - mSrcAmount = STAmount(uSrcCurrencyID, uSrcIssuerID, 1, 0, true); // -1/uSrcIssuerID/uSrcIssuerID - theApp->getOrderBookDB().setup( theApp->getLedgerMaster().getCurrentLedger()); // TODO: have the orderbook update itself rather than rebuild it from scratch each time + theApp->getOrderBookDB().setup(mLedger); + + mLoadMonitor = theApp->getJobQueue().getLoadEvent(jtPATH_FIND); // Construct the default path for later comparison. @@ -155,14 +159,14 @@ Pathfinder::Pathfinder(const RippleAddress& uSrcAccountID, const RippleAddress& if (tesSUCCESS == psDefault->terStatus) { // The default path works, remember it. - cLog(lsDEBUG) << "Pathfinder: default path: " << psDefault->getJson(); + cLog(lsTRACE) << "Pathfinder: default path: " << psDefault->getJson(); mPsDefault = psDefault; } else { // The default path doesn't work. - cLog(lsDEBUG) << "Pathfinder: default path: NONE: " << transToken(psDefault->terStatus); + cLog(lsTRACE) << "Pathfinder: default path: NONE: " << transToken(psDefault->terStatus); } } } @@ -185,7 +189,7 @@ bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMax { bool bFound = false; // True, iff found a path. - cLog(lsDEBUG) << boost::str(boost::format("findPaths> mSrcAccountID=%s mDstAccountID=%s mDstAmount=%s mSrcCurrencyID=%s mSrcIssuerID=%s") + cLog(lsTRACE) << boost::str(boost::format("findPaths> mSrcAccountID=%s mDstAccountID=%s mDstAmount=%s mSrcCurrencyID=%s mSrcIssuerID=%s") % RippleAddress::createHumanAccountID(mSrcAccountID) % RippleAddress::createHumanAccountID(mDstAccountID) % mDstAmount.getFullText() @@ -193,49 +197,123 @@ bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMax % RippleAddress::createHumanAccountID(mSrcIssuerID) ); - if (mLedger) + if (!mLedger) { - LedgerEntrySet lesActive(mLedger); - std::vector vspResults; - std::queue qspExplore; // Path stubs to explore. + cLog(lsDEBUG) << "findPaths< no ledger"; - STPath spSeed; - bool bForcedIssuer = !!mSrcCurrencyID && mSrcIssuerID != mSrcAccountID; // Source forced an issuer. + return false; + } - // The end is the cursor, start at the source account. - STPathElement speEnd(mSrcAccountID, - mSrcCurrencyID, - !!mSrcCurrencyID - ? mSrcAccountID // Non-XRP, start with self as issuer. - : ACCOUNT_XRP); + LedgerEntrySet lesActive(mLedger); + + SLE::pointer sleSrc = lesActive.entryCache(ltACCOUNT_ROOT, Ledger::getAccountRootIndex(mSrcAccountID)); + if (!sleSrc) + { + cLog(lsDEBUG) << boost::str(boost::format("findPaths< no source")); + + return false; + } + + SLE::pointer sleDst = lesActive.entryCache(ltACCOUNT_ROOT, Ledger::getAccountRootIndex(mDstAccountID)); + if (!sleDst) + { + cLog(lsDEBUG) << boost::str(boost::format("findPaths< no dest")); + + return false; + } + + std::vector vspResults; + std::queue qspExplore; // Path stubs to explore. + + STPath spSeed; + bool bForcedIssuer = !!mSrcCurrencyID && mSrcIssuerID != mSrcAccountID; // Source forced an issuer. + + // The end is the cursor, start at the source account. + STPathElement speEnd(mSrcAccountID, + mSrcCurrencyID, + !!mSrcCurrencyID + ? mSrcAccountID // Non-XRP, start with self as issuer. + : ACCOUNT_XRP); + + // Build a path of one element: the source. + spSeed.addElement(speEnd); + + if (bForcedIssuer) + { + // Add forced source issuer to seed, via issuer's account. + STPathElement speIssuer(mSrcIssuerID, mSrcCurrencyID, mSrcIssuerID); - // Build a path of one element: the source. spSeed.addElement(speEnd); + } - if (bForcedIssuer) + // Push the seed path to explore. + qspExplore.push(spSeed); + + while (qspExplore.size()) { // Have paths to explore? + STPath spPath = qspExplore.front(); + + qspExplore.pop(); // Pop the first path from the queue. + + speEnd = spPath.mPath.back(); // Get the last node from the path. + + if (!speEnd.mCurrencyID // Tail output is XRP. + && !mDstAmount.getCurrency()) // Which is dst currency. { - // Add forced source issuer to seed, via issuer's account. - STPathElement speIssuer(mSrcIssuerID, mSrcCurrencyID, mSrcIssuerID); + // Done, cursor produces XRP and dest wants XRP. - spSeed.addElement(speEnd); + // Remove implied source. + spPath.mPath.erase(spPath.mPath.begin()); + + if (bForcedIssuer) + { + // Remove implied source issuer. + spPath.mPath.erase(spPath.mPath.begin()); + } + + if (spPath.size()) + { + // There is an actual path element. + cLog(lsTRACE) << "findPaths: adding path: " << spPath.getJson(0); + + vspResults.push_back(spPath); // Potential result. + } + else + { + cLog(lsTRACE) << "findPaths: empty path: XRP->XRP"; + } + + continue; } - // Push the seed path to explore. - qspExplore.push(spSeed); + cLog(lsDEBUG) << "findPaths: finish? account: " << (speEnd.mAccountID == mDstAccountID); + cLog(lsDEBUG) << "findPaths: finish? currency: " << (speEnd.mCurrencyID == mDstAmount.getCurrency()); + cLog(lsTRACE) << "findPaths: finish? issuer: " + << RippleAddress::createHumanAccountID(speEnd.mIssuerID) + << " / " + << RippleAddress::createHumanAccountID(mDstAmount.getIssuer()) + << " / " + << RippleAddress::createHumanAccountID(mDstAccountID); + cLog(lsDEBUG) << "findPaths: finish? issuer is desired: " << (speEnd.mIssuerID == mDstAmount.getIssuer()); - while (qspExplore.size()) { // Have paths to explore? - STPath spPath = qspExplore.front(); + // YYY Allows going through self. Is this wanted? + if (speEnd.mAccountID == mDstAccountID // Tail is destination account. + && speEnd.mCurrencyID == mDstAmount.getCurrency() // With correct output currency. + && ( speEnd.mIssuerID == mDstAccountID // Dest always accepts own issuer. + || mDstAmount.getIssuer() == mDstAccountID // Any issuer is good. + || mDstAmount.getIssuer() == speEnd.mIssuerID)) // The desired issuer. + { + // Done, found a path to the destination. + // Cursor on the dest account with correct currency and issuer. - qspExplore.pop(); // Pop the first path from the queue. + if (bDefaultPath(spPath)) { + cLog(lsDEBUG) << "findPaths: dropping: default path: " << spPath.getJson(0); - speEnd = spPath.mPath.back(); // Get the last node from the path. - - if (!speEnd.mCurrencyID // Tail output is XRP. - && !mDstAmount.getCurrency()) // Which is dst currency. + bFound = true; + } + else { - // Done, cursor produces XRP and dest wants XRP. + // Remove implied nodes. - // Remove implied source. spPath.mPath.erase(spPath.mPath.begin()); if (bForcedIssuer) @@ -243,129 +321,81 @@ bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMax // Remove implied source issuer. spPath.mPath.erase(spPath.mPath.begin()); } + spPath.mPath.erase(spPath.mPath.begin() + spPath.mPath.size()-1); - if (spPath.size()) - { - // There is an actual path element. - cLog(lsDEBUG) << "findPaths: adding path: " << spPath.getJson(0); + vspResults.push_back(spPath); // Potential result. - vspResults.push_back(spPath); // Potential result. - } - else - { - cLog(lsDEBUG) << "findPaths: empty path: XRP->XRP"; - } - - continue; + cLog(lsDEBUG) << "findPaths: adding path: " << spPath.getJson(0); } - cLog(lsDEBUG) << "findPaths: finish? account: " << (speEnd.mAccountID == mDstAccountID); - cLog(lsDEBUG) << "findPaths: finish? currency: " << (speEnd.mCurrencyID == mDstAmount.getCurrency()); - cLog(lsDEBUG) << "findPaths: finish? issuer: " - << RippleAddress::createHumanAccountID(speEnd.mIssuerID) - << " / " - << RippleAddress::createHumanAccountID(mDstAmount.getIssuer()) - << " / " - << RippleAddress::createHumanAccountID(mDstAccountID); - cLog(lsDEBUG) << "findPaths: finish? issuer is desired: " << (speEnd.mIssuerID == mDstAmount.getIssuer()); + continue; + } - // YYY Allows going through self. Is this wanted? - if (speEnd.mAccountID == mDstAccountID // Tail is destination account. - && speEnd.mCurrencyID == mDstAmount.getCurrency() // With correct output currency. - && ( speEnd.mIssuerID == mDstAccountID // Dest always accepts own issuer. - || mDstAmount.getIssuer() == mDstAccountID // Any issuer is good. - || mDstAmount.getIssuer() == speEnd.mIssuerID)) // The desired issuer. + bool bContinued = false; // True, if wasn't a dead end. + + cLog(lsTRACE) << + boost::str(boost::format("findPaths: cursor: %s - %s/%s") + % RippleAddress::createHumanAccountID(speEnd.mAccountID) + % STAmount::createHumanCurrency(speEnd.mCurrencyID) + % RippleAddress::createHumanAccountID(speEnd.mIssuerID)); + + if (spPath.mPath.size() >= iMaxSteps) + { + // Path is at maximum size. Don't want to add more. + + cLog(lsDEBUG) + << boost::str(boost::format("findPaths: dropping: path would exceed max steps")); + + continue; + } + else if (!speEnd.mCurrencyID) + { + // Cursor is for XRP, continue with qualifying books: XRP -> non-XRP + BOOST_FOREACH(OrderBook::ref book, theApp->getOrderBookDB().getXRPInBooks()) { - // Done, found a path to the destination. - // Cursor on the dest account with correct currency and issuer. + // New end is an order book with the currency and issuer. - if (bDefaultPath(spPath)) { - cLog(lsDEBUG) << "findPaths: dropping: default path: " << spPath.getJson(0); - - bFound = true; - } - else + // Don't allow looping through same order books. + if (!spPath.hasSeen(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut())) { - // Remove implied nodes. + STPath spNew(spPath); + STPathElement speBook(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut()); + STPathElement speAccount(book->getIssuerOut(), book->getCurrencyOut(), book->getIssuerOut()); - spPath.mPath.erase(spPath.mPath.begin()); + spNew.mPath.push_back(speBook); // Add the order book. + spNew.mPath.push_back(speAccount); // Add the account and currency - if (bForcedIssuer) - { - // Remove implied source issuer. - spPath.mPath.erase(spPath.mPath.begin()); - } - spPath.mPath.erase(spPath.mPath.begin() + spPath.mPath.size()-1); + cLog(lsDEBUG) + << boost::str(boost::format("findPaths: XRP -> %s/%s") + % STAmount::createHumanCurrency(speBook.mCurrencyID) + % RippleAddress::createHumanAccountID(speBook.mIssuerID)); - vspResults.push_back(spPath); // Potential result. + qspExplore.push(spNew); - cLog(lsDEBUG) << "findPaths: adding path: " << spPath.getJson(0); + bContinued = true; } - - continue; } - bool bContinued = false; // True, if wasn't a dead end. + tLog(!bContinued, lsDEBUG) + << boost::str(boost::format("findPaths: XRP -> dead end")); + } + else + { + // Last element is for non-XRP, continue by adding ripple lines and order books. - cLog(lsDEBUG) << - boost::str(boost::format("findPaths: cursor: %s - %s/%s") + // Create new paths for each outbound account not already in the path. + AccountItems rippleLines(speEnd.mAccountID, mLedger, AccountItem::pointer(new RippleState())); + SLE::pointer sleEnd = lesActive.entryCache(ltACCOUNT_ROOT, Ledger::getAccountRootIndex(speEnd.mAccountID)); + + tLog(!sleEnd, lsDEBUG) + << boost::str(boost::format("findPaths: order book: %s/%s : ") % RippleAddress::createHumanAccountID(speEnd.mAccountID) - % STAmount::createHumanCurrency(speEnd.mCurrencyID) % RippleAddress::createHumanAccountID(speEnd.mIssuerID)); - if (spPath.mPath.size() == iMaxSteps) + if (sleEnd) { - // Path is at maximum size. Don't want to add more. - - cLog(lsDEBUG) - << boost::str(boost::format("findPaths: dropping: path would exceed max steps")); - - continue; - } - else if (!speEnd.mCurrencyID) - { - // Cursor is for XRP, continue with qualifying books: XRP -> non-XRP - BOOST_FOREACH(OrderBook::ref book, theApp->getOrderBookDB().getXRPInBooks()) - { - // New end is an order book with the currency and issuer. - - // Don't allow looping through same order books. - if (!spPath.hasSeen(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut())) - { - STPath spNew(spPath); - STPathElement speBook(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut()); - STPathElement speAccount(book->getIssuerOut(), book->getCurrencyOut(), book->getIssuerOut()); - - spNew.mPath.push_back(speBook); // Add the order book. - spNew.mPath.push_back(speAccount); // Add the account and currency - - cLog(lsDEBUG) << - boost::str(boost::format("findPaths: XRP -> %s/%s") - % STAmount::createHumanCurrency(speBook.mCurrencyID) - % RippleAddress::createHumanAccountID(speBook.mIssuerID)); - - qspExplore.push(spNew); - - bContinued = true; - } - } - - tLog(!bContinued, lsDEBUG) - << boost::str(boost::format("findPaths: XRP -> dead end")); - } - else - { - // Last element is for non-XRP, continue by adding ripple lines and order books. - - // Create new paths for each outbound account not already in the path. - AccountItems rippleLines(speEnd.mAccountID, mLedger, AccountItem::pointer(new RippleState())); - SLE::pointer sleSrc = lesActive.entryCache(ltACCOUNT_ROOT, Ledger::getAccountRootIndex(speEnd.mAccountID)); - - tLog(sleSrc, lsDEBUG) - << boost::str(boost::format("findPaths: account without root: %s") - % RippleAddress::createHumanAccountID(speEnd.mAccountID)); - - bool bRequireAuth = isSetBit(sleSrc->getFieldU32(sfFlags), lsfRequireAuth); + // On a non-XRP account: + bool bRequireAuth = isSetBit(sleEnd->getFieldU32(sfFlags), lsfRequireAuth); BOOST_FOREACH(AccountItem::ref item, rippleLines.getItems()) { @@ -388,7 +418,7 @@ bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMax || (bRequireAuth && !rspEntry->getAuth()))) // Not authorized to hold credit. { // Path has no credit left. Ignore it. - cLog(lsDEBUG) << + cLog(lsTRACE) << boost::str(boost::format("findPaths: No credit: %s/%s -> %s/%s") % RippleAddress::createHumanAccountID(speEnd.mAccountID) % STAmount::createHumanCurrency(speEnd.mCurrencyID) @@ -406,7 +436,7 @@ bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMax bContinued = true; - cLog(lsDEBUG) << + cLog(lsTRACE) << boost::str(boost::format("findPaths: push explore: %s/%s -> %s/%s") % STAmount::createHumanCurrency(speEnd.mCurrencyID) % RippleAddress::createHumanAccountID(speEnd.mAccountID) @@ -414,128 +444,131 @@ bool Pathfinder::findPaths(const unsigned int iMaxSteps, const unsigned int iMax % RippleAddress::createHumanAccountID(uPeerID)); } } + } - // Every book that wants the source currency. - std::vector books; + // Every book that wants the source currency. + std::vector books; + theApp->getOrderBookDB().getBooks(speEnd.mIssuerID, speEnd.mCurrencyID, books); - theApp->getOrderBookDB().getBooks(speEnd.mIssuerID, speEnd.mCurrencyID, books); - - BOOST_FOREACH(OrderBook::ref book, books) + BOOST_FOREACH(OrderBook::ref book, books) + { + if (!spPath.hasSeen(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut())) { - if (!spPath.hasSeen(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut())) + // A book we haven't seen before. Add it. + STPath spNew(spPath); + STPathElement speBook(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut()); + + spNew.mPath.push_back(speBook); // Add the order book. + + if (!book->getCurrencyOut()) { - // A book we haven't seen before. Add it. - STPath spNew(spPath); - STPathElement speBook(ACCOUNT_XRP, book->getCurrencyOut(), book->getIssuerOut()); - - spNew.mPath.push_back(speBook); - qspExplore.push(spNew); - - bContinued = true; - - cLog(lsDEBUG) << - boost::str(boost::format("findPaths: push book: %s/%s -> %s/%s") - % STAmount::createHumanCurrency(speEnd.mCurrencyID) - % RippleAddress::createHumanAccountID(speEnd.mIssuerID) - % STAmount::createHumanCurrency(book->getCurrencyOut()) - % RippleAddress::createHumanAccountID(book->getIssuerOut())); + // For non-XRP out, don't end on the book, add the issuing account. + STPathElement speAccount(book->getIssuerOut(), book->getCurrencyOut(), book->getIssuerOut()); + spNew.mPath.push_back(speAccount); // Add the account and currency } - } - tLog(!bContinued, lsDEBUG) - << boost::str(boost::format("findPaths: dropping: non-XRP -> dead end")); + qspExplore.push(spNew); + + bContinued = true; + + cLog(lsTRACE) << + boost::str(boost::format("findPaths: push book: %s/%s -> %s/%s") + % STAmount::createHumanCurrency(speEnd.mCurrencyID) + % RippleAddress::createHumanAccountID(speEnd.mIssuerID) + % STAmount::createHumanCurrency(book->getCurrencyOut()) + % RippleAddress::createHumanAccountID(book->getIssuerOut())); + } } + + tLog(!bContinued, lsDEBUG) + << boost::str(boost::format("findPaths: dropping: non-XRP -> dead end")); } + } - unsigned int iLimit = std::min(iMaxPaths, (unsigned int) vspResults.size()); + unsigned int iLimit = std::min(iMaxPaths, (unsigned int) vspResults.size()); - // Only filter, sort, and limit if have non-default paths. - if (iLimit) + // Only filter, sort, and limit if have non-default paths. + if (iLimit) + { + std::vector< std::pair > vMap; + + // Build map of quality to entry. + for (int i = vspResults.size(); i--;) { - std::vector< std::pair > vMap; + STAmount saMaxAmountAct; + STAmount saDstAmountAct; + std::vector vpsExpanded; + STPathSet spsPaths; + STPath& spCurrent = vspResults[i]; - // Build map of quality to entry. - for (int i = vspResults.size(); i--;) + spsPaths.addPath(spCurrent); // Just checking the current path. + + TER terResult; + + try { + terResult = RippleCalc::rippleCalc( + lesActive, + saMaxAmountAct, + saDstAmountAct, + vpsExpanded, + mSrcAmount, // --> amount to send max. + mDstAmount, // --> amount to deliver. + mDstAccountID, + mSrcAccountID, + spsPaths, + true, // --> bPartialPayment: Allow, it might contribute. + false, // --> bLimitQuality: Assume normal transaction. + true, // --> bNoRippleDirect: Providing the only path. + true); // --> bStandAlone: Don't need to delete unfundeds. + } + catch (const std::exception& e) { - STAmount saMaxAmountAct; - STAmount saDstAmountAct; - std::vector vpsExpanded; - STPathSet spsPaths; - STPath& spCurrent = vspResults[i]; + cLog(lsINFO) << "findPaths: Caught throw: " << e.what(); - spsPaths.addPath(spCurrent); // Just checking the current path. - - TER terResult; - - try { - terResult = RippleCalc::rippleCalc( - lesActive, - saMaxAmountAct, - saDstAmountAct, - vpsExpanded, - mSrcAmount, // --> amount to send max. - mDstAmount, // --> amount to deliver. - mDstAccountID, - mSrcAccountID, - spsPaths, - true, // --> bPartialPayment: Allow, it might contribute. - false, // --> bLimitQuality: Assume normal transaction. - true, // --> bNoRippleDirect: Providing the only path. - true); // --> bStandAlone: Don't need to delete unfundeds. - } - catch (const std::exception& e) - { - cLog(lsINFO) << "findPaths: Caught throw: " << e.what(); - - terResult = tefEXCEPTION; - } - - if (tesSUCCESS == terResult) - { - uint64 uQuality = STAmount::getRate(saDstAmountAct, saMaxAmountAct); - - cLog(lsDEBUG) - << boost::str(boost::format("findPaths: quality: %d: %s") - % uQuality - % spCurrent.getJson(0)); - - vMap.push_back(std::make_pair(uQuality, i)); - } - else - { - cLog(lsDEBUG) - << boost::str(boost::format("findPaths: dropping: %s: %s") - % transToken(terResult) - % spCurrent.getJson(0)); - } + terResult = tefEXCEPTION; } - if (vMap.size()) + if (tesSUCCESS == terResult) { - iLimit = std::min(iMaxPaths, (unsigned int) vMap.size()); + uint64 uQuality = STAmount::getRate(saDstAmountAct, saMaxAmountAct); - bFound = true; + cLog(lsDEBUG) + << boost::str(boost::format("findPaths: quality: %d: %s") + % uQuality + % spCurrent.getJson(0)); - std::sort(vMap.begin(), vMap.end(), bQualityCmp); // Lower is better and should be first. - - // Output best quality entries. - for (int i = 0; i != vMap.size(); ++i) - { - spsDst.addPath(vspResults[vMap[i].second]); - } - - cLog(lsDEBUG) << boost::str(boost::format("findPaths: RESULTS: %s") % spsDst.getJson(0)); + vMap.push_back(std::make_pair(uQuality, i)); } else { - cLog(lsDEBUG) << boost::str(boost::format("findPaths: RESULTS: non-defaults filtered away")); + cLog(lsDEBUG) + << boost::str(boost::format("findPaths: dropping: %s: %s") + % transToken(terResult) + % spCurrent.getJson(0)); } } - } - else - { - cLog(lsDEBUG) << boost::str(boost::format("findPaths: no ledger")); + + if (vMap.size()) + { + iLimit = std::min(iMaxPaths, (unsigned int) vMap.size()); + + bFound = true; + + std::sort(vMap.begin(), vMap.end(), bQualityCmp); // Lower is better and should be first. + + // Output best quality entries. + for (int i = 0; i != vMap.size(); ++i) + { + spsDst.addPath(vspResults[vMap[i].second]); + } + + cLog(lsDEBUG) << boost::str(boost::format("findPaths: RESULTS: %s") % spsDst.getJson(0)); + } + else + { + cLog(lsDEBUG) << boost::str(boost::format("findPaths: RESULTS: non-defaults filtered away")); + } } cLog(lsDEBUG) << boost::str(boost::format("findPaths< bFound=%d") % bFound); diff --git a/src/cpp/ripple/Pathfinder.h b/src/cpp/ripple/Pathfinder.h index e624d5167..135bc3e2f 100644 --- a/src/cpp/ripple/Pathfinder.h +++ b/src/cpp/ripple/Pathfinder.h @@ -1,10 +1,12 @@ #ifndef __PATHFINDER__ #define __PATHFINDER__ +#include + #include "SerializedTypes.h" #include "RippleAddress.h" #include "RippleCalc.h" -#include +#include "OrderBookDB.h" #if 0 // @@ -41,9 +43,9 @@ class Pathfinder uint160 mSrcIssuerID; STAmount mSrcAmount; - //OrderBookDB mOrderBook; Ledger::pointer mLedger; PathState::pointer mPsDefault; + LoadEvent::pointer mLoadMonitor; // std::list mBuildingPaths; // std::list mCompletePaths; @@ -56,7 +58,9 @@ class Pathfinder // void addPathOption(PathOption::pointer pathOption); public: - Pathfinder(const RippleAddress& srcAccountID, const RippleAddress& dstAccountID, const uint160& srcCurrencyID, const uint160& srcIssuerID, const STAmount& dstAmount); + Pathfinder(Ledger::ref ledger, + const RippleAddress& srcAccountID, const RippleAddress& dstAccountID, + const uint160& srcCurrencyID, const uint160& srcIssuerID, const STAmount& dstAmount); bool findPaths(const unsigned int iMaxSteps, const unsigned int iMaxPaths, STPathSet& spsDst); diff --git a/src/cpp/ripple/RPCHandler.cpp b/src/cpp/ripple/RPCHandler.cpp index 55ff10a56..d74cfa013 100644 --- a/src/cpp/ripple/RPCHandler.cpp +++ b/src/cpp/ripple/RPCHandler.cpp @@ -177,22 +177,28 @@ Json::Value RPCHandler::transactionSign(Json::Value jvRequest, bool bSubmit) return rpcError(rpcINVALID_PARAMS); } - Pathfinder pf(raSrcAddressID, dstAccountID, saSendMax.getCurrency(), saSendMax.getIssuer(), saSend); - - if (!pf.findPaths(7, 3, spsPaths)) + Ledger::pointer lSnapshot = boost::make_shared( + boost::ref(*theApp->getOPs().getCurrentLedger()), false); { - cLog(lsDEBUG) << "transactionSign: build_path: No paths found."; + ScopedUnlock su(theApp->getMasterLock()); + Pathfinder pf(lSnapshot, raSrcAddressID, dstAccountID, + saSendMax.getCurrency(), saSendMax.getIssuer(), saSend); - return rpcError(rpcNO_PATH); - } - else - { - cLog(lsDEBUG) << "transactionSign: build_path: " << spsPaths.getJson(0); - } + if (!pf.findPaths(theConfig.PATH_SEARCH_SIZE, 3, spsPaths)) + { + cLog(lsDEBUG) << "transactionSign: build_path: No paths found."; - if (!spsPaths.isEmpty()) - { - txJSON["Paths"]=spsPaths.getJson(0); + return rpcError(rpcNO_PATH); + } + else + { + cLog(lsDEBUG) << "transactionSign: build_path: " << spsPaths.getJson(0); + } + + if (!spsPaths.isEmpty()) + { + txJSON["Paths"]=spsPaths.getJson(0); + } } } } @@ -494,8 +500,9 @@ Json::Value RPCHandler::authorize(Ledger::ref lrLedger, } // --> strIdent: public key, account ID, or regular seed. +// --> bStrict: Only allow account id or public key. // <-- bIndex: true if iIndex > 0 and used the index. -Json::Value RPCHandler::accountFromString(Ledger::ref lrLedger, RippleAddress& naAccount, bool& bIndex, const std::string& strIdent, const int iIndex) +Json::Value RPCHandler::accountFromString(Ledger::ref lrLedger, RippleAddress& naAccount, bool& bIndex, const std::string& strIdent, const int iIndex, const bool bStrict) { RippleAddress naSeed; @@ -504,6 +511,10 @@ Json::Value RPCHandler::accountFromString(Ledger::ref lrLedger, RippleAddress& n // Got the account. bIndex = false; } + else if (bStrict) + { + return rpcError(rpcACT_MALFORMED); + } // Must be a seed. else if (!naSeed.setSeedGeneric(strIdent)) { @@ -564,6 +575,7 @@ Json::Value RPCHandler::doAcceptLedger(Json::Value jvRequest) // { // ident : , // account_index : // optional +// strict: // true, only allow public keys and addresses. false, default. // ledger_hash : // ledger_index : // } @@ -581,11 +593,12 @@ Json::Value RPCHandler::doAccountInfo(Json::Value jvRequest) std::string strIdent = jvRequest["ident"].asString(); bool bIndex; int iIndex = jvRequest.isMember("account_index") ? jvRequest["account_index"].asUInt() : 0; + bool bStrict = jvRequest.isMember("strict") && jvRequest["strict"].asBool(); RippleAddress naAccount; // Get info on account. - Json::Value jvAccepted = accountFromString(lpLedger, naAccount, bIndex, strIdent, iIndex); + Json::Value jvAccepted = accountFromString(lpLedger, naAccount, bIndex, strIdent, iIndex, bStrict); if (!jvAccepted.empty()) return jvAccepted; @@ -740,7 +753,7 @@ Json::Value RPCHandler::doNicknameInfo(Json::Value params) // 'ident' : , // 'account_index' : // optional // } -// XXX This would be better if it too the ledger. +// XXX This would be better if it took the ledger. Json::Value RPCHandler::doOwnerInfo(Json::Value jvRequest) { if (!jvRequest.isMember("ident")) @@ -755,11 +768,11 @@ Json::Value RPCHandler::doOwnerInfo(Json::Value jvRequest) // Get info on account. - Json::Value jAccepted = accountFromString(mNetOps->getClosedLedger(), raAccount, bIndex, strIdent, iIndex); + Json::Value jAccepted = accountFromString(mNetOps->getClosedLedger(), raAccount, bIndex, strIdent, iIndex, false); ret["accepted"] = jAccepted.empty() ? mNetOps->getOwnerInfo(mNetOps->getClosedLedger(), raAccount) : jAccepted; - Json::Value jCurrent = accountFromString(mNetOps->getCurrentLedger(), raAccount, bIndex, strIdent, iIndex); + Json::Value jCurrent = accountFromString(mNetOps->getCurrentLedger(), raAccount, bIndex, strIdent, iIndex, false); ret["current"] = jCurrent.empty() ? mNetOps->getOwnerInfo(mNetOps->getCurrentLedger(), raAccount) : jCurrent; @@ -768,11 +781,16 @@ Json::Value RPCHandler::doOwnerInfo(Json::Value jvRequest) Json::Value RPCHandler::doPeers(Json::Value) { - Json::Value obj(Json::objectValue); + Json::Value jvResult(Json::objectValue); - obj["peers"]=theApp->getConnectionPool().getPeersJson(); + jvResult["peers"] = theApp->getConnectionPool().getPeersJson(); - return obj; + return jvResult; +} + +Json::Value RPCHandler::doPing(Json::Value) +{ + return Json::Value(Json::objectValue); } // profile offers [submit] @@ -868,8 +886,8 @@ Json::Value RPCHandler::doProfile(Json::Value jvRequest) } // { -// account: || [] -// index: // optional, defaults to 0. +// account: || +// account_index: // optional, defaults to 0. // ledger_hash : // ledger_index : // } @@ -890,7 +908,7 @@ Json::Value RPCHandler::doAccountLines(Json::Value jvRequest) RippleAddress raAccount; - jvResult = accountFromString(lpLedger, raAccount, bIndex, strIdent, iIndex); + jvResult = accountFromString(lpLedger, raAccount, bIndex, strIdent, iIndex, false); if (!jvResult.empty()) return jvResult; @@ -946,8 +964,8 @@ Json::Value RPCHandler::doAccountLines(Json::Value jvRequest) } // { -// account: || [] -// index: // optional, defaults to 0. +// account: || +// account_index: // optional, defaults to 0. // ledger_hash : // ledger_index : // } @@ -968,7 +986,7 @@ Json::Value RPCHandler::doAccountOffers(Json::Value jvRequest) RippleAddress raAccount; - jvResult = accountFromString(lpLedger, raAccount, bIndex, strIdent, iIndex); + jvResult = accountFromString(lpLedger, raAccount, bIndex, strIdent, iIndex, false); if (!jvResult.empty()) return jvResult; @@ -1115,7 +1133,8 @@ Json::Value RPCHandler::doRipplePathFind(Json::Value jvRequest) } } - LedgerEntrySet lesSnapshot(lpCurrent); + Ledger::pointer lSnapShot = boost::make_shared(boost::ref(*lpCurrent), false); + LedgerEntrySet lesSnapshot(lSnapShot); ScopedUnlock su(theApp->getMasterLock()); // As long as we have a locked copy of the ledger, we can unlock. @@ -1148,9 +1167,9 @@ Json::Value RPCHandler::doRipplePathFind(Json::Value jvRequest) } STPathSet spsComputed; - Pathfinder pf(raSrc, raDst, uSrcCurrencyID, uSrcIssuerID, saDstAmount); + Pathfinder pf(lSnapShot, raSrc, raDst, uSrcCurrencyID, uSrcIssuerID, saDstAmount); - if (!pf.findPaths(7, 3, spsComputed)) + if (!pf.findPaths(theConfig.PATH_SEARCH_SIZE, 3, spsComputed)) { cLog(lsDEBUG) << "ripple_path_find: No paths found."; } @@ -1413,14 +1432,29 @@ Json::Value RPCHandler::doTx(Json::Value jvRequest) if (Transaction::isHexTxID(strTransaction)) { // transaction by ID - Json::Value ret; uint256 txid(strTransaction); Transaction::pointer txn = theApp->getMasterTransaction().fetch(txid, true); if (!txn) return rpcError(rpcTXN_NOT_FOUND); - return txn->getJson(0); + Json::Value ret = txn->getJson(0); + + if (txn->getLedger() != 0) + { + Ledger::pointer lgr = theApp->getLedgerMaster().getLedgerBySeq(txn->getLedger()); + if (lgr) + { + TransactionMetaSet::pointer set; + if (lgr->getTransactionMeta(txid, set)) + { + ret["meta"] = set->getJson(0); + ret["validated"] = theApp->getOPs().isValidated(lgr); + } + } + } + + return ret; } return rpcError(rpcNOT_IMPL); @@ -1510,18 +1544,18 @@ Json::Value RPCHandler::doAccountTransactions(Json::Value jvRequest) if (!raAccount.setAccountID(jvRequest["account"].asString())) return rpcError(rpcACT_MALFORMED); - if (jvRequest.isMember("ledger")) - { - minLedger = maxLedger = jvRequest["ledger"].asUInt(); - } - else if (jvRequest.isMember("ledger_min") && jvRequest.isMember("ledger_max")) + if (jvRequest.isMember("ledger_min") && jvRequest.isMember("ledger_max")) { minLedger = jvRequest["ledger_min"].asUInt(); maxLedger = jvRequest["ledger_max"].asUInt(); } else { - return rpcError(rpcLGR_IDX_MALFORMED); + Ledger::pointer l; + Json::Value ret = lookupLedger(jvRequest, l); + if (!l) + return ret; + minLedger = maxLedger = l->getLedgerSeq(); } if ((maxLedger < minLedger) || (maxLedger == 0)) @@ -1533,18 +1567,30 @@ Json::Value RPCHandler::doAccountTransactions(Json::Value jvRequest) try { #endif + int vl = theApp->getOPs().getValidatedSeq(); + ScopedUnlock su(theApp->getMasterLock()); + std::vector< std::pair > txns = mNetOps->getAccountTxs(raAccount, minLedger, maxLedger); Json::Value ret(Json::objectValue); ret["account"] = raAccount.humanAccountID(); Json::Value ledgers(Json::arrayValue); - // uint32 currentLedger = 0; for (std::vector< std::pair >::iterator it = txns.begin(), end = txns.end(); it != end; ++it) { Json::Value obj(Json::objectValue); - if (it->first) obj["tx"] = it->first->getJson(1); - if (it->second) obj["meta"] = it->second->getJson(0); + if (it->first) + obj["tx"] = it->first->getJson(1); + if (it->second) + { + obj["meta"] = it->second->getJson(0); + + uint32 s = it->second->getLgrSeq(); + if (s > vl) + obj["validated"] = false; + else if (theApp->getOPs().haveLedger(s)) + obj["validated"] = true; + } ret["transactions"].append(obj); } @@ -2066,7 +2112,7 @@ Json::Value RPCHandler::lookupLedger(Json::Value jvRequest, Ledger::pointer& lpL Json::Value jvResult; uint256 uLedger = jvRequest.isMember("ledger_hash") ? uint256(jvRequest["ledger_hash"].asString()) : 0; - uint32 uLedgerIndex = jvRequest.isMember("ledger_index") && jvRequest["ledger_index"].isNumeric() ? jvRequest["ledger_index"].asUInt() : 0; + int32 iLedgerIndex = jvRequest.isMember("ledger_index") && jvRequest["ledger_index"].isNumeric() ? jvRequest["ledger_index"].asInt() : -2; if (!!uLedger) { @@ -2076,38 +2122,51 @@ Json::Value RPCHandler::lookupLedger(Json::Value jvRequest, Ledger::pointer& lpL if (!lpLedger) { jvResult["error"] = "ledgerNotFound"; + return jvResult; } - uLedgerIndex = lpLedger->getLedgerSeq(); // Set the current index, override if needed. + iLedgerIndex = lpLedger->getLedgerSeq(); // Set the current index, override if needed. } - else if (!!uLedgerIndex) + if (-1 == iLedgerIndex) { - lpLedger = mNetOps->getLedgerBySeq(uLedgerIndex); + lpLedger = theApp->getLedgerMaster().getClosedLedger(); + iLedgerIndex = lpLedger->getLedgerSeq(); + } + if (-2 == iLedgerIndex) + { + // Default to current ledger. + lpLedger = mNetOps->getCurrentLedger(); + iLedgerIndex = lpLedger->getLedgerSeq(); + } + else if (iLedgerIndex <= 0) + { + jvResult["error"] = "ledgerNotFound"; + + return jvResult; + } + else if (iLedgerIndex) + { + lpLedger = mNetOps->getLedgerBySeq(iLedgerIndex); if (!lpLedger) { jvResult["error"] = "ledgerNotFound"; // ledger_index from future? + return jvResult; } } - else - { - // Default to current ledger. - lpLedger = mNetOps->getCurrentLedger(); - uLedgerIndex = lpLedger->getLedgerSeq(); // Set the current index. - } if (lpLedger->isClosed()) { if (!!uLedger) jvResult["ledger_hash"] = uLedger.ToString(); - jvResult["ledger_index"] = uLedgerIndex; + jvResult["ledger_index"] = iLedgerIndex; } else { - jvResult["ledger_current_index"] = uLedgerIndex; + jvResult["ledger_current_index"] = iLedgerIndex; } return jvResult; @@ -2401,10 +2460,14 @@ Json::Value RPCHandler::doSubscribe(Json::Value jvRequest) RPCSub *rspSub = mNetOps->findRpcSub(strUrl); if (!rspSub) { + cLog(lsINFO) << boost::str(boost::format("doSubscribe: building: %s") % strUrl); + rspSub = mNetOps->addRpcSub(strUrl, new RPCSub(strUrl, strUsername, strPassword)); } else { + cLog(lsINFO) << boost::str(boost::format("doSubscribe: reusing: %s") % strUrl); + if (jvRequest.isMember("username")) rspSub->setUsername(strUsername); @@ -2430,7 +2493,6 @@ Json::Value RPCHandler::doSubscribe(Json::Value jvRequest) if (streamName=="server") { mNetOps->subServer(ispSub, jvResult); - } else if (streamName=="ledger") { @@ -2483,6 +2545,8 @@ Json::Value RPCHandler::doSubscribe(Json::Value jvRequest) else { mNetOps->subAccount(ispSub, usnaAccoundIds, uLedgerIndex, false); + + cLog(lsINFO) << boost::str(boost::format("doSubscribe: accounts: %d") % usnaAccoundIds.size()); } } @@ -2702,6 +2766,7 @@ Json::Value RPCHandler::doCommand(const Json::Value& jvRequest, int iRole) // { "nickname_info", &RPCHandler::doNicknameInfo, false, optCurrent }, { "owner_info", &RPCHandler::doOwnerInfo, false, optCurrent }, { "peers", &RPCHandler::doPeers, true, optNone }, + { "ping", &RPCHandler::doPing, false, optNone }, // { "profile", &RPCHandler::doProfile, false, optCurrent }, { "random", &RPCHandler::doRandom, false, optNone }, { "ripple_path_find", &RPCHandler::doRipplePathFind, false, optCurrent }, diff --git a/src/cpp/ripple/RPCHandler.h b/src/cpp/ripple/RPCHandler.h index 3e584056b..07aec9944 100644 --- a/src/cpp/ripple/RPCHandler.h +++ b/src/cpp/ripple/RPCHandler.h @@ -41,7 +41,7 @@ class RPCHandler const RippleAddress& naVerifyGenerator); Json::Value accounts(Ledger::ref lrLedger, const RippleAddress& naMasterGenerator); - Json::Value accountFromString(Ledger::ref lrLedger, RippleAddress& naAccount, bool& bIndex, const std::string& strIdent, const int iIndex); + Json::Value accountFromString(Ledger::ref lrLedger, RippleAddress& naAccount, bool& bIndex, const std::string& strIdent, const int iIndex, const bool bStrict); Json::Value doAcceptLedger(Json::Value jvRequest); @@ -64,6 +64,7 @@ class RPCHandler Json::Value doNicknameInfo(Json::Value params); Json::Value doOwnerInfo(Json::Value params); Json::Value doPeers(Json::Value params); + Json::Value doPing(Json::Value params); Json::Value doProfile(Json::Value params); Json::Value doRandom(Json::Value jvRequest); Json::Value doRipplePathFind(Json::Value jvRequest); diff --git a/src/cpp/ripple/RPCSub.cpp b/src/cpp/ripple/RPCSub.cpp index 28daff00e..2a6ac6af8 100644 --- a/src/cpp/ripple/RPCSub.cpp +++ b/src/cpp/ripple/RPCSub.cpp @@ -8,27 +8,27 @@ SETUP_LOG(); RPCSub::RPCSub(const std::string& strUrl, const std::string& strUsername, const std::string& strPassword) - : mUrl(strUrl), mSSL(false), mUsername(strUsername), mPassword(strPassword) + : mUrl(strUrl), mSSL(false), mUsername(strUsername), mPassword(strPassword), mSending(false) { std::string strScheme; if (!parseUrl(strUrl, strScheme, mIp, mPort, mPath)) { - throw std::runtime_error("Failed to parse url."); + throw std::runtime_error("Failed to parse url."); } else if (strScheme == "https") { - mSSL = true; + mSSL = true; } else if (strScheme != "http") { - throw std::runtime_error("Only http and https is supported."); + throw std::runtime_error("Only http and https is supported."); } mSeq = 1; if (mPort < 0) - mPort = mSSL ? 443 : 80; + mPort = mSSL ? 443 : 80; } // XXX Could probably create a bunch of send jobs in a single get of the lock. @@ -39,74 +39,75 @@ void RPCSub::sendThread() do { - { - // Obtain the lock to manipulate the queue and change sending. - boost::mutex::scoped_lock sl(mLockInfo); + { + // Obtain the lock to manipulate the queue and change sending. + boost::mutex::scoped_lock sl(mLockInfo); - if (mDeque.empty()) - { - mSending = false; - bSend = false; - } - else - { - std::pair pEvent = mDeque.front(); + if (mDeque.empty()) + { + mSending = false; + bSend = false; + } + else + { + std::pair pEvent = mDeque.front(); - mDeque.pop_front(); + mDeque.pop_front(); - jvEvent = pEvent.second; - jvEvent["seq"] = pEvent.first; + jvEvent = pEvent.second; + jvEvent["seq"] = pEvent.first; - bSend = true; - } - } + bSend = true; + } + } - // Send outside of the lock. - if (bSend) - { - // XXX Might not need this in a try. - try - { - cLog(lsDEBUG) << boost::str(boost::format("callRPC calling: %s") % mIp); + // Send outside of the lock. + if (bSend) + { + // XXX Might not need this in a try. + try + { + cLog(lsINFO) << boost::str(boost::format("callRPC calling: %s") % mIp); - callRPC( - theApp->getIOService(), - mIp, mPort, - mUsername, mPassword, - mPath, "event", - jvEvent, - mSSL); - } - catch (const std::exception& e) - { - cLog(lsDEBUG) << boost::str(boost::format("callRPC exception: %s") % e.what()); - } - } + callRPC( + theApp->getIOService(), + mIp, mPort, + mUsername, mPassword, + mPath, "event", + jvEvent, + mSSL); + } + catch (const std::exception& e) + { + cLog(lsINFO) << boost::str(boost::format("callRPC exception: %s") % e.what()); + } + } } while (bSend); } -void RPCSub::send(const Json::Value& jvObj) +void RPCSub::send(const Json::Value& jvObj, bool broadcast) { boost::mutex::scoped_lock sl(mLockInfo); if (RPC_EVENT_QUEUE_MAX == mDeque.size()) { - // Drop the previous event. - - cLog(lsDEBUG) << boost::str(boost::format("callRPC drop")); - mDeque.pop_back(); + // Drop the previous event. + cLog(lsWARNING) << boost::str(boost::format("callRPC drop")); + mDeque.pop_back(); } - cLog(lsDEBUG) << boost::str(boost::format("callRPC push: %s") % jvObj); + cLog(broadcast ? lsDEBUG : lsINFO) << boost::str(boost::format("callRPC push: %s") % jvObj); mDeque.push_back(std::make_pair(mSeq++, jvObj)); if (!mSending) { - // Start a sending thread. - mSending = true; + // Start a sending thread. + mSending = true; - cLog(lsDEBUG) << boost::str(boost::format("callRPC start")); - boost::thread(boost::bind(&RPCSub::sendThread, this)).detach(); + cLog(lsINFO) << boost::str(boost::format("callRPC start")); + boost::thread(boost::bind(&RPCSub::sendThread, this)).detach(); } } + +// vim:ts=4 diff --git a/src/cpp/ripple/RPCSub.h b/src/cpp/ripple/RPCSub.h index dc2270a6d..03172410f 100644 --- a/src/cpp/ripple/RPCSub.h +++ b/src/cpp/ripple/RPCSub.h @@ -35,7 +35,7 @@ public: virtual ~RPCSub() { ; } // Implement overridden functions from base class: - void send(const Json::Value& jvObj); + void send(const Json::Value& jvObj, bool broadcast); void setUsername(const std::string& strUsername) { diff --git a/src/cpp/ripple/RippleCalc.cpp b/src/cpp/ripple/RippleCalc.cpp index 0b1ece0d3..5bf438bbb 100644 --- a/src/cpp/ripple/RippleCalc.cpp +++ b/src/cpp/ripple/RippleCalc.cpp @@ -107,7 +107,7 @@ TER PathState::pushImply( uIssuerID); } - cLog(lsDEBUG) << boost::str(boost::format("pushImply< : %s") % transToken(terResult)); + cLog(lsINFO) << boost::str(boost::format("pushImply< : %s") % transToken(terResult)); return terResult; } @@ -280,7 +280,7 @@ TER PathState::pushNode( vpnNodes.push_back(pnCur); } } - cLog(lsDEBUG) << boost::str(boost::format("pushNode< : %s") % transToken(terResult)); + cLog(lsINFO) << boost::str(boost::format("pushNode< : %s") % transToken(terResult)); return terResult; } @@ -1660,7 +1660,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c ? lesActive.rippleOwed(uCurAccountID, uNxtAccountID, uCurrencyID) : STAmount(uCurrencyID, uCurAccountID); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev> uNode=%d/%d uPrvAccountID=%s uCurAccountID=%s uNxtAccountID=%s uCurrencyID=%s uQualityIn=%d uQualityOut=%d saPrvOwed=%s saPrvLimit=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev> uNode=%d/%d uPrvAccountID=%s uCurAccountID=%s uNxtAccountID=%s uCurrencyID=%s uQualityIn=%d uQualityOut=%d saPrvOwed=%s saPrvLimit=%s") % uNode % uLast % RippleAddress::createHumanAccountID(uPrvAccountID) @@ -1681,7 +1681,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c STAmount& saPrvIssueAct = pnPrv.saRevIssue; // For !bPrvAccount - const STAmount saPrvDeliverReq = STAmount::saFromSigned(uCurrencyID, uCurAccountID, -1); // Unlimited. + const STAmount saPrvDeliverReq = STAmount(uCurrencyID, uCurAccountID, -1); // Unlimited. STAmount& saPrvDeliverAct = pnPrv.saRevDeliver; // For bNxtAccount @@ -1695,14 +1695,14 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c const STAmount& saCurDeliverReq = pnCur.saRevDeliver; STAmount saCurDeliverAct(saCurDeliverReq.getCurrency(), saCurDeliverReq.getIssuer()); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: saPrvRedeemReq=%s saPrvIssueReq=%s saCurRedeemReq=%s saCurIssueReq=%s saNxtOwed=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: saPrvRedeemReq=%s saPrvIssueReq=%s saCurRedeemReq=%s saCurIssueReq=%s saNxtOwed=%s") % saPrvRedeemReq.getFullText() % saPrvIssueReq.getFullText() % saCurRedeemReq.getFullText() % saCurIssueReq.getFullText() % saNxtOwed.getFullText()); - cLog(lsINFO) << psCur.getJson(); + cLog(lsDEBUG) << psCur.getJson(); assert(!saCurRedeemReq || (-saNxtOwed) >= saCurRedeemReq); // Current redeem req can't be more than IOUs on hand. assert(!saCurIssueReq // If not issuing, fine. @@ -1727,7 +1727,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c : psCur.saOutReq-psCur.saOutAct; // Previous is an offer, no limit: redeem own IOUs. STAmount saCurWantedAct(saCurWantedReq.getCurrency(), saCurWantedReq.getIssuer()); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: account --> ACCOUNT --> $ : saCurWantedReq=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: account --> ACCOUNT --> $ : saCurWantedReq=%s") % saCurWantedReq.getFullText()); // Calculate redeem @@ -1740,7 +1740,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c uRateMax = STAmount::uRateOne; - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: Redeem at 1:1 saPrvRedeemReq=%s (available) saPrvRedeemAct=%s uRateMax=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Redeem at 1:1 saPrvRedeemReq=%s (available) saPrvRedeemAct=%s uRateMax=%s") % saPrvRedeemReq.getFullText() % saPrvRedeemAct.getFullText() % STAmount::saFromRate(uRateMax).getText()); @@ -1761,7 +1761,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c // If we previously redeemed and this has a poorer rate, this won't be included the current increment. calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurWantedReq, saPrvIssueAct, saCurWantedAct, uRateMax); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: Issuing: Rate: quality in : 1.0 saPrvIssueAct=%s saCurWantedAct=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Issuing: Rate: quality in : 1.0 saPrvIssueAct=%s saCurWantedAct=%s") % saPrvIssueAct.getFullText() % saCurWantedAct.getFullText()); } @@ -1785,7 +1785,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c // Rate : 1.0 : quality out calcNodeRipple(QUALITY_ONE, uQualityOut, saPrvRedeemReq, saCurRedeemReq, saPrvRedeemAct, saCurRedeemAct, uRateMax); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: Rate : 1.0 : quality out saPrvRedeemAct=%s saCurRedeemAct=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Rate : 1.0 : quality out saPrvRedeemAct=%s saCurRedeemAct=%s") % saPrvRedeemAct.getFullText() % saCurRedeemAct.getFullText()); } @@ -1797,7 +1797,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c // Rate: quality in : quality out calcNodeRipple(uQualityIn, uQualityOut, saPrvIssueReq, saCurRedeemReq, saPrvIssueAct, saCurRedeemAct, uRateMax); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: Rate: quality in : quality out: saPrvIssueAct=%s saCurRedeemAct=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Rate: quality in : quality out: saPrvIssueAct=%s saCurRedeemAct=%s") % saPrvIssueAct.getFullText() % saCurRedeemAct.getFullText()); } @@ -1824,7 +1824,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c // Rate: quality in : 1.0 calcNodeRipple(uQualityIn, QUALITY_ONE, saPrvIssueReq, saCurIssueReq, saPrvIssueAct, saCurIssueAct, uRateMax); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: Rate: quality in : 1.0: saPrvIssueAct=%s saCurIssueAct=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: Rate: quality in : 1.0: saPrvIssueAct=%s saCurIssueAct=%s") % saPrvIssueAct.getFullText() % saCurIssueAct.getFullText()); } @@ -1835,7 +1835,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c terResult = tecPATH_DRY; } - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: ^|account --> ACCOUNT --> account : saCurRedeemReq=%s saCurIssueReq=%s saPrvOwed=%s saCurRedeemAct=%s saCurIssueAct=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: ^|account --> ACCOUNT --> account : saCurRedeemReq=%s saCurIssueReq=%s saPrvOwed=%s saCurRedeemAct=%s saCurIssueAct=%s") % saCurRedeemReq.getFullText() % saCurIssueReq.getFullText() % saPrvOwed.getFullText() @@ -1847,7 +1847,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c { // account --> ACCOUNT --> offer // Note: deliver is always issue as ACCOUNT is the issuer for the offer input. - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: account --> ACCOUNT --> offer")); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: account --> ACCOUNT --> offer")); saPrvRedeemAct.zero(saCurRedeemReq); saPrvIssueAct.zero(saCurRedeemReq); @@ -1874,7 +1874,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c terResult = tecPATH_DRY; } - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: saCurDeliverReq=%s saCurDeliverAct=%s saPrvOwed=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: saCurDeliverReq=%s saCurDeliverAct=%s saPrvOwed=%s") % saCurDeliverReq.getFullText() % saCurDeliverAct.getFullText() % saPrvOwed.getFullText()); @@ -1891,7 +1891,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c : psCur.saOutReq-psCur.saOutAct; // Previous is an offer, no limit: redeem own IOUs. STAmount saCurWantedAct(saCurWantedReq.getCurrency(), saCurWantedReq.getIssuer()); - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> $ : saCurWantedReq=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> $ : saCurWantedReq=%s") % saCurWantedReq.getFullText()); // Rate: quality in : 1.0 @@ -1907,7 +1907,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c { // offer --> ACCOUNT --> account // Note: offer is always delivering(redeeming) as account is issuer. - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> account")); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> account")); // deliver -> redeem if (saCurRedeemReq) // Next wants us to redeem. @@ -1924,7 +1924,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c calcNodeRipple(QUALITY_ONE, lesActive.rippleTransferRate(uCurAccountID), saPrvDeliverReq, saCurIssueReq, saPrvDeliverAct, saCurIssueAct, uRateMax); } - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: saCurRedeemReq=%s saCurIssueAct=%s saCurIssueReq=%s saPrvDeliverAct=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: saCurRedeemReq=%s saCurIssueAct=%s saCurIssueReq=%s saPrvDeliverAct=%s") % saCurRedeemReq.getFullText() % saCurRedeemAct.getFullText() % saCurIssueReq.getFullText() @@ -1941,7 +1941,7 @@ TER RippleCalc::calcNodeAccountRev(const unsigned int uNode, PathState& psCur, c { // offer --> ACCOUNT --> offer // deliver/redeem -> deliver/issue. - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> offer")); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountRev: offer --> ACCOUNT --> offer")); saPrvDeliverAct.zero(saCurRedeemReq); @@ -2025,7 +2025,7 @@ TER RippleCalc::calcNodeAccountFwd( // For !uNode STAmount& saCurSendMaxPass = psCur.saInPass; // Report how much pass sends. - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd> uNode=%d/%d saPrvRedeemReq=%s saPrvIssueReq=%s saPrvDeliverReq=%s saCurRedeemReq=%s saCurIssueReq=%s saCurDeliverReq=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd> uNode=%d/%d saPrvRedeemReq=%s saPrvIssueReq=%s saPrvDeliverReq=%s saCurRedeemReq=%s saCurIssueReq=%s saCurDeliverReq=%s") % uNode % uLast % saPrvRedeemReq.getFullText() @@ -2069,7 +2069,7 @@ TER RippleCalc::calcNodeAccountFwd( saCurSendMaxPass += saCurIssueAct; - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: ^ --> ACCOUNT --> account : saInReq=%s saInAct=%s saCurRedeemAct=%s saCurIssueReq=%s saCurIssueAct=%s saCurSendMaxPass=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: ^ --> ACCOUNT --> account : saInReq=%s saInAct=%s saCurRedeemAct=%s saCurIssueReq=%s saCurIssueAct=%s saCurSendMaxPass=%s") % psCur.saInReq.getFullText() % psCur.saInAct.getFullText() % saCurRedeemAct.getFullText() @@ -2080,7 +2080,7 @@ TER RippleCalc::calcNodeAccountFwd( else if (uNode == uLast) { // account --> ACCOUNT --> $ - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> $ : uPrvAccountID=%s uCurAccountID=%s saPrvRedeemReq=%s saPrvIssueReq=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> $ : uPrvAccountID=%s uCurAccountID=%s saPrvRedeemReq=%s saPrvIssueReq=%s") % RippleAddress::createHumanAccountID(uPrvAccountID) % RippleAddress::createHumanAccountID(uCurAccountID) % saPrvRedeemReq.getFullText() @@ -2103,7 +2103,7 @@ TER RippleCalc::calcNodeAccountFwd( else { // account --> ACCOUNT --> account - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> account")); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: account --> ACCOUNT --> account")); saCurRedeemAct.zero(saCurRedeemReq); saCurIssueAct.zero(saCurIssueReq); @@ -2223,7 +2223,7 @@ TER RippleCalc::calcNodeAccountFwd( if (uNode == uLast) { // offer --> ACCOUNT --> $ - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> $ : %s") % saPrvDeliverReq.getFullText()); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> $ : %s") % saPrvDeliverReq.getFullText()); STAmount& saCurReceive = psCur.saOutPass; @@ -2235,7 +2235,7 @@ TER RippleCalc::calcNodeAccountFwd( else { // offer --> ACCOUNT --> account - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> account")); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> account")); saCurRedeemAct.zero(saCurRedeemReq); saCurIssueAct.zero(saCurIssueReq); @@ -2264,7 +2264,7 @@ TER RippleCalc::calcNodeAccountFwd( { // offer --> ACCOUNT --> offer // deliver/redeem -> deliver/issue. - cLog(lsINFO) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> offer")); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeAccountFwd: offer --> ACCOUNT --> offer")); saCurDeliverAct.zero(saCurDeliverReq); @@ -2286,7 +2286,7 @@ TER RippleCalc::calcNodeFwd(const unsigned int uNode, PathState& psCur, const bo const PaymentNode& pnCur = psCur.vpnNodes[uNode]; const bool bCurAccount = isSetBit(pnCur.uFlags, STPathElement::typeAccount); - cLog(lsINFO) << boost::str(boost::format("calcNodeFwd> uNode=%d") % uNode); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeFwd> uNode=%d") % uNode); TER terResult = bCurAccount ? calcNodeAccountFwd(uNode, psCur, bMultiQuality) @@ -2297,7 +2297,7 @@ TER RippleCalc::calcNodeFwd(const unsigned int uNode, PathState& psCur, const bo terResult = calcNodeFwd(uNode+1, psCur, bMultiQuality); } - cLog(lsINFO) << boost::str(boost::format("calcNodeFwd< uNode=%d terResult=%d") % uNode % terResult); + cLog(lsDEBUG) << boost::str(boost::format("calcNodeFwd< uNode=%d terResult=%d") % uNode % terResult); return terResult; } @@ -2323,7 +2323,7 @@ TER RippleCalc::calcNodeRev(const unsigned int uNode, PathState& psCur, const bo saTransferRate = STAmount::saFromRate(lesActive.rippleTransferRate(uCurIssuerID)); - cLog(lsINFO) << boost::str(boost::format("calcNodeRev> uNode=%d uIssuerID=%s saTransferRate=%s") + cLog(lsDEBUG) << boost::str(boost::format("calcNodeRev> uNode=%d uIssuerID=%s saTransferRate=%s") % uNode % RippleAddress::createHumanAccountID(uCurIssuerID) % saTransferRate.getFullText()); @@ -2368,7 +2368,7 @@ void RippleCalc::pathNext(PathState::ref psrCur, const bool bMultiQuality, const psrCur->vUnfundedBecame.clear(); psrCur->umReverse.clear(); - cLog(lsINFO) << "pathNext: Path In: " << psrCur->getJson(); + cLog(lsDEBUG) << "pathNext: Path In: " << psrCur->getJson(); assert(psrCur->vpnNodes.size() >= 2); @@ -2377,7 +2377,7 @@ void RippleCalc::pathNext(PathState::ref psrCur, const bool bMultiQuality, const psrCur->terStatus = calcNodeRev(uLast, *psrCur, bMultiQuality); - cLog(lsINFO) << "pathNext: Path after reverse: " << psrCur->getJson(); + cLog(lsDEBUG) << "pathNext: Path after reverse: " << psrCur->getJson(); if (tesSUCCESS == psrCur->terStatus) { @@ -2400,7 +2400,7 @@ void RippleCalc::pathNext(PathState::ref psrCur, const bool bMultiQuality, const psrCur->uQuality = STAmount::getRate(psrCur->saOutPass, psrCur->saInPass); // Calculate relative quality. - cLog(lsINFO) << "pathNext: Path after forward: " << psrCur->getJson(); + cLog(lsDEBUG) << "pathNext: Path after forward: " << psrCur->getJson(); } else { diff --git a/src/cpp/ripple/SHAMapSync.cpp b/src/cpp/ripple/SHAMapSync.cpp index 0fd2ca8dd..4f3824a84 100644 --- a/src/cpp/ripple/SHAMapSync.cpp +++ b/src/cpp/ripple/SHAMapSync.cpp @@ -63,7 +63,7 @@ void SHAMap::getMissingNodes(std::vector& nodeIDs, std::vector(childID, nodeData, mSeq - 1, snfPREFIX, childHash); - cLog(lsTRACE) << "Got sync node from cache: " << *d; + cLog(lsTRACE) << "Got sync node from cache: " << *ptr; mTNByID[*ptr] = ptr; d = ptr.get(); } diff --git a/src/cpp/ripple/SerializedTypes.h b/src/cpp/ripple/SerializedTypes.h index 0a05fa225..d501ab4aa 100644 --- a/src/cpp/ripple/SerializedTypes.h +++ b/src/cpp/ripple/SerializedTypes.h @@ -233,6 +233,34 @@ protected: : SerializedType(name), mCurrency(cur), mIssuer(iss), mValue(val), mOffset(off), mIsNative(isNat), mIsNegative(isNeg) { ; } + void set(int64 v) + { + if (v < 0) + { + mIsNegative = true; + mValue = static_cast(-v); + } + else + { + mIsNegative = false; + mValue = static_cast(v); + } + } + + void set(int v) + { + if (v < 0) + { + mIsNegative = true; + mValue = static_cast(-v); + } + else + { + mIsNegative = false; + mValue = static_cast(v); + } + } + public: static const int cMinOffset = -96, cMaxOffset = 80; static const uint64 cMinValue = 1000000000000000ull, cMaxValue = 9999999999999999ull; @@ -249,16 +277,52 @@ public: : SerializedType(n), mValue(v), mOffset(0), mIsNative(true), mIsNegative(isNeg) { ; } - STAmount(const uint160& uCurrencyID, const uint160& uIssuerID, uint64 uV = 0, int iOff = 0, bool bNegative = false) + STAmount(SField::ref n, int64 v) : SerializedType(n), mOffset(0), mIsNative(true) + { set(v); } + + STAmount(const uint160& uCurrencyID, const uint160& uIssuerID, + uint64 uV = 0, int iOff = 0, bool bNegative = false) + : mCurrency(uCurrencyID), mIssuer(uIssuerID), mValue(uV), mOffset(iOff), mIsNegative(bNegative) + { canonicalize(); } + + STAmount(const uint160& uCurrencyID, const uint160& uIssuerID, + uint32 uV, int iOff = 0, bool bNegative = false) : mCurrency(uCurrencyID), mIssuer(uIssuerID), mValue(uV), mOffset(iOff), mIsNegative(bNegative) { canonicalize(); } - // YYY This should probably require issuer too. STAmount(SField::ref n, const uint160& currency, const uint160& issuer, uint64 v = 0, int off = 0, bool isNeg = false) : SerializedType(n), mCurrency(currency), mIssuer(issuer), mValue(v), mOffset(off), mIsNegative(isNeg) { canonicalize(); } + STAmount(const uint160& uCurrencyID, const uint160& uIssuerID, int64 v, int iOff = 0) + : mCurrency(uCurrencyID), mIssuer(uIssuerID), mOffset(iOff) + { + set(v); + canonicalize(); + } + + STAmount(SField::ref n, const uint160& currency, const uint160& issuer, int64 v, int off = 0) + : SerializedType(n), mCurrency(currency), mIssuer(issuer), mOffset(off) + { + set(v); + canonicalize(); + } + + STAmount(const uint160& uCurrencyID, const uint160& uIssuerID, int v, int iOff = 0) + : mCurrency(uCurrencyID), mIssuer(uIssuerID), mOffset(iOff) + { + set(v); + canonicalize(); + } + + STAmount(SField::ref n, const uint160& currency, const uint160& issuer, int v, int off = 0) + : SerializedType(n), mCurrency(currency), mIssuer(issuer), mOffset(off) + { + set(v); + canonicalize(); + } + STAmount(SField::ref, const Json::Value&); static STAmount createFromInt64(SField::ref n, int64 v); @@ -271,9 +335,6 @@ public: static STAmount saFromRate(uint64 uRate = 0) { return STAmount(CURRENCY_ONE, ACCOUNT_ONE, uRate, -9, false); } - static STAmount saFromSigned(const uint160& uCurrencyID, const uint160& uIssuerID, int64 iV = 0, int iOff = 0) - { return STAmount(uCurrencyID, uIssuerID, iV < 0 ? -iV : iV, iOff, iV < 0); } - SerializedTypeID getSType() const { return STI_AMOUNT; } std::string getText() const; std::string getRaw() const; diff --git a/src/cpp/ripple/TaggedCache.h b/src/cpp/ripple/TaggedCache.h index 7b373acf2..627361eee 100644 --- a/src/cpp/ripple/TaggedCache.h +++ b/src/cpp/ripple/TaggedCache.h @@ -56,7 +56,7 @@ protected: mutable boost::recursive_mutex mLock; std::string mName; // Used for logging - unsigned int mTargetSize; // Desired number of cache entries (0 = ignore) + int mTargetSize; // Desired number of cache entries (0 = ignore) int mTargetAge; // Desired maximum cache age int mCacheCount; // Number of items cached diff --git a/src/cpp/ripple/TransactionMeta.cpp b/src/cpp/ripple/TransactionMeta.cpp index bd87829a7..99ea1e4c3 100644 --- a/src/cpp/ripple/TransactionMeta.cpp +++ b/src/cpp/ripple/TransactionMeta.cpp @@ -68,6 +68,8 @@ std::vector TransactionMetaSet::getAffectedAccounts() std::vector accounts; accounts.reserve(10); + // This code should match the behavior of the JS method: + // Meta#getAffectedAccounts BOOST_FOREACH(const STObject& it, mNodes) { int index = it.getFieldIndex((it.getFName() == sfCreatedNode) ? sfNewFields : sfFinalFields); diff --git a/src/cpp/ripple/TransactionMeta.h b/src/cpp/ripple/TransactionMeta.h index fd07635c8..4728daedc 100644 --- a/src/cpp/ripple/TransactionMeta.h +++ b/src/cpp/ripple/TransactionMeta.h @@ -19,6 +19,7 @@ class TransactionMetaSet { public: typedef boost::shared_ptr pointer; + typedef const pointer& ref; protected: uint256 mTransactionID; diff --git a/src/cpp/ripple/Transactor.cpp b/src/cpp/ripple/Transactor.cpp index b9ace30fd..f3bd4e1a4 100644 --- a/src/cpp/ripple/Transactor.cpp +++ b/src/cpp/ripple/Transactor.cpp @@ -206,15 +206,15 @@ TER Transactor::apply() mHasAuthKey = mTxnAccount->isFieldPresent(sfRegularKey); } + terResult = checkSeq(); + if (terResult != tesSUCCESS) return(terResult); + terResult = payFee(); if (terResult != tesSUCCESS) return(terResult); terResult = checkSig(); if (terResult != tesSUCCESS) return(terResult); - terResult = checkSeq(); - if (terResult != tesSUCCESS) return(terResult); - mEngine->entryModify(mTxnAccount); return doApply(); diff --git a/src/cpp/ripple/WSConnection.h b/src/cpp/ripple/WSConnection.h index 6e2417fe1..c09c1553b 100644 --- a/src/cpp/ripple/WSConnection.h +++ b/src/cpp/ripple/WSConnection.h @@ -72,11 +72,11 @@ public: } // Implement overridden functions from base class: - void send(const Json::Value& jvObj) + void send(const Json::Value& jvObj, bool broadcast) { connection_ptr ptr = mConnection.lock(); if (ptr) - mHandler->send(ptr, jvObj); + mHandler->send(ptr, jvObj, broadcast); } // Utilities diff --git a/src/cpp/ripple/WSHandler.h b/src/cpp/ripple/WSHandler.h index 04dadefd3..b2d4acb8c 100644 --- a/src/cpp/ripple/WSHandler.h +++ b/src/cpp/ripple/WSHandler.h @@ -61,11 +61,11 @@ public: } } - void send(connection_ptr cpClient, const std::string& strMessage) + void send(connection_ptr cpClient, const std::string& strMessage, bool broadcast) { try { - cLog(lsDEBUG) << "Ws:: Sending '" << strMessage << "'"; + cLog(broadcast ? lsTRACE : lsDEBUG) << "Ws:: Sending '" << strMessage << "'"; cpClient->send(strMessage); } @@ -75,13 +75,13 @@ public: } } - void send(connection_ptr cpClient, const Json::Value& jvObj) + void send(connection_ptr cpClient, const Json::Value& jvObj, bool broadcast) { Json::FastWriter jfwWriter; // cLog(lsDEBUG) << "Ws:: Object '" << jfwWriter.write(jvObj) << "'"; - send(cpClient, jfwWriter.write(jvObj)); + send(cpClient, jfwWriter.write(jvObj), broadcast); } void pingTimer(connection_ptr cpClient) @@ -177,7 +177,7 @@ public: jvResult["type"] = "error"; jvResult["error"] = "wsTextRequired"; // We only accept text messages. - send(cpClient, jvResult); + send(cpClient, jvResult, false); } else if (!jrReader.parse(mpMessage->get_payload(), jvRequest) || jvRequest.isNull() || !jvRequest.isObject()) { @@ -187,7 +187,7 @@ public: jvResult["error"] = "jsonInvalid"; // Received invalid json. jvResult["value"] = mpMessage->get_payload(); - send(cpClient, jvResult); + send(cpClient, jvResult, false); } else { @@ -200,7 +200,7 @@ public: return; conn = it->second; } - send(cpClient, conn->invokeCommand(jvRequest)); + send(cpClient, conn->invokeCommand(jvRequest), false); } } diff --git a/src/js/account.js b/src/js/account.js index b12643522..93347682a 100644 --- a/src/js/account.js +++ b/src/js/account.js @@ -9,15 +9,121 @@ // var network = require("./network.js"); -var EventEmitter = require('events').EventEmitter; -var Amount = require('./amount.js').Amount; -var UInt160 = require('./amount.js').UInt160; +var EventEmitter = require('events').EventEmitter; +var Amount = require('./amount').Amount; +var UInt160 = require('./uint160').UInt160; -var Account = function (network, account) { - this._network = network; - this._account = UInt160.json_rewrite(account); +var extend = require('extend'); - return this; +var Account = function (remote, account) { + var self = this; + + this._remote = remote; + this._account = UInt160.from_json(account); + this._account_id = this._account.to_json(); + this._subs = 0; + + // Ledger entry object + // Important: This must never be overwritten, only extend()-ed + this._entry = {}; + + this.on('newListener', function (type, listener) { + if (Account.subscribe_events.indexOf(type) !== -1) { + if (!this._subs && 'open' === this._remote._online_state) { + this._remote.request_subscribe() + .accounts(this._account_id) + .request(); + } + this._subs += 1; + } + }); + + this.on('removeListener', function (type, listener) { + if (Account.subscribe_events.indexOf(type) !== -1) { + this._subs -= 1; + + if (!this._subs && 'open' === this._remote._online_state) { + this._remote.request_unsubscribe() + .accounts(this._account_id) + .request(); + } + } + }); + + this._remote.on('connect', function () { + if (self._subs) { + this._remote.request_subscribe() + .accounts(this._account_id) + .request(); + } + }); + + this.on('transaction', function (msg) { + var changed = false; + msg.mmeta.each(function (an) { + if (an.entryType === 'AccountRoot' && + an.fields.Account === self._account_id) { + extend(self._entry, an.fieldsNew, an.fieldsFinal); + changed = true; + } + }); + if (changed) { + self.emit('entry', self._entry); + } + }); + + return this; +}; + +Account.prototype = new EventEmitter; + +/** + * List of events that require a remote subscription to the account. + */ +Account.subscribe_events = ['transaction', 'entry']; + +Account.prototype.to_json = function () +{ + return this._account.to_json(); +}; + +/** + * Whether the AccountId is valid. + * + * Note: This does not tell you whether the account exists in the ledger. + */ +Account.prototype.is_valid = function () +{ + return this._account.is_valid(); +}; + +/** + * Retrieve the current AccountRoot entry. + * + * To keep up-to-date with changes to the AccountRoot entry, subscribe to the + * "entry" event. + * + * @param {function (err, entry)} callback Called with the result + */ +Account.prototype.entry = function (callback) +{ + var self = this; + + self._remote.request_account_info(this._account_id) + .on('success', function (e) { + extend(self._entry, e.account_data); + self.emit('entry', self._entry); + + if ("function" === typeof callback) { + callback(null, e); + } + }) + .on('error', function (e) { + callback(e); + }) + .request(); + + return this; }; exports.Account = Account; diff --git a/src/js/amount.js b/src/js/amount.js index a690616d8..408f85818 100644 --- a/src/js/amount.js +++ b/src/js/amount.js @@ -380,7 +380,12 @@ Amount.prototype.ratio_human = function (denominator) { * @return {Amount} The product. Unit will be the same as the first factor. */ Amount.prototype.product_human = function (factor) { - factor = Amount.from_json(factor); + if ("number" === typeof factor && parseInt(factor) === factor) { + // Special handling of integer arguments + factor = Amount.from_json("" + factor + ".0"); + } else { + factor = Amount.from_json(factor); + } var product = this.multiply(factor); @@ -413,6 +418,10 @@ Amount.prototype.is_negative = function () { : false; // NaN is not negative }; +Amount.prototype.is_positive = function () { + return !this.is_zero() && !this.is_negative(); +}; + // Only checks the value. Not the currency and issuer. Amount.prototype.is_valid = function () { return this._value instanceof BigInteger; @@ -436,7 +445,7 @@ Amount.prototype.issuer = function () { Amount.prototype.multiply = function (v) { var result; - if (this.is_zero()) { + if (this.is_zero()) { result = this.clone(); } else if (v.is_zero()) { @@ -690,6 +699,7 @@ Amount.prototype.set_currency = function (c) { { c.copyTo(this._currency); } + this._is_native = this._currency.is_native(); return this; }; diff --git a/src/js/meta.js b/src/js/meta.js new file mode 100644 index 000000000..46c0e46cb --- /dev/null +++ b/src/js/meta.js @@ -0,0 +1,108 @@ +var extend = require('extend'); +var UInt160 = require('./uint160').UInt160; +var Amount = require('./amount').Amount; + +/** + * Meta data processing facility. + */ +var Meta = function (raw_data) +{ + this.nodes = []; + + for (var i = 0, l = raw_data.AffectedNodes.length; i < l; i++) { + var an = raw_data.AffectedNodes[i], + result = {}; + + ["CreatedNode", "ModifiedNode", "DeletedNode"].forEach(function (x) { + if (an[x]) result.diffType = x; + }); + + if (!result.diffType) return null; + + an = an[result.diffType]; + + result.entryType = an.LedgerEntryType; + result.ledgerIndex = an.LedgerIndex; + + result.fields = extend({}, an.PreviousFields, an.NewFields, an.FinalFields); + result.fieldsPrev = an.PreviousFields || {}; + result.fieldsNew = an.NewFields || {}; + result.fieldsFinal = an.FinalFields || {}; + + this.nodes.push(result); + } +}; + +/** + * Execute a function on each affected node. + * + * The callback is passed two parameters. The first is a node object which looks + * like this: + * + * { + * // Type of diff, e.g. CreatedNode, ModifiedNode + * diffType: 'CreatedNode' + * + * // Type of node affected, e.g. RippleState, AccountRoot + * entryType: 'RippleState', + * + * // Index of the ledger this change occurred in + * ledgerIndex: '01AB01AB...', + * + * // Contains all fields with later versions taking precedence + * // + * // This is a shorthand for doing things like checking which account + * // this affected without having to check the diffType. + * fields: {...}, + * + * // Old fields (before the change) + * fieldsPrev: {...}, + * + * // New fields (that have been added) + * fieldsNew: {...}, + * + * // Changed fields + * fieldsFinal: {...} + * } + * + * The second parameter to the callback is the index of the node in the metadata + * (first entry is index 0). + */ +Meta.prototype.each = function (fn) +{ + for (var i = 0, l = this.nodes.length; i < l; i++) { + fn(this.nodes[i], i); + } +}; + +var amountFieldsAffectingIssuer = [ + "LowLimit", "HighLimit", "TakerPays", "TakerGets" +]; +Meta.prototype.getAffectedAccounts = function () +{ + var accounts = []; + + // This code should match the behavior of the C++ method: + // TransactionMetaSet::getAffectedAccounts + this.each(function (an) { + var fields = (an.diffType === "CreatedNode") ? an.fieldsNew : an.fieldsFinal; + + for (var i in fields) { + var field = fields[i]; + + if ("string" === typeof field && UInt160.is_valid(field)) { + accounts.push(field); + } else if (amountFieldsAffectingIssuer.indexOf(i) !== -1) { + var amount = Amount.from_json(field); + var issuer = amount.issuer(); + if (issuer.is_valid() && !issuer.is_zero()) { + accounts.push(issuer.to_json()); + } + } + } + }); + + return accounts; +}; + +exports.Meta = Meta; diff --git a/src/js/remote.js b/src/js/remote.js index efe050bfa..9fdd70f66 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -20,6 +20,8 @@ var Amount = require('./amount').Amount; var Currency = require('./amount').Currency; var UInt160 = require('./amount').UInt160; var Transaction = require('./transaction').Transaction; +var Account = require('./account').Account; +var Meta = require('./meta').Meta; var utils = require('./utils'); var config = require('./config'); @@ -233,6 +235,7 @@ var Remote = function (opts, trace) { } // Cache information for accounts. + // DEPRECATED, will be removed this.accounts = { // Consider sequence numbers stable if you know you're not generating bad transactions. // Otherwise, clear it to have it automatically refreshed from the network. @@ -241,6 +244,9 @@ var Remote = function (opts, trace) { }; + // Hash map of Account objects by AccountId. + this._accounts = {}; + // List of secrets that we know about. this.secrets = { // Secrets can be set by calling set_secret(account, secret). @@ -342,11 +348,13 @@ Remote.prototype._set_state = function (state) { switch (state) { case 'online': this._online_state = 'open'; + this.emit('connect'); this.emit('connected'); break; case 'offline': this._online_state = 'closed'; + this.emit('disconnect'); this.emit('disconnected'); break; } @@ -387,11 +395,13 @@ Remote.prototype.ledger_hash = function () { // Stop from open state. Remote.prototype._connect_stop = function () { - delete this.ws.onerror; - delete this.ws.onclose; + if (this.ws) { + delete this.ws.onerror; + delete this.ws.onclose; - this.ws.terminate(); - delete this.ws; + this.ws.close(); + delete this.ws; + } this._set_state('offline'); }; @@ -450,6 +460,12 @@ Remote.prototype._connect_start = function () { if (this.trace) console.log("remote: connect: %s", url); + // There should not be an active connection at this point, but if there is + // we will shut it down so we don't end up with a duplicate. + if (this.ws) { + this._connect_stop(); + } + var WebSocket = require('ws'); var ws = this.ws = new WebSocket(url); @@ -507,6 +523,7 @@ Remote.prototype._connect_start = function () { // It is possible for messages to be dispatched after the connection is closed. Remote.prototype._connect_message = function (ws, json) { + var self = this; var message = JSON.parse(json); var unexpected = false; var request; @@ -556,6 +573,23 @@ Remote.prototype._connect_message = function (ws, json) { case 'account': // XXX If not trusted, need proof. + if (this.trace) utils.logObject("remote: account: %s", message); + + // Process metadata + message.mmeta = new Meta(message.meta); + + // Pass the event on to any related Account objects + var affected = message.mmeta.getAffectedAccounts(); + 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); + } + } this.emit('account', message); break; @@ -930,7 +964,7 @@ Remote.prototype._server_subscribe = function () { self._load_factor = message.load_factor || 1.0; self._fee_ref = message.fee_ref; self._fee_base = message.fee_base; - self._reserve_base = message.reverse_base; + self._reserve_base = message.reserve_base; self._reserve_inc = message.reserve_inc; self._server_status = message.server_status; @@ -994,6 +1028,14 @@ Remote.prototype.request_owner_count = function (account, current) { }); }; +Remote.prototype.account = function (accountId) { + var account = new Account(this, accountId); + + if (!account.is_valid()) return account; + + return this._accounts[account.to_json()] = account; +}; + // Return the next account sequence if possible. // <-- undefined or Sequence Remote.prototype.account_seq = function (account, advance) { diff --git a/src/js/transaction.js b/src/js/transaction.js index a6e0621ed..77b8841d4 100644 --- a/src/js/transaction.js +++ b/src/js/transaction.js @@ -401,8 +401,8 @@ Transaction.prototype.destination_tag = function (tag) { Transaction._path_rewrite = function (path) { var path_new = []; - for (var index in path) { - var node = path[index]; + for (var i = 0, l = path.length; i < l; i++) { + var node = path[i]; var node_new = {}; if ('account' in node) @@ -421,7 +421,7 @@ Transaction._path_rewrite = function (path) { } Transaction.prototype.path_add = function (path) { - this.tx_json.Paths = this.tx_json.Paths || [] + this.tx_json.Paths = this.tx_json.Paths || []; this.tx_json.Paths.push(Transaction._path_rewrite(path)); return this; @@ -430,8 +430,8 @@ Transaction.prototype.path_add = function (path) { // --> paths: undefined or array of path // A path is an array of objects containing some combination of: account, currency, issuer Transaction.prototype.paths = function (paths) { - for (var index in paths) { - this.path_add(paths[index]); + for (var i = 0, l = paths.length; i < l; i++) { + this.path_add(paths[i]); } return this; diff --git a/src/js/uint.js b/src/js/uint.js index 100dfc9c8..d95ccb51f 100644 --- a/src/js/uint.js +++ b/src/js/uint.js @@ -20,8 +20,8 @@ var UInt = function () { this._value = NaN; }; -UInt.json_rewrite = function (j) { - return this.from_json(j).to_json(); +UInt.json_rewrite = function (j, opts) { + return this.from_json(j).to_json(opts); }; // Return a new UInt from j. @@ -92,6 +92,10 @@ UInt.prototype.is_valid = function () { return this._value instanceof BigInteger; }; +UInt.prototype.is_zero = function () { + return this._value.equals(BigInteger.ZERO); +}; + // value = NaN on error. UInt.prototype.parse_generic = function (j) { // Canonicalize and validate diff --git a/src/js/uint160.js b/src/js/uint160.js index ee17524dc..12c7ff04c 100644 --- a/src/js/uint160.js +++ b/src/js/uint160.js @@ -59,8 +59,8 @@ UInt160.prototype.to_json = function (opts) { var output = Base.encode_check(Base.VER_ACCOUNT_ID, this.to_bytes()); - if (config.gateways && output in config.gateways && !opts.no_gateway) - output = config.gateways[output]; + if (opts.gateways && output in opts.gateways) + output = opts.gateways[output]; return output; }; diff --git a/test/offer-test.js b/test/offer-test.js index c7d4f7c83..0df8a6bdf 100644 --- a/test/offer-test.js +++ b/test/offer-test.js @@ -1354,6 +1354,93 @@ buster.testCase("Offer tests 3", { // 'setUp' : testutils.build_setup({ verbose: true, standalone: true }), 'tearDown' : testutils.build_teardown(), + "offer fee consumes funds" : + function (done) { + var self = this; + var final_create; + + async.waterfall([ + function (callback) { + // Provide micro amounts to compensate for fees to make results round nice. + self.what = "Create accounts."; + + testutils.create_accounts(self.remote, "root", "350.000020", ["alice", "bob", "mtgox"], callback); + }, + function (callback) { + self.what = "Set limits."; + + testutils.credit_limits(self.remote, + { + "alice" : "1000/USD/mtgox", + "bob" : "1000/USD/mtgox", + }, + callback); + }, + function (callback) { + self.what = "Distribute funds."; + + testutils.payments(self.remote, + { + "mtgox" : [ "500/USD/bob" ], + }, + callback); + }, + function (callback) { + self.what = "Create offer bob."; + + self.remote.transaction() + .offer_create("bob", "200.0", "200/USD/mtgox") + .on('proposed', function (m) { + // console.log("proposed: offer_create: %s", json.stringify(m)); + callback(m.result !== 'tesSUCCESS'); + + seq_carol = m.tx_json.sequence; + }) + .submit(); + }, + function (callback) { + // Alice has 350 fees - a reserve of 50 = 250 reserve = 100 available. + // Ask for more than available to prove reserve works. + self.what = "Create offer alice."; + + self.remote.transaction() + .offer_create("alice", "200/USD/mtgox", "200.0") + .on('proposed', function (m) { + // console.log("proposed: offer_create: %s", json.stringify(m)); + callback(m.result !== 'tesSUCCESS'); + + seq_carol = m.tx_json.sequence; + }) + .submit(); + }, +// function (callback) { +// self.what = "Display ledger"; +// +// self.remote.request_ledger('current', true) +// .on('success', function (m) { +// console.log("Ledger: %s", JSON.stringify(m, undefined, 2)); +// +// callback(); +// }) +// .request(); +// }, + function (callback) { + self.what = "Verify balances."; + + testutils.verify_balances(self.remote, + { + "alice" : [ "100/USD/mtgox", "250.0" ], + "bob" : "400/USD/mtgox", + }, + callback); + }, + ], function (error) { + // console.log("result: error=%s", error); + buster.refute(error); + + done(); + }); + }, "offer create then cross offer" : function (done) { var self = this; diff --git a/test/path-test.js b/test/path-test.js index 4c021ce56..00fbaef41 100644 --- a/test/path-test.js +++ b/test/path-test.js @@ -906,7 +906,6 @@ buster.testCase("Via offers", { // 'setUp' : testutils.build_setup({ verbose: true, no_server: true }), 'tearDown' : testutils.build_teardown(), - // XXX Triggers bad path expansion. "Via gateway" : // carol holds mtgoxAUD, sells mtgoxAUD for XRP // bob will hold mtgoxAUD @@ -1138,4 +1137,53 @@ buster.testCase("Via offers", { }, }); +buster.testCase("Indirect paths", { + // 'setUp' : testutils.build_setup(), + 'setUp' : testutils.build_setup({ verbose: true }), + // 'setUp' : testutils.build_setup({ verbose: true, no_server: true }), + 'tearDown' : testutils.build_teardown(), + + "//path find" : + function (done) { + var self = this; + + async.waterfall([ + function (callback) { + self.what = "Create accounts."; + + testutils.create_accounts(self.remote, "root", "10000.0", ["alice", "bob", "carol"], callback); + }, + function (callback) { + self.what = "Set credit limits."; + + testutils.credit_limits(self.remote, + { + "bob" : "1000/USD/alice", + "carol" : "2000/USD/bob", + }, + callback); + }, + function (callback) { + self.what = "Find path from alice to carol"; + + self.remote.request_ripple_path_find("alice", "carol", "5/USD/carol", + [ { 'currency' : "USD" } ]) + .on('success', function (m) { + console.log("proposed: %s", JSON.stringify(m)); + + // 1 alternative. + buster.assert.equals(1, m.alternatives.length) + // Path is empty. + buster.assert.equals(0, m.alternatives[0].paths_canonical.length) + + callback(); + }) + .request(); + }, + ], function (error) { + buster.refute(error, self.what); + done(); + }); + }, +}); // vim:sw=2:sts=2:ts=8:et