Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 28 additions & 21 deletions _code-samples/verify-credential/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,34 @@ Quick install & usage:

```sh
npm install
node ./verify_credential.js
```

`verify_credential.js` can also be used as a commandline utility. Full usage statement:

```sh
$ ./verify_credential.js -h

Usage: verify-credential [options] [issuer] [subject] [credential_type]

Verify an XRPL credential

Arguments:
issuer Credential issuer address as base58 (default:
"rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS")
subject Credential subject (holder) address as base58 (default:
"rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA")
credential_type Credential type as string. (default: "my_credential")

Options:
-b, --binary Use binary (hexadecimal) for credential_type
-n, --network <network> {devnet,testnet,mainnet} Use the specified network for lookup (default: "devnet")
-q, --quiet Don't print log messages
-h, --help display help for command
The output should look something like this:

```text
Looking up credential...
{
"command": "ledger_entry",
"credential": {
"subject": "rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA",
"issuer": "rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS",
"credential_type": "6D795F63726564656E7469616C"
},
"ledger_index": "validated"
}
Found credential:
{
"CredentialType": "6D795F63726564656E7469616C",
"Flags": 65536,
"Issuer": "rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS",
"IssuerNode": "0",
"LedgerEntryType": "Credential",
"PreviousTxnID": "7D1257779E2D298C07C7E0C73CD446534B143FBD1F13DB268A878E40FD153B9A",
"PreviousTxnLgrSeq": 803275,
"Subject": "rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA",
"SubjectNode": "0",
"index": "9603F0E204A8B1C61823625682EB0ECE98A4ECF22FF46CD4845FA9BFA3606B24"
}
Credential is valid.
```
15 changes: 3 additions & 12 deletions _code-samples/verify-credential/js/package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
{
"name": "issuer_service",
"version": "1.0.0",
"description": "",
"main": "verify_credential.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"name": "verify_credentials",
"version": "2.0.0",
"description": "Sample code showing how to check if a credential on the XRPL exists and is valid.",
"type": "module",
"dependencies": {
"commander": "^13.1.0",
"winston": "^3.17.0",
"xrpl": "^4.2.5"
}
}
226 changes: 55 additions & 171 deletions _code-samples/verify-credential/js/verify_credential.js
Original file line number Diff line number Diff line change
@@ -1,184 +1,68 @@
#!/usr/bin/env node

import { Command } from "commander";
import { Client, rippleTimeToISOTime, convertStringToHex } from "xrpl";
import winston from "winston";

// 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(),
});

// 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;
}
import { Client, rippleTimeToISOTime, convertStringToHex } from "xrpl"

const client = new Client("wss://s.devnet.rippletest.net:51233")
await client.connect()

const subject_address = "rsYhHbanGpnYe3M6bsaMeJT5jnLTfDEzoA"
const issuer_address = "rEzikzbnH6FQJ2cCr4Bqmf6c3jyWLzkonS"
const credential_type = convertStringToHex("my_credential").toUpperCase()

// Look up Credential ledger entry --------------------------------------------
const ledgerEntryRequest = {
command: "ledger_entry",
credential: {
subject: subject_address,
issuer: issuer_address,
credential_type: credential_type,
},
ledger_index: "validated",
}

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;

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.
*/

// Encode credentialType as uppercase hex, if needed
let credentialTypeHex = "";
if (binary) {
credentialTypeHex = credentialType.toUpperCase();
console.log("Looking up credential...")
console.log(JSON.stringify(ledgerEntryRequest, null, 2))

let xrplResponse
try {
xrplResponse = await client.request(ledgerEntryRequest)
} catch (err) {
if (err.data?.error === "entryNotFound") {
console.error("Credential was not found")
} 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.");
}

// 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));

// Check if the credential has been accepted ---------------------------
if (!(credential.Flags & LSF_ACCEPTED)) {
logger.info("Credential is not accepted.");
return false
}

// 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;
}
console.error(err)
}

// Credential has passed all checks ---------------------------------------
logger.info("Credential is valid.");
return true;
process.exit(1)
}

const credential = xrplResponse.result.node
console.log("Found credential:")
console.log(JSON.stringify(credential, null, 2))

// 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/",
};


// 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")
// Call verify_credential with appropriate args ----------------------------
.action(async (issuer, subject, credentialType, options) => {
const client = new Client(NETWORKS[options.network]);
await client.connect();
// Check if the credential has been accepted ----------------------------------
const lsfAccepted = 0x00010000
if (!(credential.Flags & lsfAccepted)) {
console.log("Credential is not accepted.")
process.exit(2)
}

// Use INFO level by default when called from the commandline.
if (!options.quiet) { logger.level = "info" }
// Confirm that the credential is not expired ---------------------------------
if (credential.Expiration) {
const expirationTime = rippleTimeToISOTime(credential.Expiration)
console.log(`Credential has expiration: ${expirationTime}`)
console.log("Looking up validated ledger to check for expiration.")

// 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);
const ledgerResponse = await client.request({
command: "ledger",
ledger_index: "validated",
})

await client.disconnect();
});
await program.parseAsync(process.argv);
const closeTime = rippleTimeToISOTime(ledgerResponse.result.ledger.close_time)
console.log(`Most recent validated ledger was at: ${closeTime}`)

// Return a nonzero exit code if credential verification failed -----------
if (!result) {
process.exit(1);
if (new Date(closeTime) > new Date(expirationTime)) {
console.log("Credential is expired.")
process.exit(3)
}
}

main().catch((err) => {
console.error("Fatal startup error:", err);
process.exit(1);
});
// Credential has passed all checks -------------------------------------------
console.log("Credential is valid.")
client.disconnect()
3 changes: 3 additions & 0 deletions redirects.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/resources/contribute-documentation/tutorial-structure/:
to: /resources/contribute-documentation/tutorial-template/
type: 301
/docs/infrastructure/installation/rippled-1-3-migration-instructions/:
to: /docs/infrastructure/installation/
type: 301
Expand Down
Loading