rwa-build
Version:
MCP library for AI-assisted Real World Asset tokenization on XRPL
1,261 lines (1,248 loc) • 126 kB
JavaScript
#!/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",