Compare commits

...

2 Commits

Author SHA1 Message Date
Richard Holland
256f84b4de first partial draft of featureEmail, not finished 2024-08-12 14:23:50 +10:00
Denis Angell
18d76d3082 fix delivered amount (#310)
* fix delivered amount
2024-07-16 08:56:30 +10:00
10 changed files with 647 additions and 128 deletions

493
src/ripple/protocol/Email.h Normal file
View File

@@ -0,0 +1,493 @@
#include <iostream>
#include <string>
#include <vector>
#include <regex>
#include <sstream>
#include <stdexcept>
// make this typedef to keep dkim happy
typedef int _Bool;
#include <opendkim/dkim.h>
using namespace ripple;
namespace Email
{
enum EmailType : uint8_t
{
INVALID = 0,
REMIT = 1,
REKEY = 2
};
struct EmailDetails
{
std::string domain; // from address domain
std::string dkimDomain; // dkim signature domain
AccountID from;
std::string fromEmail;
std::optional<std::string> toEmail;
std::optional<AccountID> to;
EmailType emailType { EmailType::INVALID };
std::optional<STAmount> amount; // only valid if REMIT type
std::optional<AccountID> rekey; // only valid if REKEY type
};
class OpenDKIM
{
private:
DKIM_STAT status;
public:
DKIM_LIB* dkim_lib;
DKIM* dkim;
bool sane()
{
return !!dkim_lib && !!dkim;
}
OpenDKIM()
{
// do nothing
}
// setup is in its own function not the constructor to make failure graceful
bool setup(beast::Journal& j)
{
dkim_lib = dkim_init(nullptr, nullptr);
if (!dkim_lib)
{
JLOG(j.warn()) << "EmailAmendment: Failed to init dkim_lib.";
return false;
}
DKIM_STAT status;
DKIM* dkim = dkim_verify(dkim_lib, (uint8_t const*)"id", nullptr, &status);
if (!dkim_lib)
{
JLOG(j.warn()) << "EmailAmendment: Failed to init dkim_verify.";
return false;
}
return true;
}
~OpenDKIM()
{
if (dkim)
{
dkim_free(dkim);
dkim = nullptr;
}
if (dkim_lib)
{
dkim_close(dkim_lib);
dkim_lib = nullptr;
}
}
};
inline
std::optional<std::pair<std::string /* canonical email addr */, std::string /* canonical domain */>>
canonicalizeEmailAddress(const std::string& rawEmailAddr)
{
if (rawEmailAddr.empty())
return {};
// trim
auto start = std::find_if_not(str.begin(), str.end(), ::isspace);
auto end = std::find_if_not(str.rbegin(), str.rend(), ::isspace).base();
if (end >= start)
return {};
std::email = std::string(start, end);
if (email.empty())
return {};
// to lower
std::transform(email.begin(), email.end(), email.begin(), ::tolower);
// find the @
size_t atPos = email.find('@');
if (atPos == std::string::npos || atPos == email.size() - 1)
return {};
std::string localPart = email.substr(0, atPos);
std::string domain = email.substr(atPos + 1);
if (domain.empty() || localPart.empty())
return {};
// ensure there's only one @
if (domain.find('@') != std::string::npos)
return {};
// canonicalize domain part
{
std::string result = domain;
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
while (!result.empty() && result.back() == '.')
result.pop_back();
doamin = result;
}
if (domain.empty())
return {};
// canonicalize local part
{
std::string part = localPart;
part.erase(std::remove_if(
part.begin(), part.end(),
[](char c) { return c == '(' || c == ')' || std::isspace(c); }), part.end());
size_t plusPos = part.find('+');
if (plusPos != std::string::npos)
part = part.substr(0, plusPos);
while (!part.empty() && part.back() == '.')
part.pop_back();
// gmail ignores dots
if (domain == "gmail.com")
part.erase(std::remove(part.begin(), part.end(), '.'), part.end());
localPart = part;
}
if (localPart.empty())
return {};
return {{localPart + "@" + domain, domain}};
};
// Warning: must supply already canonicalzied email
inline
std::optional<AccountID>
emailToAccountID(const std::string& canonicalEmail)
{
uint8_t innerHash[SHA512_DIGEST_LENGTH + 4];
SHA512_CTX sha512;
SHA512_Init(&sha512);
SHA512_Update(&sha512, canonicalEmail.c_str(), canonicalEmail.size());
SHA512_Final(innerHash + 4, &sha512);
innerHash[0] = 0xEEU;
innerHash[1] = 0xEEU;
innerHash[2] = 0xFFU;
innerHash[3] = 0xFFU;
{
uint8_t hash[SHA512_DIGEST_LENGTH];
SHA512_CTX sha512;
SHA512_Init(&sha512);
SHA512_Update(&sha512, innerHash, sizeof(innerHash));
SHA512_Final(hash, &sha512);
return AccountID::fromVoid((void*)hash);
}
}
inline
std::optional<EmailDetails>
parseEmail(std::string const& rawEmail, beast::Journal& j)
{
EmailDetails out;
// parse email into headers and body
std::vector<std::string> headers;
std::string body;
{
std::istringstream stream(rawEmail);
std::string line;
while (std::getline(stream, line))
{
if (line.empty() || line == "\r")
break;
// Handle header line continuations
while (stream.peek() == ' ' || stream.peek() == '\t') {
std::string continuation;
std::getline(stream, continuation);
line += '\n' + continuation;
}
if (!line.empty()) {
headers.push_back(line.substr(0, line.size() - (line.back() == '\r' ? 1 : 0)));
}
}
std::ostringstream body_stream;
while (std::getline(stream, line))
body_stream << line << "\n";
body = body_stream.str();
}
// find the from address, canonicalize it and extract the domain
bool foundFrom = false;
bool foundTo = false;
{
static const std::regex
from_regex(R"(^From:\s*(?:.*<)?([^<>\s]+@[^<>\s]+)(?:>)?)", std::regex::icase);
static const std::regex
to_regex(R"(^To:\s*(?:.*<)?([^<>\s]+@[^<>\s]+)(?:>)?)", std::regex::icase);
for (const auto& header : headers)
{
if (foundFrom && foundTo)
break;
std::smatch match;
if (!foundFrom && std::regex_search(header, match, from_regex) && match.size() > 1)
{
auto canon = canonicalizeEmailAddress(match[1].str());
if (!canon)
{
JLOG(j.warn())
<< "EmailAmendment: Cannot parse From address: `"
<< match[1].str() << "`";
return {};
}
out.fromEmail = canon->first;
out.domain = canon->second;
out.from = emailToAccountID(out.fromEmail);
foundFrom = true;
continue;
}
if (std::regex_search(header, match, to_regex) && match.size() > 1)
{
auto canon = canonicalizeEmailAddress(match[1].str());
if (!canon)
{
JLOG(j.warn())
<< "EmailAmendment: Cannot parse To address: `"
<< match[1].str() << "`";
return {};
}
out.toEmail = canon->first;
out.to = emailToAccountID(out.toEmail);
foundTo = true;
continue;
}
}
if (!foundFrom)
{
JLOG(j.warn()) << "EmailAmendment: No From address present in email.";
return {};
}
}
// execution to here means we have:
// 1. Parsed headers and body
// 2. Found a from address and canonicalzied it
// 3. Potentially found a to address and canonicalized it.
// Find instructions
{
static const std::regex
remitPattern(R"(^REMIT (\d+(?:\.\d+)?) ([A-Z]{3})(?:/([r][a-zA-Z0-9]{24,34}))?)");
static const std::regex
rekeyPattern(R"(^REKEY ([r][a-zA-Z0-9]{24,34}))");
std::istringstream stream(body);
std::string line;
out.emailType = EmailType::INVALID;
while (std::getline(stream, line, '\n'))
{
if (!line.empty() && line.back() == '\r')
line.pop_back(); // Remove '\r' if present
std::smatch match;
if (std::regex_match(line, match, remitPattern))
{
try
{
Currency cur;
if (!to_currency(cur, match[2]))
{
JLOG(j.warn()) << "EmailAmendment: Could not parse currency code.";
return {};
}
AccountID issuer = noAccount();
if (match[3].matched)
{
if (isXRP(cur))
{
JLOG(j.warn()) << "EmailAmendment: Native currency cannot specify issuer.";
return {};
}
issuer = decodeBase58Token(match[3], TokenType::AccountID);
if (issuer.empty())
{
JLOG(j.warn()) << "EmailAmendment: Could not parse issuer address.";
return {};
}
}
out.amount = amountFromString({cur, issuer}, match[1]);
}
catch (std::exception const& e)
{
JLOG(j.warn()) << "EmailAmendment: Exception while parsing REMIT. " << e.what();
return {};
}
out.emailType = EmailType::REMIT;
break;
}
if (std::regex_match(line, match, rekeyPattern))
{
AccountID rekey = decodeBase58Token(match[1], TokenType::AccountID);
if (rekey.empty())
{
JLOG(j.warn()) << "EmailAmendment: Could not parse rekey address.";
return {};
}
out.rekey = rekey;
out.emailType = EmailType::REKEY;
break;
}
}
if (out.emailType == EmailType::INVALID)
{
JLOG(j.warn()) << "EmailAmendment: Invalid email type, could not find REMIT or REKEY.";
return{};
}
}
// perform DKIM checks...
// to do this we will use OpenDKIM, and manage it with a smart pointer to prevent
// any leaks from uncommon exit pathways
std::unique<OpenDKIM> odkim;
// perform setup
if (!odkim->setup(j) || !odkim->sane())
return {};
// when odkim goes out of scope it will call the C-apis to destroy the dkim instances
DKIM_STAT status;
DKIM_LIB* dkim_lib = odkim->dkim_lib;
DKIM* dkim = odkim->dkim;
// feed opendkim all headers
{
for (const auto& header : headers)
{
status = dkim_header(dkim, (uint8_t*)header.c_str(), header.length());
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM Failed to process header: "
<< dkim_geterror(dkim);
return {};
}
}
status = dkim_eoh(dkim);
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM Failed to send end-of-headers"l
return {};
}
}
// feed opendkim email body
{
status = dkim_body(dkim, (uint8_t*)body.c_str(), body.size());
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM Failed to process body: "
<< dkim_geterror(dkim);
return {};
}
_Bool testkey;
status = dkim_eom(dkim, &testkey);
if (status != DKIM_STAT_OK)
{
JLOG(j.warn())
<< "EmailAmendment: OpenDKIM end-of-message error: "
<< dkim_geterror(dkim);
return {};
}
DKIM_SIGINFO* sig = dkim_getsignature(dkim);
if (!sig)
{
JLOG(j.warn())
<< "EmailAmendment: No DKIM signature found";
return {};
}
if (dkim_sig_getbh(sig) != DKIM_SIGBH_MATCH)
{
JLOG(j.warn())
<< "EmailAmendment: DKIM body hash mismatch";
return {};
}
DKIM_SIGINFO* sig = dkim_getsignature(dkim);
if (!sig)
{
JLOG(j.warn())
<< "EmailAmendment: DKIM signature not found.";
return {};
}
out.dkimDomain =
std::string(reinterpret_cast<char const*>(
reinterpret_cast<void const*>(dkim_sig_getdomain(sig))));
if (out.dkimDomain.empty())
{
JLOG(j.warn())
<< "EmailAmendment: DKIM signature domain empty.";
return {};
}
// RH TODO: decide whether to relax this or not
// strict domain check
if (out.dkimDomain != out.domain)
{
JLOG(j.warn())
<< "EmailAmendment: DKIM domain does not match From address domain.";
return {};
}
}
// execution to here means all checks passed and the instruction was correctly parsed
return out;
}
}

