diff --git a/CMakeLists.txt b/CMakeLists.txt index dcad6c29..7bafe992 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,7 @@ target_sources(clio PRIVATE src/rpc/handlers/AccountLines.cpp src/rpc/handlers/AccountOffers.cpp src/rpc/handlers/AccountObjects.cpp + src/rpc/handlers/GatewayBalances.cpp # Ledger src/rpc/handlers/Ledger.cpp src/rpc/handlers/LedgerData.cpp diff --git a/src/rpc/Handlers.h b/src/rpc/Handlers.h index 913a857b..b6b522b5 100644 --- a/src/rpc/Handlers.h +++ b/src/rpc/Handlers.h @@ -27,6 +27,9 @@ doAccountObjects(Context const& context); Result doAccountOffers(Context const& context); +Result +doGatewayBalances(Context const& context); + // channels methods Result diff --git a/src/rpc/RPC.cpp b/src/rpc/RPC.cpp index 68967a2b..06ac9d02 100644 --- a/src/rpc/RPC.cpp +++ b/src/rpc/RPC.cpp @@ -105,6 +105,7 @@ static std::unordered_map> {"account_objects", &doAccountObjects}, {"account_offers", &doAccountOffers}, {"account_tx", &doAccountTx}, + {"gateway_balances", &doGatewayBalances}, {"book_offers", &doBookOffers}, {"channel_authorize", &doChannelAuthorize}, {"channel_verify", &doChannelVerify}, @@ -145,6 +146,10 @@ shouldForwardToRippled(Context const& ctx) } } + if (ctx.method == "account_info" && request.contains("queue") && + request.at("queue").as_bool()) + return true; + return false; } @@ -152,7 +157,12 @@ Result buildResponse(Context const& ctx) { if (shouldForwardToRippled(ctx)) - return ctx.balancer->forwardToRippled(ctx.params); + { + auto res = ctx.balancer->forwardToRippled(ctx.params); + if (res.size() == 0) + return Status{Error::rpcFAILED_TO_FORWARD}; + return res; + } if (handlerTable.find(ctx.method) == handlerTable.end()) return Status{Error::rpcUNKNOWN_COMMAND}; diff --git a/src/rpc/RPCHelpers.cpp b/src/rpc/RPCHelpers.cpp index 4a21e59b..0b961005 100644 --- a/src/rpc/RPCHelpers.cpp +++ b/src/rpc/RPCHelpers.cpp @@ -331,7 +331,7 @@ traverseOwnedNodes( if (!ownedNode) { - throw std::runtime_error("Could not find owned node"); + break; } ripple::SerialIter it{ownedNode->data(), ownedNode->size()}; diff --git a/src/rpc/handlers/AccountChannels.cpp b/src/rpc/handlers/AccountChannels.cpp index 79de210e..eab19889 100644 --- a/src/rpc/handlers/AccountChannels.cpp +++ b/src/rpc/handlers/AccountChannels.cpp @@ -6,13 +6,12 @@ #include #include #include -#include #include #include #include +#include -namespace RPC -{ +namespace RPC { void addChannel(boost::json::array& jsonLines, ripple::SLE const& line) @@ -20,7 +19,8 @@ addChannel(boost::json::array& jsonLines, ripple::SLE const& line) boost::json::object jDst; jDst["channel_id"] = ripple::to_string(line.key()); jDst["account"] = ripple::to_string(line.getAccountID(ripple::sfAccount)); - jDst["destination_account"] = ripple::to_string(line.getAccountID(ripple::sfDestination)); + jDst["destination_account"] = + ripple::to_string(line.getAccountID(ripple::sfDestination)); jDst["amount"] = line[ripple::sfAmount].getText(); jDst["balance"] = line[ripple::sfBalance].getText(); if (publicKeyType(line[ripple::sfPublicKey])) @@ -54,13 +54,13 @@ doAccountChannels(Context const& context) auto lgrInfo = std::get(v); - if(!request.contains("account")) + if (!request.contains("account")) return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; - if(!request.at("account").is_string()) + if (!request.at("account").is_string()) return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; - - auto accountID = + + auto accountID = accountFromStringStrict(request.at("account").as_string().c_str()); if (!accountID) @@ -82,7 +82,7 @@ doAccountChannels(Context const& context) std::uint32_t limit = 200; if (request.contains("limit")) { - if(!request.at("limit").is_int64()) + if (!request.at("limit").is_int64()) return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; limit = request.at("limit").as_int64(); @@ -90,13 +90,13 @@ doAccountChannels(Context const& context) return Status{Error::rpcINVALID_PARAMS, "limitNotPositive"}; } - ripple::uint256 cursor; - if (request.contains("cursor")) + ripple::uint256 marker; + if (request.contains("marker")) { - if(!request.at("cursor").is_string()) - return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; + if (!request.at("marker").is_string()) + return Status{Error::rpcINVALID_PARAMS, "markerNotString"}; - if (!cursor.parseHex(request.at("cursor").as_string().c_str())) + if (!marker.parseHex(request.at("marker").as_string().c_str())) return Status{Error::rpcINVALID_PARAMS, "malformedCursor"}; } @@ -108,26 +108,21 @@ doAccountChannels(Context const& context) if (sle.getType() == ripple::ltPAYCHAN && sle.getAccountID(ripple::sfAccount) == *accountID && (!destAccount || - *destAccount == sle.getAccountID(ripple::sfDestination))) + *destAccount == sle.getAccountID(ripple::sfDestination))) { if (limit-- == 0) { return false; } - + addChannel(jsonChannels, sle); } return true; }; - auto nextCursor = - traverseOwnedNodes( - *context.backend, - *accountID, - lgrInfo.seq, - cursor, - addToResponse); + auto nextCursor = traverseOwnedNodes( + *context.backend, *accountID, lgrInfo.seq, marker, addToResponse); response["ledger_hash"] = ripple::strHex(lgrInfo.hash); response["ledger_index"] = lgrInfo.seq; @@ -137,4 +132,4 @@ doAccountChannels(Context const& context) return response; } -} // namespace RPC +} // namespace RPC diff --git a/src/rpc/handlers/AccountCurrencies.cpp b/src/rpc/handlers/AccountCurrencies.cpp index 1e5f5949..17996a0b 100644 --- a/src/rpc/handlers/AccountCurrencies.cpp +++ b/src/rpc/handlers/AccountCurrencies.cpp @@ -7,13 +7,10 @@ #include #include -#include #include -#include -#include +#include -namespace RPC -{ +namespace RPC { Result doAccountCurrencies(Context const& context) @@ -27,13 +24,13 @@ doAccountCurrencies(Context const& context) auto lgrInfo = std::get(v); - if(!request.contains("account")) + if (!request.contains("account")) return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; - if(!request.at("account").is_string()) + if (!request.at("account").is_string()) return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; - - auto accountID = + + auto accountID = accountFromStringStrict(request.at("account").as_string().c_str()); if (!accountID) @@ -43,36 +40,35 @@ doAccountCurrencies(Context const& context) auto const addToResponse = [&](ripple::SLE const& sle) { if (sle.getType() == ripple::ltRIPPLE_STATE) { - ripple::STAmount const& balance = - sle.getFieldAmount(ripple::sfBalance); + ripple::STAmount balance = sle.getFieldAmount(ripple::sfBalance); auto lowLimit = sle.getFieldAmount(ripple::sfLowLimit); auto highLimit = sle.getFieldAmount(ripple::sfHighLimit); bool viewLowest = (lowLimit.getIssuer() == accountID); auto lineLimit = viewLowest ? lowLimit : highLimit; auto lineLimitPeer = !viewLowest ? lowLimit : highLimit; + if (!viewLowest) + balance.negate(); if (balance < lineLimit) receive.insert(ripple::to_string(balance.getCurrency())); if ((-balance) < lineLimitPeer) send.insert(ripple::to_string(balance.getCurrency())); } - + return true; }; traverseOwnedNodes( - *context.backend, - *accountID, - lgrInfo.seq, - beast::zero, - addToResponse); + *context.backend, *accountID, lgrInfo.seq, beast::zero, addToResponse); response["ledger_hash"] = ripple::strHex(lgrInfo.hash); response["ledger_index"] = lgrInfo.seq; - response["receive_currencies"] = boost::json::value(boost::json::array_kind); - boost::json::array& jsonReceive = response.at("receive_currencies").as_array(); + response["receive_currencies"] = + boost::json::value(boost::json::array_kind); + boost::json::array& jsonReceive = + response.at("receive_currencies").as_array(); for (auto const& currency : receive) jsonReceive.push_back(currency.c_str()); @@ -86,4 +82,4 @@ doAccountCurrencies(Context const& context) return response; } -} // namespace RPC +} // namespace RPC diff --git a/src/rpc/handlers/AccountInfo.cpp b/src/rpc/handlers/AccountInfo.cpp index 6503bb9e..5efe2968 100644 --- a/src/rpc/handlers/AccountInfo.cpp +++ b/src/rpc/handlers/AccountInfo.cpp @@ -55,6 +55,9 @@ doAccountInfo(Context const& context) else return Status{Error::rpcACT_MALFORMED}; + // We only need to fetch the ledger header because the ledger hash is + // supposed to be included in the response. The ledger sequence is specified + // in the request auto v = ledgerInfoFromRequest(context); if (auto status = std::get_if(&v)) return *status; diff --git a/src/rpc/handlers/AccountObjects.cpp b/src/rpc/handlers/AccountObjects.cpp index 1120df1d..a2d9021b 100644 --- a/src/rpc/handlers/AccountObjects.cpp +++ b/src/rpc/handlers/AccountObjects.cpp @@ -1,5 +1,5 @@ -#include #include +#include #include #include #include @@ -12,11 +12,9 @@ #include #include +namespace RPC { -namespace RPC -{ - -std::unordered_map types { +std::unordered_map types{ {"state", ripple::ltRIPPLE_STATE}, {"ticket", ripple::ltTICKET}, {"signer_list", ripple::ltSIGNER_LIST}, @@ -39,13 +37,13 @@ doAccountObjects(Context const& context) auto lgrInfo = std::get(v); - if(!request.contains("account")) + if (!request.contains("account")) return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; - if(!request.at("account").is_string()) + if (!request.at("account").is_string()) return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; - - auto accountID = + + auto accountID = accountFromStringStrict(request.at("account").as_string().c_str()); if (!accountID) @@ -54,7 +52,7 @@ doAccountObjects(Context const& context) std::uint32_t limit = 200; if (request.contains("limit")) { - if(!request.at("limit").is_int64()) + if (!request.at("limit").is_int64()) return Status{Error::rpcINVALID_PARAMS, "limitNotInt"}; limit = request.at("limit").as_int64(); @@ -65,7 +63,7 @@ doAccountObjects(Context const& context) ripple::uint256 cursor; if (request.contains("cursor")) { - if(!request.at("cursor").is_string()) + if (!request.at("cursor").is_string()) return Status{Error::rpcINVALID_PARAMS, "cursorNotString"}; if (!cursor.parseHex(request.at("cursor").as_string().c_str())) @@ -75,19 +73,19 @@ doAccountObjects(Context const& context) std::optional objectType = {}; if (request.contains("type")) { - if(!request.at("type").is_string()) + if (!request.at("type").is_string()) return Status{Error::rpcINVALID_PARAMS, "typeNotString"}; std::string typeAsString = request.at("type").as_string().c_str(); - if(types.find(typeAsString) == types.end()) + if (types.find(typeAsString) == types.end()) return Status{Error::rpcINVALID_PARAMS, "typeInvalid"}; objectType = types[typeAsString]; } - + response["account"] = ripple::to_string(*accountID); response["account_objects"] = boost::json::value(boost::json::array_kind); - boost::json::array& jsonObjects = response.at("objects").as_array(); + boost::json::array& jsonObjects = response.at("account_objects").as_array(); auto const addToResponse = [&](ripple::SLE const& sle) { if (!objectType || objectType == sle.getType()) @@ -102,14 +100,9 @@ doAccountObjects(Context const& context) return true; }; - - auto nextCursor = - traverseOwnedNodes( - *context.backend, - *accountID, - lgrInfo.seq, - cursor, - addToResponse); + + auto nextCursor = traverseOwnedNodes( + *context.backend, *accountID, lgrInfo.seq, cursor, addToResponse); response["ledger_hash"] = ripple::strHex(lgrInfo.hash); response["ledger_index"] = lgrInfo.seq; @@ -120,4 +113,4 @@ doAccountObjects(Context const& context) return response; } -} // namespace RPC \ No newline at end of file +} // namespace RPC diff --git a/src/rpc/handlers/GatewayBalances.cpp b/src/rpc/handlers/GatewayBalances.cpp new file mode 100644 index 00000000..ef30251a --- /dev/null +++ b/src/rpc/handlers/GatewayBalances.cpp @@ -0,0 +1,195 @@ +#include +#include + +namespace RPC { + +Result +doGatewayBalances(Context const& context) +{ + auto request = context.params; + boost::json::object response = {}; + + if (!request.contains("account")) + return Status{Error::rpcINVALID_PARAMS, "missingAccount"}; + + if (!request.at("account").is_string()) + return Status{Error::rpcINVALID_PARAMS, "accountNotString"}; + + auto accountID = + accountFromStringStrict(request.at("account").as_string().c_str()); + + if (!accountID) + return Status{Error::rpcINVALID_PARAMS, "malformedAccount"}; + + auto v = ledgerInfoFromRequest(context); + if (auto status = std::get_if(&v)) + return *status; + + auto lgrInfo = std::get(v); + + std::map sums; + std::map> hotBalances; + std::map> assets; + std::map> frozenBalances; + std::set hotWallets; + + if (request.contains("hot_wallet")) + { + auto getAccountID = + [](auto const& j) -> std::optional { + if (j.is_string()) + { + auto const pk = ripple::parseBase58( + ripple::TokenType::AccountPublic, j.as_string().c_str()); + if (pk) + { + return ripple::calcAccountID(*pk); + } + + return ripple::parseBase58( + j.as_string().c_str()); + } + return {}; + }; + + auto const& hw = request.at("hot_wallet"); + bool valid = true; + + // null is treated as a valid 0-sized array of hotwallet + if (hw.is_array()) + { + auto const& arr = hw.as_array(); + for (unsigned i = 0; i < arr.size(); ++i) + { + if (auto id = getAccountID(arr[i])) + hotWallets.insert(*id); + else + valid = false; + } + } + else if (hw.is_string()) + { + if (auto id = getAccountID(hw)) + hotWallets.insert(*id); + else + valid = false; + } + else + { + valid = false; + } + + if (!valid) + { + response["error"] = "invalidHotWallet"; + return response; + } + } + + // Traverse the cold wallet's trust lines + auto const addToResponse = [&](ripple::SLE const& sle) { + if (sle.getType() == ripple::ltRIPPLE_STATE) + { + ripple::STAmount balance = sle.getFieldAmount(ripple::sfBalance); + + auto lowLimit = sle.getFieldAmount(ripple::sfLowLimit); + auto highLimit = sle.getFieldAmount(ripple::sfHighLimit); + auto lowID = lowLimit.getIssuer(); + auto highID = highLimit.getIssuer(); + bool viewLowest = (lowLimit.getIssuer() == accountID); + auto lineLimit = viewLowest ? lowLimit : highLimit; + auto lineLimitPeer = !viewLowest ? lowLimit : highLimit; + auto flags = sle.getFieldU32(ripple::sfFlags); + auto freeze = flags & + (viewLowest ? ripple::lsfLowFreeze : ripple::lsfHighFreeze); + if (!viewLowest) + balance.negate(); + + int balSign = balance.signum(); + if (balSign == 0) + return true; + + auto const& peer = !viewLowest ? lowID : highID; + + // Here, a negative balance means the cold wallet owes (normal) + // A positive balance means the cold wallet has an asset + // (unusual) + + if (hotWallets.count(peer) > 0) + { + // This is a specified hot wallet + hotBalances[peer].push_back(balance); + } + else if (balSign > 0) + { + // This is a gateway asset + assets[peer].push_back(balance); + } + else if (freeze) + { + // An obligation the gateway has frozen + frozenBalances[peer].push_back(balance); + } + else + { + // normal negative balance, obligation to customer + auto& bal = sums[balance.getCurrency()]; + if (bal == beast::zero) + { + // This is needed to set the currency code correctly + bal = -balance; + } + else + bal -= balance; + } + } + return true; + }; + traverseOwnedNodes( + *context.backend, *accountID, lgrInfo.seq, beast::zero, addToResponse); + + if (!sums.empty()) + { + boost::json::object obj; + for (auto const& [k, v] : sums) + { + obj[ripple::to_string(k)] = v.getText(); + } + response["obligations"] = std::move(obj); + } + + auto toJson = + [](std::map> const& + balances) { + boost::json::object obj; + if (!balances.empty()) + { + for (auto const& [accId, accBalances] : balances) + { + boost::json::array arr; + for (auto const& balance : accBalances) + { + boost::json::object entry; + entry["currency"] = + ripple::to_string(balance.issue().currency); + entry["value"] = balance.getText(); + arr.push_back(std::move(entry)); + } + obj[ripple::to_string(accId)] = std::move(arr); + } + } + return obj; + }; + + if (auto balances = toJson(hotBalances); balances.size()) + response["balances"] = balances; + if (auto balances = toJson(frozenBalances); balances.size()) + response["frozen_balances"] = balances; + if (auto balances = toJson(assets); assets.size()) + response["assets"] = toJson(assets); + response["account"] = request.at("account"); + response["ledger_index"] = lgrInfo.seq; + response["ledger_hash"] = ripple::strHex(lgrInfo.hash); + return response; +} +} // namespace RPC