//------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio Copyright (c) 2022, the clio developers. Permission to use, copy, modify, and 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 #include #include #include #include #include #include #include using namespace clio; // local to compilation unit loggers namespace { clio::Logger gLog{"RPC"}; } // namespace namespace RPC { std::optional getBool(boost::json::object const& request, std::string const& field) { if (!request.contains(field)) return {}; else if (request.at(field).is_bool()) return request.at(field).as_bool(); else throw InvalidParamsError("Invalid field " + field + ", not bool."); } bool getBool(boost::json::object const& request, std::string const& field, bool dfault) { if (auto res = getBool(request, field)) return *res; else return dfault; } bool getRequiredBool(boost::json::object const& request, std::string const& field) { if (auto res = getBool(request, field)) return *res; else throw InvalidParamsError("Missing field " + field); } std::optional getUInt(boost::json::object const& request, std::string const& field) { if (!request.contains(field)) return {}; else if (request.at(field).is_uint64()) return request.at(field).as_uint64(); else if (request.at(field).is_int64()) return request.at(field).as_int64(); else throw InvalidParamsError("Invalid field " + field + ", not uint."); } std::uint32_t getUInt(boost::json::object const& request, std::string const& field, std::uint32_t const dfault) { if (auto res = getUInt(request, field)) return *res; else return dfault; } std::uint32_t getRequiredUInt(boost::json::object const& request, std::string const& field) { if (auto res = getUInt(request, field)) return *res; else throw InvalidParamsError("Missing field " + field); } std::optional parseAccountCursor(std::optional jsonCursor) { ripple::uint256 cursorIndex = beast::zero; std::uint64_t startHint = 0; if (!jsonCursor) return AccountCursor({cursorIndex, startHint}); // Cursor is composed of a comma separated index and start hint. The // former will be read as hex, and the latter using boost lexical cast. std::stringstream cursor(*jsonCursor); std::string value; if (!std::getline(cursor, value, ',')) return {}; if (!cursorIndex.parseHex(value)) return {}; if (!std::getline(cursor, value, ',')) return {}; try { startHint = boost::lexical_cast(value); } catch (boost::bad_lexical_cast&) { return {}; } return AccountCursor({cursorIndex, startHint}); } std::optional getString(boost::json::object const& request, std::string const& field) { if (!request.contains(field)) return {}; else if (request.at(field).is_string()) return request.at(field).as_string().c_str(); else throw InvalidParamsError("Invalid field " + field + ", not string."); } std::string getRequiredString(boost::json::object const& request, std::string const& field) { if (auto res = getString(request, field)) return *res; else throw InvalidParamsError("Missing field " + field); } std::string getString(boost::json::object const& request, std::string const& field, std::string dfault) { if (auto res = getString(request, field)) return *res; else return dfault; } Status getHexMarker(boost::json::object const& request, ripple::uint256& marker) { if (request.contains(JS(marker))) { if (!request.at(JS(marker)).is_string()) return Status{RippledError::rpcINVALID_PARAMS, "markerNotString"}; if (!marker.parseHex(request.at(JS(marker)).as_string().c_str())) return Status{RippledError::rpcINVALID_PARAMS, "malformedMarker"}; } return {}; } Status getAccount( boost::json::object const& request, ripple::AccountID& account, boost::string_view const& field, bool required) { if (!request.contains(field)) { if (required) return Status{RippledError::rpcINVALID_PARAMS, field.to_string() + "Missing"}; return {}; } if (!request.at(field).is_string()) return Status{RippledError::rpcINVALID_PARAMS, field.to_string() + "NotString"}; if (auto a = accountFromStringStrict(request.at(field).as_string().c_str()); a) { account = a.value(); return {}; } return Status{RippledError::rpcACT_MALFORMED, field.to_string() + "Malformed"}; } Status getOptionalAccount( boost::json::object const& request, std::optional& account, boost::string_view const& field) { if (!request.contains(field)) { account = {}; return {}; } if (!request.at(field).is_string()) return Status{RippledError::rpcINVALID_PARAMS, field.to_string() + "NotString"}; if (auto a = accountFromStringStrict(request.at(field).as_string().c_str()); a) { account = a.value(); return {}; } return Status{RippledError::rpcINVALID_PARAMS, field.to_string() + "Malformed"}; } Status getAccount(boost::json::object const& request, ripple::AccountID& accountId) { return getAccount(request, accountId, JS(account), true); } Status getAccount(boost::json::object const& request, ripple::AccountID& destAccount, boost::string_view const& field) { return getAccount(request, destAccount, field, false); } Status getTaker(boost::json::object const& request, ripple::AccountID& takerID) { if (request.contains(JS(taker))) { auto parsed = parseTaker(request.at(JS(taker))); if (auto status = std::get_if(&parsed); status) return *status; else takerID = std::get(parsed); } return {}; } Status getChannelId(boost::json::object const& request, ripple::uint256& channelId) { if (!request.contains(JS(channel_id))) return Status{RippledError::rpcINVALID_PARAMS, "missingChannelID"}; if (!request.at(JS(channel_id)).is_string()) return Status{RippledError::rpcINVALID_PARAMS, "channelIDNotString"}; if (!channelId.parseHex(request.at(JS(channel_id)).as_string().c_str())) return Status{RippledError::rpcCHANNEL_MALFORMED, "malformedChannelID"}; return {}; } std::optional getDeliveredAmount( std::shared_ptr const& txn, std::shared_ptr const& meta, std::uint32_t const ledgerSequence, uint32_t date) { if (meta->hasDeliveredAmount()) return meta->getDeliveredAmount(); if (txn->isFieldPresent(ripple::sfAmount)) { using namespace std::chrono_literals; // 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 (ledgerSequence >= 4594095 || date > 446000000) { return txn->getFieldAmount(ripple::sfAmount); } } return {}; } bool canHaveDeliveredAmount( std::shared_ptr const& txn, std::shared_ptr const& meta) { ripple::TxType const tt{txn->getTxnType()}; if (tt != ripple::ttPAYMENT && tt != ripple::ttCHECK_CASH && tt != ripple::ttACCOUNT_DELETE) return false; /* if (tt == ttCHECK_CASH && !getFix1623Enabled()) return false; */ if (meta->getResultTER() != ripple::tesSUCCESS) return false; return true; } std::optional accountFromStringStrict(std::string const& account) { auto blob = ripple::strUnHex(account); std::optional publicKey = {}; if (blob && ripple::publicKeyType(ripple::makeSlice(*blob))) { publicKey = ripple::PublicKey(ripple::Slice{blob->data(), blob->size()}); } else { publicKey = ripple::parseBase58(ripple::TokenType::AccountPublic, account); } std::optional result; if (publicKey) result = ripple::calcAccountID(*publicKey); else result = ripple::parseBase58(account); if (result) return result.value(); else return {}; } std::pair, std::shared_ptr> deserializeTxPlusMeta(Backend::TransactionAndMetadata const& blobs) { try { std::pair, std::shared_ptr> result; { ripple::SerialIter s{blobs.transaction.data(), blobs.transaction.size()}; result.first = std::make_shared(s); } { ripple::SerialIter s{blobs.metadata.data(), blobs.metadata.size()}; result.second = std::make_shared(s, ripple::sfMetadata); } return result; } catch (std::exception const& e) { std::stringstream txn; std::stringstream meta; std::copy(blobs.transaction.begin(), blobs.transaction.end(), std::ostream_iterator(txn)); std::copy(blobs.metadata.begin(), blobs.metadata.end(), std::ostream_iterator(meta)); gLog.error() << "Failed to deserialize transaction. txn = " << txn.str() << " - meta = " << meta.str() << " txn length = " << std::to_string(blobs.transaction.size()) << " meta length = " << std::to_string(blobs.metadata.size()); throw e; } } std::pair, std::shared_ptr> deserializeTxPlusMeta(Backend::TransactionAndMetadata const& blobs, std::uint32_t seq) { auto [tx, meta] = deserializeTxPlusMeta(blobs); std::shared_ptr m = std::make_shared(tx->getTransactionID(), seq, *meta); return {tx, m}; } boost::json::object toJson(ripple::STBase const& obj) { boost::json::value value = boost::json::parse(obj.getJson(ripple::JsonOptions::none).toStyledString()); return value.as_object(); } std::pair toExpandedJson(Backend::TransactionAndMetadata const& blobs) { auto [txn, meta] = deserializeTxPlusMeta(blobs, blobs.ledgerSequence); auto txnJson = toJson(*txn); auto metaJson = toJson(*meta); insertDeliveredAmount(metaJson, txn, meta, blobs.date); return {txnJson, metaJson}; } bool insertDeliveredAmount( boost::json::object& metaJson, std::shared_ptr const& txn, std::shared_ptr const& meta, uint32_t date) { if (canHaveDeliveredAmount(txn, meta)) { if (auto amt = getDeliveredAmount(txn, meta, meta->getLgrSeq(), date)) metaJson["delivered_amount"] = toBoostJson(amt->getJson(ripple::JsonOptions::include_date)); else metaJson["delivered_amount"] = "unavailable"; return true; } return false; } boost::json::object toJson(ripple::TxMeta const& meta) { boost::json::value value = boost::json::parse(meta.getJson(ripple::JsonOptions::none).toStyledString()); return value.as_object(); } boost::json::value toBoostJson(Json::Value const& value) { boost::json::value boostValue = boost::json::parse(value.toStyledString()); return boostValue; } boost::json::object toJson(ripple::SLE const& sle) { boost::json::value value = boost::json::parse(sle.getJson(ripple::JsonOptions::none).toStyledString()); if (sle.getType() == ripple::ltACCOUNT_ROOT) { if (sle.isFieldPresent(ripple::sfEmailHash)) { auto const& hash = sle.getFieldH128(ripple::sfEmailHash); std::string md5 = strHex(hash); boost::algorithm::to_lower(md5); value.as_object()["urlgravatar"] = str(boost::format("http://www.gravatar.com/avatar/%s") % md5); } } return value.as_object(); } boost::json::object toJson(ripple::LedgerInfo const& lgrInfo) { boost::json::object header; header["ledger_sequence"] = lgrInfo.seq; header["ledger_hash"] = ripple::strHex(lgrInfo.hash); header["txns_hash"] = ripple::strHex(lgrInfo.txHash); header["state_hash"] = ripple::strHex(lgrInfo.accountHash); header["parent_hash"] = ripple::strHex(lgrInfo.parentHash); header["total_coins"] = ripple::to_string(lgrInfo.drops); header["close_flags"] = lgrInfo.closeFlags; // Always show fields that contribute to the ledger hash header["parent_close_time"] = lgrInfo.parentCloseTime.time_since_epoch().count(); header["close_time"] = lgrInfo.closeTime.time_since_epoch().count(); header["close_time_resolution"] = lgrInfo.closeTimeResolution.count(); return header; } std::optional parseStringAsUInt(std::string const& value) { std::optional index = {}; try { index = boost::lexical_cast(value); } catch (boost::bad_lexical_cast const&) { } return index; } std::variant ledgerInfoFromRequest(std::shared_ptr const& backend, Web::Context const& ctx) { auto hashValue = ctx.params.contains("ledger_hash") ? ctx.params.at("ledger_hash") : nullptr; if (!hashValue.is_null()) { if (!hashValue.is_string()) return Status{RippledError::rpcINVALID_PARAMS, "ledgerHashNotString"}; ripple::uint256 ledgerHash; if (!ledgerHash.parseHex(hashValue.as_string().c_str())) return Status{RippledError::rpcINVALID_PARAMS, "ledgerHashMalformed"}; auto lgrInfo = backend->fetchLedgerByHash(ledgerHash, ctx.yield); if (!lgrInfo || lgrInfo->seq > ctx.range.maxSequence) return Status{RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"}; return *lgrInfo; } auto indexValue = ctx.params.contains("ledger_index") ? ctx.params.at("ledger_index") : nullptr; std::optional ledgerSequence = {}; if (!indexValue.is_null()) { if (indexValue.is_string()) { boost::json::string const& stringIndex = indexValue.as_string(); if (stringIndex == "validated") ledgerSequence = ctx.range.maxSequence; else ledgerSequence = parseStringAsUInt(stringIndex.c_str()); } else if (indexValue.is_int64()) ledgerSequence = indexValue.as_int64(); } else { ledgerSequence = ctx.range.maxSequence; } if (!ledgerSequence) return Status{RippledError::rpcINVALID_PARAMS, "ledgerIndexMalformed"}; auto lgrInfo = backend->fetchLedgerBySequence(*ledgerSequence, ctx.yield); if (!lgrInfo || lgrInfo->seq > ctx.range.maxSequence) return Status{RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"}; return *lgrInfo; } // extract ledgerInfoFromRequest's parameter from context std::variant getLedgerInfoFromHashOrSeq( BackendInterface const& backend, boost::asio::yield_context& yield, std::optional ledgerHash, std::optional ledgerIndex, uint32_t maxSeq) { std::optional lgrInfo; auto const err = Status{RippledError::rpcLGR_NOT_FOUND, "ledgerNotFound"}; if (ledgerHash) { // invoke uint256's constructor to parse the hex string , instead of // copying buffer ripple::uint256 ledgerHash256{std::string_view(*ledgerHash)}; lgrInfo = backend.fetchLedgerByHash(ledgerHash256, yield); if (!lgrInfo || lgrInfo->seq > maxSeq) return err; return *lgrInfo; } auto const ledgerSequence = ledgerIndex.value_or(maxSeq); // return without check db if (ledgerSequence > maxSeq) return err; lgrInfo = backend.fetchLedgerBySequence(ledgerSequence, yield); if (!lgrInfo) return err; return *lgrInfo; } std::vector ledgerInfoToBlob(ripple::LedgerInfo const& info, bool includeHash) { ripple::Serializer s; s.add32(info.seq); s.add64(info.drops.drops()); s.addBitString(info.parentHash); s.addBitString(info.txHash); s.addBitString(info.accountHash); s.add32(info.parentCloseTime.time_since_epoch().count()); s.add32(info.closeTime.time_since_epoch().count()); s.add8(info.closeTimeResolution.count()); s.add8(info.closeFlags); if (includeHash) s.addBitString(info.hash); return s.peekData(); } std::uint64_t getStartHint(ripple::SLE const& sle, ripple::AccountID const& accountID) { if (sle.getType() == ripple::ltRIPPLE_STATE) { if (sle.getFieldAmount(ripple::sfLowLimit).getIssuer() == accountID) return sle.getFieldU64(ripple::sfLowNode); else if (sle.getFieldAmount(ripple::sfHighLimit).getIssuer() == accountID) return sle.getFieldU64(ripple::sfHighNode); } if (!sle.isFieldPresent(ripple::sfOwnerNode)) return 0; return sle.getFieldU64(ripple::sfOwnerNode); } std::variant traverseOwnedNodes( BackendInterface const& backend, ripple::AccountID const& accountID, std::uint32_t sequence, std::uint32_t limit, std::optional jsonCursor, boost::asio::yield_context& yield, std::function atOwnedNode) { if (!backend.fetchLedgerObject(ripple::keylet::account(accountID).key, sequence, yield)) return Status{RippledError::rpcACT_NOT_FOUND}; auto const maybeCursor = parseAccountCursor(jsonCursor); if (!maybeCursor) return Status(ripple::rpcINVALID_PARAMS, "Malformed cursor"); auto [hexCursor, startHint] = *maybeCursor; return traverseOwnedNodes( backend, ripple::keylet::ownerDir(accountID), hexCursor, startHint, sequence, limit, jsonCursor, yield, atOwnedNode); } std::variant ngTraverseOwnedNodes( BackendInterface const& backend, ripple::AccountID const& accountID, std::uint32_t sequence, std::uint32_t limit, std::optional jsonCursor, boost::asio::yield_context& yield, std::function atOwnedNode) { auto const maybeCursor = parseAccountCursor(jsonCursor); // the format is checked in RPC framework level auto const [hexCursor, startHint] = *maybeCursor; return traverseOwnedNodes( backend, ripple::keylet::ownerDir(accountID), hexCursor, startHint, sequence, limit, jsonCursor, yield, atOwnedNode); } std::variant traverseOwnedNodes( BackendInterface const& backend, ripple::Keylet const& owner, ripple::uint256 const& hexMarker, std::uint32_t const startHint, std::uint32_t sequence, std::uint32_t limit, std::optional jsonCursor, boost::asio::yield_context& yield, std::function atOwnedNode) { auto cursor = AccountCursor({beast::zero, 0}); auto const rootIndex = owner; auto currentIndex = rootIndex; // track the current page we are accessing, will return it as the next hint auto currentPage = startHint; std::vector keys; // Only reserve 2048 nodes when fetching all owned ledger objects. If there // are more, then keys will allocate more memory, which is suboptimal, but // should only occur occasionally. keys.reserve(std::min(std::uint32_t{2048}, limit)); auto start = std::chrono::system_clock::now(); // If startAfter is not zero try jumping to that page using the hint if (hexMarker.isNonZero()) { auto const hintIndex = ripple::keylet::page(rootIndex, startHint); auto hintDir = backend.fetchLedgerObject(hintIndex.key, sequence, yield); if (!hintDir) return Status(ripple::rpcINVALID_PARAMS, "Invalid marker"); ripple::SerialIter it{hintDir->data(), hintDir->size()}; ripple::SLE sle{it, hintIndex.key}; if (auto const& indexes = sle.getFieldV256(ripple::sfIndexes); std::find(std::begin(indexes), std::end(indexes), hexMarker) == std::end(indexes)) { // the index specified by marker is not in the page specified by marker return Status(ripple::rpcINVALID_PARAMS, "Invalid marker"); } currentIndex = hintIndex; bool found = false; for (;;) { auto const ownerDir = backend.fetchLedgerObject(currentIndex.key, sequence, yield); if (!ownerDir) return Status(ripple::rpcINVALID_PARAMS, "Owner directory not found"); ripple::SerialIter it{ownerDir->data(), ownerDir->size()}; ripple::SLE sle{it, currentIndex.key}; for (auto const& key : sle.getFieldV256(ripple::sfIndexes)) { if (!found) { if (key == hexMarker) found = true; } else { keys.push_back(key); if (--limit == 0) { break; } } } if (limit == 0) { cursor = AccountCursor({keys.back(), currentPage}); break; } // the next page auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext); if (uNodeNext == 0) break; currentIndex = ripple::keylet::page(rootIndex, uNodeNext); currentPage = uNodeNext; } } else { for (;;) { auto const ownerDir = backend.fetchLedgerObject(currentIndex.key, sequence, yield); if (!ownerDir) break; ripple::SerialIter it{ownerDir->data(), ownerDir->size()}; ripple::SLE sle{it, currentIndex.key}; for (auto const& key : sle.getFieldV256(ripple::sfIndexes)) { keys.push_back(key); if (--limit == 0) break; } if (limit == 0) { cursor = AccountCursor({keys.back(), currentPage}); break; } auto const uNodeNext = sle.getFieldU64(ripple::sfIndexNext); if (uNodeNext == 0) break; currentIndex = ripple::keylet::page(rootIndex, uNodeNext); currentPage = uNodeNext; } } auto end = std::chrono::system_clock::now(); gLog.debug() << "Time loading owned directories: " << std::chrono::duration_cast(end - start).count() << " milliseconds"; auto [objects, timeDiff] = util::timed([&]() { return backend.fetchLedgerObjects(keys, sequence, yield); }); gLog.debug() << "Time loading owned entries: " << timeDiff << " milliseconds"; for (auto i = 0; i < objects.size(); ++i) { ripple::SerialIter it{objects[i].data(), objects[i].size()}; atOwnedNode(ripple::SLE{it, keys[i]}); } if (limit == 0) return cursor; return AccountCursor({beast::zero, 0}); } std::shared_ptr read( std::shared_ptr const& backend, ripple::Keylet const& keylet, ripple::LedgerInfo const& lgrInfo, Web::Context const& context) { if (auto const blob = backend->fetchLedgerObject(keylet.key, lgrInfo.seq, context.yield); blob) { return std::make_shared(ripple::SerialIter{blob->data(), blob->size()}, keylet.key); } return nullptr; } std::optional parseRippleLibSeed(boost::json::value const& value) { // ripple-lib encodes seed used to generate an Ed25519 wallet in a // non-standard way. While rippled never encode seeds that way, we // try to detect such keys to avoid user confusion. if (!value.is_string()) return {}; auto const result = ripple::decodeBase58Token(value.as_string().c_str(), ripple::TokenType::None); if (result.size() == 18 && static_cast(result[0]) == std::uint8_t(0xE1) && static_cast(result[1]) == std::uint8_t(0x4B)) return ripple::Seed(ripple::makeSlice(result.substr(2))); return {}; } std::variant> keypairFromRequst(boost::json::object const& request) { bool const has_key_type = request.contains("key_type"); // All of the secret types we allow, but only one at a time. // The array should be constexpr, but that makes Visual Studio unhappy. static std::string const secretTypes[]{"passphrase", "secret", "seed", "seed_hex"}; // Identify which secret type is in use. std::string secretType = ""; int count = 0; for (auto t : secretTypes) { if (request.contains(t)) { ++count; secretType = t; } } if (count == 0) return Status{RippledError::rpcINVALID_PARAMS, "missing field secret"}; if (count > 1) { return Status{ RippledError::rpcINVALID_PARAMS, "Exactly one of the following must be specified: " " passphrase, secret, seed, or seed_hex"}; } std::optional keyType; std::optional seed; if (has_key_type) { if (!request.at("key_type").is_string()) return Status{RippledError::rpcINVALID_PARAMS, "keyTypeNotString"}; std::string key_type = request.at("key_type").as_string().c_str(); keyType = ripple::keyTypeFromString(key_type); if (!keyType) return Status{RippledError::rpcINVALID_PARAMS, "invalidFieldKeyType"}; if (secretType == "secret") return Status{RippledError::rpcINVALID_PARAMS, "The secret field is not allowed if key_type is used."}; } // ripple-lib encodes seed used to generate an Ed25519 wallet in a // non-standard way. While we never encode seeds that way, we try // to detect such keys to avoid user confusion. if (secretType != "seed_hex") { seed = parseRippleLibSeed(request.at(secretType)); if (seed) { // If the user passed in an Ed25519 seed but *explicitly* // requested another key type, return an error. if (keyType.value_or(ripple::KeyType::ed25519) != ripple::KeyType::ed25519) return Status{RippledError::rpcINVALID_PARAMS, "Specified seed is for an Ed25519 wallet."}; keyType = ripple::KeyType::ed25519; } } if (!keyType) keyType = ripple::KeyType::secp256k1; if (!seed) { if (has_key_type) { if (!request.at(secretType).is_string()) return Status{RippledError::rpcINVALID_PARAMS, "secret value must be string"}; std::string key = request.at(secretType).as_string().c_str(); if (secretType == "seed") seed = ripple::parseBase58(key); else if (secretType == "passphrase") seed = ripple::parseGenericSeed(key); else if (secretType == "seed_hex") { ripple::uint128 s; if (s.parseHex(key)) seed.emplace(ripple::Slice(s.data(), s.size())); } } else { if (!request.at("secret").is_string()) return Status{RippledError::rpcINVALID_PARAMS, "field secret should be a string"}; std::string secret = request.at("secret").as_string().c_str(); seed = ripple::parseGenericSeed(secret); } } if (!seed) return Status{RippledError::rpcBAD_SEED, "Bad Seed: invalid field message secretType"}; if (keyType != ripple::KeyType::secp256k1 && keyType != ripple::KeyType::ed25519) return Status{RippledError::rpcINVALID_PARAMS, "keypairForSignature: invalid key type"}; return generateKeyPair(*keyType, *seed); } std::vector getAccountsFromTransaction(boost::json::object const& transaction) { std::vector accounts = {}; for (auto const& [key, value] : transaction) { if (value.is_object()) { auto inObject = getAccountsFromTransaction(value.as_object()); accounts.insert(accounts.end(), inObject.begin(), inObject.end()); } else if (value.is_string()) { auto account = accountFromStringStrict(value.as_string().c_str()); if (account) { accounts.push_back(*account); } } } return accounts; } bool isGlobalFrozen( BackendInterface const& backend, std::uint32_t sequence, ripple::AccountID const& issuer, boost::asio::yield_context& yield) { if (ripple::isXRP(issuer)) return false; auto key = ripple::keylet::account(issuer).key; auto blob = backend.fetchLedgerObject(key, sequence, yield); if (!blob) return false; ripple::SerialIter it{blob->data(), blob->size()}; ripple::SLE sle{it, key}; return sle.isFlag(ripple::lsfGlobalFreeze); } bool isFrozen( BackendInterface const& backend, std::uint32_t sequence, ripple::AccountID const& account, ripple::Currency const& currency, ripple::AccountID const& issuer, boost::asio::yield_context& yield) { if (ripple::isXRP(currency)) return false; auto key = ripple::keylet::account(issuer).key; auto blob = backend.fetchLedgerObject(key, sequence, yield); if (!blob) return false; ripple::SerialIter it{blob->data(), blob->size()}; ripple::SLE sle{it, key}; if (sle.isFlag(ripple::lsfGlobalFreeze)) return true; if (issuer != account) { key = ripple::keylet::line(account, issuer, currency).key; blob = backend.fetchLedgerObject(key, sequence, yield); if (!blob) return false; ripple::SerialIter issuerIt{blob->data(), blob->size()}; ripple::SLE issuerLine{issuerIt, key}; auto frozen = (issuer > account) ? ripple::lsfHighFreeze : ripple::lsfLowFreeze; if (issuerLine.isFlag(frozen)) return true; } return false; } ripple::XRPAmount xrpLiquid( BackendInterface const& backend, std::uint32_t sequence, ripple::AccountID const& id, boost::asio::yield_context& yield) { auto key = ripple::keylet::account(id).key; auto blob = backend.fetchLedgerObject(key, sequence, yield); if (!blob) return beast::zero; ripple::SerialIter it{blob->data(), blob->size()}; ripple::SLE sle{it, key}; std::uint32_t const ownerCount = sle.getFieldU32(ripple::sfOwnerCount); auto const reserve = backend.fetchFees(sequence, yield)->accountReserve(ownerCount); auto const balance = sle.getFieldAmount(ripple::sfBalance); ripple::STAmount amount = balance - reserve; if (balance < reserve) amount.clear(); return amount.xrp(); } ripple::STAmount accountFunds( BackendInterface const& backend, std::uint32_t const sequence, ripple::STAmount const& amount, ripple::AccountID const& id, boost::asio::yield_context& yield) { if (!amount.native() && amount.getIssuer() == id) { return amount; } else { return accountHolds(backend, sequence, id, amount.getCurrency(), amount.getIssuer(), true, yield); } } ripple::STAmount accountHolds( BackendInterface const& backend, std::uint32_t sequence, ripple::AccountID const& account, ripple::Currency const& currency, ripple::AccountID const& issuer, bool const zeroIfFrozen, boost::asio::yield_context& yield) { ripple::STAmount amount; if (ripple::isXRP(currency)) { return {xrpLiquid(backend, sequence, account, yield)}; } auto key = ripple::keylet::line(account, issuer, currency).key; auto const blob = backend.fetchLedgerObject(key, sequence, yield); if (!blob) { amount.clear({currency, issuer}); return amount; } ripple::SerialIter it{blob->data(), blob->size()}; ripple::SLE sle{it, key}; if (zeroIfFrozen && isFrozen(backend, sequence, account, currency, issuer, yield)) { amount.clear(ripple::Issue(currency, issuer)); } else { amount = sle.getFieldAmount(ripple::sfBalance); if (account > issuer) { // Put balance in account terms. amount.negate(); } amount.setIssuer(issuer); } return amount; } ripple::Rate transferRate( BackendInterface const& backend, std::uint32_t sequence, ripple::AccountID const& issuer, boost::asio::yield_context& yield) { auto key = ripple::keylet::account(issuer).key; auto blob = backend.fetchLedgerObject(key, sequence, yield); if (blob) { ripple::SerialIter it{blob->data(), blob->size()}; ripple::SLE sle{it, key}; if (sle.isFieldPresent(ripple::sfTransferRate)) return ripple::Rate{sle.getFieldU32(ripple::sfTransferRate)}; } return ripple::parityRate; } boost::json::array postProcessOrderBook( std::vector const& offers, ripple::Book const& book, ripple::AccountID const& takerID, Backend::BackendInterface const& backend, std::uint32_t const ledgerSequence, boost::asio::yield_context& yield) { boost::json::array jsonOffers; std::map umBalance; bool globalFreeze = isGlobalFrozen(backend, ledgerSequence, book.out.account, yield) || isGlobalFrozen(backend, ledgerSequence, book.in.account, yield); auto rate = transferRate(backend, ledgerSequence, book.out.account, yield); for (auto const& obj : offers) { try { ripple::SerialIter it{obj.blob.data(), obj.blob.size()}; ripple::SLE offer{it, obj.key}; ripple::uint256 bookDir = offer.getFieldH256(ripple::sfBookDirectory); auto const uOfferOwnerID = offer.getAccountID(ripple::sfAccount); auto const& saTakerGets = offer.getFieldAmount(ripple::sfTakerGets); auto const& saTakerPays = offer.getFieldAmount(ripple::sfTakerPays); ripple::STAmount saOwnerFunds; bool firstOwnerOffer = true; if (book.out.account == uOfferOwnerID) { // If an offer is selling issuer's own IOUs, it is fully // funded. saOwnerFunds = saTakerGets; } else if (globalFreeze) { // If either asset is globally frozen, consider all offers // that aren't ours to be totally unfunded saOwnerFunds.clear(book.out); } else { auto umBalanceEntry = umBalance.find(uOfferOwnerID); if (umBalanceEntry != umBalance.end()) { // Found in running balance table. saOwnerFunds = umBalanceEntry->second; firstOwnerOffer = false; } else { saOwnerFunds = accountHolds( backend, ledgerSequence, uOfferOwnerID, book.out.currency, book.out.account, true, yield); if (saOwnerFunds < beast::zero) saOwnerFunds.clear(); } } boost::json::object offerJson = toJson(offer); ripple::STAmount saTakerGetsFunded; ripple::STAmount saOwnerFundsLimit = saOwnerFunds; ripple::Rate offerRate = ripple::parityRate; ripple::STAmount dirRate = ripple::amountFromQuality(getQuality(bookDir)); if (rate != ripple::parityRate // Have a tranfer fee. && takerID != book.out.account // Not taking offers of own IOUs. && book.out.account != uOfferOwnerID) // Offer owner not issuing ownfunds { // Need to charge a transfer fee to offer owner. offerRate = rate; saOwnerFundsLimit = ripple::divide(saOwnerFunds, offerRate); } if (saOwnerFundsLimit >= saTakerGets) { // Sufficient funds no shenanigans. saTakerGetsFunded = saTakerGets; } else { saTakerGetsFunded = saOwnerFundsLimit; offerJson["taker_gets_funded"] = toBoostJson(saTakerGetsFunded.getJson(ripple::JsonOptions::none)); offerJson["taker_pays_funded"] = toBoostJson(std::min(saTakerPays, ripple::multiply(saTakerGetsFunded, dirRate, saTakerPays.issue())) .getJson(ripple::JsonOptions::none)); } ripple::STAmount saOwnerPays = (ripple::parityRate == offerRate) ? saTakerGetsFunded : std::min(saOwnerFunds, ripple::multiply(saTakerGetsFunded, offerRate)); umBalance[uOfferOwnerID] = saOwnerFunds - saOwnerPays; if (firstOwnerOffer) offerJson["owner_funds"] = saOwnerFunds.getText(); offerJson["quality"] = dirRate.getText(); jsonOffers.push_back(offerJson); } catch (std::exception const& e) { gLog.error() << "caught exception: " << e.what(); } } return jsonOffers; } // get book via currency type std::variant parseBook(ripple::Currency pays, ripple::AccountID payIssuer, ripple::Currency gets, ripple::AccountID getIssuer) { if (isXRP(pays) && !isXRP(payIssuer)) return Status{ RippledError::rpcSRC_ISR_MALFORMED, "Unneeded field 'taker_pays.issuer' for XRP currency " "specification."}; if (!isXRP(pays) && isXRP(payIssuer)) return Status{ RippledError::rpcSRC_ISR_MALFORMED, "Invalid field 'taker_pays.issuer', expected non-XRP " "issuer."}; if (ripple::isXRP(gets) && !ripple::isXRP(getIssuer)) return Status{ RippledError::rpcDST_ISR_MALFORMED, "Unneeded field 'taker_gets.issuer' for XRP currency " "specification."}; if (!ripple::isXRP(gets) && ripple::isXRP(getIssuer)) return Status{ RippledError::rpcDST_ISR_MALFORMED, "Invalid field 'taker_gets.issuer', expected non-XRP issuer."}; if (pays == gets && payIssuer == getIssuer) return Status{RippledError::rpcBAD_MARKET, "badMarket"}; return ripple::Book{{pays, payIssuer}, {gets, getIssuer}}; } std::variant parseBook(boost::json::object const& request) { if (!request.contains("taker_pays")) return Status{RippledError::rpcINVALID_PARAMS, "Missing field 'taker_pays'"}; if (!request.contains("taker_gets")) return Status{RippledError::rpcINVALID_PARAMS, "Missing field 'taker_gets'"}; if (!request.at("taker_pays").is_object()) return Status{RippledError::rpcINVALID_PARAMS, "Field 'taker_pays' is not an object"}; if (!request.at("taker_gets").is_object()) return Status{RippledError::rpcINVALID_PARAMS, "Field 'taker_gets' is not an object"}; auto taker_pays = request.at("taker_pays").as_object(); if (!taker_pays.contains("currency")) return Status{RippledError::rpcSRC_CUR_MALFORMED}; if (!taker_pays.at("currency").is_string()) return Status{RippledError::rpcSRC_CUR_MALFORMED}; auto taker_gets = request.at("taker_gets").as_object(); if (!taker_gets.contains("currency")) return Status{RippledError::rpcDST_AMT_MALFORMED}; if (!taker_gets.at("currency").is_string()) return Status{ RippledError::rpcDST_AMT_MALFORMED, }; ripple::Currency pay_currency; if (!ripple::to_currency(pay_currency, taker_pays.at("currency").as_string().c_str())) return Status{RippledError::rpcSRC_CUR_MALFORMED}; ripple::Currency get_currency; if (!ripple::to_currency(get_currency, taker_gets["currency"].as_string().c_str())) return Status{RippledError::rpcDST_AMT_MALFORMED}; ripple::AccountID pay_issuer; if (taker_pays.contains("issuer")) { if (!taker_pays.at("issuer").is_string()) return Status{RippledError::rpcINVALID_PARAMS, "takerPaysIssuerNotString"}; if (!ripple::to_issuer(pay_issuer, taker_pays.at("issuer").as_string().c_str())) return Status{RippledError::rpcSRC_ISR_MALFORMED}; if (pay_issuer == ripple::noAccount()) return Status{RippledError::rpcSRC_ISR_MALFORMED}; } else { pay_issuer = ripple::xrpAccount(); } if (isXRP(pay_currency) && !isXRP(pay_issuer)) return Status{ RippledError::rpcSRC_ISR_MALFORMED, "Unneeded field 'taker_pays.issuer' for XRP currency " "specification."}; if (!isXRP(pay_currency) && isXRP(pay_issuer)) return Status{ RippledError::rpcSRC_ISR_MALFORMED, "Invalid field 'taker_pays.issuer', expected non-XRP " "issuer."}; if ((!isXRP(pay_currency)) && (!taker_pays.contains("issuer"))) return Status{RippledError::rpcSRC_ISR_MALFORMED, "Missing non-XRP issuer."}; ripple::AccountID get_issuer; if (taker_gets.contains("issuer")) { if (!taker_gets["issuer"].is_string()) return Status{RippledError::rpcINVALID_PARAMS, "taker_gets.issuer should be string"}; if (!ripple::to_issuer(get_issuer, taker_gets.at("issuer").as_string().c_str())) return Status{RippledError::rpcDST_ISR_MALFORMED, "Invalid field 'taker_gets.issuer', bad issuer."}; if (get_issuer == ripple::noAccount()) return Status{ RippledError::rpcDST_ISR_MALFORMED, "Invalid field 'taker_gets.issuer', bad issuer account " "one."}; } else { get_issuer = ripple::xrpAccount(); } if (ripple::isXRP(get_currency) && !ripple::isXRP(get_issuer)) return Status{ RippledError::rpcDST_ISR_MALFORMED, "Unneeded field 'taker_gets.issuer' for XRP currency " "specification."}; if (!ripple::isXRP(get_currency) && ripple::isXRP(get_issuer)) return Status{ RippledError::rpcDST_ISR_MALFORMED, "Invalid field 'taker_gets.issuer', expected non-XRP issuer."}; if (pay_currency == get_currency && pay_issuer == get_issuer) return Status{RippledError::rpcBAD_MARKET, "badMarket"}; return ripple::Book{{pay_currency, pay_issuer}, {get_currency, get_issuer}}; } std::variant parseTaker(boost::json::value const& taker) { std::optional takerID = {}; if (!taker.is_string()) return {Status{RippledError::rpcINVALID_PARAMS, "takerNotString"}}; takerID = accountFromStringStrict(taker.as_string().c_str()); if (!takerID) return Status{RippledError::rpcBAD_ISSUER, "invalidTakerAccount"}; return *takerID; } bool specifiesCurrentOrClosedLedger(boost::json::object const& request) { if (request.contains("ledger_index")) { auto indexValue = request.at("ledger_index"); if (indexValue.is_string()) { std::string index = indexValue.as_string().c_str(); return index == "current" || index == "closed"; } } return false; } std::variant getNFTID(boost::json::object const& request) { if (!request.contains(JS(nft_id))) return Status{RippledError::rpcINVALID_PARAMS, "missingTokenID"}; if (!request.at(JS(nft_id)).is_string()) return Status{RippledError::rpcINVALID_PARAMS, "tokenIDNotString"}; ripple::uint256 tokenid; if (!tokenid.parseHex(request.at(JS(nft_id)).as_string().c_str())) return Status{RippledError::rpcINVALID_PARAMS, "malformedTokenID"}; return tokenid; } } // namespace RPC