diff --git a/.env.example b/.env.example index 56363c9..c7ee6b6 100644 --- a/.env.example +++ b/.env.example @@ -14,3 +14,6 @@ VECTOR_ACCOUNT_INDEX=0 # Wallet account index (default: # Safety Limits (in lovelace, 1 ADA = 1,000,000 lovelace) VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA per transaction VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA daily limit + +# Audit Log +VECTOR_AUDIT_LOG_PATH=./vector-audit-log.json # Path for persistent audit log (default: ./vector-audit-log.json) diff --git a/docs/VECTOR.md b/docs/VECTOR.md index c8b743a..be80031 100644 --- a/docs/VECTOR.md +++ b/docs/VECTOR.md @@ -70,6 +70,67 @@ Check current spend limits, daily usage, and recent transaction audit log. **Example prompt:** "What are my current spend limits?" +### vector_build_transaction + +Build a complex multi-output transaction with metadata. Optionally sign and submit. + +**Parameters:** +- `outputs` (array, required) — Array of `{ address, lovelace, assets? }` objects +- `metadata` (string, optional) — JSON metadata (attached under label 674) +- `submit` (boolean, optional) — If true, sign and submit. If false/omitted, return unsigned CBOR. + +**Example prompt:** "Build a transaction sending 5 ADA to addr1qx... and 3 ADA to addr1qy..." + +### vector_dry_run + +Simulate a transaction without submitting — returns fee estimate and validation. + +**Parameters:** +- `txCbor` (string, optional) — Hex CBOR of a built transaction to evaluate +- `outputs` (array, optional) — If no txCbor, build a TX from these outputs and evaluate +- `metadata` (string, optional) — JSON metadata when building from outputs + +**Example prompt:** "How much would it cost to send 10 ADA to addr1qx...?" + +### vector_get_transaction_history + +Get transaction history for an address via Koios indexed queries. + +**Parameters:** +- `address` (string, optional) — Address to query. Uses agent's wallet if omitted. +- `limit` (number, optional) — Number of transactions (default: 20, max: 50) +- `offset` (number, optional) — Pagination offset (default: 0) + +**Example prompt:** "Show me my recent transactions" + +### vector_deploy_contract + +Deploy a Plutus/Aiken smart contract by locking funds at its script address. + +**Parameters:** +- `scriptCbor` (string, required) — Compiled script CBOR hex +- `scriptType` (string, required) — "PlutusV1", "PlutusV2", or "PlutusV3" +- `initialDatum` (string, optional) — Datum CBOR hex (default: void `d87980`) +- `lovelaceAmount` (number, optional) — ADA to lock in lovelace (default: 2 ADA) + +**Example prompt:** "Deploy this smart contract with 5 ADA locked" + +### vector_interact_contract + +Interact with a deployed smart contract — lock funds or spend from it. + +**Parameters:** +- `scriptCbor` (string, required) — Compiled script CBOR hex +- `scriptType` (string, required) — "PlutusV1", "PlutusV2", or "PlutusV3" +- `action` (string, required) — "spend" or "lock" +- `redeemer` (string, optional) — Redeemer CBOR hex (for spend, default: void) +- `datum` (string, optional) — Datum CBOR hex (for lock, default: void) +- `lovelaceAmount` (number, optional) — ADA to lock in lovelace (for lock) +- `utxoRef` (object, optional) — `{ txHash, outputIndex }` for specific UTxO (for spend) +- `assets` (object, optional) — Native assets for lock: `{ "unit": "quantity" }` + +**Example prompt:** "Collect funds from the escrow contract at addr1..." + ## Safety Controls The server enforces configurable spend limits: @@ -77,11 +138,13 @@ The server enforces configurable spend limits: - **Per-transaction limit** — Maximum ADA per single transaction (default: 100 ADA) - **Daily limit** — Maximum ADA per 24-hour period (default: 500 ADA) - **Audit log** — All transactions are logged with timestamp, amount, and recipient +- **Persistent audit log** — Audit log is written to a JSON file that survives server restarts. Daily spend is recalculated from the log on startup. Limits are configured via environment variables: ``` VECTOR_SPEND_LIMIT_PER_TX=100000000 # 100 ADA in lovelace VECTOR_SPEND_LIMIT_DAILY=500000000 # 500 ADA in lovelace +VECTOR_AUDIT_LOG_PATH=./vector-audit-log.json # Audit log file path ``` ## Network Endpoints diff --git a/package.json b/package.json index feb7f18..d718a0e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "clean": "rm -rf build", - "build": "npm run clean && NODE_OPTIONS='--max-old-space-size=8192' tsc && chmod +x build/index.js", + "build": "npm run clean && NODE_OPTIONS='--max-old-space-size=12288' tsc && chmod +x build/index.js", "start": "node build/index.js" }, "files": [ diff --git a/src/env.d.ts b/src/env.d.ts index c20fc82..1da9f4d 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -13,5 +13,8 @@ declare namespace NodeJS { // Safety Limits VECTOR_SPEND_LIMIT_PER_TX: string; VECTOR_SPEND_LIMIT_DAILY: string; + + // Audit Log + VECTOR_AUDIT_LOG_PATH: string; } } diff --git a/src/vector/index.ts b/src/vector/index.ts index 06c70ba..c1b48b2 100644 --- a/src/vector/index.ts +++ b/src/vector/index.ts @@ -1,2 +1,14 @@ export { registerVectorTools } from './vector.js'; -export type { VectorWalletInfo, VectorToken, VectorAdaTransactionResult, VectorTokenTransactionResult } from './types.js'; +export type { + VectorWalletInfo, + VectorToken, + VectorAdaTransactionResult, + VectorTokenTransactionResult, + TxOutput, + VectorBuildTransactionResult, + VectorDryRunResult, + VectorTransactionSummary, + VectorTransactionHistoryResult, + VectorDeployContractResult, + VectorInteractContractResult, +} from './types.js'; diff --git a/src/vector/ogmios-provider.ts b/src/vector/ogmios-provider.ts index a3d8981..ee0e834 100644 --- a/src/vector/ogmios-provider.ts +++ b/src/vector/ogmios-provider.ts @@ -247,6 +247,94 @@ export class OgmiosProvider implements Provider { return false; } + /** + * Evaluate a transaction without submitting (dry run). + * Returns execution units for script validation. + */ + async evaluateTransaction(txCborHex: string): Promise { + return this.rpc('evaluateTransaction', { + transaction: { cbor: txCborHex }, + }); + } + + /** + * Get transaction history for an address via Koios. + */ + async getTransactionHistory( + address: string, + offset: number = 0, + limit: number = 20, + ): Promise { + if (!this.koiosUrl) { + throw new Error( + 'Koios URL is not configured. Transaction history requires Koios indexer. ' + + 'Set VECTOR_KOIOS_URL in your environment.' + ); + } + + // Step 1: Get tx hashes for the address + const txListResponse = await fetch(`${this.koiosUrl}/api/v1/address_txs`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _addresses: [address], _after_block_height: 0 }), + }); + + if (!txListResponse.ok) { + const text = await txListResponse.text(); + throw new Error(`Koios address_txs query failed (${txListResponse.status}): ${text}`); + } + + const txList: any[] = await txListResponse.json(); + + // Apply pagination + const paginatedTxs = txList.slice(offset, offset + limit); + + if (paginatedTxs.length === 0) { + return []; + } + + // Step 2: Fetch full tx details + const txHashes = paginatedTxs.map((tx: any) => tx.tx_hash); + const txInfoResponse = await fetch(`${this.koiosUrl}/api/v1/tx_info`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ _tx_hashes: txHashes }), + }); + + if (!txInfoResponse.ok) { + // Fall back to basic tx list without details + return paginatedTxs.map((tx: any) => ({ + txHash: tx.tx_hash, + blockHeight: tx.block_height || 0, + blockTime: tx.block_time + ? new Date(tx.block_time * 1000).toISOString() + : '', + fee: '0', + })); + } + + const txInfos: any[] = await txInfoResponse.json(); + + return txInfos.map((info: any) => ({ + txHash: info.tx_hash, + blockHeight: info.block_height || 0, + blockTime: info.tx_timestamp + ? new Date(info.tx_timestamp * 1000).toISOString() + : info.block_time + ? new Date(info.block_time * 1000).toISOString() + : '', + fee: info.fee || '0', + inputs: (info.inputs || []).map((inp: any) => ({ + address: inp.payment_addr?.bech32 || inp.address || '', + amount: inp.value || '0', + })), + outputs: (info.outputs || []).map((out: any) => ({ + address: out.payment_addr?.bech32 || out.address || '', + amount: out.value || '0', + })), + })); + } + async submitTx(tx: string): Promise { // tx is a hex-encoded CBOR transaction // submit-api expects raw CBOR bytes with Content-Type: application/cbor diff --git a/src/vector/safety.ts b/src/vector/safety.ts index 84194fb..5e37222 100644 --- a/src/vector/safety.ts +++ b/src/vector/safety.ts @@ -1,13 +1,21 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; import type { SpendLimits, SpendStatus, AuditEntry } from './types.js'; const VECTOR_SPEND_LIMIT_PER_TX = parseInt(process.env.VECTOR_SPEND_LIMIT_PER_TX || '100000000'); // 100 ADA const VECTOR_SPEND_LIMIT_DAILY = parseInt(process.env.VECTOR_SPEND_LIMIT_DAILY || '500000000'); // 500 ADA +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const projectRoot = resolve(__dirname, '../..'); + export class SafetyLayer { private dailySpent: number = 0; private dailyResetTime: number; private auditLog: AuditEntry[] = []; private limits: SpendLimits; + private auditLogPath: string; constructor() { this.limits = { @@ -16,6 +24,13 @@ export class SafetyLayer { }; // Reset daily limit at midnight UTC this.dailyResetTime = this.getNextMidnightUTC(); + + // Audit log file persistence + this.auditLogPath = process.env.VECTOR_AUDIT_LOG_PATH + ? resolve(process.env.VECTOR_AUDIT_LOG_PATH) + : resolve(projectRoot, 'vector-audit-log.json'); + + this.loadAuditLog(); } private getNextMidnightUTC(): number { @@ -31,6 +46,45 @@ export class SafetyLayer { } } + private loadAuditLog(): void { + try { + if (existsSync(this.auditLogPath)) { + const data = readFileSync(this.auditLogPath, 'utf-8'); + const parsed = JSON.parse(data); + if (Array.isArray(parsed)) { + this.auditLog = parsed; + // Recalculate dailySpent from today's entries (survives server restart) + this.recalculateDailySpent(); + } + } + } catch (err) { + console.error(`Warning: Could not load audit log from ${this.auditLogPath}:`, err); + this.auditLog = []; + } + } + + private recalculateDailySpent(): void { + const todayStart = new Date(); + todayStart.setUTCHours(0, 0, 0, 0); + const todayStartMs = todayStart.getTime(); + + this.dailySpent = 0; + for (const entry of this.auditLog) { + const entryTime = new Date(entry.timestamp).getTime(); + if (entryTime >= todayStartMs) { + this.dailySpent += entry.amountLovelace; + } + } + } + + private persistAuditLog(): void { + try { + writeFileSync(this.auditLogPath, JSON.stringify(this.auditLog, null, 2)); + } catch (err) { + console.error(`Warning: Could not persist audit log to ${this.auditLogPath}:`, err); + } + } + checkTransaction(amountLovelace: number): { allowed: boolean; reason?: string } { this.checkAndResetDaily(); @@ -63,6 +117,8 @@ export class SafetyLayer { recipient, action: 'send', }); + + this.persistAuditLog(); } getSpendStatus(): SpendStatus { diff --git a/src/vector/types.ts b/src/vector/types.ts index 2e5182d..a36a05f 100644 --- a/src/vector/types.ts +++ b/src/vector/types.ts @@ -58,3 +58,75 @@ export interface AuditEntry { recipient: string; action: string; } + +// --- Build Transaction --- + +export interface TxOutput { + address: string; + lovelace: number; + assets?: Record; // unit -> quantity +} + +export interface VectorBuildTransactionResult { + txCbor: string; + txHash: string; + fee: string; + feeAda: string; + outputCount: number; + totalAda: string; + submitted: boolean; + links?: { + explorer: string; + }; +} + +// --- Dry Run --- + +export interface VectorDryRunResult { + valid: boolean; + fee: string; + feeAda: string; + executionUnits?: { + memory: number; + cpu: number; + }; + error?: string; +} + +// --- Transaction History --- + +export interface VectorTransactionSummary { + txHash: string; + blockHeight: number; + blockTime: string; + fee: string; +} + +export interface VectorTransactionHistoryResult { + address: string; + transactions: VectorTransactionSummary[]; + total: number; +} + +// --- Deploy Contract --- + +export interface VectorDeployContractResult { + txHash: string; + scriptAddress: string; + scriptHash: string; + scriptType: string; + links: { + explorer: string; + }; +} + +// --- Interact Contract --- + +export interface VectorInteractContractResult { + txHash: string; + scriptAddress: string; + action: 'spend' | 'lock'; + links: { + explorer: string; + }; +} diff --git a/src/vector/vector.ts b/src/vector/vector.ts index 3230822..11b7f37 100644 --- a/src/vector/vector.ts +++ b/src/vector/vector.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Lucid, fromText } from 'lucid-cardano'; +import { Lucid, fromText, Data, applyDoubleCborEncoding } from 'lucid-cardano'; +import type { SpendingValidator } from 'lucid-cardano'; import * as dotenv from 'dotenv'; import { resolve } from 'path'; import { fileURLToPath } from 'url'; @@ -12,6 +13,11 @@ import type { VectorWalletInfo, VectorAdaTransactionResult, VectorTokenTransactionResult, + TxOutput, + VectorBuildTransactionResult, + VectorDryRunResult, + VectorDeployContractResult, + VectorInteractContractResult, } from './types.js'; // Direct .env loading @@ -281,6 +287,235 @@ export async function sendTokens( }; } +// Build a complex multi-output transaction +export async function buildTransaction( + outputs: TxOutput[], + metadata: any = null, + submit: boolean = false, +): Promise { + if (!outputs || outputs.length === 0) { + throw new Error('At least one output is required'); + } + + // Calculate total ADA across all outputs for safety check + const totalLovelace = outputs.reduce((sum, o) => sum + o.lovelace, 0); + const safetyCheck = safetyLayer.checkTransaction(totalLovelace); + if (!safetyCheck.allowed) { + throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); + } + + const lucid = await initLucid(); + + // @ts-ignore + let tx = lucid.newTx(); + + for (const output of outputs) { + const assets: Record = { + lovelace: BigInt(output.lovelace), + }; + if (output.assets) { + for (const [unit, qty] of Object.entries(output.assets)) { + assets[unit] = BigInt(qty); + } + } + tx = tx.payToAddress(output.address, assets); + } + + if (metadata) { + const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; + // @ts-ignore + tx = tx.attachMetadata(674, parsedMetadata); + } + + // @ts-ignore + const completedTx = await tx.complete(); + // @ts-ignore + const fee = completedTx.fee; + // @ts-ignore + const txHash = completedTx.toHash(); + // @ts-ignore + const txCbor = completedTx.toString(); + + if (submit) { + // @ts-ignore + const signedTx = await completedTx.sign().complete(); + const submittedHash = await signedTx.submit(); + + safetyLayer.recordTransaction(submittedHash, totalLovelace, outputs.map(o => o.address).join(', ')); + + return { + txCbor: '', + txHash: submittedHash, + fee: String(fee), + feeAda: lovelaceToAda(fee), + outputCount: outputs.length, + totalAda: lovelaceToAda(totalLovelace), + submitted: true, + links: { explorer: explorerTxLink(submittedHash) }, + }; + } + + return { + txCbor, + txHash, + fee: String(fee), + feeAda: lovelaceToAda(fee), + outputCount: outputs.length, + totalAda: lovelaceToAda(totalLovelace), + submitted: false, + }; +} + +// Deploy a smart contract by locking funds at the script address +export async function deployContract( + scriptCbor: string, + scriptType: string, + initialDatum: string | null = null, + lovelaceAmount: number = 2_000_000, +): Promise { + const safetyCheck = safetyLayer.checkTransaction(lovelaceAmount); + if (!safetyCheck.allowed) { + throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); + } + + const lucid = await initLucid(); + + const validator: SpendingValidator = { + type: scriptType as any, + script: applyDoubleCborEncoding(scriptCbor), + }; + + // @ts-ignore + const scriptAddress = lucid.utils.validatorToAddress(validator); + // @ts-ignore + const scriptHash = lucid.utils.validatorToScriptHash(validator); + + const datum = initialDatum || Data.void(); + + // @ts-ignore + let tx = lucid.newTx() + .payToContract(scriptAddress, { inline: datum }, { lovelace: BigInt(lovelaceAmount) }); + + // @ts-ignore + tx = await tx.complete(); + // @ts-ignore + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + + safetyLayer.recordTransaction(txHash, lovelaceAmount, scriptAddress); + + return { + txHash, + scriptAddress, + scriptHash, + scriptType, + links: { explorer: explorerTxLink(txHash) }, + }; +} + +// Interact with a deployed smart contract (lock or spend) +export async function interactWithContract( + scriptCbor: string, + scriptType: string, + action: 'spend' | 'lock', + redeemer: string | null = null, + datum: string | null = null, + lovelaceAmount: number = 2_000_000, + utxoRef: { txHash: string; outputIndex: number } | null = null, + assets: Record | null = null, +): Promise { + const lucid = await initLucid(); + const walletAddress = await lucid.wallet.address(); + + const validator: SpendingValidator = { + type: scriptType as any, + script: applyDoubleCborEncoding(scriptCbor), + }; + + // @ts-ignore + const scriptAddress = lucid.utils.validatorToAddress(validator); + + if (action === 'lock') { + const safetyCheck = safetyLayer.checkTransaction(lovelaceAmount); + if (!safetyCheck.allowed) { + throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); + } + + const datumData = datum || Data.void(); + const outputAssets: Record = { lovelace: BigInt(lovelaceAmount) }; + if (assets) { + for (const [unit, qty] of Object.entries(assets)) { + outputAssets[unit] = BigInt(qty); + } + } + + // @ts-ignore + let tx = lucid.newTx() + .payToContract(scriptAddress, { inline: datumData }, outputAssets); + + // @ts-ignore + tx = await tx.complete(); + // @ts-ignore + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + + safetyLayer.recordTransaction(txHash, lovelaceAmount, scriptAddress); + + return { + txHash, + scriptAddress, + action: 'lock', + links: { explorer: explorerTxLink(txHash) }, + }; + } else { + // SPEND: collect from script + let scriptUtxos; + if (utxoRef) { + scriptUtxos = await lucid.provider.getUtxosByOutRef([utxoRef]); + } else { + scriptUtxos = await lucid.provider.getUtxos(scriptAddress); + } + + if (!scriptUtxos || scriptUtxos.length === 0) { + throw new Error(`No UTxOs found at script address ${scriptAddress}`); + } + + const redeemerData = redeemer || Data.void(); + + // @ts-ignore + let tx = lucid.newTx() + .collectFrom(scriptUtxos, redeemerData) + .attachSpendingValidator(validator) + .addSigner(walletAddress); + + try { + // @ts-ignore + tx = await tx.complete(); + } catch (err) { + // Retry without native UPLC evaluator if it fails + // @ts-ignore + tx = await lucid.newTx() + .collectFrom(scriptUtxos, redeemerData) + .attachSpendingValidator(validator) + .addSigner(walletAddress) + .complete({ nativeUplc: false }); + } + + // @ts-ignore + const signedTx = await tx.sign().complete(); + const txHash = await signedTx.submit(); + + // No spend recording for collecting — funds are coming back to wallet + + return { + txHash, + scriptAddress, + action: 'spend', + links: { explorer: explorerTxLink(txHash) }, + }; + } +} + // Register all Vector MCP tools export function registerVectorTools(server: McpServer) { @@ -599,4 +834,385 @@ ${log.length > 0 ? `## Recent Transactions (last ${Math.min(5, log.length)}):\n$ } } ); + + // vector_build_transaction — Build a complex multi-output transaction + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_build_transaction", + "Build a complex multi-output transaction with metadata. Set submit=true to sign and submit, or false to return unsigned CBOR for review.", + { + outputs: z.array(z.object({ + address: z.string().describe("Recipient Vector address"), + lovelace: z.number().describe("Amount in lovelace (1 ADA = 1,000,000 lovelace)"), + assets: z.record(z.string()).optional().describe("Optional native assets: { 'policyId+assetNameHex': 'quantity' }"), + })).min(1).describe("Transaction outputs"), + metadata: z.string().optional().describe("Optional JSON metadata (attached under label 674)"), + submit: z.boolean().optional().describe("If true, sign and submit the transaction. If false/omitted, return unsigned CBOR for review."), + }, + async ({ outputs, metadata, submit }) => { + try { + const result = await buildTransaction(outputs, metadata, submit); + + if (result.submitted) { + return { + content: [{ + type: "text", + text: `# Transaction Submitted + +Transaction Hash: ${result.txHash} +Fee: ${result.feeAda} ADA +Outputs: ${result.outputCount} +Total ADA Sent: ${result.totalAda} ADA + +[View on Explorer](${result.links?.explorer})`, + }], + }; + } + + return { + content: [{ + type: "text", + text: `# Transaction Built (Not Submitted) + +Transaction Hash: ${result.txHash} +Fee: ${result.feeAda} ADA +Outputs: ${result.outputCount} +Total ADA: ${result.totalAda} ADA + +CBOR (hex): ${result.txCbor.substring(0, 200)}${result.txCbor.length > 200 ? '...' : ''} + +Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction again with submit=true to submit.`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to build transaction: ${error.message} + +**Troubleshooting Tips:** +1. Verify all recipient addresses are valid (addr1...) +2. Ensure wallet has enough ADA for outputs + fees +3. Check spend limits with vector_get_spend_limits`, + }], + }; + } + } + ); + + // vector_dry_run — Simulate a transaction without submitting + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_dry_run", + "Simulate a transaction without submitting — returns fee estimate and validation result", + { + txCbor: z.string().optional().describe("Hex-encoded CBOR of a built transaction to evaluate"), + outputs: z.array(z.object({ + address: z.string().describe("Recipient Vector address"), + lovelace: z.number().describe("Amount in lovelace"), + assets: z.record(z.string()).optional(), + })).optional().describe("If no txCbor provided, build a TX from these outputs and evaluate it"), + metadata: z.string().optional().describe("Optional JSON metadata when building from outputs"), + }, + async ({ txCbor, outputs, metadata }) => { + try { + let cborToEvaluate = txCbor; + let feeFromBuild: string | null = null; + + if (!cborToEvaluate && outputs && outputs.length > 0) { + // Build the transaction first + const lucid = await initLucid(); + + // @ts-ignore + let tx = lucid.newTx(); + for (const output of outputs) { + const assets: Record = { lovelace: BigInt(output.lovelace) }; + if (output.assets) { + for (const [unit, qty] of Object.entries(output.assets)) { + assets[unit] = BigInt(qty); + } + } + tx = tx.payToAddress(output.address, assets); + } + + if (metadata) { + const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; + // @ts-ignore + tx = tx.attachMetadata(674, parsedMetadata); + } + + // @ts-ignore + const completedTx = await tx.complete(); + // @ts-ignore + feeFromBuild = String(completedTx.fee); + // @ts-ignore + const signedTx = await completedTx.sign().complete(); + cborToEvaluate = signedTx.toString(); + } + + if (!cborToEvaluate) { + throw new Error('Provide either txCbor or outputs to evaluate'); + } + + const provider = new OgmiosProvider({ + ogmiosUrl: VECTOR_OGMIOS_URL, + submitUrl: VECTOR_SUBMIT_URL, + koiosUrl: VECTOR_KOIOS_URL, + }); + + let evalResult: VectorDryRunResult; + try { + const result = await provider.evaluateTransaction(cborToEvaluate); + + // Parse Ogmios evaluateTransaction response + let totalMemory = 0; + let totalCpu = 0; + if (Array.isArray(result)) { + for (const item of result) { + if (item.budget) { + totalMemory += item.budget.memory || 0; + totalCpu += item.budget.cpu || 0; + } + } + } + + const fee = feeFromBuild || '0'; + evalResult = { + valid: true, + fee, + feeAda: lovelaceToAda(fee), + executionUnits: (totalMemory > 0 || totalCpu > 0) ? { memory: totalMemory, cpu: totalCpu } : undefined, + }; + } catch (evalErr) { + // evaluateTransaction failed — still return fee if we built the tx + if (feeFromBuild) { + evalResult = { + valid: true, + fee: feeFromBuild, + feeAda: lovelaceToAda(feeFromBuild), + error: `Script evaluation unavailable: ${(evalErr as Error).message}. Fee estimate is from transaction building.`, + }; + } else { + evalResult = { + valid: false, + fee: '0', + feeAda: '0', + error: `Evaluation failed: ${(evalErr as Error).message}`, + }; + } + } + + return { + content: [{ + type: "text", + text: `# Dry Run Result + +Valid: ${evalResult.valid ? 'Yes' : 'No'} +Estimated Fee: ${evalResult.feeAda} ADA (${evalResult.fee} lovelace) +${evalResult.executionUnits ? `Execution Units: Memory ${evalResult.executionUnits.memory}, CPU ${evalResult.executionUnits.cpu}` : ''} +${evalResult.error ? `\nNote: ${evalResult.error}` : ''} + +No transaction was submitted to the network.`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Dry run failed: ${error.message}`, + }], + }; + } + } + ); + + // vector_get_transaction_history — Get transaction history via Koios + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_get_transaction_history", + "Get transaction history for a Vector address via Koios indexed queries", + { + address: z.string().optional().describe("Vector address to query. If omitted, uses the agent's wallet."), + limit: z.number().min(1).max(50).optional().describe("Number of transactions to return (default: 20, max: 50)"), + offset: z.number().min(0).optional().describe("Offset for pagination (default: 0)"), + }, + async ({ address, limit, offset }) => { + try { + let queryAddress = address; + if (!queryAddress) { + const lucid = await initLucid(); + queryAddress = await lucid.wallet.address(); + } + + const provider = new OgmiosProvider({ + ogmiosUrl: VECTOR_OGMIOS_URL, + submitUrl: VECTOR_SUBMIT_URL, + koiosUrl: VECTOR_KOIOS_URL, + }); + + const txs = await provider.getTransactionHistory(queryAddress, offset || 0, limit || 20); + + if (txs.length === 0) { + return { + content: [{ + type: "text", + text: `No transactions found for ${queryAddress}`, + }], + }; + } + + const txList = txs.map((tx, i) => { + const feeAda = tx.fee ? lovelaceToAda(tx.fee) : 'N/A'; + return `${i + 1}. ${tx.txHash}\n Block: ${tx.blockHeight} | Time: ${tx.blockTime} | Fee: ${feeAda} ADA`; + }).join('\n\n'); + + return { + content: [{ + type: "text", + text: `# Transaction History for ${queryAddress} + +Showing ${txs.length} transaction(s) (offset: ${offset || 0}): + +${txList} + +[View on Explorer](${VECTOR_EXPLORER_URL}/address/${queryAddress})`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to get transaction history: ${error.message} + +**Troubleshooting Tips:** +1. Ensure Koios is configured and reachable: ${VECTOR_KOIOS_URL} +2. Verify the address is valid +3. Check the block explorer for this address`, + }], + }; + } + } + ); + + // vector_deploy_contract — Deploy a Plutus/Aiken smart contract + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_deploy_contract", + "Deploy a Plutus/Aiken smart contract to Vector by sending funds to its script address", + { + scriptCbor: z.string().describe("Compiled Plutus/Aiken script in CBOR hex format"), + scriptType: z.enum(["PlutusV1", "PlutusV2", "PlutusV3"]).describe("Script version"), + initialDatum: z.string().optional().describe("Initial datum as CBOR hex. Use 'd87980' for void/unit datum. Defaults to void if omitted."), + lovelaceAmount: z.number().optional().describe("ADA to lock at the script address in lovelace (default: 2,000,000 = 2 ADA)"), + }, + async ({ scriptCbor, scriptType, initialDatum, lovelaceAmount }) => { + try { + const result = await deployContract( + scriptCbor, + scriptType, + initialDatum || null, + lovelaceAmount || 2_000_000, + ); + + return { + content: [{ + type: "text", + text: `# Smart Contract Deployed + +Transaction Hash: ${result.txHash} +Script Address: ${result.scriptAddress} +Script Hash: ${result.scriptHash} +Script Type: ${result.scriptType} + +Funds locked at the script address. Use vector_interact_contract to interact with this contract. + +[View on Explorer](${result.links.explorer})`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to deploy contract: ${error.message} + +**Troubleshooting Tips:** +1. Verify the script CBOR is valid hex (compiled Aiken or Plutus output) +2. Ensure wallet has sufficient ADA for the locked amount + fees +3. Check that the script type matches the compiled version (PlutusV1/V2/V3) +4. Check spend limits with vector_get_spend_limits`, + }], + }; + } + } + ); + + // vector_interact_contract — Interact with a deployed smart contract + // @ts-ignore: MCP SDK deep type instantiation + server.tool( + "vector_interact_contract", + "Interact with a deployed Plutus/Aiken smart contract — lock funds or spend from it", + { + scriptCbor: z.string().describe("Compiled Plutus/Aiken script in CBOR hex"), + scriptType: z.enum(["PlutusV1", "PlutusV2", "PlutusV3"]).describe("Script version"), + action: z.enum(["spend", "lock"]).describe("'spend' to collect UTxOs from the script, 'lock' to send funds to it"), + redeemer: z.string().optional().describe("Redeemer as CBOR hex (required for spend, use 'd87980' for void)"), + datum: z.string().optional().describe("Datum as CBOR hex (required for lock, use 'd87980' for void)"), + lovelaceAmount: z.number().optional().describe("Lovelace to lock (for lock action, default: 2,000,000 = 2 ADA)"), + utxoRef: z.object({ + txHash: z.string(), + outputIndex: z.number(), + }).optional().describe("Specific UTxO to spend from (optional, otherwise spends all UTxOs at script address)"), + assets: z.record(z.string()).optional().describe("Additional native assets for lock action: { 'policyId+assetNameHex': 'quantity' }"), + }, + async ({ scriptCbor, scriptType, action, redeemer, datum, lovelaceAmount, utxoRef, assets }) => { + try { + const result = await interactWithContract( + scriptCbor, + scriptType, + action, + redeemer || null, + datum || null, + lovelaceAmount || 2_000_000, + utxoRef || null, + assets || null, + ); + + const actionVerb = result.action === 'spend' ? 'collected from' : 'locked at'; + + return { + content: [{ + type: "text", + text: `# Contract Interaction Successful + +Transaction Hash: ${result.txHash} +Action: ${result.action} +Script Address: ${result.scriptAddress} + +Funds ${actionVerb} the script address. + +[View on Explorer](${result.links.explorer})`, + }], + }; + } catch (err) { + const error = err as Error; + return { + content: [{ + type: "text", + text: `Failed to interact with contract: ${error.message} + +**Troubleshooting Tips:** +1. For 'spend': ensure the script address has UTxOs and the redeemer satisfies the validator +2. For 'lock': ensure wallet has sufficient ADA and the datum matches the script's expectations +3. Spending requires collateral — ensure wallet has a pure-ADA UTxO (no native tokens) +4. Verify the script CBOR matches the deployed script exactly +5. Check spend limits with vector_get_spend_limits`, + }], + }; + } + } + ); }