feat: Add Credential support

This commit is contained in:
Mayukha Vadari
2026-07-06 17:21:46 -04:00
parent 7868d6ed79
commit 5bce58357f
4 changed files with 147 additions and 9 deletions

View File

@@ -8,6 +8,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/ReadView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -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;
};

View File

@@ -4,6 +4,7 @@
#include <xrpl/ledger/ApplyView.h>
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -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;
}

View File

@@ -7,6 +7,7 @@
#include <xrpl/ledger/helpers/AccountRootHelpers.h>
#include <xrpl/ledger/helpers/CredentialHelpers.h> // IWYU pragma: keep
#include <xrpl/ledger/helpers/DirectoryHelpers.h>
#include <xrpl/ledger/helpers/SponsorHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Keylet.h>
@@ -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_)

View File

@@ -5,6 +5,7 @@
#include <test/jtx/amount.h>
#include <test/jtx/batch.h>
#include <test/jtx/check.h>
#include <test/jtx/credentials.h>
#include <test/jtx/delegate.h>
#include <test/jtx/deposit.h>
#include <test/jtx/did.h>
@@ -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);