mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-05 01:37:00 +00:00
1412 lines
49 KiB
C++
1412 lines
49 KiB
C++
|
|
#include <test/jtx/Account.h>
|
|
#include <test/jtx/Env.h>
|
|
#include <test/jtx/TestHelpers.h>
|
|
#include <test/jtx/amount.h>
|
|
#include <test/jtx/balance.h> // IWYU pragma: keep
|
|
#include <test/jtx/credentials.h>
|
|
#include <test/jtx/deposit.h>
|
|
#include <test/jtx/escrow.h>
|
|
#include <test/jtx/fee.h>
|
|
#include <test/jtx/flags.h>
|
|
#include <test/jtx/noop.h>
|
|
#include <test/jtx/offer.h>
|
|
#include <test/jtx/owners.h>
|
|
#include <test/jtx/paths.h>
|
|
#include <test/jtx/pay.h>
|
|
#include <test/jtx/require.h>
|
|
#include <test/jtx/sendmax.h>
|
|
#include <test/jtx/seq.h>
|
|
#include <test/jtx/ter.h>
|
|
#include <test/jtx/ticket.h>
|
|
#include <test/jtx/trust.h>
|
|
#include <test/jtx/txflags.h>
|
|
|
|
#include <xrpl/basics/strHex.h>
|
|
#include <xrpl/beast/unit_test/suite.h>
|
|
#include <xrpl/json/json_value.h>
|
|
#include <xrpl/json/to_string.h>
|
|
#include <xrpl/protocol/AccountID.h>
|
|
#include <xrpl/protocol/Feature.h>
|
|
#include <xrpl/protocol/LedgerFormats.h>
|
|
#include <xrpl/protocol/SField.h>
|
|
#include <xrpl/protocol/STAmount.h>
|
|
#include <xrpl/protocol/TER.h>
|
|
#include <xrpl/protocol/TxFlags.h>
|
|
#include <xrpl/protocol/XRPAmount.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <random>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace xrpl::test {
|
|
|
|
// Helper function that returns the reserve on an account based on
|
|
// the passed in number of owners.
|
|
static XRPAmount
|
|
reserve(jtx::Env& env, std::uint32_t count)
|
|
{
|
|
return env.current()->fees().accountReserve(count);
|
|
}
|
|
|
|
// Helper function that returns true if acct has the lsfDepositAuth flag set.
|
|
static bool
|
|
hasDepositAuth(jtx::Env const& env, jtx::Account const& acct)
|
|
{
|
|
return env.le(acct)->isFlag(lsfDepositAuth);
|
|
}
|
|
|
|
struct DepositAuth_test : public beast::unit_test::Suite
|
|
{
|
|
void
|
|
testEnable()
|
|
{
|
|
testcase("Enable");
|
|
|
|
using namespace jtx;
|
|
Account const alice{"alice"};
|
|
|
|
{
|
|
Env env(*this);
|
|
env.fund(XRP(10000), alice);
|
|
|
|
env(fset(alice, asfDepositAuth));
|
|
env.close();
|
|
BEAST_EXPECT(hasDepositAuth(env, alice));
|
|
|
|
env(fclear(alice, asfDepositAuth));
|
|
env.close();
|
|
BEAST_EXPECT(!hasDepositAuth(env, alice));
|
|
}
|
|
}
|
|
|
|
void
|
|
testPayIOU()
|
|
{
|
|
// Exercise IOU payments and non-direct XRP payments to an account
|
|
// that has the lsfDepositAuth flag set.
|
|
testcase("Pay IOU");
|
|
|
|
using namespace jtx;
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const carol{"carol"};
|
|
Account const gw{"gw"};
|
|
IOU const usd = gw["USD"];
|
|
|
|
Env env(*this);
|
|
|
|
env.fund(XRP(10000), alice, bob, carol, gw);
|
|
env.close();
|
|
env.trust(usd(1000), alice, bob);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, usd(150)));
|
|
env(offer(carol, usd(100), XRP(100)));
|
|
env.close();
|
|
|
|
// Make sure bob's trust line is all set up so he can receive USD.
|
|
env(pay(alice, bob, usd(50)));
|
|
env.close();
|
|
|
|
// bob sets the lsfDepositAuth flag.
|
|
env(fset(bob, asfDepositAuth), Require(Flags(bob, asfDepositAuth)));
|
|
env.close();
|
|
|
|
// None of the following payments should succeed.
|
|
auto failedIouPayments = [this, &env, &alice, &bob, &usd]() {
|
|
env.require(Flags(bob, asfDepositAuth));
|
|
|
|
// Capture bob's balances before hand to confirm they don't change.
|
|
PrettyAmount const bobXrpBalance{env.balance(bob, XRP)};
|
|
PrettyAmount const bobUsdBalance{env.balance(bob, usd)};
|
|
|
|
env(pay(alice, bob, usd(50)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Note that even though alice is paying bob in XRP, the payment
|
|
// is still not allowed since the payment passes through an offer.
|
|
env(pay(alice, bob, drops(1)), Sendmax(usd(1)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(bobXrpBalance == env.balance(bob, XRP));
|
|
BEAST_EXPECT(bobUsdBalance == env.balance(bob, usd));
|
|
};
|
|
|
|
// Test when bob has an XRP balance > base reserve.
|
|
failedIouPayments();
|
|
|
|
// Set bob's XRP balance == base reserve. Also demonstrate that
|
|
// bob can make payments while his lsfDepositAuth flag is set.
|
|
env(pay(bob, alice, usd(25)));
|
|
env.close();
|
|
|
|
{
|
|
STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)};
|
|
XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)};
|
|
env(pay(bob, alice, bobPaysXRP), Fee(bobPaysFee));
|
|
env.close();
|
|
}
|
|
|
|
// Test when bob's XRP balance == base reserve.
|
|
BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));
|
|
BEAST_EXPECT(env.balance(bob, usd) == usd(25));
|
|
failedIouPayments();
|
|
|
|
// Test when bob has an XRP balance == 0.
|
|
env(noop(bob), Fee(reserve(env, 0)));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(env.balance(bob, XRP) == XRP(0));
|
|
failedIouPayments();
|
|
|
|
// Give bob enough XRP for the fee to clear the lsfDepositAuth flag.
|
|
env(pay(alice, bob, drops(env.current()->fees().base)));
|
|
|
|
// bob clears the lsfDepositAuth and the next payment succeeds.
|
|
env(fclear(bob, asfDepositAuth));
|
|
env.close();
|
|
|
|
env(pay(alice, bob, usd(50)));
|
|
env.close();
|
|
|
|
env(pay(alice, bob, drops(1)), Sendmax(usd(1)));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testPayXRP()
|
|
{
|
|
// Exercise direct XRP payments to an account that has the
|
|
// lsfDepositAuth flag set.
|
|
testcase("Pay XRP");
|
|
|
|
using namespace jtx;
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
Env env(*this);
|
|
auto const baseFee = env.current()->fees().base;
|
|
|
|
env.fund(XRP(10000), alice, bob);
|
|
env.close();
|
|
|
|
// bob sets the lsfDepositAuth flag.
|
|
env(fset(bob, asfDepositAuth), Fee(drops(baseFee)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == XRP(10000) - drops(baseFee));
|
|
|
|
// bob has more XRP than the base reserve. Any XRP payment should fail.
|
|
env(pay(alice, bob, drops(1)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == XRP(10000) - drops(baseFee));
|
|
|
|
// Change bob's XRP balance to exactly the base reserve.
|
|
{
|
|
STAmount const bobPaysXRP{env.balance(bob, XRP) - reserve(env, 1)};
|
|
XRPAmount const bobPaysFee{reserve(env, 1) - reserve(env, 0)};
|
|
env(pay(bob, alice, bobPaysXRP), Fee(bobPaysFee));
|
|
env.close();
|
|
}
|
|
|
|
// bob has exactly the base reserve. A small enough direct XRP
|
|
// payment should succeed.
|
|
BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));
|
|
env(pay(alice, bob, drops(1)));
|
|
env.close();
|
|
|
|
// bob has exactly the base reserve + 1. No payment should succeed.
|
|
BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0) + drops(1));
|
|
env(pay(alice, bob, drops(1)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// Take bob down to a balance of 0 XRP.
|
|
env(noop(bob), Fee(reserve(env, 0) + drops(1)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == drops(0));
|
|
|
|
// We should not be able to pay bob more than the base reserve.
|
|
env(pay(alice, bob, reserve(env, 0) + drops(1)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// However a payment of exactly the base reserve should succeed.
|
|
env(pay(alice, bob, reserve(env, 0) + drops(0)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0));
|
|
|
|
// We should be able to pay bob the base reserve one more time.
|
|
env(pay(alice, bob, reserve(env, 0) + drops(0)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == (reserve(env, 0) + reserve(env, 0)));
|
|
|
|
// bob's above the threshold again. Any payment should fail.
|
|
env(pay(alice, bob, drops(1)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == (reserve(env, 0) + reserve(env, 0)));
|
|
|
|
// Take bob back down to a zero XRP balance.
|
|
env(noop(bob), Fee(env.balance(bob, XRP)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == drops(0));
|
|
|
|
// bob should not be able to clear lsfDepositAuth.
|
|
env(fclear(bob, asfDepositAuth), Ter(terINSUF_FEE_B));
|
|
env.close();
|
|
|
|
// We should be able to pay bob 1 drop now.
|
|
env(pay(alice, bob, drops(1)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == drops(1));
|
|
|
|
// Pay bob enough so he can afford the fee to clear lsfDepositAuth.
|
|
env(pay(alice, bob, drops(baseFee - 1)));
|
|
env.close();
|
|
|
|
// Interestingly, at this point the terINSUF_FEE_B retry grabs the
|
|
// request to clear lsfDepositAuth. So the balance should be zero
|
|
// and lsfDepositAuth should be cleared.
|
|
BEAST_EXPECT(env.balance(bob, XRP) == drops(0));
|
|
env.require(Nflags(bob, asfDepositAuth));
|
|
|
|
// Since bob no longer has lsfDepositAuth set we should be able to
|
|
// pay him more than the base reserve.
|
|
env(pay(alice, bob, reserve(env, 0) + drops(1)));
|
|
env.close();
|
|
BEAST_EXPECT(env.balance(bob, XRP) == reserve(env, 0) + drops(1));
|
|
}
|
|
|
|
void
|
|
testNoRipple()
|
|
{
|
|
// It its current incarnation the DepositAuth flag does not change
|
|
// any behaviors regarding rippling and the NoRipple flag.
|
|
// Demonstrate that.
|
|
testcase("No Ripple");
|
|
|
|
using namespace jtx;
|
|
Account const gw1("gw1");
|
|
Account const gw2("gw2");
|
|
Account const alice("alice");
|
|
Account const bob("bob");
|
|
|
|
IOU const usD1(gw1["USD"]);
|
|
IOU const usD2(gw2["USD"]);
|
|
|
|
auto testIssuer = [&](FeatureBitset const& features,
|
|
bool noRipplePrev,
|
|
bool noRippleNext,
|
|
bool withDepositAuth) {
|
|
Env env(*this, features);
|
|
|
|
env.fund(XRP(10000), gw1, alice, bob);
|
|
env.close();
|
|
env(trust(gw1, alice["USD"](10), noRipplePrev ? tfSetNoRipple : 0));
|
|
env(trust(gw1, bob["USD"](10), noRippleNext ? tfSetNoRipple : 0));
|
|
env.trust(usD1(10), alice, bob);
|
|
|
|
env(pay(gw1, alice, usD1(10)));
|
|
|
|
if (withDepositAuth)
|
|
env(fset(gw1, asfDepositAuth));
|
|
|
|
TER const result = (noRippleNext && noRipplePrev) ? TER{tecPATH_DRY} : TER{tesSUCCESS};
|
|
env(pay(alice, bob, usD1(10)), Path(gw1), Ter(result));
|
|
};
|
|
|
|
auto testNonIssuer = [&](FeatureBitset const& features,
|
|
bool noRipplePrev,
|
|
bool noRippleNext,
|
|
bool withDepositAuth) {
|
|
Env env(*this, features);
|
|
|
|
env.fund(XRP(10000), gw1, gw2, alice);
|
|
env.close();
|
|
env(trust(alice, usD1(10), noRipplePrev ? tfSetNoRipple : 0));
|
|
env(trust(alice, usD2(10), noRippleNext ? tfSetNoRipple : 0));
|
|
env(pay(gw2, alice, usD2(10)));
|
|
|
|
if (withDepositAuth)
|
|
env(fset(alice, asfDepositAuth));
|
|
|
|
TER const result = (noRippleNext && noRipplePrev) ? TER{tecPATH_DRY} : TER{tesSUCCESS};
|
|
env(pay(gw1, gw2, usD2(10)), Path(alice), Sendmax(usD1(10)), Ter(result));
|
|
};
|
|
|
|
// Test every combo of noRipplePrev, noRippleNext, and withDepositAuth
|
|
for (int i = 0; i < 8; ++i)
|
|
{
|
|
auto const noRipplePrev = i & 0x1;
|
|
auto const noRippleNext = i & 0x2;
|
|
auto const withDepositAuth = i & 0x4;
|
|
testIssuer(
|
|
testableAmendments(), noRipplePrev != 0, noRippleNext != 0, withDepositAuth != 0);
|
|
|
|
testNonIssuer(
|
|
testableAmendments(), noRipplePrev != 0, noRippleNext != 0, withDepositAuth != 0);
|
|
}
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
testEnable();
|
|
testPayIOU();
|
|
testPayXRP();
|
|
testNoRipple();
|
|
}
|
|
};
|
|
|
|
static json::Value
|
|
ledgerEntryDepositPreauth(
|
|
jtx::Env& env,
|
|
jtx::Account const& acc,
|
|
std::vector<jtx::deposit::AuthorizeCredentials> const& auth)
|
|
{
|
|
json::Value jvParams;
|
|
jvParams[jss::ledger_index] = jss::validated;
|
|
jvParams[jss::deposit_preauth][jss::owner] = acc.human();
|
|
jvParams[jss::deposit_preauth][jss::authorized_credentials] = json::ValueType::Array;
|
|
auto& arr(jvParams[jss::deposit_preauth][jss::authorized_credentials]);
|
|
for (auto const& o : auth)
|
|
{
|
|
arr.append(o.toLEJson());
|
|
}
|
|
return env.rpc("json", "ledger_entry", to_string(jvParams));
|
|
}
|
|
|
|
struct DepositPreauth_test : public beast::unit_test::Suite
|
|
{
|
|
void
|
|
testEnable()
|
|
{
|
|
testcase("Enable");
|
|
|
|
using namespace jtx;
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
{
|
|
// o We should be able to add and remove an entry, and
|
|
// o That entry should cost one reserve.
|
|
// o The reserve should be returned when the entry is removed.
|
|
Env env(*this);
|
|
env.fund(XRP(10000), alice, becky);
|
|
env.close();
|
|
|
|
// Add a DepositPreauth to alice.
|
|
env(deposit::auth(alice, becky));
|
|
env.close();
|
|
env.require(Owners(alice, 1));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// Remove a DepositPreauth from alice.
|
|
env(deposit::unauth(alice, becky));
|
|
env.close();
|
|
env.require(Owners(alice, 0));
|
|
env.require(Owners(becky, 0));
|
|
}
|
|
{
|
|
// Verify that an account can be preauthorized and unauthorized
|
|
// using tickets.
|
|
Env env(*this);
|
|
env.fund(XRP(10000), alice, becky);
|
|
env.close();
|
|
|
|
env(ticket::create(alice, 2));
|
|
std::uint32_t const aliceSeq{env.seq(alice)};
|
|
env.close();
|
|
env.require(tickets(alice, 2));
|
|
|
|
// Consume the tickets from biggest seq to smallest 'cuz we can.
|
|
std::uint32_t aliceTicketSeq{env.seq(alice)};
|
|
|
|
// Add a DepositPreauth to alice.
|
|
env(deposit::auth(alice, becky), ticket::Use(--aliceTicketSeq));
|
|
env.close();
|
|
// Alice uses a ticket but gains a preauth entry.
|
|
env.require(tickets(alice, 1));
|
|
env.require(Owners(alice, 2));
|
|
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
|
env.require(Owners(becky, 0));
|
|
|
|
// Remove a DepositPreauth from alice.
|
|
env(deposit::unauth(alice, becky), ticket::Use(--aliceTicketSeq));
|
|
env.close();
|
|
env.require(tickets(alice, 0));
|
|
env.require(Owners(alice, 0));
|
|
BEAST_EXPECT(env.seq(alice) == aliceSeq);
|
|
env.require(Owners(becky, 0));
|
|
}
|
|
}
|
|
|
|
void
|
|
testInvalid()
|
|
{
|
|
testcase("Invalid");
|
|
|
|
using namespace jtx;
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
Account const carol{"carol"};
|
|
|
|
Env env(*this);
|
|
|
|
// Tell env about alice, becky and carol since they are not yet funded.
|
|
env.memoize(alice);
|
|
env.memoize(becky);
|
|
env.memoize(carol);
|
|
|
|
// Add DepositPreauth to an unfunded account.
|
|
env(deposit::auth(alice, becky), Seq(1), Ter(terNO_ACCOUNT));
|
|
|
|
env.fund(XRP(10000), alice, becky);
|
|
env.close();
|
|
|
|
// Bad fee.
|
|
env(deposit::auth(alice, becky), Fee(drops(-10)), Ter(temBAD_FEE));
|
|
env.close();
|
|
|
|
// Bad flags.
|
|
env(deposit::auth(alice, becky), Txflags(tfSell), Ter(temINVALID_FLAG));
|
|
env.close();
|
|
|
|
{
|
|
// Neither auth not unauth.
|
|
json::Value tx{deposit::auth(alice, becky)};
|
|
tx.removeMember(sfAuthorize.jsonName);
|
|
env(tx, Ter(temMALFORMED));
|
|
env.close();
|
|
}
|
|
{
|
|
// Both auth and unauth.
|
|
json::Value tx{deposit::auth(alice, becky)};
|
|
tx[sfUnauthorize.jsonName] = becky.human();
|
|
env(tx, Ter(temMALFORMED));
|
|
env.close();
|
|
}
|
|
{
|
|
// Alice authorizes a zero account.
|
|
json::Value tx{deposit::auth(alice, becky)};
|
|
tx[sfAuthorize.jsonName] = to_string(xrpAccount());
|
|
env(tx, Ter(temINVALID_ACCOUNT_ID));
|
|
env.close();
|
|
}
|
|
|
|
// alice authorizes herself.
|
|
env(deposit::auth(alice, alice), Ter(temCANNOT_PREAUTH_SELF));
|
|
env.close();
|
|
|
|
// alice authorizes an unfunded account.
|
|
env(deposit::auth(alice, carol), Ter(tecNO_TARGET));
|
|
env.close();
|
|
|
|
// alice successfully authorizes becky.
|
|
env.require(Owners(alice, 0));
|
|
env.require(Owners(becky, 0));
|
|
env(deposit::auth(alice, becky));
|
|
env.close();
|
|
env.require(Owners(alice, 1));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// alice attempts to create a duplicate authorization.
|
|
env(deposit::auth(alice, becky), Ter(tecDUPLICATE));
|
|
env.close();
|
|
env.require(Owners(alice, 1));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// carol attempts to preauthorize but doesn't have enough reserve.
|
|
env.fund(drops(249'999'999), carol);
|
|
env.close();
|
|
|
|
env(deposit::auth(carol, becky), Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
env.require(Owners(carol, 0));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// carol gets enough XRP to (barely) meet the reserve.
|
|
env(pay(alice, carol, drops(env.current()->fees().base + 1)));
|
|
env.close();
|
|
env(deposit::auth(carol, becky));
|
|
env.close();
|
|
env.require(Owners(carol, 1));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// But carol can't meet the reserve for another pre-authorization.
|
|
env(deposit::auth(carol, alice), Ter(tecINSUFFICIENT_RESERVE));
|
|
env.close();
|
|
env.require(Owners(carol, 1));
|
|
env.require(Owners(becky, 0));
|
|
env.require(Owners(alice, 1));
|
|
|
|
// alice attempts to remove an authorization she doesn't have.
|
|
env(deposit::unauth(alice, carol), Ter(tecNO_ENTRY));
|
|
env.close();
|
|
env.require(Owners(alice, 1));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// alice successfully removes her authorization of becky.
|
|
env(deposit::unauth(alice, becky));
|
|
env.close();
|
|
env.require(Owners(alice, 0));
|
|
env.require(Owners(becky, 0));
|
|
|
|
// alice removes becky again and gets an error.
|
|
env(deposit::unauth(alice, becky), Ter(tecNO_ENTRY));
|
|
env.close();
|
|
env.require(Owners(alice, 0));
|
|
env.require(Owners(becky, 0));
|
|
}
|
|
|
|
void
|
|
testPayment(FeatureBitset features)
|
|
{
|
|
testcase("Payment");
|
|
|
|
using namespace jtx;
|
|
Account const alice{"alice"};
|
|
Account const becky{"becky"};
|
|
Account const gw{"gw"};
|
|
IOU const usd(gw["USD"]);
|
|
|
|
{
|
|
// The initial implementation of DepositAuth had a bug where an
|
|
// account with the DepositAuth flag set could not make a payment
|
|
// to itself. That bug was fixed in the DepositPreauth amendment.
|
|
Env env(*this, features);
|
|
env.fund(XRP(5000), alice, becky, gw);
|
|
env.close();
|
|
|
|
env.trust(usd(1000), alice);
|
|
env.trust(usd(1000), becky);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, usd(500)));
|
|
env.close();
|
|
|
|
env(offer(alice, XRP(100), usd(100), tfPassive), Require(offers(alice, 1)));
|
|
env.close();
|
|
|
|
// becky pays herself USD (10) by consuming part of alice's offer.
|
|
// Make sure the payment works if PaymentAuth is not involved.
|
|
env(pay(becky, becky, usd(10)), Path(~usd), Sendmax(XRP(10)));
|
|
env.close();
|
|
|
|
// becky decides to require authorization for deposits.
|
|
env(fset(becky, asfDepositAuth));
|
|
env.close();
|
|
|
|
// becky pays herself again.
|
|
env(pay(becky, becky, usd(10)), Path(~usd), Sendmax(XRP(10)), Ter(tesSUCCESS));
|
|
env.close();
|
|
|
|
{
|
|
// becky setup depositpreauth with credentials
|
|
char const credType[] = "abcde";
|
|
Account const carol{"carol"};
|
|
env.fund(XRP(5000), carol);
|
|
env.close();
|
|
|
|
bool const supportsCredentials = features[featureCredentials];
|
|
|
|
TER const expectTer(!supportsCredentials ? TER(temDISABLED) : TER(tesSUCCESS));
|
|
|
|
env(deposit::authCredentials(becky, {{carol, credType}}), Ter(expectTer));
|
|
env.close();
|
|
|
|
// gw accept credentials
|
|
env(credentials::create(gw, carol, credType), Ter(expectTer));
|
|
env.close();
|
|
env(credentials::accept(gw, carol, credType), Ter(expectTer));
|
|
env.close();
|
|
|
|
auto jv = credentials::ledgerEntry(env, gw, carol, credType);
|
|
std::string const credIdx = supportsCredentials
|
|
? jv[jss::result][jss::index].asString()
|
|
: "48004829F915654A81B11C4AB8218D96FED67F209B58328A72314FB6"
|
|
"EA288BE4";
|
|
|
|
env(pay(gw, becky, usd(100)), credentials::Ids({credIdx}), Ter(expectTer));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
// Make sure DepositPreauthorization works for payments.
|
|
|
|
Account const carol{"carol"};
|
|
|
|
Env env(*this, features);
|
|
env.fund(XRP(5000), alice, becky, carol, gw);
|
|
env.close();
|
|
|
|
env.trust(usd(1000), alice);
|
|
env.trust(usd(1000), becky);
|
|
env.trust(usd(1000), carol);
|
|
env.close();
|
|
|
|
env(pay(gw, alice, usd(1000)));
|
|
env.close();
|
|
|
|
// Make XRP and IOU payments from alice to becky. Should be fine.
|
|
env(pay(alice, becky, XRP(100)));
|
|
env(pay(alice, becky, usd(100)));
|
|
env.close();
|
|
|
|
// becky decides to require authorization for deposits.
|
|
env(fset(becky, asfDepositAuth));
|
|
env.close();
|
|
|
|
// alice can no longer pay becky.
|
|
env(pay(alice, becky, XRP(100)), Ter(tecNO_PERMISSION));
|
|
env(pay(alice, becky, usd(100)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// becky preauthorizes carol for deposit, which doesn't provide
|
|
// authorization for alice.
|
|
env(deposit::auth(becky, carol));
|
|
env.close();
|
|
|
|
// alice still can't pay becky.
|
|
env(pay(alice, becky, XRP(100)), Ter(tecNO_PERMISSION));
|
|
env(pay(alice, becky, usd(100)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// becky preauthorizes alice for deposit.
|
|
env(deposit::auth(becky, alice));
|
|
env.close();
|
|
|
|
// alice can now pay becky.
|
|
env(pay(alice, becky, XRP(100)));
|
|
env(pay(alice, becky, usd(100)));
|
|
env.close();
|
|
|
|
// alice decides to require authorization for deposits.
|
|
env(fset(alice, asfDepositAuth));
|
|
env.close();
|
|
|
|
// Even though alice is authorized to pay becky, becky is not
|
|
// authorized to pay alice.
|
|
env(pay(becky, alice, XRP(100)), Ter(tecNO_PERMISSION));
|
|
env(pay(becky, alice, usd(100)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// becky unauthorizes carol. Should have no impact on alice.
|
|
env(deposit::unauth(becky, carol));
|
|
env.close();
|
|
|
|
env(pay(alice, becky, XRP(100)));
|
|
env(pay(alice, becky, usd(100)));
|
|
env.close();
|
|
|
|
// becky unauthorizes alice. alice now can't pay becky.
|
|
env(deposit::unauth(becky, alice));
|
|
env.close();
|
|
|
|
env(pay(alice, becky, XRP(100)), Ter(tecNO_PERMISSION));
|
|
env(pay(alice, becky, usd(100)), Ter(tecNO_PERMISSION));
|
|
env.close();
|
|
|
|
// becky decides to remove authorization for deposits. Now
|
|
// alice can pay becky again.
|
|
env(fclear(becky, asfDepositAuth));
|
|
env.close();
|
|
|
|
env(pay(alice, becky, XRP(100)));
|
|
env(pay(alice, becky, usd(100)));
|
|
env.close();
|
|
}
|
|
|
|
void
|
|
testCredentialsPayment()
|
|
{
|
|
using namespace jtx;
|
|
|
|
char const credType[] = "abcde";
|
|
Account const issuer{"issuer"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const maria{"maria"};
|
|
Account const john{"john"};
|
|
|
|
{
|
|
testcase("Payment failure with disabled credentials rule.");
|
|
|
|
Env env(*this, testableAmendments() - featureCredentials);
|
|
|
|
env.fund(XRP(5000), issuer, bob, alice);
|
|
env.close();
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
|
|
// Setup DepositPreauth object failed - amendent is not supported
|
|
env(deposit::authCredentials(bob, {{issuer, credType}}), Ter(temDISABLED));
|
|
env.close();
|
|
|
|
// But can create old DepositPreauth
|
|
env(deposit::auth(bob, alice));
|
|
env.close();
|
|
|
|
// And alice can't pay with any credentials, amendment is not
|
|
// enabled
|
|
std::string const invalidIdx =
|
|
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
|
|
"01E034";
|
|
env(pay(alice, bob, XRP(10)), credentials::Ids({invalidIdx}), Ter(temDISABLED));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("Payment with credentials.");
|
|
|
|
Env env(*this);
|
|
|
|
env.fund(XRP(5000), issuer, alice, bob, john);
|
|
env.close();
|
|
|
|
// Issuer create credentials, but Alice didn't accept them yet
|
|
env(credentials::create(alice, issuer, credType));
|
|
env.close();
|
|
|
|
// Get the index of the credentials
|
|
auto const jv = credentials::ledgerEntry(env, alice, issuer, credType);
|
|
std::string const credIdx = jv[jss::result][jss::index].asString();
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
|
|
// Bob will accept payments from accounts with credentials signed
|
|
// by 'issuer'
|
|
env(deposit::authCredentials(bob, {{issuer, credType}}));
|
|
env.close();
|
|
|
|
auto const jDP = ledgerEntryDepositPreauth(env, bob, {{issuer, credType}});
|
|
BEAST_EXPECT(
|
|
jDP.isObject() && jDP.isMember(jss::result) &&
|
|
!jDP[jss::result].isMember(jss::error) && jDP[jss::result].isMember(jss::node) &&
|
|
jDP[jss::result][jss::node].isMember("LedgerEntryType") &&
|
|
jDP[jss::result][jss::node]["LedgerEntryType"] == jss::DepositPreauth);
|
|
|
|
// Alice can't pay - empty credentials array
|
|
{
|
|
auto jv = pay(alice, bob, XRP(100));
|
|
jv[sfCredentialIDs.jsonName] = json::ValueType::Array;
|
|
env(jv, Ter(temMALFORMED));
|
|
env.close();
|
|
}
|
|
|
|
// Alice can't pay - not accepted credentials
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}), Ter(tecBAD_CREDENTIALS));
|
|
env.close();
|
|
|
|
// Alice accept the credentials
|
|
env(credentials::accept(alice, issuer, credType));
|
|
env.close();
|
|
|
|
// Now Alice can pay
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}));
|
|
env.close();
|
|
|
|
// Alice can pay Maria without depositPreauth enabled
|
|
env(pay(alice, maria, XRP(250)), credentials::Ids({credIdx}));
|
|
env.close();
|
|
|
|
// john can accept payment with old depositPreauth and valid
|
|
// credentials
|
|
env(fset(john, asfDepositAuth));
|
|
env(deposit::auth(john, alice));
|
|
env(pay(alice, john, XRP(100)), credentials::Ids({credIdx}));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
testcase("Payment failure with invalid credentials.");
|
|
|
|
Env env(*this);
|
|
|
|
env.fund(XRP(10000), issuer, alice, bob, maria);
|
|
env.close();
|
|
|
|
// Issuer create credentials, but Alice didn't accept them yet
|
|
env(credentials::create(alice, issuer, credType));
|
|
env.close();
|
|
// Alice accept the credentials
|
|
env(credentials::accept(alice, issuer, credType));
|
|
env.close();
|
|
// Get the index of the credentials
|
|
auto const jv = credentials::ledgerEntry(env, alice, issuer, credType);
|
|
std::string const credIdx = jv[jss::result][jss::index].asString();
|
|
|
|
{
|
|
// Success as destination didn't enable pre-authorization so
|
|
// valid credentials will not fail
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}));
|
|
}
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
|
|
{
|
|
// Fail as destination didn't setup DepositPreauth object
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}), Ter(tecNO_PERMISSION));
|
|
}
|
|
|
|
// Bob setup DepositPreauth object, duplicates is not allowed
|
|
env(deposit::authCredentials(bob, {{issuer, credType}, {issuer, credType}}),
|
|
Ter(temMALFORMED));
|
|
|
|
// Bob setup DepositPreauth object
|
|
env(deposit::authCredentials(bob, {{issuer, credType}}));
|
|
env.close();
|
|
|
|
{
|
|
std::string const invalidIdx =
|
|
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
|
|
"01E034";
|
|
// Alice can't pay with non-existing credentials
|
|
env(pay(alice, bob, XRP(100)),
|
|
credentials::Ids({invalidIdx}),
|
|
Ter(tecBAD_CREDENTIALS));
|
|
}
|
|
|
|
{ // maria can't pay using valid credentials but issued for
|
|
// different account
|
|
env(pay(maria, bob, XRP(100)),
|
|
credentials::Ids({credIdx}),
|
|
Ter(tecBAD_CREDENTIALS));
|
|
}
|
|
|
|
{
|
|
// create another valid credential
|
|
char const credType2[] = "fghij";
|
|
env(credentials::create(alice, issuer, credType2));
|
|
env.close();
|
|
env(credentials::accept(alice, issuer, credType2));
|
|
env.close();
|
|
auto const jv = credentials::ledgerEntry(env, alice, issuer, credType2);
|
|
std::string const credIdx2 = jv[jss::result][jss::index].asString();
|
|
|
|
// Alice can't pay with invalid set of valid credentials
|
|
env(pay(alice, bob, XRP(100)),
|
|
credentials::Ids({credIdx, credIdx2}),
|
|
Ter(tecNO_PERMISSION));
|
|
}
|
|
|
|
// Error, duplicate credentials
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, credIdx}), Ter(temMALFORMED));
|
|
|
|
// Alice can pay
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx}));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
void
|
|
testCredentialsCreation()
|
|
{
|
|
using namespace jtx;
|
|
|
|
char const credType[] = "abcde";
|
|
Account const issuer{"issuer"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const maria{"maria"};
|
|
|
|
{
|
|
testcase("Creating / deleting with credentials.");
|
|
|
|
Env env(*this);
|
|
|
|
env.fund(XRP(5000), issuer, alice, bob);
|
|
env.close();
|
|
|
|
{
|
|
// both included [AuthorizeCredentials UnauthorizeCredentials]
|
|
auto jv = deposit::authCredentials(bob, {{issuer, credType}});
|
|
jv[sfUnauthorizeCredentials.jsonName] = json::ValueType::Array;
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
// both included [Unauthorize, AuthorizeCredentials]
|
|
auto jv = deposit::authCredentials(bob, {{issuer, credType}});
|
|
jv[sfUnauthorize.jsonName] = issuer.human();
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
// both included [Authorize, AuthorizeCredentials]
|
|
auto jv = deposit::authCredentials(bob, {{issuer, credType}});
|
|
jv[sfAuthorize.jsonName] = issuer.human();
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
// both included [Unauthorize, UnauthorizeCredentials]
|
|
auto jv = deposit::unauthCredentials(bob, {{issuer, credType}});
|
|
jv[sfUnauthorize.jsonName] = issuer.human();
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
// both included [Authorize, UnauthorizeCredentials]
|
|
auto jv = deposit::unauthCredentials(bob, {{issuer, credType}});
|
|
jv[sfAuthorize.jsonName] = issuer.human();
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
// AuthorizeCredentials is empty
|
|
auto jv = deposit::authCredentials(bob, {});
|
|
env(jv, Ter(temARRAY_EMPTY));
|
|
}
|
|
|
|
{
|
|
// invalid issuer
|
|
auto jv = deposit::authCredentials(bob, {});
|
|
auto& arr(jv[sfAuthorizeCredentials.jsonName]);
|
|
json::Value cred = json::ValueType::Object;
|
|
cred[jss::Issuer] = to_string(xrpAccount());
|
|
cred[sfCredentialType.jsonName] = strHex(std::string_view(credType));
|
|
json::Value credParent;
|
|
credParent[jss::Credential] = cred;
|
|
arr.append(std::move(credParent));
|
|
|
|
env(jv, Ter(temINVALID_ACCOUNT_ID));
|
|
}
|
|
|
|
{
|
|
// empty credential type
|
|
auto jv = deposit::authCredentials(bob, {{issuer, {}}});
|
|
env(jv, Ter(temMALFORMED));
|
|
}
|
|
|
|
{
|
|
// AuthorizeCredentials is larger than 8 elements
|
|
Account const a("a"), b("b"), c("c"), d("d"), e("e"), f("f"), g("g"), h("h"),
|
|
i("i");
|
|
auto const& z = credType;
|
|
auto jv = deposit::authCredentials(
|
|
bob, {{a, z}, {b, z}, {c, z}, {d, z}, {e, z}, {f, z}, {g, z}, {h, z}, {i, z}});
|
|
env(jv, Ter(temARRAY_TOO_LARGE));
|
|
}
|
|
|
|
{
|
|
// Can't create with non-existing issuer
|
|
Account const rick{"rick"};
|
|
auto jv = deposit::authCredentials(bob, {{rick, credType}});
|
|
env(jv, Ter(tecNO_ISSUER));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
// not enough reserve
|
|
Account const john{"john"};
|
|
env.fund(env.current()->fees().accountReserve(0), john);
|
|
env.close();
|
|
auto jv = deposit::authCredentials(john, {{issuer, credType}});
|
|
env(jv, Ter(tecINSUFFICIENT_RESERVE));
|
|
}
|
|
|
|
{
|
|
// NO deposit object exists
|
|
env(deposit::unauthCredentials(bob, {{issuer, credType}}), Ter(tecNO_ENTRY));
|
|
}
|
|
|
|
// Create DepositPreauth object
|
|
{
|
|
env(deposit::authCredentials(bob, {{issuer, credType}}));
|
|
env.close();
|
|
|
|
auto const jDP = ledgerEntryDepositPreauth(env, bob, {{issuer, credType}});
|
|
BEAST_EXPECT(
|
|
jDP.isObject() && jDP.isMember(jss::result) &&
|
|
!jDP[jss::result].isMember(jss::error) &&
|
|
jDP[jss::result].isMember(jss::node) &&
|
|
jDP[jss::result][jss::node].isMember("LedgerEntryType") &&
|
|
jDP[jss::result][jss::node]["LedgerEntryType"] == jss::DepositPreauth);
|
|
|
|
// Check object fields
|
|
BEAST_EXPECT(jDP[jss::result][jss::node][jss::Account] == bob.human());
|
|
auto const& credentials(jDP[jss::result][jss::node]["AuthorizeCredentials"]);
|
|
BEAST_EXPECT(credentials.isArray() && credentials.size() == 1);
|
|
for (auto const& o : credentials)
|
|
{
|
|
auto const& c(o[jss::Credential]);
|
|
BEAST_EXPECT(c[jss::Issuer].asString() == issuer.human());
|
|
BEAST_EXPECT(
|
|
c["CredentialType"].asString() == strHex(std::string_view(credType)));
|
|
}
|
|
|
|
// can't create duplicate
|
|
env(deposit::authCredentials(bob, {{issuer, credType}}), Ter(tecDUPLICATE));
|
|
}
|
|
|
|
// Delete DepositPreauth object
|
|
{
|
|
env(deposit::unauthCredentials(bob, {{issuer, credType}}));
|
|
env.close();
|
|
auto const jDP = ledgerEntryDepositPreauth(env, bob, {{issuer, credType}});
|
|
BEAST_EXPECT(
|
|
jDP.isObject() && jDP.isMember(jss::result) &&
|
|
jDP[jss::result].isMember(jss::error) &&
|
|
jDP[jss::result][jss::error] == "entryNotFound");
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
testExpiredCreds()
|
|
{
|
|
using namespace jtx;
|
|
char const credType[] = "abcde";
|
|
char const credType2[] = "fghijkl";
|
|
Account const issuer{"issuer"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
Account const gw{"gw"};
|
|
IOU const usd = gw["USD"];
|
|
Account const zelda{"zelda"};
|
|
|
|
{
|
|
testcase("Payment failure with expired credentials.");
|
|
|
|
Env env(*this);
|
|
|
|
env.fund(XRP(10000), issuer, alice, bob, gw);
|
|
env.close();
|
|
|
|
// Create credentials
|
|
auto jv = credentials::create(alice, issuer, credType);
|
|
// Current time in XRPL epoch.
|
|
// Every time ledger close, unittest timer increase by 10s
|
|
uint32_t const t =
|
|
env.current()->header().parentCloseTime.time_since_epoch().count() + 60;
|
|
jv[sfExpiration.jsonName] = t;
|
|
env(jv);
|
|
env.close();
|
|
|
|
// Alice accept the credentials
|
|
env(credentials::accept(alice, issuer, credType));
|
|
env.close();
|
|
|
|
// Create credential which not expired
|
|
jv = credentials::create(alice, issuer, credType2);
|
|
uint32_t const t2 =
|
|
env.current()->header().parentCloseTime.time_since_epoch().count() + 1000;
|
|
jv[sfExpiration.jsonName] = t2;
|
|
env(jv);
|
|
env.close();
|
|
env(credentials::accept(alice, issuer, credType2));
|
|
env.close();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 2);
|
|
|
|
// Get the index of the credentials
|
|
jv = credentials::ledgerEntry(env, alice, issuer, credType);
|
|
std::string const credIdx = jv[jss::result][jss::index].asString();
|
|
jv = credentials::ledgerEntry(env, alice, issuer, credType2);
|
|
std::string const credIdx2 = jv[jss::result][jss::index].asString();
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
// Bob setup DepositPreauth object
|
|
env(deposit::authCredentials(bob, {{issuer, credType}, {issuer, credType2}}));
|
|
env.close();
|
|
|
|
{
|
|
// Alice can pay
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids({credIdx, credIdx2}));
|
|
env.close();
|
|
env.close();
|
|
|
|
// Ledger closed, time increased, alice can't pay anymore
|
|
env(pay(alice, bob, XRP(100)),
|
|
credentials::Ids({credIdx, credIdx2}),
|
|
Ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
{
|
|
// check that expired credentials were deleted
|
|
auto const jDelCred = credentials::ledgerEntry(env, alice, issuer, credType);
|
|
BEAST_EXPECT(
|
|
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
|
jDelCred[jss::result].isMember(jss::error) &&
|
|
jDelCred[jss::result][jss::error] == "entryNotFound");
|
|
}
|
|
|
|
{
|
|
// check that non-expired credential still present
|
|
auto const jle = credentials::ledgerEntry(env, alice, issuer, credType2);
|
|
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] == alice.human() &&
|
|
jle[jss::result][jss::node]["CredentialType"] ==
|
|
strHex(std::string_view(credType2)));
|
|
}
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
|
}
|
|
|
|
{
|
|
auto jv = credentials::create(gw, issuer, credType);
|
|
uint32_t const t =
|
|
env.current()->header().parentCloseTime.time_since_epoch().count() + 40;
|
|
jv[sfExpiration.jsonName] = t;
|
|
env(jv);
|
|
env.close();
|
|
env(credentials::accept(gw, issuer, credType));
|
|
env.close();
|
|
|
|
jv = credentials::ledgerEntry(env, gw, issuer, credType);
|
|
std::string const credIdx = jv[jss::result][jss::index].asString();
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, gw) == 1);
|
|
|
|
env.close();
|
|
env.close();
|
|
env.close();
|
|
|
|
// credentials are expired
|
|
env(pay(gw, bob, usd(150)), credentials::Ids({credIdx}), Ter(tecEXPIRED));
|
|
env.close();
|
|
|
|
// check that expired credentials were deleted
|
|
auto const jDelCred = credentials::ledgerEntry(env, gw, issuer, credType);
|
|
BEAST_EXPECT(
|
|
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
|
jDelCred[jss::result].isMember(jss::error) &&
|
|
jDelCred[jss::result][jss::error] == "entryNotFound");
|
|
|
|
BEAST_EXPECT(ownerCount(env, issuer) == 0);
|
|
BEAST_EXPECT(ownerCount(env, gw) == 0);
|
|
}
|
|
}
|
|
|
|
{
|
|
using namespace std::chrono;
|
|
|
|
testcase("Escrow failure with expired credentials.");
|
|
|
|
Env env(*this);
|
|
|
|
env.fund(XRP(5000), issuer, alice, bob, zelda);
|
|
env.close();
|
|
|
|
// Create credentials
|
|
auto jv = credentials::create(zelda, issuer, credType);
|
|
uint32_t const t =
|
|
env.current()->header().parentCloseTime.time_since_epoch().count() + 50;
|
|
jv[sfExpiration.jsonName] = t;
|
|
env(jv);
|
|
env.close();
|
|
|
|
// Zelda accept the credentials
|
|
env(credentials::accept(zelda, issuer, credType));
|
|
env.close();
|
|
|
|
// Get the index of the credentials
|
|
jv = credentials::ledgerEntry(env, zelda, issuer, credType);
|
|
std::string const credIdx = jv[jss::result][jss::index].asString();
|
|
|
|
// Bob require pre-authorization
|
|
env(fset(bob, asfDepositAuth));
|
|
env.close();
|
|
// Bob setup DepositPreauth object
|
|
env(deposit::authCredentials(bob, {{issuer, credType}}));
|
|
env.close();
|
|
|
|
auto const seq = env.seq(alice);
|
|
env(escrow::create(alice, bob, XRP(1000)), escrow::kFINISH_TIME(env.now() + 1s));
|
|
env.close();
|
|
|
|
// zelda can't finish escrow with invalid credentials
|
|
{
|
|
env(escrow::finish(zelda, alice, seq), credentials::Ids({}), Ter(temMALFORMED));
|
|
env.close();
|
|
}
|
|
|
|
{
|
|
// zelda can't finish escrow with invalid credentials
|
|
std::string const invalidIdx =
|
|
"0E0B04ED60588A758B67E21FBBE95AC5A63598BA951761DC0EC9C08D7E"
|
|
"01E034";
|
|
|
|
env(escrow::finish(zelda, alice, seq),
|
|
credentials::Ids({invalidIdx}),
|
|
Ter(tecBAD_CREDENTIALS));
|
|
env.close();
|
|
}
|
|
|
|
{ // Ledger closed, time increased, zelda can't finish escrow
|
|
env(escrow::finish(zelda, alice, seq),
|
|
credentials::Ids({credIdx}),
|
|
Fee(1500),
|
|
Ter(tecEXPIRED));
|
|
env.close();
|
|
}
|
|
|
|
// check that expired credentials were deleted
|
|
auto const jDelCred = credentials::ledgerEntry(env, zelda, issuer, credType);
|
|
BEAST_EXPECT(
|
|
jDelCred.isObject() && jDelCred.isMember(jss::result) &&
|
|
jDelCred[jss::result].isMember(jss::error) &&
|
|
jDelCred[jss::result][jss::error] == "entryNotFound");
|
|
}
|
|
}
|
|
|
|
void
|
|
testSortingCredentials()
|
|
{
|
|
using namespace jtx;
|
|
|
|
Account const stock{"stock"};
|
|
Account const alice{"alice"};
|
|
Account const bob{"bob"};
|
|
|
|
Env env(*this);
|
|
|
|
testcase("Sorting credentials.");
|
|
|
|
env.fund(XRP(5000), stock, alice, bob);
|
|
|
|
std::vector<deposit::AuthorizeCredentials> credentials = {
|
|
{"a", "a"},
|
|
{"b", "b"},
|
|
{"c", "c"},
|
|
{"d", "d"},
|
|
{"e", "e"},
|
|
{"f", "f"},
|
|
{"g", "g"},
|
|
{"h", "h"}};
|
|
|
|
for (auto const& c : credentials)
|
|
env.fund(XRP(5000), c.issuer);
|
|
env.close();
|
|
|
|
std::random_device rd;
|
|
std::mt19937 gen(rd());
|
|
|
|
{
|
|
std::unordered_map<std::string, Account> pubKey2Acc;
|
|
for (auto const& c : credentials)
|
|
pubKey2Acc.emplace(c.issuer.human(), c.issuer);
|
|
|
|
// check sorting in object
|
|
for (int i = 0; i < 10; ++i)
|
|
{
|
|
std::ranges::shuffle(credentials, gen);
|
|
env(deposit::authCredentials(stock, credentials));
|
|
env.close();
|
|
|
|
auto const dp = ledgerEntryDepositPreauth(env, stock, credentials);
|
|
auto const& authCred(dp[jss::result][jss::node]["AuthorizeCredentials"]);
|
|
BEAST_EXPECT(authCred.isArray() && authCred.size() == credentials.size());
|
|
std::vector<std::pair<Account, std::string>> readCreds;
|
|
for (auto const& o : authCred)
|
|
{
|
|
auto const& c(o[jss::Credential]);
|
|
auto issuer = c[jss::Issuer].asString();
|
|
|
|
if (BEAST_EXPECT(pubKey2Acc.contains(issuer)))
|
|
{
|
|
readCreds.emplace_back(
|
|
pubKey2Acc.at(issuer), c["CredentialType"].asString());
|
|
}
|
|
}
|
|
|
|
BEAST_EXPECT(std::ranges::is_sorted(readCreds));
|
|
|
|
env(deposit::unauthCredentials(stock, credentials));
|
|
env.close();
|
|
}
|
|
}
|
|
|
|
{
|
|
std::ranges::shuffle(credentials, gen);
|
|
env(deposit::authCredentials(stock, credentials));
|
|
env.close();
|
|
|
|
// check sorting in params
|
|
for (int i = 0; i < 10; ++i)
|
|
{
|
|
std::ranges::shuffle(credentials, gen);
|
|
env(deposit::authCredentials(stock, credentials), Ter(tecDUPLICATE));
|
|
}
|
|
}
|
|
|
|
testcase("Check duplicate credentials.");
|
|
{
|
|
// check duplicates in depositPreauth params
|
|
std::vector<deposit::AuthorizeCredentials> copyCredentials(
|
|
credentials.begin(), credentials.end() - 1);
|
|
|
|
std::ranges::shuffle(copyCredentials, gen);
|
|
for (auto const& c : copyCredentials)
|
|
{
|
|
auto credentials2 = copyCredentials;
|
|
credentials2.push_back(c);
|
|
env(deposit::authCredentials(stock, credentials2), Ter(temMALFORMED));
|
|
}
|
|
|
|
// create batch of credentials and save their hashes
|
|
std::vector<std::string> credentialIDs;
|
|
for (auto const& c : credentials)
|
|
{
|
|
env(credentials::create(alice, c.issuer, c.credType));
|
|
env.close();
|
|
env(credentials::accept(alice, c.issuer, c.credType));
|
|
env.close();
|
|
|
|
credentialIDs.push_back(
|
|
credentials::ledgerEntry(
|
|
env, alice, c.issuer, c.credType)[jss::result][jss::index]
|
|
.asString());
|
|
}
|
|
|
|
// check duplicates in payment params
|
|
for (auto const& h : credentialIDs)
|
|
{
|
|
auto credentialIDs2 = credentialIDs;
|
|
credentialIDs2.push_back(h);
|
|
|
|
env(pay(alice, bob, XRP(100)), credentials::Ids(credentialIDs2), Ter(temMALFORMED));
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
run() override
|
|
{
|
|
testEnable();
|
|
testInvalid();
|
|
auto const supported{jtx::testableAmendments()};
|
|
testPayment(supported - featureCredentials);
|
|
testPayment(supported);
|
|
testCredentialsPayment();
|
|
testCredentialsCreation();
|
|
testExpiredCreds();
|
|
testSortingCredentials();
|
|
}
|
|
};
|
|
|
|
BEAST_DEFINE_TESTSUITE(DepositAuth, app, xrpl);
|
|
BEAST_DEFINE_TESTSUITE(DepositPreauth, app, xrpl);
|
|
|
|
} // namespace xrpl::test
|