Created
August 14, 2025 16:39
-
-
Save mikekoro/56cd2e500060c6fef172b7e2bd0c41db to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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, | |
}; | |
} | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
good