Skip to content

Instantly share code, notes, and snippets.

@mikekoro
Created August 14, 2025 16:40
Show Gist options
  • Save mikekoro/f2f577f1d466006a9f9e5c8c316ac817 to your computer and use it in GitHub Desktop.
Save mikekoro/f2f577f1d466006a9f9e5c8c316ac817 to your computer and use it in GitHub Desktop.
/**
* 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