rwa-build
Version:
MCP library for AI-assisted Real World Asset tokenization on XRPL
1 lines ⢠239 kB
Source Map (JSON)
{"version":3,"sources":["../node_modules/dotenv/package.json","../node_modules/dotenv/lib/main.js","../src/config.ts","../src/index.ts","../src/mcp/wallet/get_wallet_info_tool.ts","../src/mcp/wallet/get_account_balances_tool.ts","../src/mcp/wallet/send_xrp_tool.ts","../src/mcp/wallet/create_trustline_tool.ts","../src/mcp/wallet/get_transaction_history_tool.ts","../src/mcp/wallet/validate_address_tool.ts","../src/mcp/rwa/tokenize_asset_tool.ts","../src/mcp/rwa/get_asset_info_tool.ts","../src/mcp/rwa/send_rwa_token_tool.ts","../src/mcp/rwa/create_amm_tool.ts","../src/mcp/rwa/swap_amm_tool.ts","../src/utils/xrpl_helpers.ts","../src/mcp/rwa/add_liquidity_amm_tool.ts","../src/mcp/rwa/remove_liquidity_amm_tool.ts","../src/mcp/webapp/generate_webapp_project_tool.ts","../src/mcp/index.ts","../src/agent/index.ts"],"sourcesContent":["{\n \"name\": \"dotenv\",\n \"version\": \"16.5.0\",\n \"description\": \"Loads environment variables from .env file\",\n \"main\": \"lib/main.js\",\n \"types\": \"lib/main.d.ts\",\n \"exports\": {\n \".\": {\n \"types\": \"./lib/main.d.ts\",\n \"require\": \"./lib/main.js\",\n \"default\": \"./lib/main.js\"\n },\n \"./config\": \"./config.js\",\n \"./config.js\": \"./config.js\",\n \"./lib/env-options\": \"./lib/env-options.js\",\n \"./lib/env-options.js\": \"./lib/env-options.js\",\n \"./lib/cli-options\": \"./lib/cli-options.js\",\n \"./lib/cli-options.js\": \"./lib/cli-options.js\",\n \"./package.json\": \"./package.json\"\n },\n \"scripts\": {\n \"dts-check\": \"tsc --project tests/types/tsconfig.json\",\n \"lint\": \"standard\",\n \"pretest\": \"npm run lint && npm run dts-check\",\n \"test\": \"tap run --allow-empty-coverage --disable-coverage --timeout=60000\",\n \"test:coverage\": \"tap run --show-full-coverage --timeout=60000 --coverage-report=lcov\",\n \"prerelease\": \"npm test\",\n \"release\": \"standard-version\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git://github.com/motdotla/dotenv.git\"\n },\n \"homepage\": \"https://github.com/motdotla/dotenv#readme\",\n \"funding\": \"https://dotenvx.com\",\n \"keywords\": [\n \"dotenv\",\n \"env\",\n \".env\",\n \"environment\",\n \"variables\",\n \"config\",\n \"settings\"\n ],\n \"readmeFilename\": \"README.md\",\n \"license\": \"BSD-2-Clause\",\n \"devDependencies\": {\n \"@types/node\": \"^18.11.3\",\n \"decache\": \"^4.6.2\",\n \"sinon\": \"^14.0.1\",\n \"standard\": \"^17.0.0\",\n \"standard-version\": \"^9.5.0\",\n \"tap\": \"^19.2.0\",\n \"typescript\": \"^4.8.4\"\n },\n \"engines\": {\n \"node\": \">=12\"\n },\n \"browser\": {\n \"fs\": false\n }\n}\n","const fs = require('fs')\nconst path = require('path')\nconst os = require('os')\nconst crypto = require('crypto')\nconst packageJson = require('../package.json')\n\nconst version = packageJson.version\n\nconst LINE = /(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/mg\n\n// Parse src into an Object\nfunction parse (src) {\n const obj = {}\n\n // Convert buffer to string\n let lines = src.toString()\n\n // Convert line breaks to same format\n lines = lines.replace(/\\r\\n?/mg, '\\n')\n\n let match\n while ((match = LINE.exec(lines)) != null) {\n const key = match[1]\n\n // Default undefined or null to empty string\n let value = (match[2] || '')\n\n // Remove whitespace\n value = value.trim()\n\n // Check if double quoted\n const maybeQuote = value[0]\n\n // Remove surrounding quotes\n value = value.replace(/^(['\"`])([\\s\\S]*)\\1$/mg, '$2')\n\n // Expand newlines if double quoted\n if (maybeQuote === '\"') {\n value = value.replace(/\\\\n/g, '\\n')\n value = value.replace(/\\\\r/g, '\\r')\n }\n\n // Add to object\n obj[key] = value\n }\n\n return obj\n}\n\nfunction _parseVault (options) {\n const vaultPath = _vaultPath(options)\n\n // Parse .env.vault\n const result = DotenvModule.configDotenv({ path: vaultPath })\n if (!result.parsed) {\n const err = new Error(`MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`)\n err.code = 'MISSING_DATA'\n throw err\n }\n\n // handle scenario for comma separated keys - for use with key rotation\n // example: DOTENV_KEY=\"dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenvx.com/vault/.env.vault?environment=prod\"\n const keys = _dotenvKey(options).split(',')\n const length = keys.length\n\n let decrypted\n for (let i = 0; i < length; i++) {\n try {\n // Get full key\n const key = keys[i].trim()\n\n // Get instructions for decrypt\n const attrs = _instructions(result, key)\n\n // Decrypt\n decrypted = DotenvModule.decrypt(attrs.ciphertext, attrs.key)\n\n break\n } catch (error) {\n // last key\n if (i + 1 >= length) {\n throw error\n }\n // try next key\n }\n }\n\n // Parse decrypted .env string\n return DotenvModule.parse(decrypted)\n}\n\nfunction _warn (message) {\n console.log(`[dotenv@${version}][WARN] ${message}`)\n}\n\nfunction _debug (message) {\n console.log(`[dotenv@${version}][DEBUG] ${message}`)\n}\n\nfunction _dotenvKey (options) {\n // prioritize developer directly setting options.DOTENV_KEY\n if (options && options.DOTENV_KEY && options.DOTENV_KEY.length > 0) {\n return options.DOTENV_KEY\n }\n\n // secondary infra already contains a DOTENV_KEY environment variable\n if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {\n return process.env.DOTENV_KEY\n }\n\n // fallback to empty string\n return ''\n}\n\nfunction _instructions (result, dotenvKey) {\n // Parse DOTENV_KEY. Format is a URI\n let uri\n try {\n uri = new URL(dotenvKey)\n } catch (error) {\n if (error.code === 'ERR_INVALID_URL') {\n 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')\n err.code = 'INVALID_DOTENV_KEY'\n throw err\n }\n\n throw error\n }\n\n // Get decrypt key\n const key = uri.password\n if (!key) {\n const err = new Error('INVALID_DOTENV_KEY: Missing key part')\n err.code = 'INVALID_DOTENV_KEY'\n throw err\n }\n\n // Get environment\n const environment = uri.searchParams.get('environment')\n if (!environment) {\n const err = new Error('INVALID_DOTENV_KEY: Missing environment part')\n err.code = 'INVALID_DOTENV_KEY'\n throw err\n }\n\n // Get ciphertext payload\n const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`\n const ciphertext = result.parsed[environmentKey] // DOTENV_VAULT_PRODUCTION\n if (!ciphertext) {\n const err = new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate environment ${environmentKey} in your .env.vault file.`)\n err.code = 'NOT_FOUND_DOTENV_ENVIRONMENT'\n throw err\n }\n\n return { ciphertext, key }\n}\n\nfunction _vaultPath (options) {\n let possibleVaultPath = null\n\n if (options && options.path && options.path.length > 0) {\n if (Array.isArray(options.path)) {\n for (const filepath of options.path) {\n if (fs.existsSync(filepath)) {\n possibleVaultPath = filepath.endsWith('.vault') ? filepath : `${filepath}.vault`\n }\n }\n } else {\n possibleVaultPath = options.path.endsWith('.vault') ? options.path : `${options.path}.vault`\n }\n } else {\n possibleVaultPath = path.resolve(process.cwd(), '.env.vault')\n }\n\n if (fs.existsSync(possibleVaultPath)) {\n return possibleVaultPath\n }\n\n return null\n}\n\nfunction _resolveHome (envPath) {\n return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath\n}\n\nfunction _configVault (options) {\n const debug = Boolean(options && options.debug)\n if (debug) {\n _debug('Loading env from encrypted .env.vault')\n }\n\n const parsed = DotenvModule._parseVault(options)\n\n let processEnv = process.env\n if (options && options.processEnv != null) {\n processEnv = options.processEnv\n }\n\n DotenvModule.populate(processEnv, parsed, options)\n\n return { parsed }\n}\n\nfunction configDotenv (options) {\n const dotenvPath = path.resolve(process.cwd(), '.env')\n let encoding = 'utf8'\n const debug = Boolean(options && options.debug)\n\n if (options && options.encoding) {\n encoding = options.encoding\n } else {\n if (debug) {\n _debug('No encoding is specified. UTF-8 is used by default')\n }\n }\n\n let optionPaths = [dotenvPath] // default, look for .env\n if (options && options.path) {\n if (!Array.isArray(options.path)) {\n optionPaths = [_resolveHome(options.path)]\n } else {\n optionPaths = [] // reset default\n for (const filepath of options.path) {\n optionPaths.push(_resolveHome(filepath))\n }\n }\n }\n\n // Build the parsed data in a temporary object (because we need to return it). Once we have the final\n // parsed data, we will combine it with process.env (or options.processEnv if provided).\n let lastError\n const parsedAll = {}\n for (const path of optionPaths) {\n try {\n // Specifying an encoding returns a string instead of a buffer\n const parsed = DotenvModule.parse(fs.readFileSync(path, { encoding }))\n\n DotenvModule.populate(parsedAll, parsed, options)\n } catch (e) {\n if (debug) {\n _debug(`Failed to load ${path} ${e.message}`)\n }\n lastError = e\n }\n }\n\n let processEnv = process.env\n if (options && options.processEnv != null) {\n processEnv = options.processEnv\n }\n\n DotenvModule.populate(processEnv, parsedAll, options)\n\n if (lastError) {\n return { parsed: parsedAll, error: lastError }\n } else {\n return { parsed: parsedAll }\n }\n}\n\n// Populates process.env from .env file\nfunction config (options) {\n // fallback to original dotenv if DOTENV_KEY is not set\n if (_dotenvKey(options).length === 0) {\n return DotenvModule.configDotenv(options)\n }\n\n const vaultPath = _vaultPath(options)\n\n // dotenvKey exists but .env.vault file does not exist\n if (!vaultPath) {\n _warn(`You set DOTENV_KEY but you are missing a .env.vault file at ${vaultPath}. Did you forget to build it?`)\n\n return DotenvModule.configDotenv(options)\n }\n\n return DotenvModule._configVault(options)\n}\n\nfunction decrypt (encrypted, keyStr) {\n const key = Buffer.from(keyStr.slice(-64), 'hex')\n let ciphertext = Buffer.from(encrypted, 'base64')\n\n const nonce = ciphertext.subarray(0, 12)\n const authTag = ciphertext.subarray(-16)\n ciphertext = ciphertext.subarray(12, -16)\n\n try {\n const aesgcm = crypto.createDecipheriv('aes-256-gcm', key, nonce)\n aesgcm.setAuthTag(authTag)\n return `${aesgcm.update(ciphertext)}${aesgcm.final()}`\n } catch (error) {\n const isRange = error instanceof RangeError\n const invalidKeyLength = error.message === 'Invalid key length'\n const decryptionFailed = error.message === 'Unsupported state or unable to authenticate data'\n\n if (isRange || invalidKeyLength) {\n const err = new Error('INVALID_DOTENV_KEY: It must be 64 characters long (or more)')\n err.code = 'INVALID_DOTENV_KEY'\n throw err\n } else if (decryptionFailed) {\n const err = new Error('DECRYPTION_FAILED: Please check your DOTENV_KEY')\n err.code = 'DECRYPTION_FAILED'\n throw err\n } else {\n throw error\n }\n }\n}\n\n// Populate process.env with parsed values\nfunction populate (processEnv, parsed, options = {}) {\n const debug = Boolean(options && options.debug)\n const override = Boolean(options && options.override)\n\n if (typeof parsed !== 'object') {\n const err = new Error('OBJECT_REQUIRED: Please check the processEnv argument being passed to populate')\n err.code = 'OBJECT_REQUIRED'\n throw err\n }\n\n // Set process.env\n for (const key of Object.keys(parsed)) {\n if (Object.prototype.hasOwnProperty.call(processEnv, key)) {\n if (override === true) {\n processEnv[key] = parsed[key]\n }\n\n if (debug) {\n if (override === true) {\n _debug(`\"${key}\" is already defined and WAS overwritten`)\n } else {\n _debug(`\"${key}\" is already defined and was NOT overwritten`)\n }\n }\n } else {\n processEnv[key] = parsed[key]\n }\n }\n}\n\nconst DotenvModule = {\n configDotenv,\n _configVault,\n _parseVault,\n config,\n decrypt,\n parse,\n populate\n}\n\nmodule.exports.configDotenv = DotenvModule.configDotenv\nmodule.exports._configVault = DotenvModule._configVault\nmodule.exports._parseVault = DotenvModule._parseVault\nmodule.exports.config = DotenvModule.config\nmodule.exports.decrypt = DotenvModule.decrypt\nmodule.exports.parse = DotenvModule.parse\nmodule.exports.populate = DotenvModule.populate\n\nmodule.exports = DotenvModule\n","import * as dotenv from 'dotenv';\n\nimport { RWAConfig } from './types';\n\ndotenv.config();\n\nconst getArgs = () =>\n process.argv.reduce((args: any, arg: any) => {\n // long arg\n if (arg.slice(0, 2) === \"--\") {\n const longArg = arg.split(\"=\");\n const longArgFlag = longArg[0].slice(2);\n const longArgValue = longArg.length > 1 ? longArg[1] : true;\n args[longArgFlag] = longArgValue;\n }\n // flags\n else if (arg[0] === \"-\") {\n const flags = arg.slice(1).split(\"\");\n flags.forEach((flag: any) => {\n args[flag] = true;\n });\n }\n return args;\n }, {});\n\nexport function getRWAConfig(): RWAConfig {\n\n const args = getArgs();\n \n const hasPrivateKey = !!(args?.xrpl_private_key || process.env.XRPL_PRIVATE_KEY); \n const network = ((args?.xrpl_network || process.env.XRPL_NETWORK) || 'testnet') as 'testnet' | 'mainnet' | 'devnet';\n\n if (!hasPrivateKey) {\n throw new Error('XRPL_PRIVATE_KEY environment variable is required');\n }\n\n const servers = {\n testnet: 'wss://s.altnet.rippletest.net:51233',\n devnet: 'wss://s.devnet.rippletest.net:51233',\n mainnet: 'wss://xrplcluster.com'\n };\n\n return {\n privateKey: args?.xrpl_private_key || process.env.XRPL_PRIVATE_KEY,\n network,\n server: servers[network]\n };\n}\n\nexport function validateEnvironment(): void {\n try {\n getRWAConfig();\n console.error('ā
Environment configuration valid');\n } catch (error) {\n console.error('ā Environment configuration error:', error);\n throw error;\n }\n}\n","#!/usr/bin/env node\n\nimport { validateEnvironment } from './config';\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { RWAMcpTools } from \"./mcp\"\nimport { RWAAgent } from './agent';\n\n/**\n * Creates an MCP server for RWA tokenization on XRPL\n */\nfunction createMcpServer(agent: RWAAgent) {\n // Create MCP server instance\n const server = new McpServer({\n name: \"rwa-build\",\n version: \"0.1.0\"\n });\n\n // Register all RWA tools\n for (const [_key, tool] of Object.entries(RWAMcpTools)) {\n server.tool(tool.name, tool.description, tool.schema, async (params: any): Promise<any> => {\n try {\n // Execute the handler with the params directly\n const result = await tool.handler(agent, params);\n\n // Format the result as MCP tool response\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result, null, 2),\n },\n ],\n };\n } catch (error) {\n console.error(\"Tool execution error:\", error);\n // Handle errors in MCP format\n return {\n isError: true,\n content: [\n {\n type: \"text\",\n text: error instanceof Error\n ? error.message\n : \"Unknown error occurred\",\n },\n ],\n };\n }\n });\n }\n\n return server;\n}\n\nasync function main() {\n try {\n console.error(\"šļø Starting RWA.build MCP Server...\");\n\n // Validate environment before proceeding\n validateEnvironment();\n\n // Create RWA agent\n const rwaAgent = new RWAAgent();\n\n // Create and start MCP server\n const server = createMcpServer(rwaAgent);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n console.error(\"ā
RWA.build MCP Server is running!\");\n console.error(\"šÆ Available tools (8 functional):\");\n \n console.error(\"\\nš Core Operations:\");\n console.error(\" ⢠rwa_tokenize_asset - Set up issuer account (partial)\");\n console.error(\" ⢠rwa_distribute_yield - Send XRP yield payments ā
\");\n \n console.error(\"\\nš³ Wallet Management (6 tools - all functional):\");\n console.error(\" ⢠rwa_get_wallet_info - Get wallet address and balance ā
\");\n console.error(\" ⢠rwa_send_xrp - Send XRP payments ā
\");\n console.error(\" ⢠rwa_create_trustline - Create trustlines for tokens ā
\");\n console.error(\" ⢠rwa_get_account_balances - Check all balances ā
\");\n console.error(\" ⢠rwa_get_transaction_history - View transaction history ā
\");\n console.error(\" ⢠rwa_validate_address - Validate XRPL addresses ā
\");\n \n console.error(\"\");\n console.error(\"š” Try: 'Check my wallet balance and create trustline for BLD tokens'\");\n console.error(\"š° Try: 'Send 100 XRP yield distribution to 5 token holders'\");\n\n } catch (error) {\n console.error('ā Error starting RWA.build MCP server:', error);\n process.exit(1);\n }\n}\n\nmain();\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const GetWalletInfoTool: McpTool = {\n name: \"rwa_get_wallet_info\",\n description: \"Get wallet address and basic account information\",\n schema: {},\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const walletInfo = await agent.getWalletInfo();\n const balanceInXRP = Number(walletInfo.account_data.Balance) / 1000000;\n\n return {\n status: \"success\",\n message: \"ā
Wallet information retrieved successfully\",\n wallet_details: {\n address: agent.wallet.address,\n network: agent.network,\n balance: `${balanceInXRP.toFixed(6)} XRP`,\n balance_in_drops: walletInfo.account_data.Balance,\n sequence: walletInfo.account_data.Sequence,\n account_flags: walletInfo.account_data.Flags || 0\n },\n account_status: {\n activated: true,\n reserve_requirement: \"10 XRP minimum\",\n can_tokenize: balanceInXRP >= 10,\n ready_for_operations: balanceInXRP >= 1\n },\n recommendations: balanceInXRP < 10 \n ? [\n \"ā ļø Low XRP balance detected\",\n \"Fund wallet with at least 10 XRP for tokenization\",\n \"Reserve requirement: 10 XRP for account operations\",\n `Current balance: ${balanceInXRP.toFixed(6)} XRP`\n ]\n : [\n \"ā
Wallet has sufficient balance for operations\",\n \"Ready to tokenize assets\",\n \"Ready to create trustlines and distribute yields\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to get wallet info: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const GetAccountBalancesTool: McpTool = {\n name: \"rwa_get_account_balances\",\n description: \"Get all token balances and trustlines for an account\",\n schema: {\n account_address: z.string()\n .regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/)\n .optional()\n .describe(\"XRPL address to check (optional, defaults to wallet address)\")\n },\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const targetAddress = input.account_address || agent.wallet.address;\n const isOwnWallet = targetAddress === agent.wallet.address;\n\n // Get account info for XRP balance\n const accountInfo = await agent.client.request({\n command: 'account_info',\n account: targetAddress,\n ledger_index: 'validated'\n });\n\n const xrpBalance = Number(accountInfo.result.account_data.Balance) / 1000000;\n\n // Get trustlines (token balances)\n const trustLines = await agent.client.request({\n command: 'account_lines',\n account: targetAddress,\n ledger_index: 'validated'\n });\n\n const tokenBalances = trustLines.result.lines.map((line: any) => ({\n currency: line.currency,\n issuer: line.account,\n balance: Math.abs(Number(line.balance)), // Absolute value for holders\n limit: line.limit,\n quality_in: line.quality_in,\n quality_out: line.quality_out,\n is_frozen: line.freeze || false,\n asset_id: `${line.currency}.${line.account}`\n }));\n\n const totalTokenTypes = tokenBalances.length;\n const activeBalances = tokenBalances.filter(token => token.balance > 0);\n\n return {\n status: \"success\",\n message: `ā
Account balances retrieved for ${targetAddress.substring(0, 8)}...`,\n account_info: {\n address: targetAddress,\n network: agent.network,\n is_own_wallet: isOwnWallet\n },\n xrp_balance: {\n amount: `${xrpBalance.toFixed(6)} XRP`,\n drops: accountInfo.result.account_data.Balance,\n available_for_fees: xrpBalance > 10,\n reserve_status: xrpBalance >= 10 ? \"ā
Above reserve\" : \"ā ļø Below reserve (10 XRP)\"\n },\n token_balances: tokenBalances.length > 0 ? tokenBalances : [],\n portfolio_summary: {\n total_trustlines: totalTokenTypes,\n active_balances: activeBalances.length,\n xrp_balance: xrpBalance,\n portfolio_status: activeBalances.length > 0 \n ? `${activeBalances.length} active token positions`\n : \"No token holdings\"\n },\n insights: [\n `XRP Balance: ${xrpBalance.toFixed(6)} XRP`,\n `Token Types: ${totalTokenTypes}`,\n `Active Positions: ${activeBalances.length}`,\n ...(isOwnWallet ? [\n xrpBalance < 1 ? \"ā ļø Low XRP - may need funding for transactions\" : \"ā
Sufficient XRP for operations\",\n totalTokenTypes === 0 ? \"š” Create trustlines to hold RWA tokens\" : `š Tracking ${totalTokenTypes} different tokens`\n ] : [\n \"š External account analysis complete\"\n ])\n ]\n };\n } catch (error: any) {\n if (error.message.includes('actNotFound')) {\n return {\n status: \"error\",\n message: \"ā Account not found\",\n error_details: {\n address: input.account_address,\n issue: \"Account does not exist on XRPL\",\n solution: \"Check address format or fund account with minimum 10 XRP\"\n }\n };\n }\n throw new Error(`Failed to get account balances: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const SendXRPTool: McpTool = {\n name: \"rwa_send_xrp\",\n description: \"Send XRP to another XRPL address\",\n schema: {\n destination: z.string()\n .regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/)\n .describe(\"Recipient's XRPL address\"),\n amount: z.number()\n .positive()\n .describe(\"Amount of XRP to send\"),\n memo: z.string()\n .optional()\n .describe(\"Optional memo for the transaction\"),\n destination_tag: z.number()\n .int()\n .min(0)\n .max(4294967295)\n .optional()\n .describe(\"Optional destination tag\")\n },\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n // Check sender balance first\n const walletInfo = await agent.getWalletInfo();\n const currentBalance = Number(walletInfo.account_data.Balance) / 1000000;\n const requiredAmount = input.amount + 0.000012; // Amount + fee\n\n if (currentBalance < requiredAmount) {\n return {\n status: \"error\",\n message: \"ā Insufficient XRP balance\",\n error_details: {\n current_balance: `${currentBalance.toFixed(6)} XRP`,\n required_amount: `${requiredAmount.toFixed(6)} XRP`,\n shortfall: `${(requiredAmount - currentBalance).toFixed(6)} XRP`,\n note: \"Amount includes 0.000012 XRP transaction fee\"\n }\n };\n }\n\n // Prepare payment transaction\n const payment: any = {\n TransactionType: 'Payment',\n Account: agent.wallet.address,\n Destination: input.destination,\n Amount: (input.amount * 1000000).toString(), // Convert to drops\n Fee: '12' // Standard fee in drops\n };\n\n // Add optional fields\n if (input.destination_tag !== undefined) {\n payment.DestinationTag = input.destination_tag;\n }\n\n if (input.memo) {\n payment.Memos = [{\n Memo: {\n MemoData: Buffer.from(input.memo, 'utf8').toString('hex')\n }\n }];\n }\n\n // Submit transaction\n const result = await agent.client.submitAndWait(payment, { wallet: agent.wallet });\n\n // Get new balance\n const newWalletInfo = await agent.getWalletInfo();\n const newBalance = Number(newWalletInfo.account_data.Balance) / 1000000;\n\n return {\n status: \"success\",\n message: `ā
Successfully sent ${input.amount} XRP to ${input.destination.substring(0, 8)}...`,\n transaction_details: {\n transaction_hash: result.result.hash,\n ledger_index: result.result.ledger_index,\n fee_paid: \"0.000012 XRP\",\n from_address: agent.wallet.address,\n to_address: input.destination,\n amount_sent: `${input.amount} XRP`,\n memo: input.memo || \"None\",\n destination_tag: input.destination_tag || \"None\"\n },\n balance_changes: {\n previous_balance: `${currentBalance.toFixed(6)} XRP`,\n new_balance: `${newBalance.toFixed(6)} XRP`,\n total_deducted: `${(currentBalance - newBalance).toFixed(6)} XRP`\n }\n };\n } catch (error: any) {\n if (error.message.includes('tecUNFUNDED_PAYMENT')) {\n return {\n status: \"error\",\n message: \"ā Payment failed - insufficient funds\",\n error_details: {\n issue: \"Account does not have enough XRP for payment + fees\",\n suggestion: \"Check balance and reduce payment amount\"\n }\n };\n }\n\n if (error.message.includes('tecNO_DST')) {\n return {\n status: \"error\",\n message: \"ā Payment failed - destination account not found\",\n error_details: {\n issue: \"Destination account does not exist\",\n suggestion: \"Verify recipient address or ensure account is activated\"\n }\n };\n }\n\n throw new Error(`Failed to send XRP: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const CreateTrustlineTool: McpTool = {\n name: \"rwa_create_trustline\",\n description: \"Create a trustline to hold RWA tokens from an issuer\",\n schema: {\n currency: z.string()\n .length(3)\n .regex(/^[A-Z0-9]{3}$/)\n .describe(\"3-letter currency code (e.g., 'BLD', 'TBL', 'GLD')\"),\n issuer: z.string()\n .regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/)\n .describe(\"Issuer's XRPL address\"),\n limit: z.number()\n .positive()\n .optional()\n .describe(\"Maximum amount willing to hold (optional, defaults to 1000000000)\")\n },\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n // Check if trustline already exists\n const existingLines = await agent.client.request({\n command: 'account_lines',\n account: agent.wallet.address,\n ledger_index: 'validated'\n });\n\n const existingTrustline = existingLines.result.lines.find((line: any) => \n line.currency === input.currency && line.account === input.issuer\n );\n\n if (existingTrustline) {\n return {\n status: \"info\",\n message: `ā¹ļø Trustline already exists for ${input.currency}.${input.issuer.substring(0, 8)}...`,\n existing_trustline: {\n currency: existingTrustline.currency,\n issuer: existingTrustline.account,\n current_balance: Math.abs(Number(existingTrustline.balance)),\n limit: existingTrustline.limit,\n asset_id: `${existingTrustline.currency}.${existingTrustline.account}`,\n status: \"Active\"\n },\n recommendation: \"Trustline is ready to receive tokens\"\n };\n }\n\n // Create new trustline\n const trustSet: any = {\n TransactionType: 'TrustSet',\n Account: agent.wallet.address,\n LimitAmount: {\n currency: input.currency,\n issuer: input.issuer,\n value: (input.limit || 1000000000).toString()\n },\n Fee: '12'\n };\n\n const result = await agent.client.submitAndWait(trustSet, { wallet: agent.wallet });\n\n return {\n status: \"success\",\n message: `ā
Trustline created for ${input.currency} tokens`,\n trustline_details: {\n currency: input.currency,\n issuer: input.issuer,\n limit: input.limit || 1000000000,\n asset_id: `${input.currency}.${input.issuer}`,\n current_balance: 0,\n ready_to_receive: true\n },\n transaction_info: {\n transaction_hash: result.result.hash,\n ledger_index: result.result.ledger_index,\n fee_paid: \"0.000012 XRP\",\n network: agent.network\n },\n next_steps: [\n `Ready to receive ${input.currency} tokens from issuer`,\n \"Tokens can now be sent to your address\",\n \"Monitor balance with rwa_get_account_balances\",\n \"Participate in yield distributions if applicable\"\n ],\n investment_info: {\n note: `This trustline allows you to hold up to ${(input.limit || 1000000000).toLocaleString()} ${input.currency} tokens`,\n issuer_info: `Tokens issued by: ${input.issuer}`,\n risk_notice: \"Only create trustlines for assets you trust and understand\"\n }\n };\n } catch (error: any) {\n if (error.message.includes('tecNO_AUTH')) {\n return {\n status: \"error\",\n message: \"ā Trustline creation failed - authorization required\",\n error_details: {\n issue: \"Issuer requires authorization for trustlines\",\n currency: input.currency,\n issuer: input.issuer,\n solution: \"Contact the asset issuer for authorization\"\n }\n };\n }\n\n if (error.message.includes('tecNO_LINE_INSUF_RESERVE')) {\n return {\n status: \"error\",\n message: \"ā Insufficient XRP reserve for trustline\",\n error_details: {\n issue: \"Each trustline requires 2 XRP reserve\",\n solution: \"Add more XRP to your wallet (minimum 2 XRP per trustline)\",\n current_currency: input.currency\n }\n };\n }\n\n throw new Error(`Failed to create trustline: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const GetTransactionHistoryTool: McpTool = {\n name: \"rwa_get_transaction_history\",\n description: \"Get recent transaction history for an account\",\n schema: {\n account_address: z.string()\n .regex(/^r[1-9A-HJ-NP-Za-km-z]{25,34}$/)\n .optional()\n .describe(\"XRPL address to check (optional, defaults to wallet address)\"),\n limit: z.number()\n .int()\n .min(1)\n .max(100)\n .default(20)\n .describe(\"Number of transactions to retrieve (max 100)\")\n },\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const targetAddress = input.account_address || agent.wallet.address;\n const isOwnWallet = targetAddress === agent.wallet.address;\n\n // Get transaction history\n const transactions = await agent.client.request({\n command: 'account_tx',\n account: targetAddress,\n limit: input.limit || 20,\n ledger_index_min: -1,\n ledger_index_max: -1\n });\n\n const processedTxs = transactions.result.transactions.map((tx: any) => {\n const transaction = tx.tx;\n const meta = tx.meta;\n const successful = meta.TransactionResult === 'tesSUCCESS';\n \n // Determine transaction type and details\n let txType = transaction.TransactionType;\n let description = '';\n let amount = '';\n let counterparty = '';\n\n switch (transaction.TransactionType) {\n case 'Payment':\n const isOutgoing = transaction.Account === targetAddress;\n counterparty = isOutgoing ? transaction.Destination : transaction.Account;\n \n if (typeof transaction.Amount === 'string') {\n // XRP payment\n const xrpAmount = Number(transaction.Amount) / 1000000;\n amount = `${xrpAmount} XRP`;\n description = isOutgoing ? `Sent ${amount} to ${counterparty.substring(0, 8)}...` \n : `Received ${amount} from ${counterparty.substring(0, 8)}...`;\n } else {\n // Token payment\n amount = `${transaction.Amount.value} ${transaction.Amount.currency}`;\n description = isOutgoing ? `Sent ${amount} to ${counterparty.substring(0, 8)}...`\n : `Received ${amount} from ${counterparty.substring(0, 8)}...`;\n }\n break;\n \n case 'TrustSet':\n const currency = transaction.LimitAmount.currency;\n const issuer = transaction.LimitAmount.issuer;\n description = `Created trustline for ${currency} (${issuer.substring(0, 8)}...)`;\n break;\n \n case 'OfferCreate':\n description = `Created trading offer`;\n break;\n \n case 'OfferCancel':\n description = `Cancelled trading offer`;\n break;\n \n case 'EscrowCreate':\n description = `Created escrow`;\n break;\n \n case 'EscrowFinish':\n description = `Released escrow`;\n break;\n \n default:\n description = `${txType} transaction`;\n }\n\n return {\n hash: transaction.hash,\n type: txType,\n description: description,\n amount: amount,\n counterparty: counterparty || 'N/A',\n successful: successful,\n fee: `${Number(transaction.Fee) / 1000000} XRP`,\n ledger_index: tx.ledger_index,\n date: new Date((transaction.date + 946684800) * 1000).toISOString(), // Ripple epoch to Unix\n status: successful ? 'ā
Success' : 'ā Failed',\n result_code: meta.TransactionResult\n };\n });\n\n // Calculate summary stats\n const successfulTxs = processedTxs.filter(tx => tx.successful);\n const failedTxs = processedTxs.filter(tx => !tx.successful);\n const payments = processedTxs.filter(tx => tx.type === 'Payment');\n const trustlines = processedTxs.filter(tx => tx.type === 'TrustSet');\n\n return {\n status: \"success\",\n message: `ā
Retrieved ${processedTxs.length} transactions for ${targetAddress.substring(0, 8)}...`,\n account_info: {\n address: targetAddress,\n is_own_wallet: isOwnWallet,\n network: agent.network\n },\n transaction_summary: {\n total_transactions: processedTxs.length,\n successful: successfulTxs.length,\n failed: failedTxs.length,\n payments: payments.length,\n trustlines_created: trustlines.length,\n success_rate: `${((successfulTxs.length / processedTxs.length) * 100).toFixed(1)}%`\n },\n transactions: processedTxs,\n insights: [\n `Account has ${processedTxs.length} recent transactions`,\n `Success rate: ${((successfulTxs.length / processedTxs.length) * 100).toFixed(1)}%`,\n `Payment transactions: ${payments.length}`,\n `Trustlines created: ${trustlines.length}`,\n ...(isOwnWallet ? [\n failedTxs.length > 0 ? `ā ļø ${failedTxs.length} failed transactions detected` : \"ā
All recent transactions successful\"\n ] : [])\n ]\n };\n } catch (error: any) {\n if (error.message.includes('actNotFound')) {\n return {\n status: \"error\",\n message: \"ā Account not found\",\n error_details: {\n address: input.account_address,\n issue: \"Account does not exist on XRPL or has no transaction history\",\n solution: \"Verify address or check if account has been activated\"\n }\n };\n }\n throw new Error(`Failed to get transaction history: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const ValidateAddressTool: McpTool = {\n name: \"rwa_validate_address\",\n description: \"Validate XRPL address format and check if account exists\",\n schema: {\n address: z.string()\n .describe(\"XRPL address to validate\")\n },\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n const address = input.address.trim();\n \n // Format validation\n const xrplAddressRegex = /^r[1-9A-HJ-NP-Za-km-z]{25,34}$/;\n const isValidFormat = xrplAddressRegex.test(address);\n \n if (!isValidFormat) {\n return {\n status: \"invalid\",\n message: \"ā Invalid XRPL address format\",\n validation_results: {\n address: address,\n format_valid: false,\n exists_on_ledger: false,\n issues: [\n \"XRPL addresses must start with 'r'\",\n \"Must be 25-34 characters long\",\n \"Can only contain base58 characters (no 0, O, I, l)\"\n ],\n examples: [\n \"rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH\",\n \"rPT1Sjq2YGrBMTttX4GZHjKu9dyfQeEBUs\"\n ]\n }\n };\n }\n\n await agent.connect();\n\n // Check if account exists on ledger\n let accountExists = false;\n let accountInfo: any = null;\n let errorReason = '';\n\n try {\n const response = await agent.client.request({\n command: 'account_info',\n account: address,\n ledger_index: 'validated'\n });\n accountExists = true;\n accountInfo = response.result.account_data;\n } catch (error: any) {\n if (error.message.includes('actNotFound')) {\n accountExists = false;\n errorReason = 'Account not activated on XRPL';\n } else {\n throw error;\n }\n }\n\n // Additional checks if account exists\n let accountDetails: any = {};\n if (accountExists && accountInfo) {\n const balance = Number(accountInfo.Balance) / 1000000;\n accountDetails = {\n balance: `${balance.toFixed(6)} XRP`,\n sequence: accountInfo.Sequence,\n flags: accountInfo.Flags || 0,\n activated: true,\n reserve_met: balance >= 10,\n can_receive_payments: true\n };\n }\n\n return {\n status: accountExists ? \"valid\" : \"format_valid\",\n message: accountExists \n ? `ā
Valid XRPL address with active account`\n : `ā ļø Valid format but account not found on ledger`,\n validation_results: {\n address: address,\n format_valid: true,\n exists_on_ledger: accountExists,\n network: agent.network,\n ...(accountExists ? {\n account_details: accountDetails\n } : {\n activation_required: true,\n reason: errorReason,\n note: \"Account needs to receive minimum 10 XRP to activate\"\n })\n },\n recommendations: accountExists ? [\n \"ā
Address is valid and active\",\n \"Safe to send payments to this address\",\n \"Account can receive both XRP and tokens\"\n ] : [\n \"ā ļø Account not yet activated on XRPL\",\n \"Send minimum 10 XRP to activate this address\",\n \"Address format is correct\"\n ]\n };\n } catch (error: any) {\n throw new Error(`Failed to validate address: ${error.message}`);\n } finally {\n await agent.disconnect();\n }\n }\n};\n","import { z } from \"zod\";\nimport { RWAAgent } from \"../../agent\";\nimport { type McpTool } from \"../../types\";\n\nexport const TokenizeAssetTool: McpTool = {\n name: \"rwa_tokenize_asset\",\n description: \"Tokenize a real-world asset on XRPL with basic compliance controls\",\n schema: {\n asset_type: z.enum(['real_estate', 'treasury', 'commodity', 'bond'])\n .describe(\"Type of asset to tokenize\"),\n asset_name: z.string()\n .min(1)\n .max(50)\n .describe(\"Name of the asset (e.g., 'Manhattan Office Building')\"),\n total_value: z.number()\n .positive()\n .describe(\"Total value of the asset in USD\"),\n token_symbol: z.string()\n .length(3)\n .regex(/^[A-Z0-9]{3}$/)\n .describe(\"3-letter token symbol (e.g., 'BLD', 'TBL', 'GLD')\"),\n total_supply: z.number()\n .positive()\n .int()\n .describe(\"Total number of tokens to issue\"),\n yield_rate: z.number()\n .min(0)\n .max(50)\n .optional()\n .describe(\"Annual yield percentage (e.g., 6.5 for 6.5%)\"),\n accredited_only: z.boolean()\n .default(false)\n .describe(\"Restrict to accredited investors only\")\n },\n handler: async (agent: RWAAgent, input: Record<string, any>) => {\n try {\n await agent.connect();\n\n const result = await agent.tokenizeAsset({\n type: input.asset_type,\n name: input.asset_name,\n totalValue: input.total_value,\n tokenSymbol: input.token_symbol,\n totalSupply: input.total_supply,\n yieldRate: input.yield_rate,\n accreditedOnly: input.accredited_only\n });\n\n const pricePerToken = input.total_value / input.total_supply;\n\n return {\n status: result.status,\n message: `ā
Successfully tokenized ${input.asset_name} as ${input.token_symbol}