#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { static Expected getAutofillSequence(Json::Value const& tx_json, RPC::JsonContext& context) { // autofill Sequence bool const hasTicketSeq = tx_json.isMember(sfTicketSequence.jsonName); auto const& accountStr = tx_json[jss::Account]; if (!accountStr.isString()) { // sanity check, should fail earlier // LCOV_EXCL_START return Unexpected(RPC::invalid_field_error("tx.Account")); // LCOV_EXCL_STOP } auto const srcAddressID = parseBase58(accountStr.asString()); if (!srcAddressID.has_value()) { return Unexpected(RPC::make_error(rpcSRC_ACT_MALFORMED, RPC::invalid_field_message("tx.Account"))); } std::shared_ptr const sle = context.app.openLedger().current()->read(keylet::account(*srcAddressID)); if (!hasTicketSeq && !sle) { JLOG(context.app.journal("Simulate").debug()) << "Failed to find source account " << "in current ledger: " << toBase58(*srcAddressID); return Unexpected(rpcError(rpcSRC_ACT_NOT_FOUND)); } return hasTicketSeq ? 0 : context.app.getTxQ().nextQueuableSeq(sle).value(); } static std::optional autofillSignature(Json::Value& sigObject) { if (!sigObject.isMember(jss::SigningPubKey)) { // autofill SigningPubKey sigObject[jss::SigningPubKey] = ""; } if (sigObject.isMember(jss::Signers)) { if (!sigObject[jss::Signers].isArray()) return RPC::invalid_field_error("tx.Signers"); // check multisigned signers for (unsigned index = 0; index < sigObject[jss::Signers].size(); index++) { auto& signer = sigObject[jss::Signers][index]; if (!signer.isObject() || !signer.isMember(jss::Signer) || !signer[jss::Signer].isObject()) return RPC::invalid_field_error("tx.Signers[" + std::to_string(index) + "]"); if (!signer[jss::Signer].isMember(jss::SigningPubKey)) { // autofill SigningPubKey signer[jss::Signer][jss::SigningPubKey] = ""; } if (!signer[jss::Signer].isMember(jss::TxnSignature)) { // autofill TxnSignature signer[jss::Signer][jss::TxnSignature] = ""; } else if (signer[jss::Signer][jss::TxnSignature] != "") { // Transaction must not be signed return rpcError(rpcTX_SIGNED); } } } if (!sigObject.isMember(jss::TxnSignature)) { // autofill TxnSignature sigObject[jss::TxnSignature] = ""; } else if (sigObject[jss::TxnSignature] != "") { // Transaction must not be signed return rpcError(rpcTX_SIGNED); } return std::nullopt; } static std::optional autofillTx(Json::Value& tx_json, RPC::JsonContext& context) { if (!tx_json.isMember(jss::Fee)) { // autofill Fee // Must happen after all the other autofills happen // Error handling/messaging works better that way auto feeOrError = RPC::getCurrentNetworkFee( context.role, context.app.config(), context.app.getFeeTrack(), context.app.getTxQ(), context.app, tx_json); if (feeOrError.isMember(jss::error)) return feeOrError; tx_json[jss::Fee] = feeOrError; } if (auto error = autofillSignature(tx_json)) return *error; if (!tx_json.isMember(jss::Sequence)) { auto const seq = getAutofillSequence(tx_json, context); if (!seq) return seq.error(); tx_json[sfSequence.jsonName] = *seq; } if (!tx_json.isMember(jss::NetworkID)) { auto const networkId = context.app.config().NETWORK_ID; if (networkId > 1024) tx_json[jss::NetworkID] = to_string(networkId); } return std::nullopt; } static Json::Value getTxJsonFromParams(Json::Value const& params) { Json::Value tx_json; if (params.isMember(jss::tx_blob)) { if (params.isMember(jss::tx_json)) { return RPC::make_param_error("Can only include one of `tx_blob` and `tx_json`."); } auto const tx_blob = params[jss::tx_blob]; if (!tx_blob.isString()) { return RPC::invalid_field_error(jss::tx_blob); } auto unHexed = strUnHex(tx_blob.asString()); if (!unHexed || unHexed->empty()) return RPC::invalid_field_error(jss::tx_blob); try { SerialIter sitTrans(makeSlice(*unHexed)); tx_json = STObject(std::ref(sitTrans), sfGeneric).getJson(JsonOptions::none); } catch (std::runtime_error const&) { return RPC::invalid_field_error(jss::tx_blob); } } else if (params.isMember(jss::tx_json)) { tx_json = params[jss::tx_json]; if (!tx_json.isObject()) { return RPC::object_field_error(jss::tx_json); } } else { return RPC::make_param_error("Neither `tx_blob` nor `tx_json` included."); } // basic sanity checks for transaction shape if (!tx_json.isMember(jss::TransactionType)) { return RPC::missing_field_error("tx.TransactionType"); } if (!tx_json.isMember(jss::Account)) { return RPC::missing_field_error("tx.Account"); } return tx_json; } static Json::Value simulateTxn(RPC::JsonContext& context, std::shared_ptr transaction) { Json::Value jvResult; // Process the transaction OpenView view = *context.app.openLedger().current(); auto const result = context.app.getTxQ().apply(context.app, view, transaction->getSTransaction(), tapDRY_RUN, context.j); jvResult[jss::applied] = result.applied; jvResult[jss::ledger_index] = view.seq(); bool const isBinaryOutput = context.params.get(jss::binary, false).asBool(); // Convert the TER to human-readable values std::string token; std::string message; if (transResultInfo(result.ter, token, message)) { // Engine result jvResult[jss::engine_result] = token; jvResult[jss::engine_result_code] = result.ter; jvResult[jss::engine_result_message] = message; } else { // shouldn't be hit // LCOV_EXCL_START jvResult[jss::engine_result] = "unknown"; jvResult[jss::engine_result_code] = result.ter; jvResult[jss::engine_result_message] = "unknown"; // LCOV_EXCL_STOP } if (token == "tesSUCCESS") { jvResult[jss::engine_result_message] = "The simulated transaction would have been applied."; } if (result.metadata) { if (isBinaryOutput) { auto const metaBlob = result.metadata->getAsObject().getSerializer().getData(); jvResult[jss::meta_blob] = strHex(makeSlice(metaBlob)); } else { jvResult[jss::meta] = result.metadata->getJson(JsonOptions::none); RPC::insertDeliveredAmount(jvResult[jss::meta], view, transaction->getSTransaction(), *result.metadata); RPC::insertNFTSyntheticInJson(jvResult, transaction->getSTransaction(), *result.metadata); RPC::insertMPTokenIssuanceID(jvResult[jss::meta], transaction->getSTransaction(), *result.metadata); } } if (isBinaryOutput) { auto const txBlob = transaction->getSTransaction()->getSerializer().getData(); jvResult[jss::tx_blob] = strHex(makeSlice(txBlob)); } else { jvResult[jss::tx_json] = transaction->getJson(JsonOptions::none); } return jvResult; } // { // tx_blob: XOR tx_json: , // binary: // } Json::Value doSimulate(RPC::JsonContext& context) { context.loadType = Resource::feeMediumBurdenRPC; Json::Value tx_json; // the tx as a JSON // check validity of `binary` param if (context.params.isMember(jss::binary) && !context.params[jss::binary].isBool()) { return RPC::invalid_field_error(jss::binary); } for (auto const field : {jss::secret, jss::seed, jss::seed_hex, jss::passphrase}) { if (context.params.isMember(field)) { return RPC::invalid_field_error(field); } } // get JSON equivalent of transaction tx_json = getTxJsonFromParams(context.params); if (tx_json.isMember(jss::error)) return tx_json; // autofill fields if they're not included (e.g. `Fee`, `Sequence`) if (auto error = autofillTx(tx_json, context)) return *error; STParsedJSONObject parsed(std::string(jss::tx_json), tx_json); if (!parsed.object.has_value()) return parsed.error; std::shared_ptr stTx; try { stTx = std::make_shared(std::move(parsed.object.value())); } catch (std::exception& e) { Json::Value jvResult = Json::objectValue; jvResult[jss::error] = "invalidTransaction"; jvResult[jss::error_exception] = e.what(); return jvResult; } if (stTx->getTxnType() == ttBATCH) { return RPC::make_error(rpcNOT_IMPL); } std::string reason; auto transaction = std::make_shared(stTx, reason, context.app); // Actually run the transaction through the transaction processor try { return simulateTxn(context, transaction); } // LCOV_EXCL_START this is just in case, so rippled doesn't crash catch (std::exception const& e) { Json::Value jvResult = Json::objectValue; jvResult[jss::error] = "internalSimulate"; jvResult[jss::error_exception] = e.what(); return jvResult; } // LCOV_EXCL_STOP } } // namespace xrpl