mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-08 22:05:50 +00:00
176 lines
5.7 KiB
JavaScript
176 lines
5.7 KiB
JavaScript
import express from "express";
|
|
import morgan from 'morgan';
|
|
import inquirer from "inquirer";
|
|
import dotenv from "dotenv";
|
|
import { Wallet, Client, RippledError } from "xrpl";
|
|
|
|
import {
|
|
validateCredentialRequest,
|
|
verifyDocuments,
|
|
credentialToXrpl,
|
|
serializeCredential,
|
|
parseCredentialFromXrpl,
|
|
} from "./credential.js";
|
|
import { XRPLTxError } from "./errors.js";
|
|
import { lookUpCredentials } from "./look_up_credentials.js";
|
|
|
|
dotenv.config();
|
|
|
|
async function main() {
|
|
// Set up XRPL connection ------------------------------------------------------
|
|
const wallet = await initWallet();
|
|
console.log("✅ Starting credential issuer with XRPL address", wallet.address);
|
|
|
|
const client = new Client("wss://s.devnet.rippletest.net:51233");
|
|
await client.connect();
|
|
|
|
// 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);
|
|
// 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.submit(tx, { autofill: true, wallet });
|
|
|
|
if (ccResponse.result.engine_result === "tecDUPLICATE") {
|
|
throw new XRPLTxError(ccResponse, 409);
|
|
} else if (ccResponse.result.engine_result !== "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) =>
|
|
serializeCredential(parseCredentialFromXrpl(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,
|
|
},
|
|
});
|
|
|
|
const tx = {
|
|
TransactionType: "CredentialDelete",
|
|
Account: wallet.address,
|
|
Subject: delRequest.subject,
|
|
CredentialType: credential,
|
|
};
|
|
const cdResponse = await client.submit(tx, { autofill: true, wallet });
|
|
|
|
if (cdResponse.result.engine_result === "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.engine_result !== "tesSUCCESS") {
|
|
throw new XRPLTxError(cdResponse);
|
|
}
|
|
|
|
return res.status(200).json(cdResponse.result);
|
|
} catch (err) {
|
|
if (err instanceof RippledError) {
|
|
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}`);
|
|
});
|
|
}
|
|
|
|
async function initWallet() {
|
|
let seed = process.env.ISSUER_ACCOUNT_SEED;
|
|
|
|
if (!seed) {
|
|
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 });
|
|
}
|
|
|
|
// Start the server --------------------------------------------------------------
|
|
main().catch((err) => {
|
|
console.error("❌ Fatal startup error:", err);
|
|
process.exit(1);
|
|
});
|