View File

@@ -74,7 +74,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how // Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than // large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this. // the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 70; static constexpr std::size_t numFeatures = 71;
/** Amendments that this server supports and the default voting behavior. /** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated Whether they are enabled depends on the Rules defined in the validated
@@ -358,6 +358,7 @@ extern uint256 const fixXahauV2;
extern uint256 const featureRemit; extern uint256 const featureRemit;
extern uint256 const featureZeroB2M; extern uint256 const featureZeroB2M;
extern uint256 const fixNSDelete; extern uint256 const fixNSDelete;
extern uint256 const featureEmail;
} // namespace ripple } // namespace ripple

View File

@@ -30,6 +30,7 @@
#include <ripple/protocol/TxFormats.h> #include <ripple/protocol/TxFormats.h>
#include <boost/container/flat_set.hpp> #include <boost/container/flat_set.hpp>
#include <functional> #include <functional>
#include <ripple/protocol/Email.h>
namespace ripple { namespace ripple {

View File

@@ -57,8 +57,9 @@ namespace ripple {
// Universal Transaction flags: // Universal Transaction flags:
enum UniversalFlags : uint32_t { enum UniversalFlags : uint32_t {
tfFullyCanonicalSig = 0x80000000, tfFullyCanonicalSig = 0x80000000,
tfEmailSig = 0x40000000,
}; };
constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig; constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfEmailSig;
constexpr std::uint32_t tfUniversalMask = ~tfUniversal; constexpr std::uint32_t tfUniversalMask = ~tfUniversal;
// AccountSet flags: // AccountSet flags:

View File

@@ -464,6 +464,7 @@ REGISTER_FIX (fixXahauV2, Supported::yes, VoteBehavior::De
REGISTER_FEATURE(Remit, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(Remit, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(ZeroB2M, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FEATURE(ZeroB2M, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FIX (fixNSDelete, Supported::yes, VoteBehavior::DefaultNo); REGISTER_FIX (fixNSDelete, Supported::yes, VoteBehavior::DefaultNo);
REGISTER_FEATURE(Email, Supported::yes, VoteBehavior::DefaultNo);
// The following amendments are obsolete, but must remain supported // The following amendments are obsolete, but must remain supported
// because they could potentially get enabled. // because they could potentially get enabled.

View File

@@ -304,9 +304,60 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
bool const isWildcardNetwork = bool const isWildcardNetwork =
isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535; isFieldPresent(sfNetworkID) && getFieldU32(sfNetworkID) == 65535;
// email signature flag signals that the txn is authorized
// only by the presence of a DKIM signed email in memos[0]
bool const isEmailSig =
getFlags() & tfEmailSig;
bool validSig = false; bool validSig = false;
do
try try
{ {
if (isEmailSig)
{
if (!isFieldPresent(sfMemos))
break;
auto const& memos = st.getFieldArray(sfMemos);
auto const& memo = memos[0];
auto memoObj = dynamic_cast<STObject const*>(&memo);
if (!memoObj || (memoObj->getFName() != sfMemo))
break;
bool emailValid = false;
for (auto const& memoElement : *memoObj)
{
auto const& name = memoElement.getFName();
if (name != sfMemoType && name != sfMemoData &&
name != sfMemoFormat)
break;
// The raw data is stored as hex-octets, which we want to decode.
std::optional<Blob> optData = strUnHex(memoElement.getText());
if (!optData)
break;
if (name != sfMemoData)
continue;
std::string const emailContent((char const*)(optData->data()), optData->size());
// RH UPTO
}
}
}
bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) ||
(requireCanonicalSig == RequireFullyCanonicalSig::yes); (requireCanonicalSig == RequireFullyCanonicalSig::yes);
@@ -328,7 +379,8 @@ STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const
{ {
// Assume it was a signature failure. // Assume it was a signature failure.
validSig = false; validSig = false;
} } while (0);
if (validSig == false) if (validSig == false)
return Unexpected("Invalid signature."); return Unexpected("Invalid signature.");
// Signature was verified. // Signature was verified.

View File

@@ -61,22 +61,7 @@ getDeliveredAmount(
if (serializedTx->isFieldPresent(sfAmount)) if (serializedTx->isFieldPresent(sfAmount))
{ {
using namespace std::chrono_literals; return serializedTx->getFieldAmount(sfAmount);
// Ledger 4594095 is the first ledger in which the DeliveredAmount field
// was present when a partial payment was made and its absence indicates
// that the amount delivered is listed in the Amount field.
//
// If the ledger closed long after the DeliveredAmount code was deployed
// then its absence indicates that the amount delivered is listed in the
// Amount field. DeliveredAmount went live January 24, 2014.
// 446000000 is in Feb 2014, well after DeliveredAmount went live
if (getLedgerIndex() >= 4594095 ||
getCloseTime() > NetClock::time_point{446000000s} ||
(serializedTx && serializedTx->isFieldPresent(sfNetworkID)))
{
return serializedTx->getFieldAmount(sfAmount);
}
} }
return {}; return {};

View File

@@ -5466,6 +5466,7 @@ private:
params[jss::transaction] = txIds[i]; params[jss::transaction] = txIds[i];
auto const jrr = env.rpc("json", "tx", to_string(params)); auto const jrr = env.rpc("json", "tx", to_string(params));
auto const meta = jrr[jss::result][jss::meta]; auto const meta = jrr[jss::result][jss::meta];
BEAST_EXPECT(meta[jss::delivered_amount] == "1000000");
for (auto const& node : meta[sfAffectedNodes.jsonName]) for (auto const& node : meta[sfAffectedNodes.jsonName])
{ {
auto const nodeType = node[sfLedgerEntryType.jsonName]; auto const nodeType = node[sfLedgerEntryType.jsonName];

View File

@@ -191,80 +191,73 @@ class DeliveredAmount_test : public beast::unit_test::suite
auto const gw = Account("gateway"); auto const gw = Account("gateway");
auto const USD = gw["USD"]; auto const USD = gw["USD"];
for (bool const afterSwitchTime : {true, false}) Env env{*this, features};
env.fund(XRP(10000), alice, bob, carol, gw);
env.trust(USD(1000), alice, bob, carol);
env.close();
CheckDeliveredAmount checkDeliveredAmount{true};
{ {
Env env{*this, features}; // add payments, but do no close until subscribed
env.fund(XRP(10000), alice, bob, carol, gw);
env.trust(USD(1000), alice, bob, carol);
if (afterSwitchTime)
env.close(NetClock::time_point{446000000s});
else
env.close();
CheckDeliveredAmount checkDeliveredAmount{afterSwitchTime}; // normal payments
{ env(pay(gw, alice, USD(50)));
// add payments, but do no close until subscribed checkDeliveredAmount.adjCountersSuccess();
env(pay(gw, alice, XRP(50)));
checkDeliveredAmount.adjCountersSuccess();
// normal payments // partial payment
env(pay(gw, alice, USD(50))); env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment));
checkDeliveredAmount.adjCountersSuccess(); checkDeliveredAmount.adjCountersPartialPayment();
env(pay(gw, alice, XRP(50))); env.require(balance(bob, USD(1000)));
checkDeliveredAmount.adjCountersSuccess();
// partial payment // failed payment
env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment)); env(pay(bob, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersPartialPayment(); checkDeliveredAmount.adjCountersFail();
env.require(balance(bob, USD(1000))); env.require(balance(carol, USD(0)));
// failed payment
env(pay(bob, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersFail();
env.require(balance(carol, USD(0)));
}
auto wsc = makeWSClient(env.app().config());
{
Json::Value stream;
// RPC subscribe to ledger stream
stream[jss::streams] = Json::arrayValue;
stream[jss::streams].append("ledger");
stream[jss::accounts] = Json::arrayValue;
stream[jss::accounts].append(toBase58(alice.id()));
stream[jss::accounts].append(toBase58(bob.id()));
stream[jss::accounts].append(toBase58(carol.id()));
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(
jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(
jv.isMember(jss::ripplerpc) &&
jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
}
{
env.close();
// Check stream update
while (true)
{
auto const r = wsc->findMsg(1s, [&](auto const& jv) {
return jv[jss::ledger_index] == 4;
});
if (!r)
break;
if (!r->isMember(jss::transaction))
continue;
BEAST_EXPECT(checkDeliveredAmount.checkTxn(
(*r)[jss::transaction], (*r)[jss::meta]));
}
}
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
} }
auto wsc = makeWSClient(env.app().config());
{
Json::Value stream;
// RPC subscribe to ledger stream
stream[jss::streams] = Json::arrayValue;
stream[jss::streams].append("ledger");
stream[jss::accounts] = Json::arrayValue;
stream[jss::accounts].append(toBase58(alice.id()));
stream[jss::accounts].append(toBase58(bob.id()));
stream[jss::accounts].append(toBase58(carol.id()));
auto jv = wsc->invoke("subscribe", stream);
if (wsc->version() == 2)
{
BEAST_EXPECT(
jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0");
BEAST_EXPECT(
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0");
BEAST_EXPECT(jv.isMember(jss::id) && jv[jss::id] == 5);
}
BEAST_EXPECT(jv[jss::result][jss::ledger_index] == 3);
}
{
env.close();
// Check stream update
while (true)
{
auto const r = wsc->findMsg(1s, [&](auto const& jv) {
return jv[jss::ledger_index] == 4;
});
if (!r)
break;
if (!r->isMember(jss::transaction))
continue;
BEAST_EXPECT(checkDeliveredAmount.checkTxn(
(*r)[jss::transaction], (*r)[jss::meta]));
}
}
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
} }
void void
testTxDeliveredAmountRPC(FeatureBitset features) testTxDeliveredAmountRPC(FeatureBitset features)
@@ -280,49 +273,41 @@ class DeliveredAmount_test : public beast::unit_test::suite
auto const gw = Account("gateway"); auto const gw = Account("gateway");
auto const USD = gw["USD"]; auto const USD = gw["USD"];
for (bool const afterSwitchTime : {true, false}) Env env{*this, features};
{ env.fund(XRP(10000), alice, bob, carol, gw);
Env env{*this, features}; env.trust(USD(1000), alice, bob, carol);
env.fund(XRP(10000), alice, bob, carol, gw); env.close();
env.trust(USD(1000), alice, bob, carol);
if (afterSwitchTime)
env.close(NetClock::time_point{446000000s});
else
env.close();
CheckDeliveredAmount checkDeliveredAmount{afterSwitchTime}; CheckDeliveredAmount checkDeliveredAmount{true};
// normal payments // normal payments
env(pay(gw, alice, USD(50))); env(pay(gw, alice, USD(50)));
checkDeliveredAmount.adjCountersSuccess(); checkDeliveredAmount.adjCountersSuccess();
env(pay(gw, alice, XRP(50))); env(pay(gw, alice, XRP(50)));
checkDeliveredAmount.adjCountersSuccess(); checkDeliveredAmount.adjCountersSuccess();
// partial payment // partial payment
env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment)); env(pay(gw, bob, USD(9999999)), txflags(tfPartialPayment));
checkDeliveredAmount.adjCountersPartialPayment(); checkDeliveredAmount.adjCountersPartialPayment();
env.require(balance(bob, USD(1000))); env.require(balance(bob, USD(1000)));
// failed payment // failed payment
env(pay(gw, carol, USD(9999999)), ter(tecPATH_PARTIAL)); env(pay(gw, carol, USD(9999999)), ter(tecPATH_PARTIAL));
checkDeliveredAmount.adjCountersFail(); checkDeliveredAmount.adjCountersFail();
env.require(balance(carol, USD(0))); env.require(balance(carol, USD(0)));
env.close(); env.close();
std::string index; std::string index;
Json::Value jvParams; Json::Value jvParams;
jvParams[jss::ledger_index] = 4u; jvParams[jss::ledger_index] = 4u;
jvParams[jss::transactions] = true; jvParams[jss::transactions] = true;
jvParams[jss::expand] = true; jvParams[jss::expand] = true;
auto const jtxn = env.rpc( auto const jtxn = env.rpc(
"json", "json",
"ledger", "ledger",
to_string( to_string(jvParams))[jss::result][jss::ledger][jss::transactions];
jvParams))[jss::result][jss::ledger][jss::transactions]; for (auto const& t : jtxn)
for (auto const& t : jtxn) BEAST_EXPECT(checkDeliveredAmount.checkTxn(t, t[jss::metaData]));
BEAST_EXPECT( BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
checkDeliveredAmount.checkTxn(t, t[jss::metaData]));
BEAST_EXPECT(checkDeliveredAmount.checkExpectedCounters());
}
} }
public: public:

View File

@@ -57,7 +57,6 @@ public:
{ {
Env env(*this); Env env(*this);
auto const result = env.rpc("server_definitions"); auto const result = env.rpc("server_definitions");
std::cout << "RESULT: " << result << "\n";
BEAST_EXPECT(!result[jss::result].isMember(jss::error)); BEAST_EXPECT(!result[jss::result].isMember(jss::error));
BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS)); BEAST_EXPECT(result[jss::result].isMember(jss::FIELDS));
BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES)); BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_TYPES));