Add contract tools, tx builder, dry-run, history, and persistent audit log

- Add 5 new MCP tools: vector_build_transaction, vector_dry_run,
  vector_get_transaction_history, vector_deploy_contract,
  vector_interact_contract
- Persist audit log to JSON file with daily spend recovery on restart
- Add evaluateTransaction and getTransactionHistory to OgmiosProvider
- Support PlutusV1/V2/V3 contract deployment and interaction
- Update docs, types, and bump build heap to 12GB for lucid-cardano

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
david
2026-03-17 13:21:41 +01:00
parent 5b52a3b273
commit 8a7f676fe3
9 changed files with 916 additions and 3 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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": [

3
src/env.d.ts vendored
View File

@@ -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;
}
}

View File

@@ -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';

View File

@@ -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<any> {
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<any[]> {
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<string> {
// tx is a hex-encoded CBOR transaction
// submit-api expects raw CBOR bytes with Content-Type: application/cbor

View File

@@ -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 {

View File

@@ -58,3 +58,75 @@ export interface AuditEntry {
recipient: string;
action: string;
}
// --- Build Transaction ---
export interface TxOutput {
address: string;
lovelace: number;
assets?: Record<string, string>; // 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;
};
}

View File

@@ -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<VectorBuildTransactionResult> {
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<string, bigint> = {
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<VectorDeployContractResult> {
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<string, string> | null = null,
): Promise<VectorInteractContractResult> {
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<string, bigint> = { 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<string, bigint> = { 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`,
}],
};
}
}
);
}