Verify Credentials in Javascript
This tutorial describes how to verify that an account holds a valid credential on the XRP Ledger, which has different use cases depending on the type of credential and the meaning behind it. A few possible reasons to verify a credential include:
- Confirming that a recipient has passed a background check before sending a payment.
- Checking a person's professional certifications, after verifying their identity with a DID.
- Displaying a player's achievements in a blockchain-connected game.
This tutorial uses sample code in Javascript using the xrpl-js library.
Prerequisites
- You must have Node.js installed and know how to run Javascript code from the command line. Node.js v18 is required for xrpl.js.
- You should have a basic understanding of the XRP Ledger.
- The credential you want to verify should exist in the ledger already, and you should know the addresses of both the issuer and the holder, as well as the official credential type you want to check.
- For sample code showing how to create credentials, see Build a Credential Issuing Service.
Setup
First, download the complete sample code for this tutorial from GitHub:
Then, in the appropriate directory, install dependencies:
npm install
This installs the appropriate version of the xrpl.js
library and a few other dependencies. You can view all dependencies in the package.json
file.
Overview
The Verify Credential sample code consists of one file, verify_credential.js
, and contains two main parts:
- A function,
verifyCredential(...)
which can be called with appropriate arguments to verify that a credential exists and is valid. This function can be imported into other code to be used as part of a larger application. - A commandline utility that can be used to verify any credential.
Usage
To test that you have the code installed and working properly, you can run the commandline utility with no arguments to check the existence of a default credential on Devnet, such as:
node verify_credential.js
If all goes well, you should see output such as the following:
info: Encoded credential_type as hex: 5465737443726564656E7469616C
info: Looking up credential...
info: {
"command": "ledger_entry",
"credential": {
"subject": "rBqPPjAW6ubfFdmwERgajvgP5LtM4iQSQG",
"issuer": "rPLY4DWhB4VA7PPZ8nvZLhShXeVZqeKif3",
"credential_type": "5465737443726564656E7469616C"
},
"ledger_index": "validated"
}
info: Found credential:
info: {
"CredentialType": "5465737443726564656E7469616C",
"Flags": 65536,
"Issuer": "rPLY4DWhB4VA7PPZ8nvZLhShXeVZqeKif3",
"IssuerNode": "0",
"LedgerEntryType": "Credential",
"PreviousTxnID": "B078C70D17820069BDF913146F9908A209B4E10794857A3E474F4C9C5A35CA6A",
"PreviousTxnLgrSeq": 1768183,
"Subject": "rBqPPjAW6ubfFdmwERgajvgP5LtM4iQSQG",
"SubjectNode": "0",
"index": "F2ACB7292C4F4ACB18010251F1653934DC17F06AA5BDE7F484F65B5A648D70CB"
}
info: Credential is valid.
If the code reports that the credential was not found when called with no arguments, it's possible that the example credential has been deleted or the Devnet has been reset. If you have another credential you can verify, you can provide the details as commandline arguments. For example:
node verify_credential.js rPLY4DWhB4VA7PPZ8nvZLhShXeVZqeKif3 rBqPPjAW6ubfFdmwERgajvgP5LtM4iQSQG TestCredential
A full usage statement is available with the -h
flag.
Other Examples
The following examples show other possible scenarios. The data for these examples may or may not still be present in Devnet. For example, anyone can delete an expired credential.
$ ./verify_credential.js rPLY4DWhB4VA7PPZ8nvZLhShXeVZqeKif3 rBqPPjAW6ubfFdmwERgajvgP5LtM4iQSQG TCredential777
info: Encoded credential_type as hex: 5443726564656E7469616C373737
info: Looking up credential...
info: {
"command": "ledger_entry",
"credential": {
"subject": "rBqPPjAW6ubfFdmwERgajvgP5LtM4iQSQG",
"issuer": "rPLY4DWhB4VA7PPZ8nvZLhShXeVZqeKif3",
"credential_type": "5443726564656E7469616C373737"
},
"ledger_index": "validated"
}
info: Found credential:
info: {
"CredentialType": "5443726564656E7469616C373737",
"Expiration": 798647105,
"Flags": 65536,
"Issuer": "rPLY4DWhB4VA7PPZ8nvZLhShXeVZqeKif3",
"IssuerNode": "0",
"LedgerEntryType": "Credential",
"PreviousTxnID": "D32F66D1446C810BF4E6310E21111C0CE027140292347F0C7A73322F08C07D7E",
"PreviousTxnLgrSeq": 2163057,
"Subject": "rBqPPjAW6ubfFdmwERgajvgP5LtM4iQSQG",
"SubjectNode": "0",
"URI": "746573745F757269",
"index": "6E2AF1756C22BF7DC3AA47AD303C985026585B54425E7FACFAD6CD1867DD39C2"
}
info: Credential has expiration: 2025-04-22T14:25:05.000Z
info: Looking up validated ledger to check for expiration.
info: Most recent validated ledger is: 2025-04-22T13:47:30.000Z
info: Credential is valid.
Code Walkthrough
1. Initial setup
The verify_credential.js
file implements the code for this tutorial. This file can be run as a commandline tool, so it starts with a shebang. Then it imports the relevant dependencies, including the specific parts of the xrpl.js
library:
#!/usr/bin/env node
import { Command } from "commander";
import { Client, rippleTimeToISOTime, convertStringToHex } from "xrpl";
import winston from "winston";
The next section of the code sets the default log level for messages that might be written to the console through the utility:
// Set up logging --------------------------------------------------------------
// Use WARNING by default in case verify_credential is called from elsewhere.
const logger = winston.createLogger({
level: "warn",
transports: [new winston.transports.Console()],
format: winston.format.simple(),
});
Then it defines a type of exception to throw if something goes wrong when connecting to the XRP Ledger:
// Define an error to throw when XRPL lookup fails unexpectedly
class XRPLLookupError extends Error {
constructor(error) {
super("XRPL look up error");
this.name = "XRPLLookupError";
this.body = error;
}
}
Finally, a regular expression to validate the credential format and the lsfAccepted flag are defined as constants for use further on in the code.
const CREDENTIAL_REGEX = /^[0-9A-F]{2,128}$/;
// See https://xrpl.org/docs/references/protocol/ledger-data/ledger-entry-types/credential#credential-flags
// to learn more about the lsfAccepted flag.
const LSF_ACCEPTED = 0x00010000;
2. verifyCredential function
The verifyCredential(...)
function performs the main work for this tutorial. The function definition and comments define the parameters:
async function verifyCredential(client, issuer, subject, credentialType, binary=false) {
/**
* Check whether an XRPL account holds a specified credential,
* as of the most recently validated ledger.
* Parameters:
* client - Client for interacting with rippled servers.
* issuer - Address of the credential issuer, in base58.
* subject - Address of the credential holder/subject, in base58.
* credentialType - Credential type to check for as a string,
* which will be encoded as UTF-8 (1-128 characters long).
* binary - Specifies that the credential type is provided in hexadecimal format.
* You must provide the credential_type as input.
* Returns True if the account holds the specified, valid credential.
* Returns False if the credential is missing, expired, or not accepted.
*/
The XRP Ledger APIs require the credential type to be hexadecimal, so it converts the user input if necessary:
// Encode credentialType as uppercase hex, if needed
let credentialTypeHex = "";
if (binary) {
credentialTypeHex = credentialType.toUpperCase();
} else {
credentialTypeHex = convertStringToHex(credentialType).toUpperCase();
logger.info(`Encoded credential_type as hex: ${credentialTypeHex}`);
}
if (credentialTypeHex.length % 2 !== 0 || !CREDENTIAL_REGEX.test(credentialTypeHex)) {
// Hexadecimal is always 2 chars per byte, so an odd length is invalid.
throw new Error("Credential type must be 128 characters as hexadecimal.");
}
Next, it calls the ledger_entry method to look up the requested Credential ledger entry:
// Perform XRPL lookup of Credential ledger entry --------------------------
const ledgerEntryRequest = {
command: "ledger_entry",
credential: {
subject: subject,
issuer: issuer,
credential_type: credentialTypeHex,
},
ledger_index: "validated",
};
logger.info("Looking up credential...");
logger.info(JSON.stringify(ledgerEntryRequest, null, 2));
let xrplResponse;
try {
xrplResponse = await client.request(ledgerEntryRequest);
} catch (err) {
if (err.data?.error === "entryNotFound") {
logger.info("Credential was not found");
return false;
} else {
// Other errors, for example invalidly specified addresses.
throw new XRPLLookupError(err?.data || err);
}
}
const credential = xrplResponse.result.node;
logger.info("Found credential:");
logger.info(JSON.stringify(credential, null, 2));
If it succeeds in finding the credential, the function continues by checking that the credential has been accepted by its holder. Since anyone can issue a credential to anyone else, a credential is only considered valid if its subject has accepted it.
// Check if the credential has been accepted ---------------------------
if (!(credential.Flags & LSF_ACCEPTED)) {
logger.info("Credential is not accepted.");
return false
}
Then, if the credential has an expiration time, the function checks that the credential is not expired. If the credential has no expiration, this step can be skipped. A credential is officially considered expired if its expiration time is before the official close time of the most recently validated ledger. This is more universal than comparing the expiration to your own local clock. Thus, the code uses the ledger method to look up the most recently validated ledger:
// Confirm that the credential is not expired ------------------------------
if (credential.Expiration) {
const expirationTime = rippleTimeToISOTime(credential.Expiration);
logger.info(`Credential has expiration: ${expirationTime}`);
logger.info("Looking up validated ledger to check for expiration.");
let ledgerResponse;
try {
ledgerResponse = await client.request({
command: "ledger",
ledger_index: "validated",
});
} catch (err) {
throw new XRPLLookupError(err?.data || err);
}
const closeTime = rippleTimeToISOTime(ledgerResponse.result.ledger.close_time);
logger.info(`Most recent validated ledger is: ${closeTime}`);
if (new Date(closeTime) > new Date(expirationTime)) {
logger.info("Credential is expired.");
return false;
}
}
If none of the checks up to this point have returned a false
value, then the credential must be valid. This concludes the verifyCredential(...)
function:
// Credential has passed all checks ---------------------------------------
logger.info("Credential is valid.");
return true;
}
3. Commandline interface
This file also implements a commandline utility which runs when the file is executed directly as a Node.js script. Some variables, such as the set of available networks, are only needed for this mode:
// Commandline usage -----------------------------------------------------------
async function main() {
// Websocket URLs of public servers
const NETWORKS = {
devnet: "wss://s.devnet.rippletest.net:51233",
testnet: "wss://s.altnet.rippletest.net:51233",
mainnet: "wss://xrplcluster.com/",
};
Then it uses the commander package to define and parse the arguments that the user can pass from the commandline:
// Parse arguments ---------------------------------------------------------
let result = false
const program = new Command();
program
.name("verify-credential")
.description("Verify an XRPL credential")
.argument("[issuer]", "Credential issuer address as base58", "rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS")
.argument("[subject]", "Credential subject (holder) address as base58", "rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA")
.argument("[credential_type]", "Credential type as string.", "my_credential")
.option("-b, --binary", "Use binary (hexadecimal) for credential_type")
.option(
`-n, --network <network> {${Object.keys(NETWORKS)}}`,
"Use the specified network for lookup",
(value) => {
if (!Object.keys(NETWORKS).includes(value)) {
throw new Error(`Must be one of: ${Object.keys(NETWORKS)}`);
}
return value;
},
"devnet"
)
.option("-q, --quiet", "Don't print log messages")
After parsing the commandline args, it sets the appropriate values and passes them to verifyCredential(...)
to perform the credential verification:
// Call verify_credential with appropriate args ----------------------------
.action(async (issuer, subject, credentialType, options) => {
const client = new Client(NETWORKS[options.network]);
await client.connect();
// Use INFO level by default when called from the commandline.
if (!options.quiet) { logger.level = "info" }
// Commander.js automatically sets options.binary to a boolean:
// - If you provide -b or --binary on the command line then options.binary = true
// - If you do not provide it then options.binary = false
result = await verifyCredential(client, issuer, subject, credentialType, options.binary);
await client.disconnect();
});
await program.parseAsync(process.argv);
It returns a nonzero exit code if this credential was not verified. This can be useful for shell scripts:
// Return a nonzero exit code if credential verification failed -----------
if (!result) {
process.exit(1);
}
}
Finally, the code runs the main()
function:
main().catch((err) => {
console.error("Fatal startup error:", err);
process.exit(1);
});
Next Steps
Now that you know how to use xrpl.js
to verify credentials, you can try building this or related steps together into a bigger project. For example:
- Incorporate credential verification into a wallet application.
- Issue your own credentials with a credential issuing service.