Created
August 14, 2025 16:40
-
-
Save mikekoro/f2f577f1d466006a9f9e5c8c316ac817 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
/** | |
* XRPL Service | |
* Handles XRPL blockchain operations and XUMM API integration | |
*/ | |
import { | |
Client, | |
Wallet, | |
AccountSet, | |
Payment, | |
TrustSet, | |
SetRegularKey, | |
verifyKeypairSignature, | |
isValidClassicAddress, | |
} from "xrpl"; | |
import { verifySignature } from "verify-xrpl-signature"; | |
import axios from "axios"; | |
import crypto from "crypto"; | |
// ======================================== | |
// TYPE DEFINITIONS | |
// ======================================== | |
interface IssuerWallet { | |
wallet: Wallet; | |
address: string; | |
} | |
interface XamanPayloadResponse { | |
valid: boolean; | |
account: string | null; | |
signatureValid?: boolean; | |
payload: any; | |
} | |
interface XamanSignInResponse { | |
payloadId: string; | |
qrCode: string; | |
payload: any; | |
} | |
interface XamanTrustPayloadResponse { | |
payloadId: string; | |
qrCode: string; | |
payload: any; | |
transaction: any; | |
} | |
interface TokenIssueParams { | |
clientAddress: string; | |
currency: string; | |
amount: string; | |
} | |
// ======================================== | |
// XRPL SERVICE | |
// ======================================== | |
export default () => ({ | |
// ======================================== | |
// XRPL CLIENT MANAGEMENT | |
// ======================================== | |
/** | |
* Initialize and connect to XRPL client | |
*/ | |
async getClient(): Promise<Client> { | |
const client = new Client(process.env.XRPL_NETWORK); | |
await client.connect(); | |
return client; | |
}, | |
/** | |
* Generate a new issuer wallet | |
*/ | |
generateIssuerWallet(): IssuerWallet { | |
const wallet = Wallet.generate(); | |
console.log("Generated new issuer wallet:", { | |
address: wallet.address, | |
publicKey: wallet.publicKey, | |
}); | |
return { | |
wallet: wallet, | |
address: wallet.address, | |
}; | |
}, | |
/** | |
* Generate a new issuer wallet and fund it from fund wallet | |
*/ | |
async generateFundedIssuerWallet(client: Client): Promise<IssuerWallet> { | |
try { | |
console.log("Generating funded issuer wallet from fund wallet..."); | |
// Check if FUND_WALLET_SEED is configured | |
const fundWalletSeed = process.env.FUND_WALLET_SEED; | |
const SEED_AMOUNT = process.env.SEED_AMOUNT; | |
if (!fundWalletSeed) { | |
throw new Error( | |
"FUND_WALLET_SEED not configured in environment variables" | |
); | |
} | |
// Generate a new wallet | |
const wallet = Wallet.generate(); | |
console.log("Generated wallet:", wallet.address); | |
// Create fund wallet from seed | |
const fundWallet = Wallet.fromSeed(fundWalletSeed); | |
console.log("Fund wallet address:", fundWallet.address); | |
// Check fund wallet balance | |
const fundWalletInfo = await client.request({ | |
command: "account_info", | |
account: fundWallet.address, | |
ledger_index: "validated", | |
}); | |
const balance = fundWalletInfo.result.account_data.Balance; | |
console.log("Fund wallet balance:", balance, "drops"); | |
// Calculate funding amount (5 XRP = 5,000,000 drops) | |
const fundingAmount = SEED_AMOUNT; //"5000000"; // 5 XRP in drops | |
if (parseInt(balance) < parseInt(fundingAmount) + 200000) { | |
// + 200k drops for fees | |
throw new Error( | |
`Insufficient balance in fund wallet: ${balance} drops < ${fundingAmount+200000} drops (incl. 200k drops for padding)` | |
); | |
} | |
// Create payment transaction to fund the new wallet | |
const paymentTx: Payment = { | |
TransactionType: "Payment", | |
Account: fundWallet.address, | |
Destination: wallet.address, | |
Amount: fundingAmount, | |
}; | |
// Prepare, sign, and submit the transaction | |
const prepared = await client.autofill(paymentTx); | |
const signed = fundWallet.sign(prepared); | |
const result = await client.submitAndWait(signed.tx_blob); | |
// Check if transaction was successful | |
if ( | |
result.result.meta && | |
typeof result.result.meta === "object" && | |
"TransactionResult" in result.result.meta | |
) { | |
const transactionResult = (result.result.meta as any).TransactionResult; | |
if (transactionResult === "tesSUCCESS") { | |
console.log("Wallet funded successfully from fund wallet"); | |
console.log("Transaction hash:", result.result.hash); | |
return { | |
wallet: wallet, | |
address: wallet.address, | |
}; | |
} else { | |
throw new Error(`Transaction failed: ${transactionResult}`); | |
} | |
} else { | |
// If we can't determine the result, assume success if no error was thrown | |
console.log("Wallet funded successfully from fund wallet"); | |
console.log("Transaction hash:", result.result.hash); | |
return { | |
wallet: wallet, | |
address: wallet.address, | |
}; | |
} | |
} catch (error) { | |
console.error("Error generating funded wallet:", error); | |
throw new Error(`Failed to generate funded wallet: ${error.message}`); | |
} | |
}, | |
/** | |
* Get or create issuer wallet from faucet (generates new funded wallet) | |
*/ | |
// async getOrCreateIssuerWallet(): Promise<IssuerWallet> { | |
// // Generate a new wallet and fund it directly from faucet | |
// return this.generateFundedIssuerWallet(); | |
// }, | |
// ======================================== | |
// XAMAN/XUMM API INTEGRATION | |
// ======================================== | |
/** | |
* Verify Xaman/XUMM payload with XUMM API | |
*/ | |
async verifyXamanPayload(payloadId: string): Promise<XamanPayloadResponse> { | |
try { | |
const xummApiKey = process.env.XUMM_API_KEY; | |
const xummApiSecret = process.env.XUMM_API_SECRET; | |
if (!xummApiKey || !xummApiSecret) { | |
throw new Error("XUMM API key or secret not configured"); | |
} | |
const response = await axios.get( | |
`https://xumm.app/api/v1/platform/payload/${payloadId}`, | |
{ | |
headers: { | |
"X-API-Key": xummApiKey, | |
"X-API-Secret": xummApiSecret, | |
"Content-Type": "application/json", | |
}, | |
} | |
); | |
if (response.status !== 200) { | |
const errorText = response.data; | |
console.error("XUMM API response:", response.status, errorText); | |
throw new Error(`XUMM API error: ${response.status} - ${errorText}`); | |
} | |
const data = response.data as any; | |
// Check if payload is signed and valid | |
if (data.response && data.response.account) { | |
// Verify the signature using verify-xrpl-signature | |
const signatureValid = Boolean(verifySignature(data.response.hex)); | |
return { | |
valid: true, | |
account: data.response.account, | |
signatureValid: signatureValid, | |
payload: data, | |
}; | |
} else { | |
return { | |
valid: false, | |
account: null, | |
payload: data, | |
}; | |
} | |
} catch (error) { | |
console.error("XUMM payload verification error:", error); | |
throw new Error(`Failed to verify XUMM payload: ${error.message}`); | |
} | |
}, | |
/** | |
* Create Xaman/XUMM sign-in payload | |
*/ | |
async createXamanSignInPayload(): Promise<XamanSignInResponse> { | |
try { | |
const xummApiKey = process.env.XUMM_API_KEY; | |
const xummApiSecret = process.env.XUMM_API_SECRET; | |
if (!xummApiKey || !xummApiSecret) { | |
throw new Error("XUMM API key or secret not configured"); | |
} | |
const payload = { | |
txjson: { | |
TransactionType: "SignIn", | |
}, | |
options: { | |
submit: false, | |
multisign: false, | |
expire: 5, | |
}, | |
}; | |
const response = await axios.post( | |
"https://xumm.app/api/v1/platform/payload", | |
payload, | |
{ | |
headers: { | |
"X-API-Key": xummApiKey, | |
"X-API-Secret": xummApiSecret, | |
"Content-Type": "application/json", | |
}, | |
} | |
); | |
if (response.status !== 200) { | |
const errorText = response.data; | |
console.error("XUMM API response:", response.status, errorText); | |
throw new Error(`XUMM API error: ${response.status} - ${errorText}`); | |
} | |
const data = response.data as any; | |
return { | |
payloadId: data.uuid, | |
qrCode: data.refs.qr_png, | |
payload: data, | |
}; | |
} catch (error) { | |
console.error("XUMM payload creation error:", error); | |
throw new Error(`Failed to create XUMM payload: ${error.message}`); | |
} | |
}, | |
/** | |
* Create Xaman/XUMM trustline payload | |
*/ | |
async createXamanTrustPayload( | |
transaction: any | |
): Promise<XamanTrustPayloadResponse> { | |
try { | |
const xummApiKey = process.env.XUMM_API_KEY; | |
const xummApiSecret = process.env.XUMM_API_SECRET; | |
if (!xummApiKey || !xummApiSecret) { | |
throw new Error("XUMM API key or secret not configured"); | |
} | |
// Validate transaction structure | |
if ( | |
!transaction || | |
!transaction.TransactionType || | |
transaction.TransactionType !== "TrustSet" | |
) { | |
throw new Error("Invalid trustline transaction structure"); | |
} | |
console.log( | |
"Creating XUMM trustline payload for transaction:", | |
transaction | |
); | |
const payload = { | |
txjson: transaction, | |
options: { | |
submit: true, | |
multisign: false, | |
expire: 5, | |
}, | |
}; | |
const response = await axios.post( | |
"https://xumm.app/api/v1/platform/payload", | |
payload, | |
{ | |
headers: { | |
"X-API-Key": xummApiKey, | |
"X-API-Secret": xummApiSecret, | |
"Content-Type": "application/json", | |
}, | |
} | |
); | |
if (response.status !== 200) { | |
const errorText = response.data; | |
console.error("XUMM API response:", response.status, errorText); | |
throw new Error(`XUMM API error: ${response.status} - ${errorText}`); | |
} | |
const data = response.data as any; | |
return { | |
payloadId: data.uuid, | |
qrCode: data.refs.qr_png, | |
payload: data, | |
transaction: transaction, | |
}; | |
} catch (error) { | |
console.error("XUMM trustline payload creation error:", error); | |
throw new Error( | |
`Failed to create XUMM trustline payload: ${error.message}` | |
); | |
} | |
}, | |
/** | |
* Verify Xaman/XUMM trustline payload | |
*/ | |
async verifyXamanTrustPayload( | |
payloadId: string | |
): Promise<XamanPayloadResponse> { | |
try { | |
const xummApiKey = process.env.XUMM_API_KEY; | |
const xummApiSecret = process.env.XUMM_API_SECRET; | |
if (!xummApiKey || !xummApiSecret) { | |
throw new Error("XUMM API key or secret not configured"); | |
} | |
console.log(`Verifying XUMM trustline payload: ${payloadId}`); | |
const response = await axios.get( | |
`https://xumm.app/api/v1/platform/payload/${payloadId}`, | |
{ | |
headers: { | |
"X-API-Key": xummApiKey, | |
"X-API-Secret": xummApiSecret, | |
"Content-Type": "application/json", | |
}, | |
} | |
); | |
if (response.status !== 200) { | |
const errorText = response.data; | |
console.error("XUMM API response:", response.status, errorText); | |
throw new Error(`XUMM API error: ${response.status} - ${errorText}`); | |
} | |
const data = response.data as any; | |
// Check if payload is signed and valid | |
if (data.response && data.response.account && data.response.hex) { | |
// Verify the signature using verify-xrpl-signature | |
const signatureValid = Boolean(verifySignature(data.response.hex)); | |
return { | |
valid: true, | |
account: data.response.account, | |
signatureValid: signatureValid, | |
payload: data, | |
}; | |
} else { | |
return { | |
valid: false, | |
account: null, | |
payload: data, | |
}; | |
} | |
} catch (error) { | |
console.error("XUMM trustline payload verification error:", error); | |
throw new Error( | |
`Failed to verify XUMM trustline payload: ${error.message}` | |
); | |
} | |
}, | |
/** | |
* Create XUMM/Xaman AMM payload (for AMMCreate) | |
*/ | |
async createXamanAmmPayload(transaction) { | |
try { | |
const xummApiKey = process.env.XUMM_API_KEY; | |
const xummApiSecret = process.env.XUMM_API_SECRET; | |
if (!xummApiKey || !xummApiSecret) { | |
throw new Error("XUMM API key or secret not configured"); | |
} | |
// Validate transaction structure | |
if (!transaction || transaction.TransactionType !== "AMMCreate") { | |
throw new Error("Invalid AMMCreate transaction structure"); | |
} | |
const payload = { | |
txjson: transaction, | |
options: { | |
submit: true, | |
multisign: false, | |
expire: 5, | |
}, | |
}; | |
const response = await axios.post( | |
"https://xumm.app/api/v1/platform/payload", | |
payload, | |
{ | |
headers: { | |
"X-API-Key": xummApiKey, | |
"X-API-Secret": xummApiSecret, | |
"Content-Type": "application/json", | |
}, | |
} | |
); | |
if (response.status !== 200) { | |
const errorText = response.data; | |
throw new Error(`XUMM API error: ${response.status} - ${errorText}`); | |
} | |
const data = response.data; | |
return { | |
payloadId: data.uuid, | |
qrCode: data.refs.qr_png, | |
payload: data, | |
transaction: transaction, | |
}; | |
} catch (error) { | |
throw new Error(`Failed to create XUMM AMM payload: ${error.message}`); | |
} | |
}, | |
/** | |
* Verify XUMM/Xaman AMM payload | |
*/ | |
async verifyXamanAmmPayload(payloadId) { | |
try { | |
const xummApiKey = process.env.XUMM_API_KEY; | |
const xummApiSecret = process.env.XUMM_API_SECRET; | |
if (!xummApiKey || !xummApiSecret) { | |
throw new Error("XUMM API key or secret not configured"); | |
} | |
const response = await axios.get( | |
`https://xumm.app/api/v1/platform/payload/${payloadId}`, | |
{ | |
headers: { | |
"X-API-Key": xummApiKey, | |
"X-API-Secret": xummApiSecret, | |
"Content-Type": "application/json", | |
}, | |
} | |
); | |
if (response.status !== 200) { | |
const errorText = response.data; | |
throw new Error(`XUMM API error: ${response.status} - ${errorText}`); | |
} | |
const data = response.data; | |
if (data.response && data.response.account && data.response.hex) { | |
return { | |
valid: true, | |
account: data.response.account, | |
payload: data, | |
}; | |
} else { | |
return { | |
valid: false, | |
account: null, | |
payload: data, | |
}; | |
} | |
} catch (error) { | |
throw new Error(`Failed to verify XUMM AMM payload: ${error.message}`); | |
} | |
}, | |
// ======================================== | |
// XRPL ACCOUNT OPERATIONS | |
// ======================================== | |
/** | |
* Check if account exists and is funded | |
*/ | |
async checkAccountExists(client: Client, address: string): Promise<boolean> { | |
try { | |
const accountInfo = await client.request({ | |
command: "account_info", | |
account: address, | |
ledger_index: "validated", | |
}); | |
return accountInfo.result.account_data.Account === address; | |
} catch (error) { | |
return false; | |
} | |
}, | |
/** | |
* Setup issuer account with domain, email, and flags | |
*/ | |
async setupIssuerAccount( | |
client: Client, | |
issuerWallet: Wallet, | |
domain: string, | |
email: string | |
) { | |
const accountSetTx: AccountSet = { | |
TransactionType: "AccountSet", | |
Account: issuerWallet.address, | |
Domain: Buffer.from(domain).toString("hex").toUpperCase(), | |
EmailHash: this.hashEmail(email), | |
SetFlag: 8, // DefaultRipple | |
ClearFlag: 6, // NoFreeze | |
}; | |
const prepared = await client.autofill(accountSetTx); | |
const signed = issuerWallet.sign(prepared); | |
const result = await client.submitAndWait(signed.tx_blob); | |
return result; | |
}, | |
/** | |
* Hash email for XRPL (simplified hash function) | |
*/ | |
hashEmail(email: string): string { | |
return crypto | |
.createHash("md5") | |
.update(email.trim().toLowerCase()) | |
.digest("hex") | |
.toUpperCase(); | |
}, | |
// ======================================== | |
// TOKEN OPERATIONS | |
// ======================================== | |
/** | |
* Set trustline from client to issuer | |
* Note: This creates the trustline transaction but requires client signature | |
*/ | |
async setTrustline( | |
client: Client, | |
clientAddress: string, | |
issuerAddress: string, | |
currency: string, | |
limit: string = "1000000000" // Default limit of 1000 tokens | |
) { | |
console.log( | |
`Setting trustline from ${clientAddress} to ${issuerAddress} for ${currency}` | |
); | |
// Create the trustline transaction | |
const trustSetTx: TrustSet = { | |
TransactionType: "TrustSet", | |
Account: clientAddress, | |
LimitAmount: { | |
currency: currency, | |
issuer: issuerAddress, | |
value: limit, | |
}, | |
}; | |
console.log("TrustSet transaction created:", trustSetTx); | |
// For now, we'll assume the trustline is set manually by the client | |
// In a production environment, you would: | |
// 1. Create the transaction | |
// 2. Send it to the client for signing via XUMM/Xaman | |
// 3. Wait for the client to sign and submit | |
// 4. Verify the trustline is set | |
// Return information about the required trustline | |
return { | |
status: "requires_client_action", | |
message: `Client wallet ${clientAddress} must set trustline to issuer ${issuerAddress} for currency ${currency}`, | |
trustlineInfo: { | |
clientAddress, | |
issuerAddress, | |
currency, | |
limit, | |
transaction: trustSetTx, | |
}, | |
instructions: [ | |
"The client wallet must set a trustline to the issuer before tokens can be received", | |
`Trustline details: Currency: ${currency}, Issuer: ${issuerAddress}, Limit: ${limit}`, | |
"This can be done through XUMM/Xaman wallet or any XRPL wallet", | |
], | |
}; | |
}, | |
/** | |
* Issue tokens to client address | |
*/ | |
async issueTokens( | |
client: Client, | |
issuerWallet: Wallet, | |
params: TokenIssueParams | |
) { | |
const { clientAddress, currency, amount } = params; | |
const paymentTx: Payment = { | |
TransactionType: "Payment", | |
Account: issuerWallet.address, | |
Destination: clientAddress, | |
Amount: { | |
currency: currency, | |
issuer: issuerWallet.address, | |
value: amount, | |
}, | |
}; | |
const prepared = await client.autofill(paymentTx); | |
const signed = issuerWallet.sign(prepared); | |
const result = await client.submitAndWait(signed.tx_blob); | |
return result; | |
}, | |
/** | |
* Blackhole the issuer wallet (disable master key and set RegularKey to blackhole address) | |
*/ | |
async blackholeIssuer(client: Client, issuerWallet: Wallet) { | |
// Use the genesis account as the blackhole address | |
const BLACKHOLE_ADDRESS = "rrrrrrrrrrrrrrrrrrrrBZbvji"; | |
const REG = BLACKHOLE_ADDRESS.trim(); | |
console.log("SetRegularKey RegularKey:", JSON.stringify(REG)); | |
console.log("Is valid:", isValidClassicAddress(REG)); | |
if (!isValidClassicAddress(REG)) { | |
throw new Error(`Invalid BLACKHOLE_ADDRESS: "${REG}"`); | |
} | |
// 1. Set RegularKey to blackhole address | |
const setRegularKeyTx = { | |
TransactionType: "SetRegularKey", | |
Account: issuerWallet.address, | |
RegularKey: REG, | |
} as SetRegularKey; | |
const preparedSetRegularKey = await client.autofill(setRegularKeyTx); | |
const signedSetRegularKey = issuerWallet.sign(preparedSetRegularKey); | |
const resultSetRegularKey = await client.submitAndWait( | |
signedSetRegularKey.tx_blob | |
); | |
// 2. Disable master key | |
const disableMasterKeyTx = { | |
TransactionType: "AccountSet", | |
Account: issuerWallet.address, | |
SetFlag: 4, // asfDisableMaster | |
} as AccountSet; | |
const preparedDisableMaster = await client.autofill(disableMasterKeyTx); | |
const signedDisableMaster = issuerWallet.sign(preparedDisableMaster); | |
const resultDisableMaster = await client.submitAndWait( | |
signedDisableMaster.tx_blob | |
); | |
return { | |
status: "success", | |
message: | |
"Issuer blackholed (RegularKey set to blackhole address and master key disabled)", | |
setRegularKeyResult: resultSetRegularKey, | |
disableMasterKeyResult: resultDisableMaster, | |
}; | |
}, | |
/** | |
* Create AMM (liquidity pool) on XRPL (XRP + issued token) | |
*/ | |
async createAmm({ | |
token, | |
issuer, | |
amountToken, | |
amountXRP, | |
fee, | |
walletAddress, | |
walletType, | |
}) { | |
const client = await this.getClient(); | |
try { | |
// Calculate TradingFee (0-1000) | |
const tradingFee = Math.round(parseFloat(fee) * 1000); | |
if (isNaN(tradingFee) || tradingFee < 0 || tradingFee > 1000) { | |
throw new Error( | |
"TradingFee must be between 0 and 1.0 (0-1000 in XRPL units, e.g. 0.003 = 3)" | |
); | |
} | |
// Build AMMCreate transaction for XRP + issued token | |
const tx: { | |
TransactionType: string; | |
Account: string; | |
Amount: string; | |
Amount2: { currency: string; issuer: string; value: string }; | |
TradingFee: number; | |
Fee?: number; | |
// [key: string]: any; | |
} = { | |
TransactionType: "AMMCreate", | |
Account: walletAddress, | |
Amount: (parseFloat(amountXRP) * 1_000_000).toString(), // XRP in drops | |
Amount2: { | |
currency: token, | |
issuer: issuer, | |
value: amountToken, | |
}, | |
TradingFee: tradingFee, | |
// Add more fields as needed | |
}; | |
if (walletType === "crossmark" || walletType === "xaman") { | |
tx.Fee = 200000; | |
} | |
// For now, just return the tx object for frontend/client signing | |
return { tx }; | |
} finally { | |
await client.disconnect(); | |
} | |
}, | |
// ======================================== | |
// UTILITY FUNCTIONS | |
// ======================================== | |
/** | |
* Validate currency code format (3-letter or 40-char hex) | |
*/ | |
validateCurrency(currency: string): boolean { | |
if (currency.length === 3) { | |
return /^[A-Z]{3}$/.test(currency); | |
} else { | |
return /^[0-9A-Fa-f]{40}$/.test(currency); | |
} | |
}, | |
/** | |
* Convert amount to drops (1 XRP = 1,000,000 drops) | |
*/ | |
convertToDrops(amount: number): number { | |
return Math.floor(amount * 1000000); | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment