-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add book_changes rpc #4212
Add book_changes rpc #4212
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
//------------------------------------------------------------------------------ | ||
/* | ||
This file is part of rippled: https://github.com/ripple/rippled | ||
Copyright (c) 2019 Ripple Labs Inc. | ||
|
||
Permission to use, copy, modify, and/or distribute this software for any | ||
purpose with or without fee is hereby granted, provided that the above | ||
copyright notice and this permission notice appear in all copies. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | ||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | ||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | ||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | ||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | ||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | ||
*/ | ||
//============================================================================== | ||
|
||
#ifndef RIPPLE_RPC_BOOKCHANGES_H_INCLUDED | ||
#define RIPPLE_RPC_BOOKCAHNGES_H_INCLUDED | ||
|
||
namespace Json { | ||
class Value; | ||
} | ||
|
||
namespace ripple { | ||
|
||
class ReadView; | ||
class Transaction; | ||
class TxMeta; | ||
class STTx; | ||
|
||
namespace RPC { | ||
|
||
template <class L> | ||
Json::Value | ||
computeBookChanges(std::shared_ptr<L const> const& lpAccepted) | ||
{ | ||
std::map< | ||
std::string, | ||
std::tuple< | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend using |
||
STAmount, // side A volume | ||
STAmount, // side B volume | ||
STAmount, // high rate | ||
STAmount, // low rate | ||
STAmount, // open rate | ||
STAmount // close rate | ||
>> | ||
tally; | ||
|
||
for (auto& tx : lpAccepted->txs) | ||
{ | ||
if (!tx.first || !tx.second || | ||
!tx.first->isFieldPresent(sfTransactionType)) | ||
continue; | ||
|
||
std::optional<uint32_t> offerCancel; | ||
uint16_t tt = tx.first->getFieldU16(sfTransactionType); | ||
switch (tt) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid introducing an unnecessary variable:
|
||
{ | ||
case ttOFFER_CANCEL: | ||
case ttOFFER_CREATE: { | ||
if (tx.first->isFieldPresent(sfOfferSequence)) | ||
offerCancel = tx.first->getFieldU32(sfOfferSequence); | ||
break; | ||
} | ||
// in future if any other ways emerge to cancel an offer | ||
// this switch makes them easy to add | ||
default: | ||
break; | ||
} | ||
|
||
for (auto const& node : tx.second->getFieldArray(sfAffectedNodes)) | ||
{ | ||
SField const& metaType = node.getFName(); | ||
uint16_t nodeType = node.getFieldU16(sfLedgerEntryType); | ||
|
||
// we only care about ltOFFER objects being modified or | ||
// deleted | ||
if (nodeType != ltOFFER || metaType == sfCreatedNode) | ||
continue; | ||
|
||
// if either FF or PF are missing we can't compute | ||
// but generally these are cancelled rather than crossed | ||
// so skipping them is consistent | ||
if (!node.isFieldPresent(sfFinalFields) || | ||
!node.isFieldPresent(sfPreviousFields)) | ||
continue; | ||
|
||
STObject& finalFields = (const_cast<STObject&>(node)) | ||
.getField(sfFinalFields) | ||
.downcast<STObject>(); | ||
|
||
STObject& previousFields = (const_cast<STObject&>(node)) | ||
.getField(sfPreviousFields) | ||
.downcast<STObject>(); | ||
|
||
// defensive case that should never be hit | ||
if (!finalFields.isFieldPresent(sfTakerGets) || | ||
!finalFields.isFieldPresent(sfTakerPays) || | ||
!previousFields.isFieldPresent(sfTakerGets) || | ||
!previousFields.isFieldPresent(sfTakerPays)) | ||
continue; | ||
|
||
// filter out any offers deleted by explicit offer cancels | ||
if (metaType == sfDeletedNode && offerCancel && | ||
finalFields.getFieldU32(sfSequence) == *offerCancel) | ||
continue; | ||
|
||
// compute the difference in gets and pays actually | ||
// affected onto the offer | ||
STAmount deltaGets = finalFields.getFieldAmount(sfTakerGets) - | ||
previousFields.getFieldAmount(sfTakerGets); | ||
STAmount deltaPays = finalFields.getFieldAmount(sfTakerPays) - | ||
previousFields.getFieldAmount(sfTakerPays); | ||
|
||
std::string g{to_string(deltaGets.issue())}; | ||
std::string p{to_string(deltaPays.issue())}; | ||
|
||
bool const noswap = | ||
isXRP(deltaGets) ? true : (isXRP(deltaPays) ? false : (g < p)); | ||
|
||
STAmount first = noswap ? deltaGets : deltaPays; | ||
STAmount second = noswap ? deltaPays : deltaGets; | ||
|
||
// defensively programmed, should (probably) never happen | ||
if (second == beast::zero) | ||
continue; | ||
|
||
STAmount rate = divide(first, second, noIssue()); | ||
|
||
if (first < beast::zero) | ||
first = -first; | ||
|
||
if (second < beast::zero) | ||
second = -second; | ||
|
||
std::stringstream ss; | ||
if (noswap) | ||
ss << g << "|" << p; | ||
else | ||
ss << p << "|" << g; | ||
|
||
std::string key{ss.str()}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend you avoid |
||
|
||
if (tally.find(key) == tally.end()) | ||
tally[key] = { | ||
first, // side A vol | ||
second, // side B vol | ||
rate, // high | ||
rate, // low | ||
rate, // open | ||
rate // close | ||
}; | ||
else | ||
{ | ||
// increment volume | ||
auto& entry = tally[key]; | ||
|
||
std::get<0>(entry) += first; // side A vol | ||
std::get<1>(entry) += second; // side B vol | ||
|
||
if (std::get<2>(entry) < rate) // high | ||
std::get<2>(entry) = rate; | ||
|
||
if (std::get<3>(entry) > rate) // low | ||
std::get<3>(entry) = rate; | ||
|
||
std::get<5>(entry) = rate; // close | ||
} | ||
} | ||
} | ||
|
||
Json::Value jvObj(Json::objectValue); | ||
jvObj[jss::type] = "bookChanges"; | ||
jvObj[jss::ledger_index] = lpAccepted->info().seq; | ||
jvObj[jss::ledger_hash] = to_string(lpAccepted->info().hash); | ||
jvObj[jss::ledger_time] = Json::Value::UInt( | ||
lpAccepted->info().closeTime.time_since_epoch().count()); | ||
|
||
jvObj[jss::changes] = Json::arrayValue; | ||
|
||
for (auto const& entry : tally) | ||
{ | ||
Json::Value& inner = jvObj[jss::changes].append(Json::objectValue); | ||
|
||
STAmount volA = std::get<0>(entry.second); | ||
STAmount volB = std::get<1>(entry.second); | ||
|
||
inner[jss::currency_a] = | ||
(isXRP(volA) ? "XRP_drops" : to_string(volA.issue())); | ||
inner[jss::currency_b] = | ||
(isXRP(volB) ? "XRP_drops" : to_string(volB.issue())); | ||
|
||
inner[jss::volume_a] = | ||
(isXRP(volA) ? to_string(volA.xrp()) : to_string(volA.iou())); | ||
inner[jss::volume_b] = | ||
(isXRP(volB) ? to_string(volB.xrp()) : to_string(volB.iou())); | ||
|
||
inner[jss::high] = to_string(std::get<2>(entry.second).iou()); | ||
inner[jss::low] = to_string(std::get<3>(entry.second).iou()); | ||
inner[jss::open] = to_string(std::get<4>(entry.second).iou()); | ||
inner[jss::close] = to_string(std::get<5>(entry.second).iou()); | ||
} | ||
|
||
return jvObj; | ||
} | ||
|
||
} // namespace RPC | ||
} // namespace ripple | ||
|
||
#endif |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to compute these changes outside of rippled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to compute everything outside of rippled, given that it is a public ledger with a public mesh and well defined data formats.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While theoretically true, that's not practically true. For instance, if I wanted to compute the state of each new ledger based on that ledgers transactions, outside of rippled, I'd have to copy the transaction application code and keep it up to date with amendments. While theoretically possible, this would be a huge lift, and I'd argue not practically possible. It would make sense to just rely on rippled for such a thing.
But this code just seems like parsing metadata. Is there a reason this can't be done in a simple Python script, outside of rippled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You would first need to request all the txn meta data for a given ledger from rippled then compute the result. This is a much heavier rpc call because you're pushing [potentially a lot] more data to an external service for aggregation, instead of just computing it in place where it already exists. It's equivalent to dumping every row in an SQL table because you want to find the
SUM()
of a column.