mirror of
https://github.com/Xahau/xahaud.git
synced 2025-11-18 09:35:49 +00:00
DID: Decentralized identifiers (DIDs) (XLS-40): (#4636)
Implement native support for W3C DIDs. Add a new ledger object: `DID`. Add two new transactions: 1. `DIDSet`: create or update the `DID` object. 2. `DIDDelete`: delete the `DID` object. This meets the requirements specified in the DID v1.0 specification currently recommended by the W3C Credentials Community Group. The DID format for the XRP Ledger conforms to W3C DID standards. The objects can be created and owned by any XRPL account holder. The transactions can be integrated by any service, wallet, or application.
This commit is contained in:
@@ -531,6 +531,7 @@ target_sources (rippled PRIVATE
|
||||
src/ripple/app/tx/impl/CreateTicket.cpp
|
||||
src/ripple/app/tx/impl/DeleteAccount.cpp
|
||||
src/ripple/app/tx/impl/DepositPreauth.cpp
|
||||
src/ripple/app/tx/impl/DID.cpp
|
||||
src/ripple/app/tx/impl/Escrow.cpp
|
||||
src/ripple/app/tx/impl/GenesisMint.cpp
|
||||
src/ripple/app/tx/impl/Import.cpp
|
||||
@@ -812,6 +813,7 @@ if (tests)
|
||||
src/test/app/DeliverMin_test.cpp
|
||||
src/test/app/DepositAuth_test.cpp
|
||||
src/test/app/Discrepancy_test.cpp
|
||||
src/test/app/DID_test.cpp
|
||||
src/test/app/DNS_test.cpp
|
||||
src/test/app/Escrow_test.cpp
|
||||
src/test/app/FeeVote_test.cpp
|
||||
@@ -977,6 +979,7 @@ if (tests)
|
||||
src/test/jtx/impl/check.cpp
|
||||
src/test/jtx/impl/delivermin.cpp
|
||||
src/test/jtx/impl/deposit.cpp
|
||||
src/test/jtx/impl/did.cpp
|
||||
src/test/jtx/impl/envconfig.cpp
|
||||
src/test/jtx/impl/fee.cpp
|
||||
src/test/jtx/impl/flags.cpp
|
||||
|
||||
226
src/ripple/app/tx/impl/DID.cpp
Normal file
226
src/ripple/app/tx/impl/DID.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2023 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 <ripple/app/tx/impl/DID.h>
|
||||
|
||||
#include <ripple/basics/Log.h>
|
||||
#include <ripple/ledger/ApplyView.h>
|
||||
#include <ripple/ledger/View.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/*
|
||||
DID
|
||||
======
|
||||
|
||||
Decentralized Identifiers (DIDs) are a new type of identifier that enable
|
||||
verifiable, self-sovereign digital identity and are designed to be
|
||||
compatible with any distributed ledger or network. This implementation
|
||||
conforms to the requirements specified in the DID v1.0 specification
|
||||
currently recommended by the W3C Credentials Community Group
|
||||
(https://www.w3.org/TR/did-core/).
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
NotTEC
|
||||
DIDSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureDID))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
if (!ctx.tx.isFieldPresent(sfURI) &&
|
||||
!ctx.tx.isFieldPresent(sfDIDDocument) && !ctx.tx.isFieldPresent(sfData))
|
||||
return temEMPTY_DID;
|
||||
|
||||
if (ctx.tx.isFieldPresent(sfURI) && ctx.tx[sfURI].empty() &&
|
||||
ctx.tx.isFieldPresent(sfDIDDocument) && ctx.tx[sfDIDDocument].empty() &&
|
||||
ctx.tx.isFieldPresent(sfData) && ctx.tx[sfData].empty())
|
||||
return temEMPTY_DID;
|
||||
|
||||
auto isTooLong = [&](auto const& sField, std::size_t length) -> bool {
|
||||
if (auto field = ctx.tx[~sField])
|
||||
return field->length() > length;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (isTooLong(sfURI, maxDIDURILength) ||
|
||||
isTooLong(sfDIDDocument, maxDIDDocumentLength) ||
|
||||
isTooLong(sfData, maxDIDAttestationLength))
|
||||
return temMALFORMED;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
addSLE(
|
||||
ApplyContext& ctx,
|
||||
std::shared_ptr<SLE> const& sle,
|
||||
AccountID const& owner)
|
||||
{
|
||||
auto const sleAccount = ctx.view().peek(keylet::account(owner));
|
||||
if (!sleAccount)
|
||||
return tefINTERNAL;
|
||||
|
||||
// Check reserve availability for new object creation
|
||||
{
|
||||
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
|
||||
auto const reserve =
|
||||
ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1);
|
||||
|
||||
if (balance < reserve)
|
||||
return tecINSUFFICIENT_RESERVE;
|
||||
}
|
||||
|
||||
// Add ledger object to ledger
|
||||
ctx.view().insert(sle);
|
||||
|
||||
// Add ledger object to owner's page
|
||||
{
|
||||
auto page = ctx.view().dirInsert(
|
||||
keylet::ownerDir(owner), sle->key(), describeOwnerDir(owner));
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
(*sle)[sfOwnerNode] = *page;
|
||||
}
|
||||
adjustOwnerCount(ctx.view(), sleAccount, 1, ctx.journal);
|
||||
ctx.view().update(sleAccount);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
DIDSet::doApply()
|
||||
{
|
||||
// Edit ledger object if it already exists
|
||||
Keylet const didKeylet = keylet::did(account_);
|
||||
if (auto const sleDID = ctx_.view().peek(didKeylet))
|
||||
{
|
||||
auto update = [&](auto const& sField) {
|
||||
if (auto const field = ctx_.tx[~sField])
|
||||
{
|
||||
if (field->empty())
|
||||
{
|
||||
sleDID->makeFieldAbsent(sField);
|
||||
}
|
||||
else
|
||||
{
|
||||
(*sleDID)[sField] = *field;
|
||||
}
|
||||
}
|
||||
};
|
||||
update(sfURI);
|
||||
update(sfDIDDocument);
|
||||
update(sfData);
|
||||
|
||||
if (!sleDID->isFieldPresent(sfURI) &&
|
||||
!sleDID->isFieldPresent(sfDIDDocument) &&
|
||||
!sleDID->isFieldPresent(sfData))
|
||||
{
|
||||
return tecEMPTY_DID;
|
||||
}
|
||||
ctx_.view().update(sleDID);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
// Create new ledger object otherwise
|
||||
auto const sleDID = std::make_shared<SLE>(didKeylet);
|
||||
(*sleDID)[sfAccount] = account_;
|
||||
|
||||
auto set = [&](auto const& sField) {
|
||||
if (auto const field = ctx_.tx[~sField]; field && !field->empty())
|
||||
(*sleDID)[sField] = *field;
|
||||
};
|
||||
|
||||
set(sfURI);
|
||||
set(sfDIDDocument);
|
||||
set(sfData);
|
||||
|
||||
return addSLE(ctx_, sleDID, account_);
|
||||
}
|
||||
|
||||
NotTEC
|
||||
DIDDelete::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureDID))
|
||||
return temDISABLED;
|
||||
|
||||
if (ctx.tx.getFlags() & tfUniversalMask)
|
||||
return temINVALID_FLAG;
|
||||
|
||||
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
|
||||
return ret;
|
||||
|
||||
return preflight2(ctx);
|
||||
}
|
||||
|
||||
TER
|
||||
DIDDelete::deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)
|
||||
{
|
||||
auto const sle = ctx.view().peek(sleKeylet);
|
||||
if (!sle)
|
||||
return tecNO_ENTRY;
|
||||
|
||||
return DIDDelete::deleteSLE(ctx.view(), sle, owner, ctx.journal);
|
||||
}
|
||||
|
||||
TER
|
||||
DIDDelete::deleteSLE(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> sle,
|
||||
AccountID const owner,
|
||||
beast::Journal j)
|
||||
{
|
||||
// Remove object from owner directory
|
||||
if (!view.dirRemove(
|
||||
keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
|
||||
{
|
||||
JLOG(j.fatal()) << "Unable to delete DID Token from owner.";
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
auto const sleOwner = view.peek(keylet::account(owner));
|
||||
if (!sleOwner)
|
||||
return tecINTERNAL;
|
||||
|
||||
adjustOwnerCount(view, sleOwner, -1, j);
|
||||
view.update(sleOwner);
|
||||
|
||||
// Remove object from ledger
|
||||
view.erase(sle);
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
DIDDelete::doApply()
|
||||
{
|
||||
return deleteSLE(ctx_, keylet::did(account_), account_);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
73
src/ripple/app/tx/impl/DID.h
Normal file
73
src/ripple/app/tx/impl/DID.h
Normal file
@@ -0,0 +1,73 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TX_DID_H_INCLUDED
|
||||
#define RIPPLE_TX_DID_H_INCLUDED
|
||||
|
||||
#include <ripple/app/tx/impl/Transactor.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class DIDSet : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit DIDSet(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class DIDDelete : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit DIDDelete(ApplyContext& ctx) : Transactor(ctx)
|
||||
{
|
||||
}
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
static TER
|
||||
deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner);
|
||||
|
||||
static TER
|
||||
deleteSLE(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> sle,
|
||||
AccountID const owner,
|
||||
beast::Journal j);
|
||||
|
||||
TER
|
||||
doApply() override;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <ripple/app/tx/impl/DID.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/SetSignerList.h>
|
||||
@@ -163,6 +164,18 @@ removeGeneric(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
removeDIDFromLedger(
|
||||
Application& app,
|
||||
ApplyView& view,
|
||||
AccountID const& account,
|
||||
uint256 const& delIndex,
|
||||
std::shared_ptr<SLE> const& sleDel,
|
||||
beast::Journal j)
|
||||
{
|
||||
return DIDDelete::deleteSLE(view, sleDel, account, j);
|
||||
}
|
||||
|
||||
// Return nullptr if the LedgerEntryType represents an obligation that can't
|
||||
// be deleted. Otherwise return the pointer to the function that can delete
|
||||
// the non-obligation
|
||||
@@ -183,6 +196,8 @@ nonObligationDeleter(LedgerEntryType t)
|
||||
return removeNFTokenOfferFromLedger;
|
||||
case ltURI_TOKEN:
|
||||
return removeGeneric;
|
||||
case ltDID:
|
||||
return removeDIDFromLedger;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -511,6 +511,7 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltBRIDGE:
|
||||
case ltXCHAIN_OWNED_CLAIM_ID:
|
||||
case ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID:
|
||||
case ltDID:
|
||||
break;
|
||||
default:
|
||||
invalidTypeAdded_ = true;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <ripple/app/tx/impl/CreateCheck.h>
|
||||
#include <ripple/app/tx/impl/CreateOffer.h>
|
||||
#include <ripple/app/tx/impl/CreateTicket.h>
|
||||
#include <ripple/app/tx/impl/DID.h>
|
||||
#include <ripple/app/tx/impl/DeleteAccount.h>
|
||||
#include <ripple/app/tx/impl/DepositPreauth.h>
|
||||
#include <ripple/app/tx/impl/Escrow.h>
|
||||
@@ -185,6 +186,10 @@ with_txn_type(TxType txnType, F&& f)
|
||||
return f.template operator()<XChainAddAccountCreateAttestation>();
|
||||
case ttXCHAIN_ACCOUNT_CREATE_COMMIT:
|
||||
return f.template operator()<XChainCreateAccountCommit>();
|
||||
case ttDID_SET:
|
||||
return f.template operator()<DIDSet>();
|
||||
case ttDID_DELETE:
|
||||
return f.template operator()<DIDDelete>();
|
||||
default:
|
||||
throw UnknownTxnType(txnType);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace detail {
|
||||
// Feature.cpp. Because it's only used to reserve storage, and determine how
|
||||
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
|
||||
// the actual number of amendments. A LogicError on startup will verify this.
|
||||
static constexpr std::size_t numFeatures = 86;
|
||||
static constexpr std::size_t numFeatures = 87;
|
||||
|
||||
/** Amendments that this server supports and the default voting behavior.
|
||||
Whether they are enabled depends on the Rules defined in the validated
|
||||
@@ -374,6 +374,7 @@ extern uint256 const featureClawback;
|
||||
extern uint256 const featureAMM;
|
||||
extern uint256 const featureXChainBridge;
|
||||
extern uint256 const fixDisallowIncomingV1;
|
||||
extern uint256 const featureDID;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -314,6 +314,9 @@ xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
Keylet
|
||||
xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
|
||||
@@ -204,6 +204,12 @@ enum LedgerEntryType : std::uint16_t
|
||||
*/
|
||||
ltAMM = 0x0079,
|
||||
|
||||
/** The ledger object which tracks the DID.
|
||||
|
||||
\sa keylet::did
|
||||
*/
|
||||
ltDID = 0x4449,
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
/** A special type, matching any ledger entry type.
|
||||
|
||||
|
||||
@@ -83,6 +83,15 @@ std::uint16_t constexpr maxTransferFee = 50000;
|
||||
/** The maximum length of a URI inside an NFT */
|
||||
std::size_t constexpr maxTokenURILength = 256;
|
||||
|
||||
/** The maximum length of a Data element inside a DID */
|
||||
std::size_t constexpr maxDIDDocumentLength = 256;
|
||||
|
||||
/** The maximum length of a URI inside a DID */
|
||||
std::size_t constexpr maxDIDURILength = 256;
|
||||
|
||||
/** The maximum length of an Attestation inside a DID */
|
||||
std::size_t constexpr maxDIDAttestationLength = 256;
|
||||
|
||||
/** The maximum length of a domain */
|
||||
std::size_t constexpr maxDomainLength = 256;
|
||||
|
||||
|
||||
@@ -546,6 +546,8 @@ extern SF_VL const sfCreateCode;
|
||||
extern SF_VL const sfMemoType;
|
||||
extern SF_VL const sfMemoData;
|
||||
extern SF_VL const sfMemoFormat;
|
||||
extern SF_VL const sfDIDDocument;
|
||||
extern SF_VL const sfData;
|
||||
|
||||
// variable length (uncommon)
|
||||
extern SF_VL const sfFulfillment;
|
||||
|
||||
@@ -135,6 +135,7 @@ enum TEMcodes : TERUnderlyingType {
|
||||
temXCHAIN_BRIDGE_NONDOOR_OWNER,
|
||||
temXCHAIN_BRIDGE_BAD_MIN_ACCOUNT_CREATE_AMOUNT,
|
||||
temXCHAIN_BRIDGE_BAD_REWARD_AMOUNT,
|
||||
temEMPTY_DID,
|
||||
|
||||
temHOOK_DATA_TOO_LARGE,
|
||||
};
|
||||
@@ -345,6 +346,7 @@ enum TECcodes : TERUnderlyingType {
|
||||
tecTOO_MANY_REMARKS = 189,
|
||||
tecINCOMPLETE = 190,
|
||||
// 191: tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR
|
||||
tecEMPTY_DID = 192,
|
||||
tecLAST_POSSIBLE_ENTRY = 255,
|
||||
};
|
||||
|
||||
|
||||
@@ -191,6 +191,12 @@ enum TxType : std::uint16_t
|
||||
/** This transactions creates a sidechain */
|
||||
ttXCHAIN_CREATE_BRIDGE = 57,
|
||||
|
||||
/** This transaction type creates or updates a DID */
|
||||
ttDID_SET = 58,
|
||||
|
||||
/** This transaction type deletes a DID */
|
||||
ttDID_DELETE = 59,
|
||||
|
||||
/* A note attaching transactor that allows the owner or issuer (on a object by object basis) to attach remarks */
|
||||
ttREMARKS_SET = 94,
|
||||
|
||||
|
||||
@@ -479,7 +479,8 @@ REGISTER_FIX (fixReducedOffersV1, Supported::yes, VoteBehavior::De
|
||||
REGISTER_FEATURE(Clawback, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(XChainBridge, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX(fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FIX (fixDisallowIncomingV1, Supported::yes, VoteBehavior::DefaultNo);
|
||||
REGISTER_FEATURE(DID, Supported::yes, VoteBehavior::DefaultNo);
|
||||
|
||||
// The following amendments are obsolete, but must remain supported
|
||||
// because they could potentially get enabled.
|
||||
|
||||
@@ -80,6 +80,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
BRIDGE = 'H',
|
||||
XCHAIN_CLAIM_ID = 'Q',
|
||||
XCHAIN_CREATE_ACCOUNT_CLAIM_ID = 'K',
|
||||
DID = 'I',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -510,6 +511,12 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq)
|
||||
seq)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept
|
||||
{
|
||||
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -424,6 +424,19 @@ LedgerFormats::LedgerFormats()
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::DID,
|
||||
ltDID,
|
||||
{
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfDIDDocument, soeOPTIONAL},
|
||||
{sfURI, soeOPTIONAL},
|
||||
{sfData, soeOPTIONAL},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED}
|
||||
},
|
||||
commonFields);
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
|
||||
@@ -319,6 +319,8 @@ CONSTRUCT_TYPED_SFIELD(sfHookReturnString, "HookReturnString", VL,
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookParameterName, "HookParameterName", VL, 24);
|
||||
CONSTRUCT_TYPED_SFIELD(sfHookParameterValue, "HookParameterValue", VL, 25);
|
||||
CONSTRUCT_TYPED_SFIELD(sfBlob, "Blob", VL, 26);
|
||||
CONSTRUCT_TYPED_SFIELD(sfDIDDocument, "DIDDocument", VL, 27);
|
||||
CONSTRUCT_TYPED_SFIELD(sfData, "Data", VL, 28);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRemarkValue, "RemarkValue", VL, 98);
|
||||
CONSTRUCT_TYPED_SFIELD(sfRemarkName, "RemarkName", VL, 99);
|
||||
|
||||
|
||||
@@ -119,6 +119,7 @@ transResults()
|
||||
MAKE_ERROR(tecXCHAIN_SELF_COMMIT, "Account cannot commit funds to itself."),
|
||||
MAKE_ERROR(tecXCHAIN_BAD_PUBLIC_KEY_ACCOUNT_PAIR, "Bad public key account pair in an xchain transaction."),
|
||||
MAKE_ERROR(tecXCHAIN_CREATE_ACCOUNT_DISABLED, "This bridge does not support account creation."),
|
||||
MAKE_ERROR(tecEMPTY_DID, "The DID object did not have a URI or DIDDocument field."),
|
||||
|
||||
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
|
||||
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
|
||||
@@ -191,6 +192,7 @@ transResults()
|
||||
MAKE_ERROR(temBAD_WEIGHT, "Malformed: Weight must be a positive value."),
|
||||
MAKE_ERROR(temDST_IS_SRC, "Destination may not be source."),
|
||||
MAKE_ERROR(temDST_NEEDED, "Destination not specified."),
|
||||
MAKE_ERROR(temEMPTY_DID, "Malformed: No DID data provided."),
|
||||
MAKE_ERROR(temINVALID, "The transaction is ill-formed."),
|
||||
MAKE_ERROR(temINVALID_FLAG, "The transaction has an invalid flag."),
|
||||
MAKE_ERROR(temREDUNDANT, "The transaction is redundant."),
|
||||
|
||||
@@ -598,6 +598,17 @@ TxFormats::TxFormats()
|
||||
{sfSignatureReward, soeREQUIRED},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::DIDSet,
|
||||
ttDID_SET,
|
||||
{
|
||||
{sfDIDDocument, soeOPTIONAL},
|
||||
{sfURI, soeOPTIONAL},
|
||||
{sfData, soeOPTIONAL},
|
||||
},
|
||||
commonFields);
|
||||
|
||||
add(jss::DIDDelete, ttDID_DELETE, {}, commonFields);
|
||||
}
|
||||
|
||||
TxFormats const&
|
||||
|
||||
@@ -73,6 +73,9 @@ JSS(Clawback); // transaction type.
|
||||
JSS(ClaimReward); // transaction type.
|
||||
JSS(ClearFlag); // field.
|
||||
JSS(CreateCode); // field.
|
||||
JSS(DID); // ledger type.
|
||||
JSS(DIDDelete); // transaction type.
|
||||
JSS(DIDSet); // transaction type.
|
||||
JSS(DeliverMin); // in: TransactionSign
|
||||
JSS(DepositPreauth); // transaction and ledger type.
|
||||
JSS(Destination); // in: TransactionSign; field.
|
||||
@@ -334,6 +337,7 @@ JSS(destination_currencies); // in: PathRequest, RipplePathFind
|
||||
JSS(destination_tag); // in: PathRequest
|
||||
// out: AccountChannels
|
||||
JSS(details); // out: Manifest, server_info
|
||||
JSS(did); // in: LedgerEntry
|
||||
JSS(dir_entry); // out: DirectoryEntryIterator
|
||||
JSS(dir_index); // out: DirectoryEntryIterator
|
||||
JSS(dir_root); // out: DirectoryEntryIterator
|
||||
|
||||
@@ -754,6 +754,16 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (context.params.isMember(jss::did))
|
||||
{
|
||||
expectedType = ltDID;
|
||||
auto const account =
|
||||
parseBase58<AccountID>(context.params[jss::did].asString());
|
||||
if (!account || account->isZero())
|
||||
jvResult[jss::error] = "malformedAddress";
|
||||
else
|
||||
uNodeIndex = keylet::did(*account).key;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context.params.isMember("params") &&
|
||||
|
||||
@@ -1113,7 +1113,7 @@ chooseLedgerEntryType(Json::Value const& params)
|
||||
std::pair<RPC::Status, LedgerEntryType> result{RPC::Status::OK, ltANY};
|
||||
if (params.isMember(jss::type))
|
||||
{
|
||||
static constexpr std::array<std::pair<char const*, LedgerEntryType>, 26>
|
||||
static constexpr std::array<std::pair<char const*, LedgerEntryType>, 27>
|
||||
types{
|
||||
{{jss::account, ltACCOUNT_ROOT},
|
||||
{jss::amendments, ltAMENDMENTS},
|
||||
@@ -1141,7 +1141,8 @@ chooseLedgerEntryType(Json::Value const& params)
|
||||
{jss::bridge, ltBRIDGE},
|
||||
{jss::xchain_owned_claim_id, ltXCHAIN_OWNED_CLAIM_ID},
|
||||
{jss::xchain_owned_create_account_claim_id,
|
||||
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID}}};
|
||||
ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID},
|
||||
{jss::did, ltDID}}};
|
||||
|
||||
auto const& p = params[jss::type];
|
||||
if (!p.isString())
|
||||
|
||||
@@ -148,13 +148,14 @@ public:
|
||||
env.close();
|
||||
|
||||
// Give carol a deposit preauthorization, an offer, a ticket,
|
||||
// and a signer list. Even with all that she's still deletable.
|
||||
// a signer list, and a DID. Even with all that she's still deletable.
|
||||
env(deposit::auth(carol, becky));
|
||||
std::uint32_t const carolOfferSeq{env.seq(carol)};
|
||||
env(offer(carol, gw["USD"](51), XRP(51)));
|
||||
std::uint32_t const carolTicketSeq{env.seq(carol) + 1};
|
||||
env(ticket::create(carol, 1));
|
||||
env(signers(carol, 1, {{alice, 1}, {becky, 1}}));
|
||||
env(did::setValid(carol));
|
||||
|
||||
// Deleting should fail with TOO_SOON, which is a relatively
|
||||
// cheap check compared to validating the contents of her directory.
|
||||
|
||||
406
src/test/app/DID_test.cpp
Normal file
406
src/test/app/DID_test.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012, 2013 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 <ripple/basics/strHex.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/Indexes.h>
|
||||
#include <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
// Helper function that returns the owner count of an account root.
|
||||
std::uint32_t
|
||||
ownerCount(test::jtx::Env const& env, test::jtx::Account const& acct)
|
||||
{
|
||||
std::uint32_t ret{0};
|
||||
if (auto const sleAcct = env.le(acct))
|
||||
ret = sleAcct->at(sfOwnerCount);
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool
|
||||
checkVL(Slice const& result, std::string expected)
|
||||
{
|
||||
Serializer s;
|
||||
s.addRaw(result);
|
||||
return s.getString() == expected;
|
||||
}
|
||||
|
||||
struct DID_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("featureDID Enabled");
|
||||
|
||||
using namespace jtx;
|
||||
// If the DID amendment is not enabled, you should not be able
|
||||
// to set or delete DIDs.
|
||||
Env env{*this, features - featureDID};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(5000), alice);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
env(did::setValid(alice), ter(temDISABLED));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
env(did::del(alice), ter(temDISABLED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testAccountReserve(FeatureBitset features)
|
||||
{
|
||||
// Verify that the reserve behaves as expected for minting.
|
||||
testcase("DID Account Reserve");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{*this, features};
|
||||
Account const alice{"alice"};
|
||||
|
||||
// Fund alice enough to exist, but not enough to meet
|
||||
// the reserve for creating a DID.
|
||||
auto const acctReserve = env.current()->fees().accountReserve(0);
|
||||
auto const incReserve = env.current()->fees().increment;
|
||||
env.fund(acctReserve, alice);
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice) == acctReserve);
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// alice does not have enough XRP to cover the reserve for a DID
|
||||
env(did::setValid(alice), ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// Pay alice almost enough to make the reserve for a DID.
|
||||
env(pay(env.master, alice, incReserve + drops(19)));
|
||||
BEAST_EXPECT(env.balance(alice) == acctReserve + incReserve + drops(9));
|
||||
env.close();
|
||||
|
||||
// alice still does not have enough XRP for the reserve of a DID.
|
||||
env(did::setValid(alice), ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// Pay alice enough to make the reserve for a DID.
|
||||
env(pay(env.master, alice, drops(11)));
|
||||
env.close();
|
||||
|
||||
// Now alice can create a DID.
|
||||
env(did::setValid(alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// alice deletes her DID.
|
||||
env(did::del(alice));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testSetInvalid(FeatureBitset features)
|
||||
{
|
||||
testcase("Invalid DIDSet");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(5000), alice);
|
||||
env.close();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preflight
|
||||
|
||||
// invalid flags
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
env(did::setValid(alice), txflags(0x00010000), ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// no fields
|
||||
env(did::set(alice), ter(temEMPTY_DID));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// all empty fields
|
||||
env(did::set(alice),
|
||||
did::uri(""),
|
||||
did::document(""),
|
||||
did::data(""),
|
||||
ter(temEMPTY_DID));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// uri is too long
|
||||
const std::string longString(257, 'a');
|
||||
env(did::set(alice), did::uri(longString), ter(temMALFORMED));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// document is too long
|
||||
env(did::set(alice), did::document(longString), ter(temMALFORMED));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// attestation is too long
|
||||
env(did::set(alice),
|
||||
did::document("data"),
|
||||
did::data(longString),
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
// Modifying a DID to become empty is checked in testSetModify
|
||||
}
|
||||
|
||||
void
|
||||
testDeleteInvalid(FeatureBitset features)
|
||||
{
|
||||
testcase("Invalid DIDDelete");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(5000), alice);
|
||||
env.close();
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preflight
|
||||
|
||||
// invalid flags
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
env(did::del(alice), txflags(0x00010000), ter(temINVALID_FLAG));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// doApply
|
||||
|
||||
// DID doesn't exist
|
||||
env(did::del(alice), ter(tecNO_ENTRY));
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
testSetValidInitial(FeatureBitset features)
|
||||
{
|
||||
testcase("Valid Initial DIDSet");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
Account const charlie{"charlie"};
|
||||
Account const dave{"dave"};
|
||||
Account const edna{"edna"};
|
||||
Account const francis{"francis"};
|
||||
Account const george{"george"};
|
||||
env.fund(XRP(5000), alice, bob, charlie, dave, edna, francis);
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 0);
|
||||
BEAST_EXPECT(ownerCount(env, charlie) == 0);
|
||||
|
||||
// only URI
|
||||
env(did::set(alice), did::uri("uri"));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
|
||||
// only DIDDocument
|
||||
env(did::set(bob), did::document("data"));
|
||||
BEAST_EXPECT(ownerCount(env, bob) == 1);
|
||||
|
||||
// only Data
|
||||
env(did::set(charlie), did::data("data"));
|
||||
BEAST_EXPECT(ownerCount(env, charlie) == 1);
|
||||
|
||||
// URI + Data
|
||||
env(did::set(dave), did::uri("uri"), did::data("attest"));
|
||||
BEAST_EXPECT(ownerCount(env, dave) == 1);
|
||||
|
||||
// URI + DIDDocument
|
||||
env(did::set(edna), did::uri("uri"), did::document("data"));
|
||||
BEAST_EXPECT(ownerCount(env, edna) == 1);
|
||||
|
||||
// DIDDocument + Data
|
||||
env(did::set(francis), did::document("data"), did::data("attest"));
|
||||
BEAST_EXPECT(ownerCount(env, francis) == 1);
|
||||
|
||||
// URI + DIDDocument + Data
|
||||
env(did::set(george),
|
||||
did::uri("uri"),
|
||||
did::document("data"),
|
||||
did::data("attest"));
|
||||
BEAST_EXPECT(ownerCount(env, george) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testSetModify(FeatureBitset features)
|
||||
{
|
||||
testcase("Modify DID with DIDSet");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this);
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(5000), alice);
|
||||
env.close();
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
auto const ar = env.le(alice);
|
||||
|
||||
// Create DID
|
||||
std::string const initialURI = "uri";
|
||||
{
|
||||
env(did::set(alice), did::uri(initialURI));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(sleDID);
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
// Try to delete URI, fails because no elements are set
|
||||
{
|
||||
env(did::set(alice), did::uri(""), ter(tecEMPTY_DID));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
// Set DIDDocument
|
||||
std::string const initialDocument = "data";
|
||||
{
|
||||
env(did::set(alice), did::document(initialDocument));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
// Set Data
|
||||
std::string const initialData = "attest";
|
||||
{
|
||||
env(did::set(alice), did::data(initialData));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfURI], initialURI));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData));
|
||||
}
|
||||
|
||||
// Remove URI
|
||||
{
|
||||
env(did::set(alice), did::uri(""));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfData], initialData));
|
||||
}
|
||||
|
||||
// Remove Data
|
||||
{
|
||||
env(did::set(alice), did::data(""));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], initialDocument));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
// Remove Data + set URI
|
||||
std::string const secondURI = "uri2";
|
||||
{
|
||||
env(did::set(alice), did::uri(secondURI), did::document(""));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfURI], secondURI));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
// Remove URI + set DIDDocument
|
||||
std::string const secondDocument = "data2";
|
||||
{
|
||||
env(did::set(alice), did::uri(""), did::document(secondDocument));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfDIDDocument], secondDocument));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfData));
|
||||
}
|
||||
|
||||
// Remove DIDDocument + set Data
|
||||
std::string const secondData = "randomData";
|
||||
{
|
||||
env(did::set(alice), did::document(""), did::data(secondData));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 1);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfURI));
|
||||
BEAST_EXPECT(!sleDID->isFieldPresent(sfDIDDocument));
|
||||
BEAST_EXPECT(checkVL((*sleDID)[sfData], secondData));
|
||||
}
|
||||
|
||||
// Delete DID
|
||||
{
|
||||
env(did::del(alice));
|
||||
BEAST_EXPECT(ownerCount(env, alice) == 0);
|
||||
auto const sleDID = env.le(keylet::did(alice.id()));
|
||||
BEAST_EXPECT(!sleDID);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{
|
||||
supported_amendments() | FeatureBitset{featureDID}};
|
||||
testEnabled(all);
|
||||
testAccountReserve(all);
|
||||
testSetInvalid(all);
|
||||
testDeleteInvalid(all);
|
||||
testSetModify(all);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(DID, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -581,7 +581,7 @@ class NFToken0_test : public beast::unit_test::suite
|
||||
ter(temMALFORMED));
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// preflight
|
||||
// preclaim
|
||||
|
||||
// Non-existent issuer.
|
||||
env(token::mint(alice, 0u),
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <test/jtx/check.h>
|
||||
#include <test/jtx/delivermin.h>
|
||||
#include <test/jtx/deposit.h>
|
||||
#include <test/jtx/did.h>
|
||||
#include <test/jtx/fee.h>
|
||||
#include <test/jtx/flags.h>
|
||||
#include <test/jtx/genesis.h>
|
||||
|
||||
104
src/test/jtx/did.h
Normal file
104
src/test/jtx/did.h
Normal file
@@ -0,0 +1,104 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2019 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_TEST_JTX_DID_H_INCLUDED
|
||||
#define RIPPLE_TEST_JTX_DID_H_INCLUDED
|
||||
|
||||
#include <test/jtx/Account.h>
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/owners.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/** DID operations. */
|
||||
namespace did {
|
||||
|
||||
Json::Value
|
||||
set(jtx::Account const& account);
|
||||
|
||||
Json::Value
|
||||
setValid(jtx::Account const& account);
|
||||
|
||||
/** Sets the optional DIDDocument on a DIDSet. */
|
||||
class document
|
||||
{
|
||||
private:
|
||||
std::string document_;
|
||||
|
||||
public:
|
||||
explicit document(std::string const& u) : document_(strHex(u))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(jtx::Env&, jtx::JTx& jtx) const
|
||||
{
|
||||
jtx.jv[sfDIDDocument.jsonName] = document_;
|
||||
}
|
||||
};
|
||||
|
||||
/** Sets the optional URI on a DIDSet. */
|
||||
class uri
|
||||
{
|
||||
private:
|
||||
std::string uri_;
|
||||
|
||||
public:
|
||||
explicit uri(std::string const& u) : uri_(strHex(u))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(jtx::Env&, jtx::JTx& jtx) const
|
||||
{
|
||||
jtx.jv[sfURI.jsonName] = uri_;
|
||||
}
|
||||
};
|
||||
|
||||
/** Sets the optional Attestation on a DIDSet. */
|
||||
class data
|
||||
{
|
||||
private:
|
||||
std::string data_;
|
||||
|
||||
public:
|
||||
explicit data(std::string const& u) : data_(strHex(u))
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
operator()(jtx::Env&, jtx::JTx& jtx) const
|
||||
{
|
||||
jtx.jv[sfData.jsonName] = data_;
|
||||
}
|
||||
};
|
||||
|
||||
Json::Value
|
||||
del(jtx::Account const& account);
|
||||
|
||||
} // namespace did
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
67
src/test/jtx/impl/did.cpp
Normal file
67
src/test/jtx/impl/did.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2019 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 <ripple/protocol/TxFlags.h>
|
||||
#include <ripple/protocol/jss.h>
|
||||
#include <test/jtx/did.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
namespace jtx {
|
||||
|
||||
/** DID operations. */
|
||||
namespace did {
|
||||
|
||||
Json::Value
|
||||
set(jtx::Account const& account)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::DIDSet;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
setValid(jtx::Account const& account)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::DIDSet;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
jv[sfURI.jsonName] = strHex(std::string{"uri"});
|
||||
return jv;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
del(jtx::Account const& account)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::DIDDelete;
|
||||
jv[jss::Account] = to_string(account.id());
|
||||
jv[jss::Flags] = tfUniversal;
|
||||
return jv;
|
||||
}
|
||||
|
||||
} // namespace did
|
||||
|
||||
} // namespace jtx
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -599,6 +599,7 @@ public:
|
||||
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::uri_token), 0));
|
||||
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::hook), 0));
|
||||
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::amm), 0));
|
||||
BEAST_EXPECT(acct_objs_is_size(acct_objs(gw, jss::did), 0));
|
||||
|
||||
// gw mints an NFT so we can find it.
|
||||
uint256 const nftID{token::getNextID(env, gw, 0u, tfTransferable)};
|
||||
@@ -863,6 +864,26 @@ public:
|
||||
BEAST_EXPECT(
|
||||
payChan[sfSettleDelay.jsonName].asUInt() == 24 * 60 * 60);
|
||||
}
|
||||
|
||||
{
|
||||
// gw creates a DID that we can look for in the ledger.
|
||||
Json::Value jvDID;
|
||||
jvDID[jss::TransactionType] = jss::DIDSet;
|
||||
jvDID[jss::Flags] = tfUniversal;
|
||||
jvDID[jss::Account] = gw.human();
|
||||
jvDID[sfURI.jsonName] = strHex(std::string{"uri"});
|
||||
env(jvDID);
|
||||
env.close();
|
||||
}
|
||||
{
|
||||
// Find the DID.
|
||||
Json::Value const resp = acct_objs(gw, jss::did);
|
||||
BEAST_EXPECT(acct_objs_is_size(resp, 1));
|
||||
|
||||
auto const& did = resp[jss::result][jss::account_objects][0u];
|
||||
BEAST_EXPECT(did[sfAccount.jsonName] == gw.human());
|
||||
BEAST_EXPECT(did[sfURI.jsonName] == strHex(std::string{"uri"}));
|
||||
}
|
||||
// Make gw multisigning by adding a signerList.
|
||||
env(jtx::signers(gw, 6, {{alice, 7}}));
|
||||
env.close();
|
||||
@@ -890,7 +911,7 @@ public:
|
||||
auto const& ticket = resp[jss::result][jss::account_objects][0u];
|
||||
BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human());
|
||||
BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket);
|
||||
BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 10);
|
||||
BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 11);
|
||||
}
|
||||
{
|
||||
// Create a uri token.
|
||||
|
||||
@@ -2148,6 +2148,57 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryDID()
|
||||
{
|
||||
testcase("ledger_entry Request DID");
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals::chrono_literals;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
// Lambda to create a DID.
|
||||
auto didCreate = [](test::jtx::Account const& account) {
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = jss::DIDSet;
|
||||
jv[jss::Account] = account.human();
|
||||
jv[sfDIDDocument.jsonName] = strHex(std::string{"data"});
|
||||
jv[sfURI.jsonName] = strHex(std::string{"uri"});
|
||||
return jv;
|
||||
};
|
||||
|
||||
env(didCreate(alice));
|
||||
env.close();
|
||||
|
||||
std::string const ledgerHash{to_string(env.closed()->info().hash)};
|
||||
|
||||
{
|
||||
// Request the DID using its index.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::did] = alice.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfDIDDocument.jsonName] ==
|
||||
strHex(std::string{"data"}));
|
||||
BEAST_EXPECT(
|
||||
jrr[jss::node][sfURI.jsonName] == strHex(std::string{"uri"}));
|
||||
}
|
||||
{
|
||||
// Request an index that is not a DID.
|
||||
Json::Value jvParams;
|
||||
jvParams[jss::did] = env.master.human();
|
||||
jvParams[jss::ledger_hash] = ledgerHash;
|
||||
Json::Value const jrr = env.rpc(
|
||||
"json", "ledger_entry", to_string(jvParams))[jss::result];
|
||||
checkErrorValue(jrr, "entryNotFound", "");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerEntryInvalidParams(unsigned int apiVersion)
|
||||
{
|
||||
@@ -2897,6 +2948,7 @@ public:
|
||||
testNoQueue();
|
||||
testQueue();
|
||||
testLedgerAccountsOption();
|
||||
testLedgerEntryDID();
|
||||
|
||||
test::jtx::forAllApiVersions(std::bind_front(
|
||||
&LedgerRPC_test::testLedgerEntryInvalidParams, this));
|
||||
|
||||
Reference in New Issue
Block a user