Files
xrpl-dev-portal/_code-samples/issue-credentials/js/issuer_service.js
2025-04-17 12:56:08 +01:00

209 lines
6.8 KiB
JavaScript

import express from "express";
import morgan from 'morgan';
import inquirer from "inquirer";
import dotenv from "dotenv";
import { Wallet, Client } from "xrpl";
import {
validateCredentialRequest,
verifyDocuments,
credentialToXrpl,
credentialFromXrpl,
} from "./credential.js";
import { XRPLTxError } from "./errors.js";
import { lookUpCredentials } from "./look_up_credentials.js";
dotenv.config();
async function initWallet() {
let seed = process.env.ISSUER_ACCOUNT_SEED;
if (!seed || seed.startsWith("<")) {
const { seedInput } = await inquirer.prompt([
{
type: "password",
name: "seedInput",
message: "Issuer account seed:",
validate: (input) => (input ? true : "Please specify the issuer's master seed"),
},
]);
seed = seedInput;
}
return Wallet.fromSeed(seed);
}
// Error handling --------------------------------------------------------------
function handleAppError(res, err) {
if (err.name === "ValueError") {
return res.status(err.status).json({
error: err.type,
error_message: err.message,
});
}
if (err.name === "XRPLTxError") {
return res.status(err.status).json(err.body);
}
// Default fallback
return res.status(400).json({ error_message: err.message });
}
async function main() {
// Set up XRPL connection ------------------------------------------------------
const client = new Client("wss://s.devnet.rippletest.net:51233");
await client.connect();
const wallet = await initWallet();
console.log("✅ Starting credential issuer with XRPL address", wallet.address);
// Define Express app ------------------------------------------------------
const app = express();
app.use(morgan('common')); // Logger
app.use(express.json()); // Middleware to parse JSON requests
// POST /credential - Method for users to request a credential from the service -------------------
app.post("/credential", async (req, res) => {
try {
// validateCredentialRequest() throws if the request is not validly formatted
const credRequest = validateCredentialRequest(req.body);
/**
* As a credential issuer, you typically need to verify some information
* about someone before you issue them a credential. For this example,
* the user passes relevant information in a documents field of the API request.
* The documents are kept confidential, off-chain.
*
* verifyDocuments() throws if the provided documents don't pass inspection
*/
verifyDocuments(req.body);
const credXrpl = credentialToXrpl(credRequest);
const tx = {
TransactionType: "CredentialCreate",
Account: wallet.address,
Subject: credXrpl.subject,
CredentialType: credXrpl.credential,
URI: credXrpl.uri,
Expiration: credXrpl.expiration,
};
const ccResponse = await client.submitAndWait(tx, { autofill: true, wallet });
if (ccResponse.result.meta.TransactionResult === "tecDUPLICATE") {
throw new XRPLTxError(ccResponse, 409);
} else if (ccResponse.result.meta.TransactionResult !== "tesSUCCESS") {
throw new XRPLTxError(ccResponse);
}
return res.status(201).json(ccResponse.result);
} catch (err) {
return handleAppError(res, err);
}
});
// GET /admin/credential - Method for admins to look up all credentials issued -------------------
app.get("/admin/credential", async (req, res) => {
try {
// ?accepted=yes|no|both query parameter - the default is "both"
const query = Object.fromEntries(
Object.entries(req.query).map(([k, v]) => [k.toLowerCase(), v])
);
const filterAccepted = (query.accepted || "both").toLowerCase();
const credentials = await lookUpCredentials(client, wallet.address, "", filterAccepted);
const result = credentials.map((entry) => credentialFromXrpl(entry));
return res.status(200).json({ credentials: result });
} catch (err) {
return handleAppError(res, err);
}
});
// DELETE /admin/credential - Method for admins to revoke an issued credential ----------------------------
app.delete("/admin/credential", async (req, res) => {
let delRequest;
try {
delRequest = validateCredentialRequest(req.body);
const { credential } = credentialToXrpl(delRequest);
// To save on transaction fees, check if the credential exists on ledger before attempting to delete it.
// If the credential is not found, a RippledError (`entryNotFound`) is thrown.
await client.request({
command: "ledger_entry",
credential: {
subject: delRequest.subject,
issuer: wallet.address,
credential_type: credential,
},
ledger_index: "validated",
});
const tx = {
TransactionType: "CredentialDelete",
Account: wallet.address,
Subject: delRequest.subject,
CredentialType: credential,
};
const cdResponse = await client.submitAndWait(tx, { autofill: true, wallet });
if (cdResponse.result.meta.TransactionResult === "tecNO_ENTRY") {
// Usually this won't happen since we just checked for the credential,
// but it's possible it got deleted since then.
throw new XRPLTxError(cdResponse, 404);
} else if (cdResponse.result.meta.TransactionResult !== "tesSUCCESS") {
throw new XRPLTxError(cdResponse);
}
return res.status(200).json(cdResponse.result);
} catch (err) {
if (err.data?.error === "entryNotFound") {
return res.status(404).json({
error: err.data.error,
error_message: `Credential doesn't exist for subject '${delRequest.subject}' and credential type '${delRequest.credential}'`,
});
} else {
return handleAppError(res, err);
}
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🔐 Credential issuer service running on port: ${PORT}`);
});
}
// Start the server --------------------------------------------------------------
main().catch((err) => {
console.error("❌ Fatal startup error:", err);
process.exit(1);
});
/**
* Tip: Express returns plain text by default for unhandled routes (like 404 Not Found).
* To ensure consistent JSON responses across your API, consider adding a catch-all
* handler.
*
* Example:
*
* // Handle unmatched routes
* app.use((req, res) => {
* res.status(404).json({
* error: "notFound",
* error_message: `Route not found: ${req.method} ${req.originalUrl}`,
* });
* });
*
* You can also add a global error handler for unhandled exceptions:
*
* app.use((err, req, res, next) => {
* console.error("Unhandled error:", err);
* res.status(500).json({
* error: "internalServerError",
* error_message: "Something went wrong.",
* });
* });
*/