mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 10:35:50 +00:00
Fix transaction enumeration in account_tx (RIPD-734):
In some corner cases, an incorrect resume marker could be returned, preventing the complete enumeration of account transactions. * Robust markers via improved paging support * New unit tests * Cleanup
This commit is contained in:
@@ -1726,6 +1726,14 @@
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\misc\IHashRouter.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\NetworkOPs.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
@@ -1746,6 +1754,12 @@
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\misc\SHAMapStoreImp.h">
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\tests\AccountTxPaging.test.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='debug.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='release.classic|x64'">..\..\src\soci\src\core;..\..\src\sqlite;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\tests\AmendmentTable.test.cpp">
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||
|
||||
@@ -280,6 +280,9 @@
|
||||
<Filter Include="ripple\app\misc">
|
||||
<UniqueIdentifier>{5A1509B2-871B-A7AC-1E60-544D3F398741}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="ripple\app\misc\impl">
|
||||
<UniqueIdentifier>{C4BDB9F8-7DB7-E304-D286-098085D5D16E}</UniqueIdentifier>
|
||||
</Filter>
|
||||
<Filter Include="ripple\app\misc\tests">
|
||||
<UniqueIdentifier>{815DC1A2-E2EF-E6E3-D979-19AD1476A28B}</UniqueIdentifier>
|
||||
</Filter>
|
||||
@@ -2379,6 +2382,12 @@
|
||||
<ClInclude Include="..\..\src\ripple\app\misc\IHashRouter.h">
|
||||
<Filter>ripple\app\misc</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.cpp">
|
||||
<Filter>ripple\app\misc\impl</Filter>
|
||||
</ClCompile>
|
||||
<ClInclude Include="..\..\src\ripple\app\misc\impl\AccountTxPaging.h">
|
||||
<Filter>ripple\app\misc\impl</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\NetworkOPs.cpp">
|
||||
<Filter>ripple\app\misc</Filter>
|
||||
</ClCompile>
|
||||
@@ -2397,6 +2406,9 @@
|
||||
<ClInclude Include="..\..\src\ripple\app\misc\SHAMapStoreImp.h">
|
||||
<Filter>ripple\app\misc</Filter>
|
||||
</ClInclude>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\tests\AccountTxPaging.test.cpp">
|
||||
<Filter>ripple\app\misc\tests</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\src\ripple\app\misc\tests\AmendmentTable.test.cpp">
|
||||
<Filter>ripple\app\misc\tests</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <ripple/app/ledger/Ledger.h>
|
||||
#include <ripple/basics/Time.h>
|
||||
#include <ripple/basics/StringUtilities.h>
|
||||
#include <ripple/protocol/JsonFields.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/rpc/Yield.h>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <ripple/app/misc/IHashRouter.h>
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
#include <ripple/app/misc/Validations.h>
|
||||
#include <ripple/app/misc/impl/AccountTxPaging.h>
|
||||
#include <ripple/app/peers/ClusterNodeStatus.h>
|
||||
#include <ripple/app/peers/UniqueNodeList.h>
|
||||
#include <ripple/app/tx/TransactionMaster.h>
|
||||
@@ -1916,7 +1917,6 @@ NetworkOPs::AccountTxs NetworkOPsImp::getAccountTxs (
|
||||
auto txn = Transaction::transactionFromSQL (
|
||||
ledgerSeq, status, rawTxn, Validate::NO);
|
||||
|
||||
|
||||
if (txnMeta.empty ())
|
||||
{ // Work around a bug that could leave the metadata missing
|
||||
auto const seq = rangeCheckedCast<std::uint32_t>(
|
||||
@@ -1942,7 +1942,7 @@ std::vector<NetworkOPsImp::txnMetaLedgerType> NetworkOPsImp::getAccountTxsB (
|
||||
std::uint32_t offset, int limit, bool bAdmin)
|
||||
{
|
||||
// can be called with no locks
|
||||
std::vector< txnMetaLedgerType> ret;
|
||||
std::vector<txnMetaLedgerType> ret;
|
||||
|
||||
std::string sql = NetworkOPsImp::transactionsSQL (
|
||||
"AccountTransactions.LedgerSeq,Status,RawTxn,TxnMeta", account,
|
||||
@@ -1985,255 +1985,55 @@ std::vector<NetworkOPsImp::txnMetaLedgerType> NetworkOPsImp::getAccountTxsB (
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
NetworkOPsImp::AccountTxs NetworkOPsImp::getTxsAccount (
|
||||
NetworkOPsImp::AccountTxs
|
||||
NetworkOPsImp::getTxsAccount (
|
||||
RippleAddress const& account, std::int32_t minLedger,
|
||||
std::int32_t maxLedger, bool forward, Json::Value& token,
|
||||
int limit, bool bAdmin)
|
||||
{
|
||||
AccountTxs ret;
|
||||
static std::uint32_t const page_length (200);
|
||||
|
||||
std::uint32_t NONBINARY_PAGE_LENGTH = 200;
|
||||
std::uint32_t EXTRA_LENGTH = 100;
|
||||
NetworkOPsImp::AccountTxs ret;
|
||||
|
||||
bool foundResume = token.isNull() || !token.isObject();
|
||||
|
||||
std::uint32_t numberOfResults, queryLimit;
|
||||
if (limit <= 0)
|
||||
numberOfResults = NONBINARY_PAGE_LENGTH;
|
||||
else if (!bAdmin && (limit > NONBINARY_PAGE_LENGTH))
|
||||
numberOfResults = NONBINARY_PAGE_LENGTH;
|
||||
else
|
||||
numberOfResults = limit;
|
||||
queryLimit = numberOfResults + 1 + (foundResume ? 0 : EXTRA_LENGTH);
|
||||
|
||||
std::uint32_t findLedger = 0, findSeq = 0;
|
||||
if (!foundResume)
|
||||
auto bound = [&ret](
|
||||
std::uint32_t ledger_index,
|
||||
std::string const& status,
|
||||
Blob const& rawTxn,
|
||||
Blob const& rawMeta)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!token.isMember(jss::ledger) || !token.isMember(jss::seq))
|
||||
return ret;
|
||||
findLedger = token[jss::ledger].asInt();
|
||||
findSeq = token[jss::seq].asInt();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
convertBlobsToTxResult (ret, ledger_index, status, rawTxn, rawMeta);
|
||||
};
|
||||
|
||||
// ST NOTE We're using the token reference both for passing inputs and
|
||||
// outputs, so we need to clear it in between.
|
||||
token = Json::nullValue;
|
||||
|
||||
std::string sql = boost::str (boost::format
|
||||
("SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,"
|
||||
"Status,RawTxn,TxnMeta "
|
||||
"FROM AccountTransactions INNER JOIN Transactions "
|
||||
"ON Transactions.TransID = AccountTransactions.TransID "
|
||||
"WHERE AccountTransactions.Account = '%s' "
|
||||
"AND AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' "
|
||||
"ORDER BY AccountTransactions.LedgerSeq %s, "
|
||||
"AccountTransactions.TxnSeq %s, AccountTransactions.TransID %s "
|
||||
"LIMIT %u;")
|
||||
% account.humanAccountID()
|
||||
% ((forward && (findLedger != 0)) ? findLedger : minLedger)
|
||||
% ((!forward && (findLedger != 0)) ? findLedger: maxLedger)
|
||||
% (forward ? "ASC" : "DESC")
|
||||
% (forward ? "ASC" : "DESC")
|
||||
% (forward ? "ASC" : "DESC")
|
||||
% queryLimit);
|
||||
{
|
||||
auto db = getApp().getTxnDB ().checkoutDb ();
|
||||
|
||||
boost::optional<std::uint64_t> ledgerSeq64;
|
||||
boost::optional<std::int32_t> txnSeq;
|
||||
boost::optional<std::string> status;
|
||||
soci::blob sociTxnBlob (*db), sociTxnMetaBlob (*db);
|
||||
soci::indicator rti, tmi;
|
||||
Blob rawTxn, txnMeta;
|
||||
|
||||
soci::statement st =
|
||||
(db->prepare << sql,
|
||||
soci::into(ledgerSeq64),
|
||||
soci::into(txnSeq),
|
||||
soci::into(status),
|
||||
soci::into(sociTxnBlob, rti),
|
||||
soci::into(sociTxnMetaBlob, tmi));
|
||||
|
||||
st.execute ();
|
||||
while (st.fetch ())
|
||||
{
|
||||
if (soci::i_ok == rti)
|
||||
convert(sociTxnBlob, rawTxn);
|
||||
else
|
||||
rawTxn.clear ();
|
||||
|
||||
if (soci::i_ok == tmi)
|
||||
convert (sociTxnMetaBlob, txnMeta);
|
||||
else
|
||||
txnMeta.clear ();
|
||||
|
||||
auto const ledgerSeq = rangeCheckedCast<std::uint32_t>(
|
||||
ledgerSeq64.value_or (0));
|
||||
|
||||
if (!foundResume)
|
||||
{
|
||||
foundResume = (findLedger == ledgerSeq &&
|
||||
findSeq == txnSeq.value_or (0));
|
||||
}
|
||||
else if (numberOfResults == 0)
|
||||
{
|
||||
token = Json::objectValue;
|
||||
token[jss::ledger] = ledgerSeq;
|
||||
token[jss::seq] = txnSeq.value_or (0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundResume)
|
||||
{
|
||||
auto txn = Transaction::transactionFromSQL (
|
||||
ledgerSeq64, status, rawTxn, Validate::NO);
|
||||
|
||||
if (txnMeta.empty ())
|
||||
{
|
||||
// Work around a bug that could leave the metadata missing
|
||||
m_journal.warning << "Recovering ledger " << ledgerSeq
|
||||
<< ", txn " << txn->getID();
|
||||
Ledger::pointer ledger = getLedgerBySeq(ledgerSeq);
|
||||
if (ledger)
|
||||
ledger->pendSaveValidated(false, false);
|
||||
}
|
||||
|
||||
--numberOfResults;
|
||||
|
||||
ret.emplace_back (std::move (txn),
|
||||
std::make_shared<TransactionMetaSet> (
|
||||
txn->getID (), txn->getLedger (), txnMeta));
|
||||
}
|
||||
}
|
||||
}
|
||||
accountTxPage(getApp().getTxnDB (), saveLedgerAsync, bound, account,
|
||||
minLedger, maxLedger, forward, token, limit, bAdmin, page_length);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
NetworkOPsImp::MetaTxsList NetworkOPsImp::getTxsAccountB (
|
||||
NetworkOPsImp::MetaTxsList
|
||||
NetworkOPsImp::getTxsAccountB (
|
||||
RippleAddress const& account, std::int32_t minLedger,
|
||||
std::int32_t maxLedger, bool forward, Json::Value& token,
|
||||
int limit, bool bAdmin)
|
||||
{
|
||||
static const std::uint32_t page_length (500);
|
||||
|
||||
MetaTxsList ret;
|
||||
|
||||
std::uint32_t BINARY_PAGE_LENGTH = 500;
|
||||
std::uint32_t EXTRA_LENGTH = 100;
|
||||
|
||||
bool foundResume = token.isNull() || !token.isObject();
|
||||
|
||||
std::uint32_t numberOfResults, queryLimit;
|
||||
if (limit <= 0)
|
||||
numberOfResults = BINARY_PAGE_LENGTH;
|
||||
else if (!bAdmin && (limit > BINARY_PAGE_LENGTH))
|
||||
numberOfResults = BINARY_PAGE_LENGTH;
|
||||
else
|
||||
numberOfResults = limit;
|
||||
queryLimit = numberOfResults + 1 + (foundResume ? 0 : EXTRA_LENGTH);
|
||||
|
||||
std::uint32_t findLedger = 0, findSeq = 0;
|
||||
if (!foundResume)
|
||||
auto bound = [&ret](
|
||||
std::uint32_t ledgerIndex,
|
||||
std::string const& status,
|
||||
Blob const& rawTxn,
|
||||
Blob const& rawMeta)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!token.isMember(jss::ledger) || !token.isMember(jss::seq))
|
||||
return ret;
|
||||
findLedger = token[jss::ledger].asInt();
|
||||
findSeq = token[jss::seq].asInt();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
token = Json::nullValue;
|
||||
|
||||
std::string sql = boost::str (boost::format
|
||||
("SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,"
|
||||
"Status,RawTxn,TxnMeta "
|
||||
"FROM AccountTransactions INNER JOIN Transactions "
|
||||
"ON Transactions.TransID = AccountTransactions.TransID "
|
||||
"WHERE AccountTransactions.Account = '%s' "
|
||||
"AND AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' "
|
||||
"ORDER BY AccountTransactions.LedgerSeq %s, "
|
||||
"AccountTransactions.TxnSeq %s, AccountTransactions.TransID %s "
|
||||
"LIMIT %u;")
|
||||
% account.humanAccountID()
|
||||
% ((forward && (findLedger != 0)) ? findLedger : minLedger)
|
||||
% ((!forward && (findLedger != 0)) ? findLedger: maxLedger)
|
||||
% (forward ? "ASC" : "DESC")
|
||||
% (forward ? "ASC" : "DESC")
|
||||
% (forward ? "ASC" : "DESC")
|
||||
% queryLimit);
|
||||
{
|
||||
auto db = getApp().getTxnDB ().checkoutDb ();
|
||||
|
||||
boost::optional<std::int64_t> ledgerSeq;
|
||||
boost::optional<std::int32_t> txnSeq;
|
||||
boost::optional<std::string> status;
|
||||
soci::blob sociTxnBlob (*db);
|
||||
soci::indicator rtI;
|
||||
soci::blob sociTxnMetaBlob (*db);
|
||||
soci::indicator tmI;
|
||||
|
||||
soci::statement st = (db->prepare << sql,
|
||||
soci::into (ledgerSeq),
|
||||
soci::into (txnSeq),
|
||||
soci::into (status),
|
||||
soci::into (sociTxnBlob, rtI),
|
||||
soci::into (sociTxnMetaBlob, tmI));
|
||||
|
||||
st.execute ();
|
||||
while (st.fetch ())
|
||||
{
|
||||
if (!foundResume)
|
||||
{
|
||||
if (findLedger == ledgerSeq.value_or (0) &&
|
||||
findSeq == txnSeq.value_or (0))
|
||||
{
|
||||
foundResume = true;
|
||||
}
|
||||
}
|
||||
else if (numberOfResults == 0)
|
||||
{
|
||||
token = Json::objectValue;
|
||||
token[jss::ledger] = rangeCheckedCast<std::int32_t>(
|
||||
ledgerSeq.value_or(0));
|
||||
token[jss::seq] = txnSeq.value_or(0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundResume)
|
||||
{
|
||||
Blob rawTxn;
|
||||
if (soci::i_ok == rtI)
|
||||
convert (sociTxnBlob, rawTxn);
|
||||
Blob txnMeta;
|
||||
if (soci::i_ok == tmI)
|
||||
convert (sociTxnMetaBlob, txnMeta);
|
||||
|
||||
ret.emplace_back (
|
||||
strHex (rawTxn.begin (), rawTxn.size ()),
|
||||
strHex (txnMeta.begin (), txnMeta.size ()),
|
||||
rangeCheckedCast<std::int32_t>(ledgerSeq.value_or (0)));
|
||||
--numberOfResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.emplace_back (strHex(rawTxn), strHex (rawMeta), ledgerIndex);
|
||||
};
|
||||
|
||||
accountTxPage(getApp().getTxnDB (), saveLedgerAsync, bound, account,
|
||||
minLedger, maxLedger, forward, token, limit, bAdmin, page_length);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
std::vector<RippleAddress>
|
||||
NetworkOPsImp::getLedgerAffectedAccounts (std::uint32_t ledgerSeq)
|
||||
{
|
||||
@@ -2256,7 +2056,7 @@ NetworkOPsImp::getLedgerAffectedAccounts (std::uint32_t ledgerSeq)
|
||||
convert (accountBlob, accountStr);
|
||||
else
|
||||
accountStr.clear ();
|
||||
|
||||
|
||||
if (acct.setAccountID (accountStr))
|
||||
accounts.push_back (acct);
|
||||
}
|
||||
|
||||
@@ -321,6 +321,8 @@ public:
|
||||
STTx::ref stTxn, TER terResult) = 0;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::unique_ptr<NetworkOPs>
|
||||
make_NetworkOPs (NetworkOPs::clock_type& clock, bool standalone,
|
||||
std::size_t network_quorum, JobQueue& job_queue, LedgerMaster& ledgerMaster,
|
||||
|
||||
261
src/ripple/app/misc/impl/AccountTxPaging.cpp
Normal file
261
src/ripple/app/misc/impl/AccountTxPaging.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <BeastConfig.h>
|
||||
#include <ripple/app/ledger/LedgerToJson.h>
|
||||
#include <ripple/app/main/Application.h>
|
||||
#include <ripple/app/misc/impl/AccountTxPaging.h>
|
||||
#include <ripple/app/tx/Transaction.h>
|
||||
#include <ripple/protocol/Serializer.h>
|
||||
#include <beast/cxx14/memory.h> // <memory>
|
||||
#include <boost/format.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
void
|
||||
convertBlobsToTxResult (
|
||||
NetworkOPs::AccountTxs& to,
|
||||
std::uint32_t ledger_index,
|
||||
std::string const& status,
|
||||
Blob const& rawTxn,
|
||||
Blob const& rawMeta)
|
||||
{
|
||||
SerialIter it (rawTxn);
|
||||
STTx::pointer txn = std::make_shared<STTx> (it);
|
||||
std::string reason;
|
||||
|
||||
auto tr = std::make_shared<Transaction> (txn, Validate::NO, reason);
|
||||
|
||||
tr->setStatus (Transaction::sqlTransactionStatus(status));
|
||||
tr->setLedger (ledger_index);
|
||||
|
||||
to.emplace_back(std::make_pair(std::move(tr),
|
||||
std::make_shared<TransactionMetaSet> (
|
||||
tr->getID (), tr->getLedger (), rawMeta)));
|
||||
};
|
||||
|
||||
void
|
||||
saveLedgerAsync (std::uint32_t seq)
|
||||
{
|
||||
Ledger::pointer ledger = getApp().getOPs().getLedgerBySeq(seq);
|
||||
if (ledger)
|
||||
ledger->pendSaveValidated(false, false);
|
||||
}
|
||||
|
||||
void
|
||||
accountTxPage (
|
||||
DatabaseCon& connection,
|
||||
std::function<void (std::uint32_t)> const& onUnsavedLedger,
|
||||
std::function<void (std::uint32_t,
|
||||
std::string const&,
|
||||
Blob const&,
|
||||
Blob const&)> const& onTransaction,
|
||||
RippleAddress const& account,
|
||||
std::int32_t minLedger,
|
||||
std::int32_t maxLedger,
|
||||
bool forward,
|
||||
Json::Value& token,
|
||||
int limit,
|
||||
bool bAdmin,
|
||||
std::uint32_t page_length)
|
||||
{
|
||||
bool lookingForMarker = !token.isNull() && token.isObject();
|
||||
|
||||
std::uint32_t numberOfResults;
|
||||
|
||||
if (limit <= 0 || (limit > page_length && !bAdmin))
|
||||
numberOfResults = page_length;
|
||||
else
|
||||
numberOfResults = limit;
|
||||
|
||||
// As an account can have many thousands of transactions, there is a limit
|
||||
// placed on the amount of transactions returned. If the limit is reached
|
||||
// before the result set has been exhausted (we always query for one more
|
||||
// than the limit), then we return an opaque marker that can be supplied in
|
||||
// a subsequent query.
|
||||
std::uint32_t queryLimit = numberOfResults + 1;
|
||||
std::uint32_t findLedger = 0, findSeq = 0;
|
||||
|
||||
if (lookingForMarker)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!token.isMember(jss::ledger) || !token.isMember(jss::seq))
|
||||
return;
|
||||
findLedger = token[jss::ledger].asInt();
|
||||
findSeq = token[jss::seq].asInt();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We're using the token reference both for passing inputs and outputs, so
|
||||
// we need to clear it in between.
|
||||
token = Json::nullValue;
|
||||
|
||||
static std::string const prefix (
|
||||
R"(SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,
|
||||
Status,RawTxn,TxnMeta
|
||||
FROM AccountTransactions INNER JOIN Transactions
|
||||
ON Transactions.TransID = AccountTransactions.TransID
|
||||
AND AccountTransactions.Account = '%s' WHERE
|
||||
)");
|
||||
|
||||
std::string sql;
|
||||
|
||||
// SQL's BETWEEN uses a closed interval ([a,b])
|
||||
|
||||
if (forward && (findLedger == 0))
|
||||
{
|
||||
sql = boost::str (boost::format(
|
||||
prefix +
|
||||
(R"(AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u'
|
||||
ORDER BY AccountTransactions.LedgerSeq ASC,
|
||||
AccountTransactions.TxnSeq ASC
|
||||
LIMIT %u;)"))
|
||||
% account.humanAccountID()
|
||||
% minLedger
|
||||
% maxLedger
|
||||
% queryLimit);
|
||||
}
|
||||
else if (forward && (findLedger != 0))
|
||||
{
|
||||
sql = boost::str (boost::format(
|
||||
prefix +
|
||||
(R"(
|
||||
AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' OR
|
||||
( AccountTransactions.LedgerSeq = '%u' AND
|
||||
AccountTransactions.TxnSeq >= '%u' )
|
||||
ORDER BY AccountTransactions.LedgerSeq ASC,
|
||||
AccountTransactions.TxnSeq ASC
|
||||
LIMIT %u;
|
||||
)"))
|
||||
% account.humanAccountID()
|
||||
% (findLedger + 1)
|
||||
% maxLedger
|
||||
% findLedger
|
||||
% findSeq
|
||||
% queryLimit);
|
||||
}
|
||||
else if (!forward && (findLedger == 0))
|
||||
{
|
||||
sql = boost::str (boost::format(
|
||||
prefix +
|
||||
(R"(AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u'
|
||||
ORDER BY AccountTransactions.LedgerSeq DESC,
|
||||
AccountTransactions.TxnSeq DESC
|
||||
LIMIT %u;)"))
|
||||
% account.humanAccountID()
|
||||
% minLedger
|
||||
% maxLedger
|
||||
% queryLimit);
|
||||
}
|
||||
else if (!forward && (findLedger != 0))
|
||||
{
|
||||
sql = boost::str (boost::format(
|
||||
prefix +
|
||||
(R"(AccountTransactions.LedgerSeq BETWEEN '%u' AND '%u' OR
|
||||
(AccountTransactions.LedgerSeq = '%u' AND
|
||||
AccountTransactions.TxnSeq <= '%u')
|
||||
ORDER BY AccountTransactions.LedgerSeq DESC,
|
||||
AccountTransactions.TxnSeq DESC
|
||||
LIMIT %u;)"))
|
||||
% account.humanAccountID()
|
||||
% minLedger
|
||||
% (findLedger - 1)
|
||||
% findLedger
|
||||
% findSeq
|
||||
% queryLimit);
|
||||
}
|
||||
else
|
||||
{
|
||||
assert (false);
|
||||
// sql is empty
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
auto db (connection.checkoutDb());
|
||||
|
||||
Blob rawData;
|
||||
Blob rawMeta;
|
||||
|
||||
boost::optional<std::uint64_t> ledgerSeq;
|
||||
boost::optional<std::uint32_t> txnSeq;
|
||||
boost::optional<std::string> status;
|
||||
soci::blob txnData (*db);
|
||||
soci::blob txnMeta (*db);
|
||||
soci::indicator dataPresent, metaPresent;
|
||||
|
||||
soci::statement st = (db->prepare << sql,
|
||||
soci::into (ledgerSeq),
|
||||
soci::into (txnSeq),
|
||||
soci::into (status),
|
||||
soci::into (txnData, dataPresent),
|
||||
soci::into (txnMeta, metaPresent));
|
||||
|
||||
st.execute ();
|
||||
|
||||
while (st.fetch ())
|
||||
{
|
||||
if (lookingForMarker)
|
||||
{
|
||||
if (findLedger == ledgerSeq.value_or (0) &&
|
||||
findSeq == txnSeq.value_or (0))
|
||||
{
|
||||
lookingForMarker = false;
|
||||
}
|
||||
}
|
||||
else if (numberOfResults == 0)
|
||||
{
|
||||
token = Json::objectValue;
|
||||
token[jss::ledger] = rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or (0));
|
||||
token[jss::seq] = txnSeq.value_or (0);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!lookingForMarker)
|
||||
{
|
||||
if (dataPresent == soci::i_ok)
|
||||
convert (txnData, rawData);
|
||||
else
|
||||
rawData.clear ();
|
||||
|
||||
if (metaPresent == soci::i_ok)
|
||||
convert (txnMeta, rawMeta);
|
||||
else
|
||||
rawMeta.clear ();
|
||||
|
||||
// Work around a bug that could leave the metadata missing
|
||||
if (rawMeta.size() == 0)
|
||||
onUnsavedLedger(ledgerSeq.value_or (0));
|
||||
|
||||
onTransaction(rangeCheckedCast<std::uint32_t>(ledgerSeq.value_or (0)),
|
||||
*status, rawData, rawMeta);
|
||||
--numberOfResults;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
64
src/ripple/app/misc/impl/AccountTxPaging.h
Normal file
64
src/ripple/app/misc/impl/AccountTxPaging.h
Normal file
@@ -0,0 +1,64 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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_APP_MISC_IMPL_ACCOUNTTXPAGING_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_IMPL_ACCOUNTTXPAGING_H_INCLUDED
|
||||
|
||||
#include <ripple/app/data/DatabaseCon.h>
|
||||
#include <ripple/app/misc/NetworkOPs.h>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ripple {
|
||||
|
||||
void
|
||||
convertBlobsToTxResult (
|
||||
NetworkOPs::AccountTxs& to,
|
||||
std::uint32_t ledger_index,
|
||||
std::string const& status,
|
||||
Blob const& rawTxn,
|
||||
Blob const& rawMeta);
|
||||
|
||||
void
|
||||
saveLedgerAsync (std::uint32_t seq);
|
||||
|
||||
void
|
||||
accountTxPage (
|
||||
DatabaseCon& database,
|
||||
std::function<void (std::uint32_t)> const& onUnsavedLedger,
|
||||
std::function<void (std::uint32_t,
|
||||
std::string const&,
|
||||
Blob const&,
|
||||
Blob const&)> const&,
|
||||
RippleAddress const& account,
|
||||
std::int32_t minLedger,
|
||||
std::int32_t maxLedger,
|
||||
bool forward,
|
||||
Json::Value& token,
|
||||
int limit,
|
||||
bool bAdmin,
|
||||
std::uint32_t pageLength);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
246
src/ripple/app/misc/tests/AccountTxPaging.test.cpp
Normal file
246
src/ripple/app/misc/tests/AccountTxPaging.test.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
#include <ripple/app/data/DatabaseCon.h>
|
||||
#include <ripple/app/misc/impl/AccountTxPaging.h>
|
||||
#include <beast/cxx14/memory.h> // <memory>
|
||||
#include <beast/unit_test/suite.h>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
struct AccountTxPaging_test : beast::unit_test::suite
|
||||
{
|
||||
std::unique_ptr<DatabaseCon> db_;
|
||||
NetworkOPs::AccountTxs txs_;
|
||||
RippleAddress account_;
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
std::string fixturesPath = std::getenv("TEST_FIXTURES");
|
||||
|
||||
if (fixturesPath.empty ())
|
||||
{
|
||||
fail("TEST_FIXTURES environment var not declared");
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseCon::Setup dbConf;
|
||||
dbConf.dataDir = fixturesPath + "/";
|
||||
|
||||
db_ = std::make_unique <DatabaseCon> (
|
||||
dbConf, "account-tx-transactions.db", nullptr, 0);
|
||||
|
||||
account_.setAccountID("rfu6L5p3azwPzQZsbTafuVk884N9YoKvVG");
|
||||
|
||||
testAccountTxPaging();
|
||||
}
|
||||
|
||||
void
|
||||
checkToken (Json::Value const& token, int ledger, int sequence)
|
||||
{
|
||||
expect (token.isMember ("ledger"));
|
||||
expect (token["ledger"].asInt() == ledger);
|
||||
expect (token.isMember ("seq"));
|
||||
expect (token["seq"].asInt () == sequence);
|
||||
}
|
||||
|
||||
void
|
||||
checkTransaction (NetworkOPs::AccountTx const& tx, int ledger, int index)
|
||||
{
|
||||
expect (tx.second->getLgrSeq () == ledger);
|
||||
expect (tx.second->getIndex () == index);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
next (
|
||||
int limit,
|
||||
bool forward,
|
||||
Json::Value& token,
|
||||
std::int32_t minLedger,
|
||||
std::int32_t maxLedger)
|
||||
{
|
||||
txs_.clear();
|
||||
|
||||
std::int32_t const page_length = 200;
|
||||
bool const admin = true;
|
||||
|
||||
auto& txs = txs_;
|
||||
|
||||
auto bound = [&txs](
|
||||
std::uint32_t ledger_index,
|
||||
std::string const& status,
|
||||
Blob const& rawTxn,
|
||||
Blob const& rawMeta)
|
||||
{
|
||||
convertBlobsToTxResult (txs, ledger_index, status, rawTxn, rawMeta);
|
||||
};
|
||||
|
||||
accountTxPage(*db_, [](std::uint32_t){}, bound, account_, minLedger,
|
||||
maxLedger, forward, token, limit, admin, page_length);
|
||||
|
||||
return txs_.size();
|
||||
}
|
||||
|
||||
void
|
||||
testAccountTxPaging ()
|
||||
{
|
||||
using namespace std::placeholders;
|
||||
|
||||
bool const forward = true;
|
||||
|
||||
std::int32_t min_ledger;
|
||||
std::int32_t max_ledger;
|
||||
Json::Value token;
|
||||
int limit;
|
||||
|
||||
// the supplied account-tx-transactions.db contains contains
|
||||
// transactions with the following ledger/sequence pairs.
|
||||
// 3|5
|
||||
// 4|4
|
||||
// 4|10
|
||||
// 5|4
|
||||
// 5|7
|
||||
// 6|1
|
||||
// 6|5
|
||||
// 6|6
|
||||
// 6|7
|
||||
// 6|8
|
||||
// 6|9
|
||||
// 6|10
|
||||
// 6|11
|
||||
|
||||
min_ledger = 2;
|
||||
max_ledger = 5;
|
||||
|
||||
{
|
||||
limit = 2;
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 2);
|
||||
checkTransaction (txs_[0], 3, 5);
|
||||
checkTransaction (txs_[1], 4, 4);
|
||||
checkToken (token, 4, 10);
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 2);
|
||||
checkTransaction (txs_[0], 4, 10);
|
||||
checkTransaction (txs_[1], 5, 4);
|
||||
checkToken (token, 5, 7);
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 1);
|
||||
checkTransaction (txs_[0], 5, 7);
|
||||
|
||||
expect(token["ledger"].isNull());
|
||||
expect(token["seq"].isNull());
|
||||
}
|
||||
|
||||
token = Json::nullValue;
|
||||
|
||||
min_ledger = 3;
|
||||
max_ledger = 9;
|
||||
|
||||
{
|
||||
limit = 1;
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 1);
|
||||
checkTransaction (txs_[0], 3, 5);
|
||||
checkToken (token, 4, 4);
|
||||
|
||||
expect(next(limit, forward, token, min_ledger, max_ledger) == 1);
|
||||
checkTransaction (txs_[0], 4, 4);
|
||||
checkToken (token, 4, 10);
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 1);
|
||||
checkTransaction (txs_[0], 4, 10);
|
||||
checkToken (token, 5, 4);
|
||||
}
|
||||
|
||||
{
|
||||
limit = 3;
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 3);
|
||||
checkTransaction (txs_[0], 5, 4);
|
||||
checkTransaction (txs_[1], 5, 7);
|
||||
checkTransaction (txs_[2], 6, 1);
|
||||
checkToken (token, 6, 5);
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 3);
|
||||
checkTransaction (txs_[0], 6, 5);
|
||||
checkTransaction (txs_[1], 6, 6);
|
||||
checkTransaction (txs_[2], 6, 7);
|
||||
checkToken (token, 6, 8);
|
||||
|
||||
expect (next(limit, forward, token, min_ledger, max_ledger) == 3);
|
||||
checkTransaction (txs_[0], 6, 8);
|
||||
checkTransaction (txs_[1], 6, 9);
|
||||
checkTransaction (txs_[2], 6, 10);
|
||||
checkToken (token, 6, 11);
|
||||
|
||||
expect(next(limit, forward, token, min_ledger, max_ledger) == 1);
|
||||
checkTransaction (txs_[0], 6, 11);
|
||||
|
||||
expect(token["ledger"].isNull());
|
||||
expect(token["seq"].isNull());
|
||||
}
|
||||
|
||||
token = Json::nullValue;
|
||||
|
||||
{
|
||||
limit = 2;
|
||||
|
||||
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 2);
|
||||
checkTransaction (txs_[0], 6, 11);
|
||||
checkTransaction (txs_[1], 6, 10);
|
||||
checkToken (token, 6, 9);
|
||||
|
||||
expect(next(limit, ! forward, token, min_ledger, max_ledger) == 2);
|
||||
checkTransaction (txs_[0], 6, 9);
|
||||
checkTransaction (txs_[1], 6, 8);
|
||||
checkToken (token, 6, 7);
|
||||
}
|
||||
|
||||
{
|
||||
limit = 3;
|
||||
|
||||
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 3);
|
||||
checkTransaction (txs_[0], 6, 7);
|
||||
checkTransaction (txs_[1], 6, 6);
|
||||
checkTransaction (txs_[2], 6, 5);
|
||||
checkToken (token, 6, 1);
|
||||
|
||||
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 3);
|
||||
checkTransaction (txs_[0], 6, 1);
|
||||
checkTransaction (txs_[1], 5, 7);
|
||||
checkTransaction (txs_[2], 5, 4);
|
||||
checkToken (token, 4, 10);
|
||||
|
||||
expect (next(limit, ! forward, token, min_ledger, max_ledger) == 3);
|
||||
checkTransaction (txs_[0], 4, 10);
|
||||
checkTransaction (txs_[1], 4, 4);
|
||||
checkTransaction (txs_[2], 3, 5);
|
||||
}
|
||||
|
||||
expect (token["ledger"].isNull());
|
||||
expect (token["seq"].isNull());
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE_MANUAL(AccountTxPaging,app,ripple);
|
||||
|
||||
}
|
||||
@@ -107,6 +107,24 @@ void Transaction::setStatus (TransStatus ts, std::uint32_t lseq)
|
||||
mInLedger = lseq;
|
||||
}
|
||||
|
||||
TransStatus Transaction::sqlTransactionStatus(
|
||||
boost::optional<std::string> const& status)
|
||||
{
|
||||
char const c = (status) ? (*status)[0] : TXN_SQL_UNKNOWN;
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case TXN_SQL_NEW: return NEW;
|
||||
case TXN_SQL_CONFLICT: return CONFLICTED;
|
||||
case TXN_SQL_HELD: return HELD;
|
||||
case TXN_SQL_VALIDATED: return COMMITTED;
|
||||
case TXN_SQL_INCLUDED: return INCLUDED;
|
||||
}
|
||||
|
||||
assert (c == TXN_SQL_UNKNOWN);
|
||||
return INVALID;
|
||||
}
|
||||
|
||||
Transaction::pointer Transaction::transactionFromSQL (
|
||||
boost::optional<std::uint64_t> const& ledgerSeq,
|
||||
boost::optional<std::string> const& status,
|
||||
@@ -121,40 +139,7 @@ Transaction::pointer Transaction::transactionFromSQL (
|
||||
std::string reason;
|
||||
auto tr = std::make_shared<Transaction> (txn, validate, reason);
|
||||
|
||||
TransStatus st (INVALID);
|
||||
|
||||
char const statusChar = status ? (*status)[0] : TXN_SQL_UNKNOWN;
|
||||
|
||||
switch (statusChar)
|
||||
{
|
||||
case TXN_SQL_NEW:
|
||||
st = NEW;
|
||||
break;
|
||||
|
||||
case TXN_SQL_CONFLICT:
|
||||
st = CONFLICTED;
|
||||
break;
|
||||
|
||||
case TXN_SQL_HELD:
|
||||
st = HELD;
|
||||
break;
|
||||
|
||||
case TXN_SQL_VALIDATED:
|
||||
st = COMMITTED;
|
||||
break;
|
||||
|
||||
case TXN_SQL_INCLUDED:
|
||||
st = INCLUDED;
|
||||
break;
|
||||
|
||||
case TXN_SQL_UNKNOWN:
|
||||
break;
|
||||
|
||||
default:
|
||||
assert (false);
|
||||
}
|
||||
|
||||
tr->setStatus (st);
|
||||
tr->setStatus (sqlTransactionStatus (status));
|
||||
tr->setLedger (inLedger);
|
||||
return tr;
|
||||
}
|
||||
|
||||
@@ -64,13 +64,22 @@ public:
|
||||
public:
|
||||
Transaction (STTx::ref, Validate, std::string&) noexcept;
|
||||
|
||||
static Transaction::pointer sharedTransaction (Blob const&, Validate);
|
||||
static Transaction::pointer transactionFromSQL (
|
||||
static
|
||||
Transaction::pointer
|
||||
sharedTransaction (Blob const&, Validate);
|
||||
|
||||
static
|
||||
Transaction::pointer
|
||||
transactionFromSQL (
|
||||
boost::optional<std::uint64_t> const& ledgerSeq,
|
||||
boost::optional<std::string> const& status,
|
||||
Blob const& rawTxn,
|
||||
Validate validate);
|
||||
|
||||
static
|
||||
TransStatus
|
||||
sqlTransactionStatus(boost::optional<std::string> const& status);
|
||||
|
||||
bool checkSign (std::string&) const;
|
||||
|
||||
STTx::ref getSTransaction ()
|
||||
|
||||
@@ -25,3 +25,5 @@
|
||||
#include <ripple/app/tx/LocalTxs.cpp>
|
||||
#include <ripple/app/tx/InboundTransactions.cpp>
|
||||
#include <ripple/app/misc/NetworkOPs.cpp>
|
||||
#include <ripple/app/misc/impl/AccountTxPaging.cpp>
|
||||
#include <ripple/app/misc/tests/AccountTxPaging.test.cpp>
|
||||
|
||||
BIN
test/fixtures/account-tx-transactions.db
vendored
Normal file
BIN
test/fixtures/account-tx-transactions.db
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user