#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { namespace test { namespace jtx { //------------------------------------------------------------------------------ Env::AppBundle::AppBundle( beast::unit_test::suite& suite, std::unique_ptr config, std::unique_ptr logs, beast::severities::Severity thresh) : AppBundle() { using namespace beast::severities; if (logs) { setDebugLogSink(logs->makeSink("Debug", kFatal)); } else { logs = std::make_unique(suite); // Use kFatal threshold to reduce noise from STObject. setDebugLogSink(std::make_unique("Debug", kFatal, suite)); } auto timeKeeper_ = std::make_unique(); timeKeeper = timeKeeper_.get(); // Hack so we don't have to call Config::setup HTTPClient::initializeSSLContext( config->SSL_VERIFY_DIR, config->SSL_VERIFY_FILE, config->SSL_VERIFY, debugLog()); owned = make_Application(std::move(config), std::move(logs), std::move(timeKeeper_)); app = owned.get(); app->logs().threshold(thresh); if (!app->setup({})) Throw("Env::AppBundle: setup failed"); timeKeeper->set(app->getLedgerMaster().getClosedLedger()->header().closeTime); app->start(false /*don't start timers*/); thread = std::thread([&]() { app->run(); }); client = makeJSONRPCClient(app->config()); } Env::AppBundle::~AppBundle() { client.reset(); // Make sure all jobs finish, otherwise tests // might not get the coverage they expect. if (app) { app->getJobQueue().rendezvous(); app->signalStop("~AppBundle"); } if (thread.joinable()) thread.join(); // Remove the debugLogSink before the suite goes out of scope. setDebugLogSink(nullptr); } //------------------------------------------------------------------------------ std::shared_ptr Env::closed() { return app().getLedgerMaster().getClosedLedger(); } bool Env::close(NetClock::time_point closeTime, std::optional consensusDelay) { // Round up to next distinguishable value using namespace std::chrono_literals; bool res = true; closeTime += closed()->header().closeTimeResolution - 1s; timeKeeper().set(closeTime); // Go through the rpc interface unless we need to simulate // a specific consensus delay. if (consensusDelay) app().getOPs().acceptLedger(consensusDelay); else { auto resp = rpc("ledger_accept"); if (resp["result"]["status"] != std::string("success")) { std::string reason = "internal error"; if (resp.isMember("error_what")) reason = resp["error_what"].asString(); else if (resp.isMember("error_message")) reason = resp["error_message"].asString(); else if (resp.isMember("error")) reason = resp["error"].asString(); JLOG(journal.error()) << "Env::close() failed: " << reason; res = false; } } timeKeeper().set(closed()->header().closeTime); return res; } void Env::memoize(Account const& account) { map_.emplace(account.id(), account); } Account const& Env::lookup(AccountID const& id) const { auto const iter = map_.find(id); if (iter == map_.end()) { std::cout << "Unknown account: " << id << "\n"; Throw("Env::lookup:: unknown account ID"); } return iter->second; } Account const& Env::lookup(std::string const& base58ID) const { auto const account = parseBase58(base58ID); if (!account) Throw("Env::lookup: invalid account ID"); return lookup(*account); } PrettyAmount Env::balance(Account const& account) const { auto const sle = le(account); if (!sle) return XRP(0); return {sle->getFieldAmount(sfBalance), ""}; } PrettyAmount Env::balance(Account const& account, Issue const& issue) const { if (isXRP(issue.currency)) return balance(account); auto const sle = le(keylet::line(account.id(), issue)); if (!sle) return {STAmount(issue, 0), account.name()}; auto amount = sle->getFieldAmount(sfBalance); amount.setIssuer(issue.account); if (account.id() > issue.account) amount.negate(); return {amount, lookup(issue.account).name()}; } PrettyAmount Env::balance(Account const& account, MPTIssue const& mptIssue) const { MPTID const id = mptIssue.getMptID(); if (!id) return {STAmount(mptIssue, 0), account.name()}; AccountID const issuer = mptIssue.getIssuer(); if (account.id() == issuer) { // Issuer balance auto const sle = le(keylet::mptIssuance(id)); if (!sle) return {STAmount(mptIssue, 0), account.name()}; // Make it negative STAmount const amount{mptIssue, sle->getFieldU64(sfOutstandingAmount), 0, true}; return {amount, lookup(issuer).name()}; } else { // Holder balance auto const sle = le(keylet::mptoken(id, account)); if (!sle) return {STAmount(mptIssue, 0), account.name()}; STAmount const amount{mptIssue, sle->getFieldU64(sfMPTAmount)}; return {amount, lookup(issuer).name()}; } } PrettyAmount Env::balance(Account const& account, Asset const& asset) const { return std::visit([&](auto const& issue) { return balance(account, issue); }, asset.value()); } PrettyAmount Env::limit(Account const& account, Issue const& issue) const { auto const sle = le(keylet::line(account.id(), issue)); if (!sle) return {STAmount(issue, 0), account.name()}; auto const aHigh = account.id() > issue.account; if (sle && sle->isFieldPresent(aHigh ? sfLowLimit : sfHighLimit)) return {(*sle)[aHigh ? sfLowLimit : sfHighLimit], account.name()}; return {STAmount(issue, 0), account.name()}; } std::uint32_t Env::ownerCount(Account const& account) const { auto const sle = le(account); if (!sle) Throw("missing account root"); return sle->getFieldU32(sfOwnerCount); } std::uint32_t Env::seq(Account const& account) const { auto const sle = le(account); if (!sle) Throw("missing account root"); return sle->getFieldU32(sfSequence); } std::shared_ptr Env::le(Account const& account) const { return le(keylet::account(account.id())); } std::shared_ptr Env::le(Keylet const& k) const { return current()->read(k); } void Env::fund(bool setDefaultRipple, STAmount const& amount, Account const& account) { memoize(account); if (setDefaultRipple) { // VFALCO NOTE Is the fee formula correct? apply( pay(master, account, amount + drops(current()->fees().base)), jtx::seq(jtx::autofill), fee(jtx::autofill), sig(jtx::autofill)); apply( fset(account, asfDefaultRipple), jtx::seq(jtx::autofill), fee(jtx::autofill), sig(jtx::autofill)); require(flags(account, asfDefaultRipple)); } else { apply( pay(master, account, amount), jtx::seq(jtx::autofill), fee(jtx::autofill), sig(jtx::autofill)); require(nflags(account, asfDefaultRipple)); } require(jtx::balance(account, amount)); } void Env::trust(STAmount const& amount, Account const& account) { auto const start = balance(account); apply( jtx::trust(account, amount), jtx::seq(jtx::autofill), fee(jtx::autofill), sig(jtx::autofill)); apply( pay(master, account, drops(current()->fees().base)), jtx::seq(jtx::autofill), fee(jtx::autofill), sig(jtx::autofill)); test.expect(balance(account) == start); } Env::ParsedResult Env::parseResult(Json::Value const& jr) { auto error = [](ParsedResult& parsed, Json::Value const& object) { // Use an error code that is not used anywhere in the transaction // engine to distinguish this case. parsed.ter = telENV_RPC_FAILED; // Extract information about the error if (!object.isObject()) return; if (object.isMember(jss::error_code)) parsed.rpcCode = safe_cast(object[jss::error_code].asInt()); if (object.isMember(jss::error_message)) parsed.rpcMessage = object[jss::error_message].asString(); if (object.isMember(jss::error)) parsed.rpcError = object[jss::error].asString(); if (object.isMember(jss::error_exception)) parsed.rpcException = object[jss::error_exception].asString(); }; ParsedResult parsed; if (jr.isObject() && jr.isMember(jss::result)) { auto const& result = jr[jss::result]; if (result.isMember(jss::engine_result_code)) { parsed.ter = TER::fromInt(result[jss::engine_result_code].asInt()); parsed.rpcCode.emplace(rpcSUCCESS); } else error(parsed, result); } else error(parsed, jr); return parsed; } void Env::submit(JTx const& jt, std::source_location const& loc) { ParsedResult parsedResult; auto const jr = [&]() { if (jt.stx) { txid_ = jt.stx->getTransactionID(); Serializer s; jt.stx->add(s); auto const jr = rpc("submit", strHex(s.slice())); parsedResult = parseResult(jr); test.expect(parsedResult.ter, "ter uninitialized!"); ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED); return jr; } else { // Parsing failed or the JTx is // otherwise missing the stx field. parsedResult.ter = ter_ = temMALFORMED; return Json::Value(); } }(); return postconditions(jt, parsedResult, jr, loc); } void Env::sign_and_submit(JTx const& jt, Json::Value params, std::source_location const& loc) { auto const account = lookup(jt.jv[jss::Account].asString()); auto const& passphrase = account.name(); Json::Value jr; if (params.isNull()) { // Use the command line interface auto const jv = to_string(jt.jv); jr = rpc("submit", passphrase, jv); } else { // Use the provided parameters, and go straight // to the (RPC) client. assert(params.isObject()); if (!params.isMember(jss::secret) && !params.isMember(jss::key_type) && !params.isMember(jss::seed) && !params.isMember(jss::seed_hex) && !params.isMember(jss::passphrase)) { params[jss::secret] = passphrase; } params[jss::tx_json] = jt.jv; jr = client().invoke("submit", params); } if (!txid_.parseHex(jr[jss::result][jss::tx_json][jss::hash].asString())) txid_.zero(); ParsedResult const parsedResult = parseResult(jr); test.expect(parsedResult.ter, "ter uninitialized!"); ter_ = parsedResult.ter.value_or(telENV_RPC_FAILED); return postconditions(jt, parsedResult, jr, loc); } void Env::postconditions( JTx const& jt, ParsedResult const& parsed, Json::Value const& jr, std::source_location const& loc) { auto const line = jt.testLine ? " (" + to_string(*jt.testLine) + ")" : ""; auto const locStr = std::string("(") + loc.file_name() + ":" + to_string(loc.line()) + ")"; bool bad = !test.expect(parsed.ter, "apply " + locStr + ": No ter result!" + line); bad = (jt.ter && parsed.ter && !test.expect( *parsed.ter == *jt.ter, "apply " + locStr + ": Got " + transToken(*parsed.ter) + " (" + transHuman(*parsed.ter) + "); Expected " + transToken(*jt.ter) + " (" + transHuman(*jt.ter) + ")" + line)); using namespace std::string_literals; bad = (jt.rpcCode && !test.expect( parsed.rpcCode == jt.rpcCode->first && parsed.rpcMessage == jt.rpcCode->second, "apply " + locStr + ": Got RPC result "s + (parsed.rpcCode ? RPC::get_error_info(*parsed.rpcCode).token.c_str() : "NO RESULT") + " (" + parsed.rpcMessage + "); Expected " + RPC::get_error_info(jt.rpcCode->first).token.c_str() + " (" + jt.rpcCode->second + ")" + line)) || bad; // If we have an rpcCode (just checked), then the rpcException check is // optional - the 'error' field may not be defined, but if it is, it must // match rpcError. bad = (jt.rpcException && !test.expect( (jt.rpcCode && parsed.rpcError.empty()) || (parsed.rpcError == jt.rpcException->first && (!jt.rpcException->second || parsed.rpcException == *jt.rpcException->second)), "apply " + locStr + ": Got RPC result "s + parsed.rpcError + " (" + parsed.rpcException + "); Expected " + jt.rpcException->first + " (" + jt.rpcException->second.value_or("n/a") + ")" + line)) || bad; if (bad) { test.log << pretty(jt.jv) << std::endl; if (jr) test.log << pretty(jr) << std::endl; // Don't check postconditions if // we didn't get the expected result. return; } if (trace_) { if (trace_ > 0) --trace_; test.log << pretty(jt.jv) << std::endl; } for (auto const& f : jt.require) f(*this); } std::shared_ptr Env::meta() { if (current()->txCount() != 0) { // close the ledger if it has not already been closed // (metadata is not finalized until the ledger is closed) close(); } auto const item = closed()->txRead(txid_); auto const result = item.second; if (result == nullptr) { test.log << "Env::meta: no metadata for txid: " << txid_ << std::endl; test.log << "This is probably because the transaction failed with a " "non-tec error." << std::endl; Throw("Env::meta: no metadata for txid"); } return result; } std::shared_ptr Env::tx() const { return current()->txRead(txid_).first; } void Env::autofill_sig(JTx& jt) { auto& jv = jt.jv; scope_success success([&]() { // Call all the post-signers after the main signers or autofill are done for (auto const& signer : jt.postSigners) signer(*this, jt); }); // Call all the main signers if (!jt.mainSigners.empty()) { for (auto const& signer : jt.mainSigners) signer(*this, jt); return; } // If the sig is still needed, get it here. if (!jt.fill_sig) return; auto const account = jv.isMember(sfDelegate.jsonName) ? lookup(jv[sfDelegate.jsonName].asString()) : lookup(jv[jss::Account].asString()); if (!app().checkSigs()) { jv[jss::SigningPubKey] = strHex(account.pk().slice()); // dummy sig otherwise STTx is invalid jv[jss::TxnSignature] = "00"; return; } auto const ar = le(account); if (ar && ar->isFieldPresent(sfRegularKey)) jtx::sign(jv, lookup(ar->getAccountID(sfRegularKey))); else jtx::sign(jv, account); } void Env::autofill(JTx& jt) { auto& jv = jt.jv; if (jt.fill_fee) jtx::fill_fee(jv, *current()); if (jt.fill_seq) jtx::fill_seq(jv, *current()); if (jt.fill_netid) { uint32_t networkID = app().getNetworkIDService().getNetworkID(); if (!jv.isMember(jss::NetworkID) && networkID > 1024) jv[jss::NetworkID] = std::to_string(networkID); } // Must come last try { autofill_sig(jt); } catch (parse_error const&) { if (!parseFailureExpected_) test.log << "parse failed:\n" << pretty(jv) << std::endl; Rethrow(); } } std::shared_ptr Env::st(JTx const& jt) { // The parse must succeed, since we // generated the JSON ourselves. std::optional obj; try { obj = jtx::parse(jt.jv); } catch (jtx::parse_error const&) { test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl; Rethrow(); } try { return sterilize(STTx{std::move(*obj)}); } catch (std::exception const&) { } return nullptr; } std::shared_ptr Env::ust(JTx const& jt) { // The parse must succeed, since we // generated the JSON ourselves. std::optional obj; try { obj = jtx::parse(jt.jv); } catch (jtx::parse_error const&) { test.log << "Exception: parse_error\n" << pretty(jt.jv) << std::endl; Rethrow(); } try { return std::make_shared(std::move(*obj)); } catch (std::exception const&) { } return nullptr; } Json::Value Env::do_rpc( unsigned apiVersion, std::vector const& args, std::unordered_map const& headers) { auto response = rpcClient(args, app().config(), app().logs(), apiVersion, headers); for (unsigned ctr = 0; (ctr < retries_) and (response.first == rpcINTERNAL); ++ctr) { JLOG(journal.error()) << "Env::do_rpc error, retrying, attempt #" << ctr + 1 << " ..."; std::this_thread::sleep_for(std::chrono::milliseconds(500)); response = rpcClient(args, app().config(), app().logs(), apiVersion, headers); } return response.second; } void Env::enableFeature(uint256 const feature) { // Env::close() must be called for feature // enable to take place. app().config().features.insert(feature); } void Env::disableFeature(uint256 const feature) { // Env::close() must be called for feature // enable to take place. app().config().features.erase(feature); } } // namespace jtx } // namespace test } // namespace xrpl