mirror of
https://github.com/XRPLF/rippled.git
synced 2026-04-29 15:37:57 +00:00
1317 lines
48 KiB
C++
1317 lines
48 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
This file is part of rippled: https://github.com/ripple/rippled
|
|
Copyright (c) 2012-2017 Ripple Labs Inc.
|
|
|
|
Permission to use, copy, modify, and/or 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 <test/jtx.h>
|
|
#include <test/jtx/Env.h>
|
|
#include <test/jtx/envconfig.h>
|
|
|
|
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
|
|
#include <xrpld/rpc/CTID.h>
|
|
|
|
#include <xrpl/protocol/ErrorCodes.h>
|
|
#include <xrpl/protocol/STBase.h>
|
|
#include <xrpl/protocol/STParsedJSON.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
#include <xrpl/protocol/serialize.h>
|
|
|
|
#include <optional>
|
|
#include <tuple>
|
|
|
|
namespace ripple {
|
|
|
|
namespace test {
|
|
|
|
class Simulate_test : public beast::unit_test::suite
|
|
{
|
|
void
|
|
checkBasicReturnValidity(
|
|
Json::Value const& result,
|
|
Json::Value const& tx,
|
|
int const expectedSequence,
|
|
std::string const& expectedFee)
|
|
{
|
|
BEAST_EXPECT(result[jss::applied] == false);
|
|
BEAST_EXPECT(result.isMember(jss::engine_result));
|
|
BEAST_EXPECT(result.isMember(jss::engine_result_code));
|
|
BEAST_EXPECT(result.isMember(jss::engine_result_message));
|
|
BEAST_EXPECT(
|
|
result.isMember(jss::tx_json) || result.isMember(jss::tx_blob));
|
|
|
|
Json::Value tx_json;
|
|
if (result.isMember(jss::tx_json))
|
|
{
|
|
tx_json = result[jss::tx_json];
|
|
}
|
|
else
|
|
{
|
|
auto const unHexed = strUnHex(result[jss::tx_blob].asString());
|
|
SerialIter sitTrans(makeSlice(*unHexed));
|
|
tx_json = STObject(std::ref(sitTrans), sfGeneric)
|
|
.getJson(JsonOptions::none);
|
|
}
|
|
BEAST_EXPECT(tx_json[jss::TransactionType] == tx[jss::TransactionType]);
|
|
BEAST_EXPECT(tx_json[jss::Account] == tx[jss::Account]);
|
|
BEAST_EXPECT(
|
|
tx_json[jss::SigningPubKey] == tx.get(jss::SigningPubKey, ""));
|
|
BEAST_EXPECT(
|
|
tx_json[jss::TxnSignature] == tx.get(jss::TxnSignature, ""));
|
|
BEAST_EXPECT(tx_json[jss::Fee] == tx.get(jss::Fee, expectedFee));
|
|
BEAST_EXPECT(
|
|
tx_json[jss::Sequence] == tx.get(jss::Sequence, expectedSequence));
|
|
}
|
|
|
|
void
|
|
checkBasicReturnValidity(
|
|
Json::Value const& result,
|
|
Json::Value const& tx,
|
|
int const expectedSequence,
|
|
XRPAmount const& expectedFee)
|
|
{
|
|
return checkBasicReturnValidity(
|
|
result, tx, expectedSequence, expectedFee.jsonClipped().asString());
|
|
}
|
|
|
|
void
|
|
testTx(
|
|
jtx::Env& env,
|
|
Json::Value const& tx,
|
|
std::function<void(Json::Value const&, Json::Value const&)> const&
|
|
validate,
|
|
bool testSerialized = true)
|
|
{
|
|
env.close();
|
|
|
|
Json::Value params;
|
|
params[jss::tx_json] = tx;
|
|
validate(env.rpc("json", "simulate", to_string(params)), tx);
|
|
|
|
params[jss::binary] = true;
|
|
validate(env.rpc("json", "simulate", to_string(params)), tx);
|
|
validate(env.rpc("simulate", to_string(tx)), tx);
|
|
validate(env.rpc("simulate", to_string(tx), "binary"), tx);
|
|
|
|
if (testSerialized)
|
|
{
|
|
// This cannot be tested in the multisign autofill scenario
|
|
// It is technically not a valid STObject, so the following line
|
|
// will crash
|
|
STParsedJSONObject const parsed(std::string(jss::tx_json), tx);
|
|
auto const tx_blob =
|
|
strHex(parsed.object->getSerializer().peekData());
|
|
if (BEAST_EXPECT(parsed.object.has_value()))
|
|
{
|
|
Json::Value params;
|
|
params[jss::tx_blob] = tx_blob;
|
|
validate(env.rpc("json", "simulate", to_string(params)), tx);
|
|
params[jss::binary] = true;
|
|
validate(env.rpc("json", "simulate", to_string(params)), tx);
|
|
}
|
|
validate(env.rpc("simulate", tx_blob), tx);
|
|
validate(env.rpc("simulate", tx_blob, "binary"), tx);
|
|
}
|
|
|
|
BEAST_EXPECTS(
|
|
env.current()->txCount() == 0,
|
|
std::to_string(env.current()->txCount()));
|
|
}
|
|
|
|
void
|
|
testTxJsonMetadataField(
|
|
jtx::Env& env,
|
|
Json::Value const& tx,
|
|
std::function<void(
|
|
Json::Value const&,
|
|
Json::Value const&,
|
|
Json::Value const&)> const& validate,
|
|
Json::Value const& expectedMetadataKey,
|
|
bool testSerialized = true)
|
|
{
|
|
env.close();
|
|
|
|
Json::Value params;
|
|
params[jss::tx_json] = tx;
|
|
validate(
|
|
env.rpc("json", "simulate", to_string(params)),
|
|
tx,
|
|
expectedMetadataKey);
|
|
validate(env.rpc("simulate", to_string(tx)), tx, expectedMetadataKey);
|
|
|
|
BEAST_EXPECTS(
|
|
env.current()->txCount() == 0,
|
|
std::to_string(env.current()->txCount()));
|
|
}
|
|
|
|
Json::Value
|
|
getJsonMetadata(Json::Value txResult) const
|
|
{
|
|
if (txResult.isMember(jss::meta_blob))
|
|
{
|
|
auto unHexed = strUnHex(txResult[jss::meta_blob].asString());
|
|
SerialIter sitTrans(makeSlice(*unHexed));
|
|
return STObject(std::ref(sitTrans), sfGeneric)
|
|
.getJson(JsonOptions::none);
|
|
}
|
|
|
|
return txResult[jss::meta];
|
|
}
|
|
|
|
void
|
|
testParamErrors()
|
|
{
|
|
testcase("Test parameter errors");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
Account const alice("alice");
|
|
|
|
{
|
|
// No params
|
|
Json::Value const params = Json::objectValue;
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Neither `tx_blob` nor `tx_json` included.");
|
|
}
|
|
{
|
|
// Providing both `tx_json` and `tx_blob`
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::tx_json] = Json::objectValue;
|
|
params[jss::tx_blob] = "1200";
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Can only include one of `tx_blob` and `tx_json`.");
|
|
}
|
|
{
|
|
// `binary` isn't a boolean
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::tx_blob] = "1200";
|
|
params[jss::binary] = "100";
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'binary'.");
|
|
}
|
|
{
|
|
// Invalid `tx_blob`
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::tx_blob] = "12";
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx_blob'.");
|
|
}
|
|
{
|
|
// Empty `tx_json`
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::tx_json] = Json::objectValue;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Missing field 'tx.TransactionType'.");
|
|
}
|
|
{
|
|
// No tx.Account
|
|
Json::Value params = Json::objectValue;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::Payment;
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Missing field 'tx.Account'.");
|
|
}
|
|
{
|
|
// Empty `tx_blob`
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::tx_blob] = "";
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx_blob'.");
|
|
}
|
|
{
|
|
// Non-string `tx_blob`
|
|
Json::Value params;
|
|
params[jss::tx_blob] = 1.1;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx_blob'.");
|
|
}
|
|
{
|
|
// Non-object `tx_json`
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::tx_json] = "";
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx_json', not object.");
|
|
}
|
|
{
|
|
// `seed` field included
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::seed] = "doesnt_matter";
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
params[jss::tx_json] = tx_json;
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'seed'.");
|
|
}
|
|
{
|
|
// `secret` field included
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::secret] = "doesnt_matter";
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
params[jss::tx_json] = tx_json;
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'secret'.");
|
|
}
|
|
{
|
|
// `seed_hex` field included
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::seed_hex] = "doesnt_matter";
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
params[jss::tx_json] = tx_json;
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'seed_hex'.");
|
|
}
|
|
{
|
|
// `passphrase` field included
|
|
Json::Value params = Json::objectValue;
|
|
params[jss::passphrase] = "doesnt_matter";
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
params[jss::tx_json] = tx_json;
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'passphrase'.");
|
|
}
|
|
{
|
|
// Invalid transaction
|
|
Json::Value params = Json::objectValue;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::Payment;
|
|
tx_json[jss::Account] = env.master.human();
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_exception] ==
|
|
"Field 'Destination' is required but missing.");
|
|
}
|
|
{
|
|
// Bad account
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = "badAccount";
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECTS(
|
|
resp[jss::result][jss::error] == "srcActMalformed",
|
|
resp[jss::result][jss::error].toStyledString());
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx.Account'.");
|
|
}
|
|
{
|
|
// Account doesn't exist for Sequence autofill
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = alice.human();
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Source account not found.");
|
|
}
|
|
{
|
|
// Invalid Signers field
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
tx_json[sfSigners] = "1";
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx.Signers'.");
|
|
}
|
|
{
|
|
// Invalid Signers field
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
tx_json[sfSigners] = Json::arrayValue;
|
|
tx_json[sfSigners].append("1");
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Invalid field 'tx.Signers[0]'.");
|
|
}
|
|
{
|
|
// Invalid transaction
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
tx_json["foo"] = "bar";
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Field 'tx_json.foo' is unknown.");
|
|
}
|
|
{
|
|
// non-`"binary"` second param for CLI
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = alice.human();
|
|
auto const resp = env.rpc("simulate", to_string(tx_json), "1");
|
|
BEAST_EXPECT(resp[jss::error_message] == "Invalid parameters.");
|
|
}
|
|
{
|
|
// Signed transaction
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
tx_json[jss::TxnSignature] = "1200ABCD";
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Transaction should not be signed.");
|
|
}
|
|
{
|
|
// Signed multisig transaction
|
|
Json::Value params;
|
|
Json::Value tx_json = Json::objectValue;
|
|
tx_json[jss::TransactionType] = jss::AccountSet;
|
|
tx_json[jss::Account] = env.master.human();
|
|
tx_json[sfSigners] = Json::arrayValue;
|
|
{
|
|
Json::Value signer;
|
|
signer[jss::Account] = alice.human();
|
|
signer[jss::SigningPubKey] = alice.human();
|
|
signer[jss::TxnSignature] = "1200ABCD";
|
|
Json::Value signerOuter;
|
|
signerOuter[sfSigner] = signer;
|
|
tx_json[sfSigners].append(signerOuter);
|
|
}
|
|
params[jss::tx_json] = tx_json;
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] ==
|
|
"Transaction should not be signed.");
|
|
}
|
|
}
|
|
|
|
void
|
|
testFeeError()
|
|
{
|
|
testcase("Fee failure");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
|
|
cfg->section("transaction_queue")
|
|
.set("minimum_txn_in_ledger_standalone", "3");
|
|
return cfg;
|
|
}));
|
|
|
|
Account const alice{"alice"};
|
|
env.fund(XRP(1000000), alice);
|
|
env.close();
|
|
|
|
// fill queue
|
|
auto metrics = env.app().getTxQ().getMetrics(*env.current());
|
|
for (int i = metrics.txInLedger; i <= metrics.txPerLedger; ++i)
|
|
env(noop(alice));
|
|
|
|
{
|
|
Json::Value params;
|
|
params[jss::tx_json] = noop(alice);
|
|
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
auto const result = resp[jss::result];
|
|
if (BEAST_EXPECT(result.isMember(jss::error)))
|
|
{
|
|
BEAST_EXPECT(result[jss::error] == "highFee");
|
|
BEAST_EXPECT(result[jss::error_code] == rpcHIGH_FEE);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testInvalidTransactionType()
|
|
{
|
|
testcase("Invalid transaction type");
|
|
|
|
using namespace jtx;
|
|
|
|
Env env(*this);
|
|
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
env.fund(XRP(1000000), alice, bob);
|
|
env.close();
|
|
|
|
auto const batchFee = batch::calcBatchFee(env, 0, 2);
|
|
auto const seq = env.seq(alice);
|
|
auto jt = env.jtnofill(
|
|
batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing),
|
|
batch::inner(pay(alice, bob, XRP(10)), seq + 1),
|
|
batch::inner(pay(alice, bob, XRP(10)), seq + 1));
|
|
|
|
jt.jv.removeMember(jss::TxnSignature);
|
|
Json::Value params;
|
|
params[jss::tx_json] = jt.jv;
|
|
auto const resp = env.rpc("json", "simulate", to_string(params));
|
|
BEAST_EXPECT(resp[jss::result][jss::error] == "notImpl");
|
|
BEAST_EXPECT(
|
|
resp[jss::result][jss::error_message] == "Not implemented.");
|
|
}
|
|
|
|
void
|
|
testSuccessfulTransaction()
|
|
{
|
|
testcase("Successful transaction");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->NETWORK_ID = 0;
|
|
return cfg;
|
|
})};
|
|
static auto const newDomain = "123ABC";
|
|
|
|
{
|
|
auto validateOutput = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result, tx, 1, env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == 0);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"The simulated transaction would have been applied.");
|
|
|
|
if (BEAST_EXPECT(
|
|
result.isMember(jss::meta) ||
|
|
result.isMember(jss::meta_blob)))
|
|
{
|
|
Json::Value const metadata = getJsonMetadata(result);
|
|
|
|
if (BEAST_EXPECT(
|
|
metadata.isMember(sfAffectedNodes.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
metadata[sfAffectedNodes.jsonName].size() == 1);
|
|
auto node = metadata[sfAffectedNodes.jsonName][0u];
|
|
if (BEAST_EXPECT(
|
|
node.isMember(sfModifiedNode.jsonName)))
|
|
{
|
|
auto modifiedNode = node[sfModifiedNode];
|
|
BEAST_EXPECT(
|
|
modifiedNode[sfLedgerEntryType] ==
|
|
"AccountRoot");
|
|
auto finalFields = modifiedNode[sfFinalFields];
|
|
BEAST_EXPECT(finalFields[sfDomain] == newDomain);
|
|
}
|
|
}
|
|
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
|
|
}
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::AccountSet;
|
|
tx[sfDomain] = newDomain;
|
|
|
|
// test with autofill
|
|
testTx(env, tx, validateOutput);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = 1;
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
|
|
// test without autofill
|
|
testTx(env, tx, validateOutput);
|
|
}
|
|
}
|
|
|
|
void
|
|
testTransactionNonTecFailure()
|
|
{
|
|
testcase("Transaction non-tec failure");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
Account const alice("alice");
|
|
|
|
{
|
|
std::function<void(Json::Value const&, Json::Value const&)> const&
|
|
testSimulation = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result, tx, 1, env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "temBAD_AMOUNT");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == -298);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"Malformed: Bad amount.");
|
|
|
|
BEAST_EXPECT(
|
|
!result.isMember(jss::meta) &&
|
|
!result.isMember(jss::meta_blob));
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::Payment;
|
|
tx[sfDestination] = alice.human();
|
|
tx[sfAmount] = "0"; // invalid amount
|
|
|
|
// test with autofill
|
|
testTx(env, tx, testSimulation);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = 1;
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
|
|
// test without autofill
|
|
testTx(env, tx, testSimulation);
|
|
}
|
|
}
|
|
|
|
void
|
|
testTransactionTecFailure()
|
|
{
|
|
testcase("Transaction tec failure");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
Account const alice("alice");
|
|
|
|
{
|
|
std::function<void(Json::Value const&, Json::Value const&)> const&
|
|
testSimulation = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result, tx, 1, env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result] == "tecNO_DST_INSUF_XRP");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == 125);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"Destination does not exist. Too little XRP sent to "
|
|
"create "
|
|
"it.");
|
|
|
|
if (BEAST_EXPECT(
|
|
result.isMember(jss::meta) ||
|
|
result.isMember(jss::meta_blob)))
|
|
{
|
|
Json::Value const metadata = getJsonMetadata(result);
|
|
|
|
if (BEAST_EXPECT(
|
|
metadata.isMember(sfAffectedNodes.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
metadata[sfAffectedNodes.jsonName].size() == 1);
|
|
auto node = metadata[sfAffectedNodes.jsonName][0u];
|
|
if (BEAST_EXPECT(
|
|
node.isMember(sfModifiedNode.jsonName)))
|
|
{
|
|
auto modifiedNode = node[sfModifiedNode];
|
|
BEAST_EXPECT(
|
|
modifiedNode[sfLedgerEntryType] ==
|
|
"AccountRoot");
|
|
auto finalFields = modifiedNode[sfFinalFields];
|
|
BEAST_EXPECT(
|
|
finalFields[sfBalance] ==
|
|
std::to_string(
|
|
100'000'000'000'000'000 -
|
|
env.current()->fees().base.drops()));
|
|
}
|
|
}
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionIndex.jsonName] == 0);
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionResult.jsonName] ==
|
|
"tecNO_DST_INSUF_XRP");
|
|
}
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::Payment;
|
|
tx[sfDestination] = alice.human();
|
|
tx[sfAmount] = "1"; // not enough to create an account
|
|
|
|
// test with autofill
|
|
testTx(env, tx, testSimulation);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = 1;
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
|
|
// test without autofill
|
|
testTx(env, tx, testSimulation);
|
|
}
|
|
}
|
|
|
|
void
|
|
testSuccessfulTransactionMultisigned()
|
|
{
|
|
testcase("Successful multi-signed transaction");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
static auto const newDomain = "123ABC";
|
|
Account const alice("alice");
|
|
Account const becky("becky");
|
|
Account const carol("carol");
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// set up valid multisign
|
|
env(signers(alice, 1, {{becky, 1}, {carol, 1}}));
|
|
env.close();
|
|
|
|
{
|
|
auto validateOutput = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result,
|
|
tx,
|
|
env.seq(alice),
|
|
tx.isMember(jss::Signers) ? env.current()->fees().base * 2
|
|
: env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == 0);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"The simulated transaction would have been applied.");
|
|
|
|
if (BEAST_EXPECT(
|
|
result.isMember(jss::meta) ||
|
|
result.isMember(jss::meta_blob)))
|
|
{
|
|
Json::Value const metadata = getJsonMetadata(result);
|
|
|
|
if (BEAST_EXPECT(
|
|
metadata.isMember(sfAffectedNodes.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
metadata[sfAffectedNodes.jsonName].size() == 1);
|
|
auto node = metadata[sfAffectedNodes.jsonName][0u];
|
|
if (BEAST_EXPECT(
|
|
node.isMember(sfModifiedNode.jsonName)))
|
|
{
|
|
auto modifiedNode = node[sfModifiedNode];
|
|
BEAST_EXPECT(
|
|
modifiedNode[sfLedgerEntryType] ==
|
|
"AccountRoot");
|
|
auto finalFields = modifiedNode[sfFinalFields];
|
|
BEAST_EXPECT(finalFields[sfDomain] == newDomain);
|
|
}
|
|
}
|
|
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
|
|
}
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = alice.human();
|
|
tx[jss::TransactionType] = jss::AccountSet;
|
|
tx[sfDomain] = newDomain;
|
|
|
|
// test with autofill
|
|
testTx(env, tx, validateOutput, false);
|
|
|
|
tx[sfSigners] = Json::arrayValue;
|
|
{
|
|
Json::Value signer;
|
|
signer[jss::Account] = becky.human();
|
|
Json::Value signerOuter;
|
|
signerOuter[sfSigner] = signer;
|
|
tx[sfSigners].append(signerOuter);
|
|
}
|
|
|
|
// test with just signer accounts
|
|
testTx(env, tx, validateOutput, false);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = env.seq(alice);
|
|
// transaction requires a non-base fee
|
|
tx[sfFee] =
|
|
(env.current()->fees().base * 2).jsonClipped().asString();
|
|
tx[sfSigners][0u][sfSigner][jss::SigningPubKey] = "";
|
|
tx[sfSigners][0u][sfSigner][jss::TxnSignature] = "";
|
|
|
|
// test without autofill
|
|
testTx(env, tx, validateOutput);
|
|
}
|
|
}
|
|
|
|
void
|
|
testTransactionSigningFailure()
|
|
{
|
|
testcase("Transaction with a key-related failure");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
static auto const newDomain = "123ABC";
|
|
Account const alice{"alice"};
|
|
env(regkey(env.master, alice));
|
|
env(fset(env.master, asfDisableMaster), sig(env.master));
|
|
env.close();
|
|
|
|
{
|
|
std::function<void(Json::Value const&, Json::Value const&)> const&
|
|
testSimulation =
|
|
[&](Json::Value const& resp, Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result,
|
|
tx,
|
|
env.seq(env.master),
|
|
env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result] == "tefMASTER_DISABLED");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == -188);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"Master key is disabled.");
|
|
|
|
BEAST_EXPECT(
|
|
!result.isMember(jss::meta) &&
|
|
!result.isMember(jss::meta_blob));
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::AccountSet;
|
|
tx[sfDomain] = newDomain;
|
|
// master key is disabled, so this is invalid
|
|
tx[jss::SigningPubKey] = strHex(env.master.pk().slice());
|
|
|
|
// test with autofill
|
|
testTx(env, tx, testSimulation);
|
|
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = env.seq(env.master);
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
|
|
// test without autofill
|
|
testTx(env, tx, testSimulation);
|
|
}
|
|
}
|
|
|
|
void
|
|
testInvalidSingleAndMultiSigningTransaction()
|
|
{
|
|
testcase(
|
|
"Transaction with both single-signing SigningPubKey and "
|
|
"multi-signing Signers");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
static auto const newDomain = "123ABC";
|
|
Account const alice("alice");
|
|
Account const becky("becky");
|
|
Account const carol("carol");
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// set up valid multisign
|
|
env(signers(alice, 1, {{becky, 1}, {carol, 1}}));
|
|
env.close();
|
|
|
|
{
|
|
std::function<void(Json::Value const&, Json::Value const&)> const&
|
|
testSimulation = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result,
|
|
tx,
|
|
env.seq(env.master),
|
|
env.current()->fees().base * 2);
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "temINVALID");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == -277);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"The transaction is ill-formed.");
|
|
|
|
BEAST_EXPECT(
|
|
!result.isMember(jss::meta) &&
|
|
!result.isMember(jss::meta_blob));
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::AccountSet;
|
|
tx[sfDomain] = newDomain;
|
|
// master key is disabled, so this is invalid
|
|
tx[jss::SigningPubKey] = strHex(env.master.pk().slice());
|
|
tx[sfSigners] = Json::arrayValue;
|
|
{
|
|
Json::Value signer;
|
|
signer[jss::Account] = becky.human();
|
|
Json::Value signerOuter;
|
|
signerOuter[sfSigner] = signer;
|
|
tx[sfSigners].append(signerOuter);
|
|
}
|
|
|
|
// test with autofill
|
|
testTx(env, tx, testSimulation, false);
|
|
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = env.seq(env.master);
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
tx[sfSigners][0u][sfSigner][jss::SigningPubKey] =
|
|
strHex(becky.pk().slice());
|
|
tx[sfSigners][0u][sfSigner][jss::TxnSignature] = "";
|
|
|
|
// test without autofill
|
|
testTx(env, tx, testSimulation);
|
|
}
|
|
}
|
|
|
|
void
|
|
testMultisignedBadPubKey()
|
|
{
|
|
testcase("Multi-signed transaction with a bad public key");
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
static auto const newDomain = "123ABC";
|
|
Account const alice("alice");
|
|
Account const becky("becky");
|
|
Account const carol("carol");
|
|
Account const dylan("dylan");
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
// set up valid multisign
|
|
env(signers(alice, 1, {{becky, 1}, {carol, 1}}));
|
|
|
|
{
|
|
auto validateOutput = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result, tx, env.seq(alice), env.current()->fees().base * 2);
|
|
|
|
BEAST_EXPECTS(
|
|
result[jss::engine_result] == "tefBAD_SIGNATURE",
|
|
result[jss::engine_result].toStyledString());
|
|
BEAST_EXPECT(result[jss::engine_result_code] == -186);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"A signature is provided for a non-signer.");
|
|
|
|
BEAST_EXPECT(
|
|
!result.isMember(jss::meta) &&
|
|
!result.isMember(jss::meta_blob));
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = alice.human();
|
|
tx[jss::TransactionType] = jss::AccountSet;
|
|
tx[sfDomain] = newDomain;
|
|
tx[sfSigners] = Json::arrayValue;
|
|
{
|
|
Json::Value signer;
|
|
signer[jss::Account] = becky.human();
|
|
signer[jss::SigningPubKey] = strHex(dylan.pk().slice());
|
|
Json::Value signerOuter;
|
|
signerOuter[sfSigner] = signer;
|
|
tx[sfSigners].append(signerOuter);
|
|
}
|
|
|
|
// test with autofill
|
|
testTx(env, tx, validateOutput, false);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = env.seq(alice);
|
|
// transaction requires a non-base fee
|
|
tx[sfFee] =
|
|
(env.current()->fees().base * 2).jsonClipped().asString();
|
|
tx[sfSigners][0u][sfSigner][jss::TxnSignature] = "";
|
|
|
|
// test without autofill
|
|
testTx(env, tx, validateOutput);
|
|
}
|
|
}
|
|
|
|
void
|
|
testDeleteExpiredCredentials()
|
|
{
|
|
testcase("Credentials aren't actually deleted on `tecEXPIRED`");
|
|
|
|
// scenario setup
|
|
|
|
using namespace jtx;
|
|
Env env(*this);
|
|
|
|
Account const subject{"subject"};
|
|
Account const issuer{"issuer"};
|
|
|
|
env.fund(XRP(10000), subject, issuer);
|
|
env.close();
|
|
|
|
auto const credType = "123ABC";
|
|
|
|
auto jv = credentials::create(subject, issuer, credType);
|
|
uint32_t const t =
|
|
env.current()->info().parentCloseTime.time_since_epoch().count();
|
|
jv[sfExpiration.jsonName] = t;
|
|
env(jv);
|
|
env.close();
|
|
|
|
{
|
|
auto validateOutput = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result, tx, env.seq(subject), env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "tecEXPIRED");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == 148);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"Expiration time is passed.");
|
|
|
|
if (BEAST_EXPECT(
|
|
result.isMember(jss::meta) ||
|
|
result.isMember(jss::meta_blob)))
|
|
{
|
|
Json::Value const metadata = getJsonMetadata(result);
|
|
|
|
if (BEAST_EXPECT(
|
|
metadata.isMember(sfAffectedNodes.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
metadata[sfAffectedNodes.jsonName].size() == 5);
|
|
|
|
try
|
|
{
|
|
bool found = false;
|
|
for (auto const& node :
|
|
metadata[sfAffectedNodes.jsonName])
|
|
{
|
|
if (node.isMember(sfDeletedNode.jsonName) &&
|
|
node[sfDeletedNode.jsonName]
|
|
[sfLedgerEntryType.jsonName]
|
|
.asString() == "Credential")
|
|
{
|
|
auto const deleted =
|
|
node[sfDeletedNode.jsonName]
|
|
[sfFinalFields.jsonName];
|
|
found = deleted[jss::Issuer] ==
|
|
issuer.human() &&
|
|
deleted[jss::Subject] ==
|
|
subject.human() &&
|
|
deleted["CredentialType"] ==
|
|
strHex(std::string_view(credType));
|
|
break;
|
|
}
|
|
}
|
|
BEAST_EXPECT(found);
|
|
}
|
|
catch (...)
|
|
{
|
|
fail();
|
|
}
|
|
}
|
|
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionResult.jsonName] == "tecEXPIRED");
|
|
}
|
|
};
|
|
|
|
Json::Value tx = credentials::accept(subject, issuer, credType);
|
|
|
|
// test with autofill
|
|
testTx(env, tx, validateOutput);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = env.seq(subject);
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
|
|
// test without autofill
|
|
testTx(env, tx, validateOutput);
|
|
}
|
|
|
|
// check that expired credentials weren't deleted
|
|
auto const jle =
|
|
credentials::ledgerEntry(env, subject, issuer, credType);
|
|
BEAST_EXPECT(
|
|
jle.isObject() && jle.isMember(jss::result) &&
|
|
!jle[jss::result].isMember(jss::error) &&
|
|
jle[jss::result].isMember(jss::node) &&
|
|
jle[jss::result][jss::node].isMember("LedgerEntryType") &&
|
|
jle[jss::result][jss::node]["LedgerEntryType"] == jss::Credential &&
|
|
jle[jss::result][jss::node][jss::Issuer] == issuer.human() &&
|
|
jle[jss::result][jss::node][jss::Subject] == subject.human() &&
|
|
jle[jss::result][jss::node]["CredentialType"] ==
|
|
strHex(std::string_view(credType)));
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 1);
|
|
BEAST_EXPECT(ownerCount(env, subject) == 0);
|
|
}
|
|
|
|
void
|
|
testSuccessfulTransactionNetworkID()
|
|
{
|
|
testcase("Successful transaction with a custom network ID");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->NETWORK_ID = 1025;
|
|
return cfg;
|
|
})};
|
|
static auto const newDomain = "123ABC";
|
|
|
|
{
|
|
auto validateOutput = [&](Json::Value const& resp,
|
|
Json::Value const& tx) {
|
|
auto result = resp[jss::result];
|
|
checkBasicReturnValidity(
|
|
result, tx, 1, env.current()->fees().base);
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == 0);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"The simulated transaction would have been applied.");
|
|
|
|
if (BEAST_EXPECT(
|
|
result.isMember(jss::meta) ||
|
|
result.isMember(jss::meta_blob)))
|
|
{
|
|
Json::Value const metadata = getJsonMetadata(result);
|
|
|
|
if (BEAST_EXPECT(
|
|
metadata.isMember(sfAffectedNodes.jsonName)))
|
|
{
|
|
BEAST_EXPECT(
|
|
metadata[sfAffectedNodes.jsonName].size() == 1);
|
|
auto node = metadata[sfAffectedNodes.jsonName][0u];
|
|
if (BEAST_EXPECT(
|
|
node.isMember(sfModifiedNode.jsonName)))
|
|
{
|
|
auto modifiedNode = node[sfModifiedNode];
|
|
BEAST_EXPECT(
|
|
modifiedNode[sfLedgerEntryType] ==
|
|
"AccountRoot");
|
|
auto finalFields = modifiedNode[sfFinalFields];
|
|
BEAST_EXPECT(finalFields[sfDomain] == newDomain);
|
|
}
|
|
}
|
|
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
|
|
}
|
|
};
|
|
|
|
Json::Value tx;
|
|
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::AccountSet;
|
|
tx[sfDomain] = newDomain;
|
|
|
|
// test with autofill
|
|
testTx(env, tx, validateOutput);
|
|
|
|
tx[sfSigningPubKey] = "";
|
|
tx[sfTxnSignature] = "";
|
|
tx[sfSequence] = 1;
|
|
tx[sfFee] = env.current()->fees().base.jsonClipped().asString();
|
|
tx[sfNetworkID] = 1025;
|
|
|
|
// test without autofill
|
|
testTx(env, tx, validateOutput);
|
|
}
|
|
}
|
|
|
|
void
|
|
testSuccessfulTransactionAdditionalMetadata()
|
|
{
|
|
testcase("Successful transaction with additional metadata");
|
|
|
|
using namespace jtx;
|
|
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
|
cfg->NETWORK_ID = 1025;
|
|
return cfg;
|
|
})};
|
|
|
|
Account const alice("alice");
|
|
|
|
env.fund(XRP(10000), alice);
|
|
env.close();
|
|
|
|
{
|
|
auto validateOutput = [&](Json::Value const& resp,
|
|
Json::Value const& tx,
|
|
Json::Value const& expectedMetadataKey) {
|
|
auto result = resp[jss::result];
|
|
|
|
BEAST_EXPECT(result[jss::engine_result] == "tesSUCCESS");
|
|
BEAST_EXPECT(result[jss::engine_result_code] == 0);
|
|
BEAST_EXPECT(
|
|
result[jss::engine_result_message] ==
|
|
"The simulated transaction would have been applied.");
|
|
|
|
if (BEAST_EXPECT(
|
|
result.isMember(jss::meta) ||
|
|
result.isMember(jss::meta_blob)))
|
|
{
|
|
Json::Value const metadata = getJsonMetadata(result);
|
|
|
|
BEAST_EXPECT(metadata[sfTransactionIndex.jsonName] == 0);
|
|
BEAST_EXPECT(
|
|
metadata[sfTransactionResult.jsonName] == "tesSUCCESS");
|
|
BEAST_EXPECT(
|
|
metadata.isMember(expectedMetadataKey.asString()));
|
|
}
|
|
};
|
|
|
|
{
|
|
Json::Value tx;
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::Payment;
|
|
tx[sfDestination] = alice.human();
|
|
tx[sfAmount] = "100";
|
|
|
|
// test delivered amount
|
|
testTxJsonMetadataField(
|
|
env, tx, validateOutput, jss::delivered_amount);
|
|
}
|
|
|
|
{
|
|
Json::Value tx;
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::NFTokenMint;
|
|
tx[sfNFTokenTaxon] = 1;
|
|
|
|
// test nft synthetic
|
|
testTxJsonMetadataField(
|
|
env, tx, validateOutput, jss::nftoken_id);
|
|
}
|
|
|
|
{
|
|
Json::Value tx;
|
|
tx[jss::Account] = env.master.human();
|
|
tx[jss::TransactionType] = jss::MPTokenIssuanceCreate;
|
|
|
|
// test mpt issuance id
|
|
testTxJsonMetadataField(
|
|
env, tx, validateOutput, jss::mpt_issuance_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
public:
|
|
void
|
|
run() override
|
|
{
|
|
testParamErrors();
|
|
testFeeError();
|
|
testInvalidTransactionType();
|
|
testSuccessfulTransaction();
|
|
testTransactionNonTecFailure();
|
|
testTransactionTecFailure();
|
|
testSuccessfulTransactionMultisigned();
|
|
testTransactionSigningFailure();
|
|
testInvalidSingleAndMultiSigningTransaction();
|
|
testMultisignedBadPubKey();
|
|
testDeleteExpiredCredentials();
|
|
testSuccessfulTransactionNetworkID();
|
|
testSuccessfulTransactionAdditionalMetadata();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(Simulate, rpc, ripple);
|
|
|
|
} // namespace test
|
|
|
|
} // namespace ripple
|