Skip to content

Instantly share code, notes, and snippets.

@mikekoro
Created August 14, 2025 16:39
Show Gist options
  • Save mikekoro/56cd2e500060c6fef172b7e2bd0c41db to your computer and use it in GitHub Desktop.
Save mikekoro/56cd2e500060c6fef172b7e2bd0c41db to your computer and use it in GitHub Desktop.
/**
* A set of functions called "actions" for `xrpl`
*/
import { Client, Wallet } from "xrpl";
import validator from "validator";
import { verify, deriveAddress } from "ripple-keypairs";
export default {
// ========================================
// WALLET AUTHENTICATION ACTIONS
// ========================================
/**
* Authenticate users with wallet signatures (GemWallet, Crossmark)
*/
walletLogin: async (ctx) => {
try {
const { walletAddress, signedMessage, publicKey } = ctx.request.body;
const originalMessage = "Login to XRPL Token Creator";
const messageHex = Buffer.from(originalMessage).toString("hex");
// Validate input
if (!walletAddress || !signedMessage || !publicKey) {
return ctx.badRequest(
"Missing walletAddress, signedMessage, or publicKey"
);
}
// Validate wallet address format
if (!walletAddress.match(/^r[a-km-zA-HJ-NP-Z1-9]{25,34}$/)) {
return ctx.badRequest("Invalid wallet address format");
}
try {
// Verify the signature
const isValid = verify(messageHex, signedMessage, publicKey);
// Verify the address matches the public key
const derivedAddress = deriveAddress(publicKey);
if (isValid && derivedAddress === walletAddress) {
ctx.body = {
status: "success",
message: "Authentication successful",
walletAddress,
};
} else {
return ctx.unauthorized("Invalid signature or address");
}
} catch (error) {
return ctx.internalServerError("Verification error: " + error.message);
}
} catch (error) {
console.error("Wallet login error:", error);
ctx.body = {
status: "error",
message: "Authentication failed",
error: error.message,
};
}
},
/**
* Verify Xaman/XUMM payload and authenticate user
*/
xamanLogin: async (ctx) => {
try {
const { payloadId } = ctx.request.body;
if (!payloadId) {
return ctx.badRequest("Missing payloadId");
}
// Get the XUMM service to verify the payload
const xrplService = strapi.service("api::xrpl.xrpl");
const payloadResult = await xrplService.verifyXamanPayload(payloadId);
if (payloadResult.valid && payloadResult.account) {
ctx.body = {
status: "success",
message: "Xaman authentication successful",
walletAddress: payloadResult.account,
};
} else {
return ctx.unauthorized("Invalid Xaman payload or signature");
}
} catch (error) {
console.error("Xaman login error:", error);
ctx.body = {
status: "error",
message: "Xaman authentication failed",
error: error.message,
};
}
},
/**
* Create Xaman/XUMM sign-in payload and return QR code
*/
createXamanSignIn: async (ctx) => {
try {
// Get the XUMM service to create the sign-in payload
const xrplService = strapi.service("api::xrpl.xrpl");
const payloadResult = await xrplService.createXamanSignInPayload();
ctx.body = {
status: "success",
message: "Xaman sign-in payload created",
payloadId: payloadResult.payloadId,
qrCode: payloadResult.qrCode,
};
} catch (error) {
console.error("Xaman sign-in creation error:", error);
ctx.body = {
status: "error",
message: "Failed to create Xaman sign-in payload",
error: error.message,
};
}
},
// ========================================
// TOKEN CREATION ACTIONS
// ========================================
/**
* Create custom tokens on XRPL with real-time progress updates
*/
createToken: async (ctx) => {
const xrplService = strapi.service("api::xrpl.xrpl");
let client: Client | null = null;
const sessionId = ctx.request.body.sessionId || `session-${Date.now()}`;
try {
const { currency, amount, domain, email, clientAddress } =
ctx.request.body;
// Send initial progress update
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "validating",
message: "Validating input parameters...",
progress: 10
});
// Validate all required fields
if (!currency || !amount || !domain || !email || !clientAddress) {
return ctx.badRequest(
"Missing required fields: currency, amount, domain, email, clientAddress"
);
}
// Validate currency code using service
if (!xrplService.validateCurrency(currency)) {
return ctx.badRequest("Invalid currency code format");
}
// Validate amount
const numAmount = parseFloat(amount);
if (isNaN(numAmount) || numAmount <= 0) {
return ctx.badRequest("Amount must be a positive number");
}
// Validate domain
if (!validator.isFQDN(domain) || validator.isIP(domain)) {
return ctx.badRequest(
"Domain must be a valid domain name (no IP addresses)"
);
}
// Validate email
if (!validator.isEmail(email)) {
return ctx.badRequest("Invalid email format");
}
// Validate client wallet address
if (!clientAddress.match(/^r[a-km-zA-HJ-NP-Z1-9]{25,34}$/)) {
return ctx.badRequest("Invalid client wallet address format");
}
// Convert amount to drops
const drops = xrplService.convertToDrops(numAmount);
// Send progress update for XRPL connection
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "connecting",
message: "Connecting to XRPL network...",
progress: 20
});
// Initialize XRPL client
client = await xrplService.getClient();
// Send progress update for wallet generation
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "generating_wallet",
message: "Generating issuer wallet...",
progress: 30
});
// Generate new issuer wallet
const { wallet: issuerWallet, address: issuerAddress } =
await xrplService.generateFundedIssuerWallet(client);
// Check if issuer account exists and is funded
const accountExists = await xrplService.checkAccountExists(
client,
issuerAddress
);
if (!accountExists) {
throw new Error(
`Issuer account ${issuerAddress} is not funded. Please try again in a few moments as faucet funding may take time to process.`
);
}
// Send progress update for account setup
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "setting_up_account",
message: "Setting up issuer account...",
progress: 50
});
// Setup issuer account (set flags, domain, email)
await xrplService.setupIssuerAccount(client, issuerWallet, domain, email);
console.log("Issuer account setup completed");
// Send progress update for trustline setup and wait for frontend
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "setting_trustline",
message: "Waiting for trustline setup...",
progress: 60,
requiresUserAction: true,
trustlineData: {
clientAddress: clientAddress,
issuerAddress: issuerAddress,
currency: currency,
limit: amount,
network: process.env.XRPL_NETWORK || "wss://s.altnet.rippletest.net:51233"
}
});
// Wait for frontend to complete trustline setup
const trustlineCompleted = await (strapi as any).socketService.waitForTrustlineCompletion(sessionId);
if (!trustlineCompleted) {
throw new Error("Trustline setup was not completed");
}
console.log("Trustline setup completed successfully");
// Send progress update for token issuance
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "issuing_tokens",
message: "Issuing tokens to client...",
progress: 80
});
// Issue tokens to client
const tokenResult = await xrplService.issueTokens(
client,
issuerWallet,
{
clientAddress: clientAddress,
currency: currency,
amount: amount.toString()
}
);
console.log("Tokens issued to client");
console.log("Token result:", JSON.stringify(tokenResult));
// Send progress update for cleanup
(strapi as any).socketService.sendProgressUpdate(sessionId, {
step: "cleaning_up",
message: "Securing issuer wallet...",
progress: 90
});
// Blackhole the issuer wallet
await xrplService.blackholeIssuer(client, issuerWallet);
// Send completion update
const result = {
status: "success",
message: "Token created successfully!",
token: {
issuer: issuerAddress,
currency: currency,
amount: amount,
drops: drops,
domain: domain,
email: email,
clientAddress: clientAddress,
transactionHash: tokenResult.result.hash,
},
trustline: {
required: true,
message: "Trustline setup completed successfully",
details: {
clientAddress: clientAddress,
issuerAddress: issuerAddress,
currency: currency,
limit: amount,
network: process.env.XRPL_NETWORK || "wss://s.altnet.rippletest.net:51233"
},
instructions: [
"The trustline has been set up successfully",
"Your wallet can now receive tokens from this issuer",
],
},
gravatarSuggestion: `Visit gravatar.com and sign up using your email (${email}). Upload a token logo (recommended: 128x128 PNG). XRPL block explorers use Gravatar to display token icons.`,
};
const entry = await strapi.documents('api::token.token').create({
data: {
issuer: issuerAddress,
creator: clientAddress,
amount: String(amount),
symbol: currency,
domain,
email,
transaction: tokenResult.result.hash
}
});
console.log(`New token has been saved`);
console.log(entry);
(strapi as any).socketService.sendCompletionUpdate(sessionId, result);
ctx.body = {
status: "success",
message: "Token creation started",
sessionId: sessionId,
result: result
};
} catch (error) {
console.error("Token creation error:", error);
// Send error update
(strapi as any).socketService.sendErrorUpdate(sessionId, {
message: "Token creation failed",
error: error.message
});
ctx.body = {
status: "error",
message: "Token creation failed",
error: error.message,
};
} finally {
if (client) {
await client.disconnect();
}
}
},
/**
* Generate a new issuer wallet (for testing)
*/
generateWallet: async (ctx) => {
try {
const xrplService = strapi.service("api::xrpl.xrpl");
const issuerWallet = xrplService.generateIssuerWallet();
ctx.body = {
status: "success",
message: "Issuer wallet generated",
wallet: {
address: issuerWallet.address,
publicKey: issuerWallet.wallet.publicKey,
// Note: In production, you should not expose the secret
secret: issuerWallet.wallet.seed,
},
};
} catch (error) {
console.error("Wallet generation error:", error);
ctx.body = {
status: "error",
message: "Failed to generate wallet",
error: error.message,
};
}
},
/**
* Test WebSocket connection
*/
testSocket: async (ctx) => {
try {
const socketService = (strapi as any).socketService;
if (!socketService) {
ctx.body = {
status: "error",
message: "Socket service not available",
};
return;
}
// Send a test message to all connected clients
if ((strapi as any).socket) {
(strapi as any).socket.emit('test-message', {
message: 'WebSocket is working!',
timestamp: new Date().toISOString()
});
}
ctx.body = {
status: "success",
message: "WebSocket test message sent",
socketAvailable: !!(strapi as any).socket,
socketServiceAvailable: !!socketService,
};
} catch (error) {
console.error("WebSocket test error:", error);
ctx.body = {
status: "error",
message: "WebSocket test failed",
error: error.message,
};
}
},
/**
* Create XUMM trustline payload
*/
createTrustlinePayload: async (ctx) => {
try {
const { transaction, network } = ctx.request.body;
const xrplService = strapi.service("api::xrpl.xrpl");
// Validate required parameters
if (!transaction) {
ctx.body = {
status: "error",
message: "Transaction data is required",
};
return;
}
console.log(`Creating trustline payload for network: ${network}`);
console.log("Transaction data:", transaction);
// Create XUMM payload for trustline transaction
const payloadResult = await xrplService.createXamanTrustPayload(transaction);
ctx.body = {
status: "success",
message: "Trustline payload created successfully",
payloadId: payloadResult.payloadId,
qrCode: payloadResult.qrCode,
network: network,
transaction: payloadResult.transaction,
};
} catch (error) {
console.error("Trustline payload creation error:", error);
ctx.body = {
status: "error",
message: "Failed to create trustline payload",
error: error.message,
};
}
},
/**
* Submit signed trustline transaction
*/
submitTrustline: async (ctx) => {
try {
const { signedTransaction } = ctx.request.body;
const xrplService = strapi.service("api::xrpl.xrpl");
// Submit the signed transaction to XRPL
const client = await xrplService.getClient();
const result = await client.submitAndWait(signedTransaction);
ctx.body = {
status: "success",
message: "Trustline transaction submitted",
transactionHash: result.result.hash,
};
} catch (error) {
console.error("Trustline submission error:", error);
ctx.body = {
status: "error",
message: "Failed to submit trustline transaction",
error: error.message,
};
}
},
/**
* Verify Xaman/XUMM trustline payload
*/
verifyTrustlinePayload: async (ctx) => {
try {
const { payloadId } = ctx.request.body;
const xrplService = strapi.service("api::xrpl.xrpl");
if (!payloadId) {
ctx.body = {
status: "error",
message: "Payload ID is required",
};
return;
}
console.log(`Verifying trustline payload: ${payloadId}`);
// Verify the payload with XUMM API
const verificationResult = await xrplService.verifyXamanTrustPayload(payloadId);
if (verificationResult.valid && verificationResult.signatureValid) {
ctx.body = {
status: "success",
message: "Trustline payload verified successfully",
account: verificationResult.account,
payload: verificationResult.payload,
};
} else {
ctx.body = {
status: "error",
message: "Trustline payload verification failed",
valid: verificationResult.valid,
signatureValid: verificationResult.signatureValid,
payload: verificationResult.payload,
};
}
} catch (error) {
console.error("Trustline payload verification error:", error);
ctx.body = {
status: "error",
message: "Failed to verify trustline payload",
error: error.message,
};
}
},
/**
* Check trustline status
*/
checkTrustline: async (ctx) => {
try {
const { clientAddress, issuerAddress, currency } = ctx.request.body;
const xrplService = strapi.service("api::xrpl.xrpl");
// Get client and check trustline
const client = await xrplService.getClient();
// Get account lines
const accountLines = await client.request({
command: "account_lines",
account: clientAddress,
peer: issuerAddress,
});
// Check if trustline exists for the specific currency
const trustline = accountLines.result.lines.find(
line => line.currency === currency && line.account === issuerAddress
);
ctx.body = {
status: "success",
exists: !!trustline,
limit: trustline ? trustline.limit : null,
message: trustline ? "Trustline exists" : "Trustline not found",
};
} catch (error) {
console.error("Trustline check error:", error);
ctx.body = {
status: "error",
message: "Failed to check trustline",
error: error.message,
};
}
},
/**
* Create AMM (liquidity pool) on XRPL
*/
createAmm: async (ctx) => {
try {
const { token, issuer, amountToken, amountXRP, fee, walletAddress, walletType } = ctx.request.body;
if (!token || !issuer || !amountToken || !amountXRP || !fee || !walletAddress) {
return ctx.badRequest("Missing required fields: token, issuer, amountToken, amountXRP, fee, walletAddress");
}
const xrplService = strapi.service("api::xrpl.xrpl");
const result = await xrplService.createAmm({ token, issuer, amountToken, amountXRP, fee, walletAddress, walletType });
ctx.body = {
status: "success",
message: "AMM created successfully!",
amm: result
};
} catch (error) {
console.error("AMM creation error:", error);
ctx.body = {
status: "error",
message: "Failed to create AMM",
error: error.message
};
}
},
/**
* Create XUMM/Xaman AMM payload (for QR signing)
*/
createAmmPayload: async (ctx) => {
try {
const { transaction } = ctx.request.body;
if (!transaction) {
return ctx.badRequest("Missing transaction");
}
const xrplService = strapi.service("api::xrpl.xrpl");
const payloadResult = await xrplService.createXamanAmmPayload(transaction);
ctx.body = {
status: "success",
message: "AMM payload created successfully",
payloadId: payloadResult.payloadId,
qrCode: payloadResult.qrCode,
transaction: payloadResult.transaction,
};
} catch (error) {
ctx.body = {
status: "error",
message: "Failed to create AMM payload",
error: error.message,
};
}
},
/**
* Verify XUMM/Xaman AMM payload
*/
verifyAmmPayload: async (ctx) => {
try {
const { payloadId } = ctx.request.body;
if (!payloadId) {
return ctx.badRequest("Missing payloadId");
}
const xrplService = strapi.service("api::xrpl.xrpl");
const payloadResult = await xrplService.verifyXamanAmmPayload(payloadId);
if (payloadResult.valid && payloadResult.account) {
ctx.body = {
status: "success",
message: "AMM payload signed successfully",
payload: payloadResult.payload,
account: payloadResult.account,
};
} else {
ctx.body = {
status: "pending",
message: "AMM payload not signed yet",
};
}
} catch (error) {
ctx.body = {
status: "error",
message: "Failed to verify AMM payload",
error: error.message,
};
}
},
};
@luffy1456
Copy link

good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment