From 5bce58357fdfdb9d790d82ef72cd45539b1fa7e5 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Mon, 6 Jul 2026 17:21:46 -0400 Subject: [PATCH] feat: Add Credential support --- .../ledger/helpers/CredentialHelpers.cpp | 3 +- .../credentials/CredentialAccept.cpp | 8 +- .../credentials/CredentialCreate.cpp | 22 +++- src/test/app/Sponsor_test.cpp | 123 ++++++++++++++++++ 4 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp index a7077672fc..30ad5a1416 100644 --- a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -97,7 +98,7 @@ deleteSLE(ApplyView& view, SLE::ref sleCredential, beast::Journal j) } if (isOwner) - decreaseOwnerCount(view, sleAccount, {}, 1, j); + decreaseOwnerCountForObject(view, sleAccount, sleCredential, 1, j); return tesSUCCESS; }; diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp index 85befd51b1..605deb8ff2 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -116,8 +117,11 @@ CredentialAccept::doApply() sleCred->setFieldU32(sfFlags, lsfAccepted); view().update(sleCred); - decreaseOwnerCount(view(), sleIssuer, {}, 1, j_); - increaseOwnerCount(view(), sleSubject, {}, 1, j_); + auto txSponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!txSponsorSle) + return tefINTERNAL; // LCOV_EXCL_LINE + decreaseOwnerCountForObject(view(), sleIssuer, sleCred, 1, j_); + increaseOwnerCount(view(), sleSubject, *txSponsorSle, 1, j_); return tesSUCCESS; } diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp index 57896aa025..67ab02fffa 100644 --- a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp @@ -7,6 +7,7 @@ #include #include // IWYU pragma: keep #include +#include #include #include #include @@ -130,11 +131,19 @@ CredentialCreate::doApply() if (!sleIssuer) return tefINTERNAL; // LCOV_EXCL_LINE - { - STAmount const reserve{accountReserve(view(), sleIssuer, j_, {.ownerCountDelta = 1})}; - if (preFeeBalance_ < reserve) - return tecINSUFFICIENT_RESERVE; - } + auto const sponsorSle = getTxReserveSponsor(ctx_.getApplyViewContext()); + if (!sponsorSle) + return sponsorSle.error(); // LCOV_EXCL_LINE + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), + sleIssuer, + preFeeBalance_, + *sponsorSle, + {.ownerCountDelta = 1}, + j_); + !isTesSuccess(ret)) + return ret; sleCred->setAccountID(sfSubject, subject); sleCred->setAccountID(sfIssuer, accountID_); @@ -152,7 +161,8 @@ CredentialCreate::doApply() return tecDIR_FULL; sleCred->setFieldU64(sfIssuerNode, *page); - increaseOwnerCount(view(), sleIssuer, {}, 1, j_); + increaseOwnerCount(view(), sleIssuer, *sponsorSle, 1, j_); + addSponsorToLedgerEntry(sleCred, *sponsorSle); } if (subject == accountID_) diff --git a/src/test/app/Sponsor_test.cpp b/src/test/app/Sponsor_test.cpp index 9f0d7e0398..7dfbd69976 100644 --- a/src/test/app/Sponsor_test.cpp +++ b/src/test/app/Sponsor_test.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -2782,6 +2783,127 @@ public: } } + void + testCredential(bool cosigning) + { + testcase("Credential"); + using namespace test::jtx; + Account const alice("alice"); + Account const bob("bob"); + Account const sponsor("sponsor"); + Account const sponsor2("sponsor2"); + auto const credType = std::string("test"); + + // Self-issued credential: alice creates for herself, sponsor covers reserve + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor, sponsor2); + env.close(); + + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(alice, alice, credType)); + }); + + auto const credKeylet = credentials::keylet(alice, alice, credType); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + + // Transfer sponsor + if (cosigning) + { + env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve), + Sig(sfSponsorSignature, sponsor2)); + env.close(); + } + else + { + env(sponsor::set_reserve(sponsor2, 0, 1), sponsor::SponseeAcc(alice)); + env.close(); + env(sponsor::transfer(alice, tfSponsorshipReassign, credKeylet.key), + sponsor::As(sponsor2, spfSponsorReserve)); + env.close(); + } + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 1); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor2.id()); + + // CredentialDelete + env(credentials::deleteCred(alice, alice, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor2) == 0); + } + + // Issuer creates for subject; sponsorship transfers to subject on accept + { + Env env{*this, testableAmendments()}; + env.fund(XRP(1000000), alice, bob, sponsor); + env.close(); + + // alice (issuer) creates credential for bob (subject), sponsor covers alice's reserve + testEachSponsorship( + env, + cosigning, + sponsor, + alice, + 1, + 1, + tecINSUFFICIENT_RESERVE, + [&](Env& env, auto const& submit) { + submit(credentials::create(bob, alice, credType)); + }); + + // Before accept: alice (issuer) owns the credential + auto const credKeylet = credentials::keylet(bob, alice, credType); + BEAST_EXPECT(env.le(credKeylet)->getAccountID(sfSponsor) == sponsor.id()); + + BEAST_EXPECT(ownerCount(env, alice) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 1); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + + // Bob accepts: ownership and sponsorship transfer to bob + env(credentials::accept(bob, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 1); // still sponsoring, but now bob + BEAST_EXPECT(ownerCount(env, bob) == 1); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 1); + + // CredentialDelete by subject + env(credentials::deleteCred(bob, bob, alice, credType)); + env.close(); + + BEAST_EXPECT(ownerCount(env, alice) == 0); + BEAST_EXPECT(ownerCount(env, bob) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, alice) == 0); + BEAST_EXPECT(sponsoredOwnerCount(env, bob) == 0); + BEAST_EXPECT(sponsoringOwnerCount(env, sponsor) == 0); + } + } + void testDID(bool cosigning) { @@ -4411,6 +4533,7 @@ public: testRequireFlag(); testSponsorReserveSimple(cosigning); testCheck(cosigning); + testCredential(cosigning); testDelegate(cosigning); testDepositPreauth(cosigning); testEscrow(cosigning);