#include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { 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)); // NOLINT(bugprone-unchecked-optional-access) 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) { checkBasicReturnValidity( result, tx, expectedSequence, expectedFee.jsonClipped().asString()); } void testTx( jtx::Env& env, Json::Value const& tx, std::function 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); // NOLINTNEXTLINE(bugprone-unchecked-optional-access) 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 const& validate, Json::Value const& expectedMetadataKey, Json::Value const& expectedMetadataValue) { env.close(); Json::Value params; params[jss::tx_json] = tx; validate( env.rpc("json", "simulate", to_string(params)), tx, expectedMetadataKey, expectedMetadataValue); validate( env.rpc("simulate", to_string(tx)), tx, expectedMetadataKey, expectedMetadataValue); BEAST_EXPECTS(env.current()->txCount() == 0, std::to_string(env.current()->txCount())); } static Json::Value getJsonMetadata(Json::Value txResult) { if (txResult.isMember(jss::meta_blob)) { auto unHexed = strUnHex(txResult[jss::meta_blob].asString()); SerialIter sitTrans(makeSlice(*unHexed)); // NOLINT(bugprone-unchecked-optional-access) 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] = "random_data"; 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] = "random_data"; 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] = "random_data"; 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] = "random_data"; 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."); } { // tfInnerBatchTxn flag on top-level 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::Flags] = tfInnerBatchTxn; params[jss::tx_json] = tx_json; auto const resp = env.rpc("json", "simulate", to_string(params)); BEAST_EXPECT( resp[jss::result][jss::error_message] == "tfInnerBatchTxn flag is not allowed on top-level transactions."); } } void testFeeError() { testcase("Fee failure"); using namespace jtx; Env env(*this, envconfig([](std::unique_ptr 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 + 2)); 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 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 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 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 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 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()->header().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 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; using namespace std::chrono_literals; Env env{*this, envconfig([&](std::unique_ptr cfg) { cfg->NETWORK_ID = 1025; return cfg; })}; Account const alice("alice"); Account const bob("bob"); env.fund(XRP(10000), alice, bob); env.close(); // deliver_amount is unavailable in the metadata before 2014-02-01 // so proceed to 2014-02-01 env.close(NetClock::time_point{446000000s}); { auto validateOutput = [&](Json::Value const& resp, Json::Value const& tx, Json::Value const& expectedMetadataKey, Json::Value const& expectedMetadataValue) { 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())); BEAST_EXPECT(metadata[expectedMetadataKey.asString()] == expectedMetadataValue); } }; { Json::Value tx; tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::Payment; tx[sfDestination] = bob.human(); tx[sfAmount] = "100"; // test delivered amount testTxJsonMetadataField(env, tx, validateOutput, jss::delivered_amount, "100"); } { Json::Value tx; tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::NFTokenMint; tx[sfNFTokenTaxon] = 1; Json::Value const nftokenId = to_string(token::getNextID(env, alice, 1)); // test nft synthetic testTxJsonMetadataField(env, tx, validateOutput, jss::nftoken_id, nftokenId); } { Json::Value tx; tx[jss::Account] = alice.human(); tx[jss::TransactionType] = jss::MPTokenIssuanceCreate; Json::Value const mptIssuanceId = to_string(makeMptID(env.seq(alice), alice)); // test mpt issuance id testTxJsonMetadataField( env, tx, validateOutput, jss::mpt_issuance_id, mptIssuanceId); } } } public: void run() override { testParamErrors(); testFeeError(); testInvalidTransactionType(); testSuccessfulTransaction(); testTransactionNonTecFailure(); testTransactionTecFailure(); testSuccessfulTransactionMultisigned(); testTransactionSigningFailure(); testInvalidSingleAndMultiSigningTransaction(); testMultisignedBadPubKey(); testDeleteExpiredCredentials(); testSuccessfulTransactionNetworkID(); testSuccessfulTransactionAdditionalMetadata(); } }; BEAST_DEFINE_TESTSUITE(Simulate, rpc, xrpl); } // namespace test } // namespace xrpl