UNPKG

rwa-build

Version:

MCP library for AI-assisted Real World Asset tokenization on XRPL

1,261 lines (1,248 loc) 126 kB
#!/usr/bin/env node var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); var __commonJS = (cb, mod) => function __require2() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/dotenv/package.json var require_package = __commonJS({ "node_modules/dotenv/package.json"(exports, module) { module.exports = { name: "dotenv", version: "16.5.0", description: "Loads environment variables from .env file", main: "lib/main.js", types: "lib/main.d.ts", exports: { ".": { types: "./lib/main.d.ts", require: "./lib/main.js", default: "./lib/main.js" }, "./config": "./config.js", "./config.js": "./config.js", "./lib/env-options": "./lib/env-options.js", "./lib/env-options.js": "./lib/env-options.js", "./lib/cli-options": "./lib/cli-options.js", "./lib/cli-options.js": "./lib/cli-options.js", "./package.json": "./package.json" }, scripts: { "dts-check": "tsc --project tests/types/tsconfig.json", lint: "standard", pretest: "npm run lint && npm run dts-check", test: "tap run --allow-empty-coverage --disable-coverage --timeout=60000", "test:coverage": "tap run --show-full-coverage --timeout=60000 --coverage-report=lcov", prerelease: "npm test", release: "standard-version" }, repository: { type: "git", url: "git://github.com/motdotla/dotenv.git" }, homepage: "https://github.com/motdotla/dotenv#readme", funding: "https://dotenvx.com", keywords: [ "dotenv", "env", ".env", "environment", "variables", "config", "settings" ], readmeFilename: "README.md", license: "BSD-2-Clause", devDependencies: { "@types/node": "^18.11.3", decache: "^4.6.2", sinon: "^14.0.1", standard: "^17.0.0", "standard-version": "^9.5.0", tap: "^19.2.0", typescript: "^4.8.4" }, engines: { node: ">=12" }, browser: { fs: false } }; } }); // node_modules/dotenv/lib/main.js var require_main = __commonJS({ "node_modules/dotenv/lib/main.js"(exports, module) { "use strict"; var fs = __require("fs"); var path = __require("path"); var os = __require("os"); var crypto = __require("crypto"); var packageJson = require_package(); var version = packageJson.version; var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg; function parse(src) { const obj = {}; let lines = src.toString(); lines = lines.replace(/\r\n?/mg, "\n"); let match; while ((match = LINE.exec(lines)) != null) { const key = match[1]; let value = match[2] || ""; value = value.trim(); const maybeQuote = value[0]; value = value.replace(/^(['"`])([\s\S]*)\1$/mg, "$2"); if (maybeQuote === '"') { value = value.replace(/\\n/g, "\n"); value = value.replace(/\\r/g, "\r"); } obj[key] = value; } return obj; } function _parseVault(options) { const vaultPath = _vaultPath(options); const result = DotenvModule.configDotenv({ path: vaultPath }); if (!result.parsed) { const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`); err.code = "MISSING_DATA"; throw err; } const keys = _dotenvKey(options).split(","); const length = keys.length; let decrypted; for (let i = 0; i < length; i++) { try { const key = keys[i].trim(); const attrs = _instructions(result, key); decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key); break; } catch (error) { if (i + 1 >= length) { throw error; } } } return DotenvModule.parse(decrypted); } function _warn(message) { console.log(`[dotenv@${version}][WARN] ${message}`); } function _debug(message) { console.log(`[dotenv@${version}][DEBUG] ${message}`); } function _dotenvKey(options) { if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) { return options.DOTENV_KEY; } if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) { return process.env.DOTENV_KEY; } return ""; } function _instructions(result, dotenvKey) { let uri; try { uri = new URL(dotenvKey); } catch (error) { if (error.code === "ERR_INVALID_URL") { const err = new Error("INVALID_DOTENV_KEY: Wrong format. Must be in valid uri format like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development"); err.code = "INVALID_DOTENV_KEY"; throw err; } throw error; } const key = uri.password; if (!key) { const err = new Error("INVALID_DOTENV_KEY: Missing key part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environment = uri.searchParams.get("environment"); if (!environment) { const err = new Error("INVALID_DOTENV_KEY: Missing environment part"); err.code = "INVALID_DOTENV_KEY"; throw err; } const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`; const ciphertext = result.parsed[environmentKey]; if (!ciphertext) { const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`); err.code = "NOT_FOUND_DOTENV_ENVIRONMENT"; throw err; } return { ciphertext, key }; } function _vaultPath(options) { let possibleVaultPath = null; if (options && options.path && options.path.length > 0) { if (Array.isArray(options.path)) { for (const filepath of options.path) { if (fs.existsSync(filepath)) { possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`; } } } else { possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`; } } else { possibleVaultPath = path.resolve(process.cwd(), ".env.vault"); } if (fs.existsSync(possibleVaultPath)) { return possibleVaultPath; } return null; } function _resolveHome(envPath) { return envPath[0] === "~" ? path.join(os.homedir(), envPath.slice(1)) : envPath; } function _configVault(options) { const debug = Boolean(options && options.debug); if (debug) { _debug("Loading env from encrypted .env.vault"); } const parsed = DotenvModule._parseVault(options); let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsed, options); return { parsed }; } function configDotenv(options) { const dotenvPath = path.resolve(process.cwd(), ".env"); let encoding = "utf8"; const debug = Boolean(options && options.debug); if (options && options.encoding) { encoding = options.encoding; } else { if (debug) { _debug("No encoding is specified. UTF-8 is used by default"); } } let optionPaths = [dotenvPath]; if (options && options.path) { if (!Array.isArray(options.path)) { optionPaths = [_resolveHome(options.path)]; } else { optionPaths = []; for (const filepath of options.path) { optionPaths.push(_resolveHome(filepath)); } } } let lastError; const parsedAll = {}; for (const path2 of optionPaths) { try { const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding })); DotenvModule.populate(parsedAll, parsed, options); } catch (e) { if (debug) { _debug(`Failed to load ${path2} ${e.message}`); } lastError = e; } } let processEnv = process.env; if (options && options.processEnv != null) { processEnv = options.processEnv; } DotenvModule.populate(processEnv, parsedAll, options); if (lastError) { return { parsed: parsedAll, error: lastError }; } else { return { parsed: parsedAll }; } } function config2(options) { if (_dotenvKey(options).length === 0) { return DotenvModule.configDotenv(options); } const vaultPath = _vaultPath(options); if (!vaultPath) { _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`); return DotenvModule.configDotenv(options); } return DotenvModule._configVault(options); } function decrypt(encrypted, keyStr) { const key = Buffer.from(keyStr.slice(-64), "hex"); let ciphertext = Buffer.from(encrypted, "base64"); const nonce = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(-16); ciphertext = ciphertext.subarray(12, -16); try { const aesgcm = crypto.createDecipheriv("aes-256-gcm", key, nonce); aesgcm.setAuthTag(authTag); return `${aesgcm.update(ciphertext)}${aesgcm.final()}`; } catch (error) { const isRange = error instanceof RangeError; const invalidKeyLength = error.message === "Invalid key length"; const decryptionFailed = error.message === "Unsupported state or unable to authenticate data"; if (isRange || invalidKeyLength) { const err = new Error("INVALID_DOTENV_KEY: It must be 64 characters long (or more)"); err.code = "INVALID_DOTENV_KEY"; throw err; } else if (decryptionFailed) { const err = new Error("DECRYPTION_FAILED: Please check your DOTENV_KEY"); err.code = "DECRYPTION_FAILED"; throw err; } else { throw error; } } } function populate(processEnv, parsed, options = {}) { const debug = Boolean(options && options.debug); const override = Boolean(options && options.override); if (typeof parsed !== "object") { const err = new Error("OBJECT_REQUIRED: Please check the processEnv argument being passed to populate"); err.code = "OBJECT_REQUIRED"; throw err; } for (const key of Object.keys(parsed)) { if (Object.prototype.hasOwnProperty.call(processEnv, key)) { if (override === true) { processEnv[key] = parsed[key]; } if (debug) { if (override === true) { _debug(`"${key}" is already defined and WAS overwritten`); } else { _debug(`"${key}" is already defined and was NOT overwritten`); } } } else { processEnv[key] = parsed[key]; } } } var DotenvModule = { configDotenv, _configVault, _parseVault, config: config2, decrypt, parse, populate }; module.exports.configDotenv = DotenvModule.configDotenv; module.exports._configVault = DotenvModule._configVault; module.exports._parseVault = DotenvModule._parseVault; module.exports.config = DotenvModule.config; module.exports.decrypt = DotenvModule.decrypt; module.exports.parse = DotenvModule.parse; module.exports.populate = DotenvModule.populate; module.exports = DotenvModule; } }); // src/config.ts var dotenv = __toESM(require_main()); dotenv.config(); var getArgs = () => process.argv.reduce((args, arg) => { if (arg.slice(0, 2) === "--") { const longArg = arg.split("="); const longArgFlag = longArg[0].slice(2); const longArgValue = longArg.length > 1 ? longArg[1] : true; args[longArgFlag] = longArgValue; } else if (arg[0] === "-") { const flags = arg.slice(1).split(""); flags.forEach((flag) => { args[flag] = true; }); } return args; }, {}); function getRWAConfig() { const args = getArgs(); const hasPrivateKey = !!(args?.xrpl_private_key || process.env.XRPL_PRIVATE_KEY); const network = args?.xrpl_network || process.env.XRPL_NETWORK || "testnet"; if (!hasPrivateKey) { throw new Error("XRPL_PRIVATE_KEY environment variable is required"); } const servers = { testnet: "wss://s.altnet.rippletest.net:51233", devnet: "wss://s.devnet.rippletest.net:51233", mainnet: "wss://xrplcluster.com" }; return { privateKey: args?.xrpl_private_key || process.env.XRPL_PRIVATE_KEY, network, server: servers[network] }; } function validateEnvironment() { try { getRWAConfig(); console.error("\u2705 Environment configuration valid"); } catch (error) { console.error("\u274C Environment configuration error:", error); throw error; } } // src/index.ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // src/mcp/wallet/get_wallet_info_tool.ts var GetWalletInfoTool = { name: "rwa_get_wallet_info", description: "Get wallet address and basic account information", schema: {}, handler: async (agent, input) => { try { await agent.connect(); const walletInfo = await agent.getWalletInfo(); const balanceInXRP = Number(walletInfo.account_data.Balance) / 1e6; return { status: "success", message: "\u2705 Wallet information retrieved successfully", wallet_details: { address: agent.wallet.address, network: agent.network, balance: `${balanceInXRP.toFixed(6)} XRP`, balance_in_drops: walletInfo.account_data.Balance, sequence: walletInfo.account_data.Sequence, account_flags: walletInfo.account_data.Flags || 0 }, account_status: { activated: true, reserve_requirement: "10 XRP minimum", can_tokenize: balanceInXRP >= 10, ready_for_operations: balanceInXRP >= 1 }, recommendations: balanceInXRP < 10 ? [ "\u26A0\uFE0F Low XRP balance detected", "Fund wallet with at least 10 XRP for tokenization", "Reserve requirement: 10 XRP for account operations", `Current balance: ${balanceInXRP.toFixed(6)} XRP` ] : [ "\u2705 Wallet has sufficient balance for operations", "Ready to tokenize assets", "Ready to create trustlines and distribute yields" ] }; } catch (error) { throw new Error(`Failed to get wallet info: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/wallet/get_account_balances_tool.ts import { z } from "zod"; var GetAccountBalancesTool = { name: "rwa_get_account_balances", description: "Get all token balances and trustlines for an account", schema: { account_address: z.string().regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/).optional().describe("XRPL address to check (optional, defaults to wallet address)") }, handler: async (agent, input) => { try { await agent.connect(); const targetAddress = input.account_address || agent.wallet.address; const isOwnWallet = targetAddress === agent.wallet.address; const accountInfo = await agent.client.request({ command: "account_info", account: targetAddress, ledger_index: "validated" }); const xrpBalance = Number(accountInfo.result.account_data.Balance) / 1e6; const trustLines = await agent.client.request({ command: "account_lines", account: targetAddress, ledger_index: "validated" }); const tokenBalances = trustLines.result.lines.map((line) => ({ currency: line.currency, issuer: line.account, balance: Math.abs(Number(line.balance)), // Absolute value for holders limit: line.limit, quality_in: line.quality_in, quality_out: line.quality_out, is_frozen: line.freeze || false, asset_id: `${line.currency}.${line.account}` })); const totalTokenTypes = tokenBalances.length; const activeBalances = tokenBalances.filter((token) => token.balance > 0); return { status: "success", message: `\u2705 Account balances retrieved for ${targetAddress.substring(0, 8)}...`, account_info: { address: targetAddress, network: agent.network, is_own_wallet: isOwnWallet }, xrp_balance: { amount: `${xrpBalance.toFixed(6)} XRP`, drops: accountInfo.result.account_data.Balance, available_for_fees: xrpBalance > 10, reserve_status: xrpBalance >= 10 ? "\u2705 Above reserve" : "\u26A0\uFE0F Below reserve (10 XRP)" }, token_balances: tokenBalances.length > 0 ? tokenBalances : [], portfolio_summary: { total_trustlines: totalTokenTypes, active_balances: activeBalances.length, xrp_balance: xrpBalance, portfolio_status: activeBalances.length > 0 ? `${activeBalances.length} active token positions` : "No token holdings" }, insights: [ `XRP Balance: ${xrpBalance.toFixed(6)} XRP`, `Token Types: ${totalTokenTypes}`, `Active Positions: ${activeBalances.length}`, ...isOwnWallet ? [ xrpBalance < 1 ? "\u26A0\uFE0F Low XRP - may need funding for transactions" : "\u2705 Sufficient XRP for operations", totalTokenTypes === 0 ? "\u{1F4A1} Create trustlines to hold RWA tokens" : `\u{1F4CA} Tracking ${totalTokenTypes} different tokens` ] : [ "\u{1F4CB} External account analysis complete" ] ] }; } catch (error) { if (error.message.includes("actNotFound")) { return { status: "error", message: "\u274C Account not found", error_details: { address: input.account_address, issue: "Account does not exist on XRPL", solution: "Check address format or fund account with minimum 10 XRP" } }; } throw new Error(`Failed to get account balances: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/wallet/send_xrp_tool.ts import { z as z2 } from "zod"; var SendXRPTool = { name: "rwa_send_xrp", description: "Send XRP to another XRPL address", schema: { destination: z2.string().regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/).describe("Recipient's XRPL address"), amount: z2.number().positive().describe("Amount of XRP to send"), memo: z2.string().optional().describe("Optional memo for the transaction"), destination_tag: z2.number().int().min(0).max(4294967295).optional().describe("Optional destination tag") }, handler: async (agent, input) => { try { await agent.connect(); const walletInfo = await agent.getWalletInfo(); const currentBalance = Number(walletInfo.account_data.Balance) / 1e6; const requiredAmount = input.amount + 12e-6; if (currentBalance < requiredAmount) { return { status: "error", message: "\u274C Insufficient XRP balance", error_details: { current_balance: `${currentBalance.toFixed(6)} XRP`, required_amount: `${requiredAmount.toFixed(6)} XRP`, shortfall: `${(requiredAmount - currentBalance).toFixed(6)} XRP`, note: "Amount includes 0.000012 XRP transaction fee" } }; } const payment = { TransactionType: "Payment", Account: agent.wallet.address, Destination: input.destination, Amount: (input.amount * 1e6).toString(), // Convert to drops Fee: "12" // Standard fee in drops }; if (input.destination_tag !== void 0) { payment.DestinationTag = input.destination_tag; } if (input.memo) { payment.Memos = [{ Memo: { MemoData: Buffer.from(input.memo, "utf8").toString("hex") } }]; } const result = await agent.client.submitAndWait(payment, { wallet: agent.wallet }); const newWalletInfo = await agent.getWalletInfo(); const newBalance = Number(newWalletInfo.account_data.Balance) / 1e6; return { status: "success", message: `\u2705 Successfully sent ${input.amount} XRP to ${input.destination.substring(0, 8)}...`, transaction_details: { transaction_hash: result.result.hash, ledger_index: result.result.ledger_index, fee_paid: "0.000012 XRP", from_address: agent.wallet.address, to_address: input.destination, amount_sent: `${input.amount} XRP`, memo: input.memo || "None", destination_tag: input.destination_tag || "None" }, balance_changes: { previous_balance: `${currentBalance.toFixed(6)} XRP`, new_balance: `${newBalance.toFixed(6)} XRP`, total_deducted: `${(currentBalance - newBalance).toFixed(6)} XRP` } }; } catch (error) { if (error.message.includes("tecUNFUNDED_PAYMENT")) { return { status: "error", message: "\u274C Payment failed - insufficient funds", error_details: { issue: "Account does not have enough XRP for payment + fees", suggestion: "Check balance and reduce payment amount" } }; } if (error.message.includes("tecNO_DST")) { return { status: "error", message: "\u274C Payment failed - destination account not found", error_details: { issue: "Destination account does not exist", suggestion: "Verify recipient address or ensure account is activated" } }; } throw new Error(`Failed to send XRP: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/wallet/create_trustline_tool.ts import { z as z3 } from "zod"; var CreateTrustlineTool = { name: "rwa_create_trustline", description: "Create a trustline to hold RWA tokens from an issuer", schema: { currency: z3.string().length(3).regex(/^[A-Z0-9]{3}$/).describe("3-letter currency code (e.g., 'BLD', 'TBL', 'GLD')"), issuer: z3.string().regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/).describe("Issuer's XRPL address"), limit: z3.number().positive().optional().describe("Maximum amount willing to hold (optional, defaults to 1000000000)") }, handler: async (agent, input) => { try { await agent.connect(); const existingLines = await agent.client.request({ command: "account_lines", account: agent.wallet.address, ledger_index: "validated" }); const existingTrustline = existingLines.result.lines.find( (line) => line.currency === input.currency && line.account === input.issuer ); if (existingTrustline) { return { status: "info", message: `\u2139\uFE0F Trustline already exists for ${input.currency}.${input.issuer.substring(0, 8)}...`, existing_trustline: { currency: existingTrustline.currency, issuer: existingTrustline.account, current_balance: Math.abs(Number(existingTrustline.balance)), limit: existingTrustline.limit, asset_id: `${existingTrustline.currency}.${existingTrustline.account}`, status: "Active" }, recommendation: "Trustline is ready to receive tokens" }; } const trustSet = { TransactionType: "TrustSet", Account: agent.wallet.address, LimitAmount: { currency: input.currency, issuer: input.issuer, value: (input.limit || 1e9).toString() }, Fee: "12" }; const result = await agent.client.submitAndWait(trustSet, { wallet: agent.wallet }); return { status: "success", message: `\u2705 Trustline created for ${input.currency} tokens`, trustline_details: { currency: input.currency, issuer: input.issuer, limit: input.limit || 1e9, asset_id: `${input.currency}.${input.issuer}`, current_balance: 0, ready_to_receive: true }, transaction_info: { transaction_hash: result.result.hash, ledger_index: result.result.ledger_index, fee_paid: "0.000012 XRP", network: agent.network }, next_steps: [ `Ready to receive ${input.currency} tokens from issuer`, "Tokens can now be sent to your address", "Monitor balance with rwa_get_account_balances", "Participate in yield distributions if applicable" ], investment_info: { note: `This trustline allows you to hold up to ${(input.limit || 1e9).toLocaleString()} ${input.currency} tokens`, issuer_info: `Tokens issued by: ${input.issuer}`, risk_notice: "Only create trustlines for assets you trust and understand" } }; } catch (error) { if (error.message.includes("tecNO_AUTH")) { return { status: "error", message: "\u274C Trustline creation failed - authorization required", error_details: { issue: "Issuer requires authorization for trustlines", currency: input.currency, issuer: input.issuer, solution: "Contact the asset issuer for authorization" } }; } if (error.message.includes("tecNO_LINE_INSUF_RESERVE")) { return { status: "error", message: "\u274C Insufficient XRP reserve for trustline", error_details: { issue: "Each trustline requires 2 XRP reserve", solution: "Add more XRP to your wallet (minimum 2 XRP per trustline)", current_currency: input.currency } }; } throw new Error(`Failed to create trustline: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/wallet/get_transaction_history_tool.ts import { z as z4 } from "zod"; var GetTransactionHistoryTool = { name: "rwa_get_transaction_history", description: "Get recent transaction history for an account", schema: { account_address: z4.string().regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/).optional().describe("XRPL address to check (optional, defaults to wallet address)"), limit: z4.number().int().min(1).max(100).default(20).describe("Number of transactions to retrieve (max 100)") }, handler: async (agent, input) => { try { await agent.connect(); const targetAddress = input.account_address || agent.wallet.address; const isOwnWallet = targetAddress === agent.wallet.address; const transactions = await agent.client.request({ command: "account_tx", account: targetAddress, limit: input.limit || 20, ledger_index_min: -1, ledger_index_max: -1 }); const processedTxs = transactions.result.transactions.map((tx) => { const transaction = tx.tx; const meta = tx.meta; const successful = meta.TransactionResult === "tesSUCCESS"; let txType = transaction.TransactionType; let description = ""; let amount = ""; let counterparty = ""; switch (transaction.TransactionType) { case "Payment": const isOutgoing = transaction.Account === targetAddress; counterparty = isOutgoing ? transaction.Destination : transaction.Account; if (typeof transaction.Amount === "string") { const xrpAmount = Number(transaction.Amount) / 1e6; amount = `${xrpAmount} XRP`; description = isOutgoing ? `Sent ${amount} to ${counterparty.substring(0, 8)}...` : `Received ${amount} from ${counterparty.substring(0, 8)}...`; } else { amount = `${transaction.Amount.value} ${transaction.Amount.currency}`; description = isOutgoing ? `Sent ${amount} to ${counterparty.substring(0, 8)}...` : `Received ${amount} from ${counterparty.substring(0, 8)}...`; } break; case "TrustSet": const currency = transaction.LimitAmount.currency; const issuer = transaction.LimitAmount.issuer; description = `Created trustline for ${currency} (${issuer.substring(0, 8)}...)`; break; case "OfferCreate": description = `Created trading offer`; break; case "OfferCancel": description = `Cancelled trading offer`; break; case "EscrowCreate": description = `Created escrow`; break; case "EscrowFinish": description = `Released escrow`; break; default: description = `${txType} transaction`; } return { hash: transaction.hash, type: txType, description, amount, counterparty: counterparty || "N/A", successful, fee: `${Number(transaction.Fee) / 1e6} XRP`, ledger_index: tx.ledger_index, date: new Date((transaction.date + 946684800) * 1e3).toISOString(), // Ripple epoch to Unix status: successful ? "\u2705 Success" : "\u274C Failed", result_code: meta.TransactionResult }; }); const successfulTxs = processedTxs.filter((tx) => tx.successful); const failedTxs = processedTxs.filter((tx) => !tx.successful); const payments = processedTxs.filter((tx) => tx.type === "Payment"); const trustlines = processedTxs.filter((tx) => tx.type === "TrustSet"); return { status: "success", message: `\u2705 Retrieved ${processedTxs.length} transactions for ${targetAddress.substring(0, 8)}...`, account_info: { address: targetAddress, is_own_wallet: isOwnWallet, network: agent.network }, transaction_summary: { total_transactions: processedTxs.length, successful: successfulTxs.length, failed: failedTxs.length, payments: payments.length, trustlines_created: trustlines.length, success_rate: `${(successfulTxs.length / processedTxs.length * 100).toFixed(1)}%` }, transactions: processedTxs, insights: [ `Account has ${processedTxs.length} recent transactions`, `Success rate: ${(successfulTxs.length / processedTxs.length * 100).toFixed(1)}%`, `Payment transactions: ${payments.length}`, `Trustlines created: ${trustlines.length}`, ...isOwnWallet ? [ failedTxs.length > 0 ? `\u26A0\uFE0F ${failedTxs.length} failed transactions detected` : "\u2705 All recent transactions successful" ] : [] ] }; } catch (error) { if (error.message.includes("actNotFound")) { return { status: "error", message: "\u274C Account not found", error_details: { address: input.account_address, issue: "Account does not exist on XRPL or has no transaction history", solution: "Verify address or check if account has been activated" } }; } throw new Error(`Failed to get transaction history: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/wallet/validate_address_tool.ts import { z as z5 } from "zod"; var ValidateAddressTool = { name: "rwa_validate_address", description: "Validate XRPL address format and check if account exists", schema: { address: z5.string().describe("XRPL address to validate") }, handler: async (agent, input) => { try { const address = input.address.trim(); const xrplAddressRegex = /^r[1-9A-HJ-NP-Za-km-z]{25,34}$/; const isValidFormat = xrplAddressRegex.test(address); if (!isValidFormat) { return { status: "invalid", message: "\u274C Invalid XRPL address format", validation_results: { address, format_valid: false, exists_on_ledger: false, issues: [ "XRPL addresses must start with 'r'", "Must be 25-34 characters long", "Can only contain base58 characters (no 0, O, I, l)" ], examples: [ "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH", "rPT1Sjq2YGrBMTttX4GZHjKu9dyfQeEBUs" ] } }; } await agent.connect(); let accountExists = false; let accountInfo = null; let errorReason = ""; try { const response = await agent.client.request({ command: "account_info", account: address, ledger_index: "validated" }); accountExists = true; accountInfo = response.result.account_data; } catch (error) { if (error.message.includes("actNotFound")) { accountExists = false; errorReason = "Account not activated on XRPL"; } else { throw error; } } let accountDetails = {}; if (accountExists && accountInfo) { const balance = Number(accountInfo.Balance) / 1e6; accountDetails = { balance: `${balance.toFixed(6)} XRP`, sequence: accountInfo.Sequence, flags: accountInfo.Flags || 0, activated: true, reserve_met: balance >= 10, can_receive_payments: true }; } return { status: accountExists ? "valid" : "format_valid", message: accountExists ? `\u2705 Valid XRPL address with active account` : `\u26A0\uFE0F Valid format but account not found on ledger`, validation_results: { address, format_valid: true, exists_on_ledger: accountExists, network: agent.network, ...accountExists ? { account_details: accountDetails } : { activation_required: true, reason: errorReason, note: "Account needs to receive minimum 10 XRP to activate" } }, recommendations: accountExists ? [ "\u2705 Address is valid and active", "Safe to send payments to this address", "Account can receive both XRP and tokens" ] : [ "\u26A0\uFE0F Account not yet activated on XRPL", "Send minimum 10 XRP to activate this address", "Address format is correct" ] }; } catch (error) { throw new Error(`Failed to validate address: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/rwa/tokenize_asset_tool.ts import { z as z6 } from "zod"; var TokenizeAssetTool = { name: "rwa_tokenize_asset", description: "Tokenize a real-world asset on XRPL with basic compliance controls", schema: { asset_type: z6.enum(["real_estate", "treasury", "commodity", "bond"]).describe("Type of asset to tokenize"), asset_name: z6.string().min(1).max(50).describe("Name of the asset (e.g., 'Manhattan Office Building')"), total_value: z6.number().positive().describe("Total value of the asset in USD"), token_symbol: z6.string().length(3).regex(/^[A-Z0-9]{3}$/).describe("3-letter token symbol (e.g., 'BLD', 'TBL', 'GLD')"), total_supply: z6.number().positive().int().describe("Total number of tokens to issue"), yield_rate: z6.number().min(0).max(50).optional().describe("Annual yield percentage (e.g., 6.5 for 6.5%)"), accredited_only: z6.boolean().default(false).describe("Restrict to accredited investors only") }, handler: async (agent, input) => { try { await agent.connect(); const result = await agent.tokenizeAsset({ type: input.asset_type, name: input.asset_name, totalValue: input.total_value, tokenSymbol: input.token_symbol, totalSupply: input.total_supply, yieldRate: input.yield_rate, accreditedOnly: input.accredited_only }); const pricePerToken = input.total_value / input.total_supply; return { status: result.status, message: `\u2705 Successfully tokenized ${input.asset_name} as ${input.token_symbol}`, asset_details: { name: input.asset_name, type: input.asset_type, total_value: `$${input.total_value.toLocaleString()}`, total_supply: `${input.total_supply.toLocaleString()} tokens`, price_per_token: `$${pricePerToken.toFixed(2)}`, yield_rate: input.yield_rate ? `${input.yield_rate}% annually` : "No yield configured" }, token_info: { token_id: result.tokenId, currency_code: result.currency, issuer_address: result.issuerAddress }, next_steps: [ "Set up yield distribution (if income-generating asset)", "Create trustlines for investors", "Configure secondary market trading", "Begin investor onboarding" ], compliance: { accredited_only: input.accredited_only, jurisdiction: "US (default for MVP)", regulatory_notes: input.accredited_only ? "\u2696\uFE0F Restricted to accredited investors under Regulation D" : "\u2696\uFE0F Consider SEC registration requirements for public offerings" } }; } catch (error) { throw new Error(`Failed to tokenize asset: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/rwa/get_asset_info_tool.ts import { z as z7 } from "zod"; var GetAssetInfoTool = { name: "rwa_get_asset_info", description: "Retrieve detailed information about a tokenized RWA asset with comprehensive balance analysis", schema: { asset_id: z7.string().describe("Asset ID in format 'CURRENCY.ISSUER'"), include_holders: z7.boolean().optional().describe("Include detailed holder information (default: false)") }, handler: async (agent, input) => { try { await agent.connect(); const assetId = input.asset_id; const includeHolders = input.include_holders || false; const assetInfo = await agent.getAssetInfo(assetId); if (!assetInfo) { return { status: "error", message: `Asset ${assetId} not found`, suggestion: "Verify the asset ID format (CURRENCY.ISSUER) and ensure the asset has been tokenized" }; } const walletInfo = await agent.getWalletInfo(); const [currency, issuer] = assetId.split("."); const supplyInfo = await agent.getActualTokenSupply(assetId); const rwaBalances = await agent.getRWATokenBalances(); const thisAssetBalance = rwaBalances.rwa_tokens.find((token) => token.asset_id === assetId); let holdersInfo = null; if (includeHolders) { const holders = await agent.getTokenHolders(assetId); holdersInfo = { total_holders: holders.length, top_holders: holders.slice(0, 10), // Top 10 holders holder_distribution: { whale_holders: holders.filter((h) => h.percentage >= 10).length, major_holders: holders.filter((h) => h.percentage >= 1 && h.percentage < 10).length, retail_holders: holders.filter((h) => h.percentage < 1).length } }; } return { status: "success", message: `\u2705 Comprehensive asset information retrieved for ${assetId}`, // Basic Asset Information asset_details: { asset_id: assetId, asset_name: assetInfo.name, asset_type: assetInfo.type, token_symbol: assetInfo.tokenSymbol, total_value: assetInfo.totalValue ? `$${assetInfo.totalValue.toLocaleString()}` : "Not set", planned_supply: assetInfo.totalSupply ? `${assetInfo.totalSupply.toLocaleString()} tokens` : "Not set", yield_rate: assetInfo.yieldRate ? `${assetInfo.yieldRate}% annually` : "No yield configured", price_per_token: assetInfo.totalValue && assetInfo.totalSupply ? `$${(assetInfo.totalValue / assetInfo.totalSupply).toFixed(2)}` : "Not calculated" }, // Live XRPL Token Supply Data (following XRPL dev portal patterns) live_supply_data: { actual_total_issued: `${supplyInfo.totalIssued.toLocaleString()} tokens`, circulating_supply: `${supplyInfo.circulatingSupply.toLocaleString()} tokens`, issuer_reserve: `${supplyInfo.issuerBalance.toLocaleString()} tokens`, active_holders: supplyInfo.holderCount, supply_utilization: assetInfo.totalSupply > 0 ? `${(supplyInfo.totalIssued / assetInfo.totalSupply * 100).toFixed(1)}%` : "N/A" }, // Current Wallet Holdings for this Asset wallet_holdings: thisAssetBalance ? { your_balance: `${thisAssetBalance.balance.toLocaleString()} ${currency}`, estimated_value: thisAssetBalance.estimated_value ? `$${thisAssetBalance.estimated_value.toLocaleString()}` : "Not available", ownership_percentage: supplyInfo.circulatingSupply > 0 ? `${(thisAssetBalance.balance / supplyInfo.circulatingSupply * 100).toFixed(3)}%` : "0%", trustline_limit: thisAssetBalance.limit || "No limit set" } : { your_balance: "0 tokens", status: "No trustline established", action_needed: "Create trustline to hold this RWA token" }, // Technical XRPL Information token_technical_info: { currency_code: currency, issuer_address: issuer, issuer_xrp_balance: `${Number(walletInfo.account_data.Balance) / 1e6} XRP`, account_sequence: walletInfo.account_data.Sequence, network: agent.network, ledger_flags: walletInfo.account_data.Flags, owner_count: walletInfo.account_data.OwnerCount }, // RWA Portfolio Context portfolio_context: { total_rwa_tokens_in_wallet: rwaBalances.rwa_tokens.filter((t) => t.is_rwa_token).length, total_portfolio_value: `$${rwaBalances.total_rwa_value.toLocaleString()}`, portfolio_health: rwaBalances.portfolio_summary.portfolio_health, diversification: rwaBalances.portfolio_summary.diversification }, // Trading and Liquidity Information trading_info: { tradeable: "Ready for peer-to-peer transfers", exchange_support: "Compatible with XRPL DEX and AMM", minimum_trade: "1 token (divisible to 15 decimal places)", liquidity_status: supplyInfo.holderCount > 1 ? "Multiple holders" : "Single holder", market_depth: `${supplyInfo.holderCount} active participants` }, // Compliance and Regulatory compliance_status: { issuer_flags: "Standard XRPL issuer account", transfer_restrictions: "Configurable via trustline flags", regulatory_framework: "Depends on asset type and jurisdiction", accredited_only: assetInfo.totalValue ? "Check asset metadata" : "Unknown" }, // Include holders information if requested ...holdersInfo && { token_holders: holdersInfo }, // Useful Next Actions useful_operations: [ !thisAssetBalance ? "Create trustline to hold this RWA token" : null, "Set up yield distribution if income-generating asset", "Configure trading pairs on XRPL DEX", "Monitor token holder analytics", "Generate XRPL Meta TOML file for listings" ].filter(Boolean), // Advanced Analytics analytics: { tokenization_health: { metadata_available: !!assetInfo.totalValue, supply_consistency: supplyInfo.totalIssued > 0 ? "\u2705 Tokens issued" : "\u26A0\uFE0F No tokens issued", holder_growth: supplyInfo.holderCount > 1 ? "\u2705 Multiple holders" : "\u{1F4C8} Growth opportunity", value_tracking: assetInfo.totalValue ? "\u2705 Asset valued" : "\u26A0\uFE0F Needs valuation" }, market_indicators: { distribution_score: supplyInfo.holderCount > 0 ? Math.min(100, supplyInfo.holderCount / 50 * 100).toFixed(0) + "/100" : "0/100", liquidity_score: supplyInfo.circulatingSupply > 0 ? "Active" : "Dormant", adoption_phase: supplyInfo.holderCount === 0 ? "Pre-launch" : supplyInfo.holderCount < 5 ? "Early" : supplyInfo.holderCount < 20 ? "Growing" : "Mature" } } }; } catch (error) { throw new Error(`Failed to get comprehensive asset info: ${error.message}`); } finally { await agent.disconnect(); } } }; // src/mcp/rwa/send_rwa_token_tool.ts import { z as z8 } from "zod"; var SendRWATokenTool = { name: "rwa_send_rwa_token", description: "Send RWA tokens to another XRPL address using token format like PAT.rHJZf5qYxwH2Fnms1Uwmi61VfHqoTALgXw", schema: { token_id: z8.string().regex(/^[A-Z0-9]{3}\.r[1-9A-HJ-NP-Za-km-z]{25,34}$/).describe("Token ID in format 'CURRENCY.ISSUER' (e.g., 'PAT.rHJZf5qYxwH2Fnms1Uwmi61VfHqoTALgXw')"), destination: z8.string().regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/).describe("Recipient's XRPL address"), amount: z8.number().positive().describe("Amount of RWA tokens to send"), destination_tag: z8.number().int().min(0).max(4294967295).optional().describe("Optional destination tag"), memo: z8.string().max(1e3).optional().describe("Optional memo for the transaction") }, handler: async (agent, input) => { try { await agent.connect(); const [currency, issuer] = input.token_id.split("."); const assetInfo = await agent.getAssetInfo(input.token_id); if (!assetInfo) { throw new Error(`Token ${input.token_id} not found or invalid`); } const balances = await agent.getRWATokenBalances(); const tokenBalance = balances.rwa_tokens.find( (token) => token.currency === currency && token.issuer === issuer ); if (!tokenBalance || tokenBalance.balance < input.amount) { throw new Error( `Insufficient balance. Available: ${tokenBalance?.balance || 0} ${currency}, Requested: ${input.amount}` ); } try { const destInfo = await agent.client.request({ command: "account_info", account: input.destination, ledger_index: "validated" }); if (!destInfo.result.account_data) { throw new Error(`Destination address ${input.destination} does not exist`); } } catch (error) { if (error.message.includes("actNotFound")) { throw new Error(`Destination address ${input.destination} does not exist or is not activated`); } throw error; } let hasTrustline = false; try { const destLines = await agent.client.request({ command: "account_lines", account: input.destination, ledger_index: "validated" }); hasTrustline = destLines.result.lines.some( (line) => line.currency === currency && line.account === issuer ); } catch (error) { console.error("Could not check destination trustlines:", error); } const payment = { TransactionType: "Payment", Account: agent.wallet.address, Destination: input.destination, Amount: { currency, issuer, value: input.amount.toString() }, Fee: "12" }; if (input.destination_tag !== void 0) { payment.DestinationTag = input.destination_tag; } if (input.memo) { payment.Memos = [{ Memo: { MemoType: Buffer.from("rwa_transfer",