Migrate to @lucid-evolution/lucid, fix agent registration (blake2b dkLen), contract spend (PlutusV2), and add Ogmios fallback for agent resolution
This commit is contained in:
4850
package-lock.json
generated
4850
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
"vector-mcp": "./build/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "ln -sf ../../../libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs node_modules/libsodium-wrappers-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs 2>/dev/null || true",
|
||||
"clean": "rm -rf build",
|
||||
"build": "tsup && chmod +x build/index.js",
|
||||
"typecheck": "tsc --noEmit --incremental",
|
||||
@@ -47,7 +48,8 @@
|
||||
"@modelcontextprotocol/sdk": "^1.4.1",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"lucid-cardano": "^0.10.11",
|
||||
"@lucid-evolution/lucid": "^0.4.29",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
// @ts-nocheck
|
||||
// Agent network tools: register, discover, message, get_profile, update, deregister, transfer
|
||||
// ts-nocheck isolates C (Cardano WASM) imports from tsc's complex type inference
|
||||
|
||||
import { z } from "zod";
|
||||
import { Lucid, fromText, toText, Data, Constr, C } from 'lucid-cardano';
|
||||
import { Lucid, fromText, toText, Data, Constr, validatorToAddress, validatorToScriptHash, getAddressDetails, credentialToAddress } from '@lucid-evolution/lucid';
|
||||
import { blake2b } from '@noble/hashes/blake2b';
|
||||
import { OgmiosProvider } from './ogmios-provider.js';
|
||||
import { safetyLayer } from './safety.js';
|
||||
import { rateLimiter } from './rate-limiter.js';
|
||||
|
||||
// Lucid v0.10.x lacks PlutusV3 — downcast to PlutusV2 for hashing (same algorithm)
|
||||
function lucidCompat(validator) {
|
||||
return validator.type === 'PlutusV3' ? { ...validator, type: 'PlutusV2' } : validator;
|
||||
}
|
||||
|
||||
// Env config (mirrors vector.ts)
|
||||
const VECTOR_OGMIOS_URL = process.env.VECTOR_OGMIOS_URL || 'https://ogmios.vector.testnet.apexfusion.org';
|
||||
const VECTOR_SUBMIT_URL = process.env.VECTOR_SUBMIT_URL || 'https://submit.vector.testnet.apexfusion.org/api/submit/tx';
|
||||
@@ -27,6 +22,16 @@ const AGENT_MESSAGE_LABEL = 674;
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
// Cardano metadata strings must be ≤ 64 bytes. Chunk long strings into arrays.
|
||||
function metadataStr(s: string): string | string[] {
|
||||
if (s.length <= 64) return s;
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < s.length; i += 64) {
|
||||
chunks.push(s.slice(i, i + 64));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function lovelaceToAda(lovelace) {
|
||||
return (Number(BigInt(String(lovelace))) / 1_000_000).toFixed(6);
|
||||
}
|
||||
@@ -43,7 +48,7 @@ function newProvider() {
|
||||
// Matches Aiken's derive_asset_name (verified via CBOR parity tests in agent-registry repo)
|
||||
function deriveNftAssetName(txHash, outputIndex) {
|
||||
const outRefCbor = Data.to(new Constr(0, [txHash, BigInt(outputIndex)]));
|
||||
const hashBytes = C.hash_blake2b256(Buffer.from(outRefCbor, 'hex'));
|
||||
const hashBytes = blake2b(Buffer.from(outRefCbor, 'hex'), { dkLen: 32 });
|
||||
return Buffer.from(hashBytes).toString('hex');
|
||||
}
|
||||
|
||||
@@ -101,7 +106,24 @@ function parseDid(agentId) {
|
||||
|
||||
async function resolveAgentUtxo(provider, agentId) {
|
||||
const { unit } = parseDid(agentId);
|
||||
const utxo = await provider.getUtxoByUnit(unit);
|
||||
|
||||
let utxo;
|
||||
try {
|
||||
utxo = await provider.getUtxoByUnit(unit);
|
||||
} catch {
|
||||
// Koios unavailable or asset not found — will try Ogmios fallback below
|
||||
}
|
||||
|
||||
// If Koios returned a UTxO without datum, or didn't find one, scan via Ogmios
|
||||
if (!utxo || !utxo.datum) {
|
||||
const registryAddress = await getRegistryAddress();
|
||||
const allUtxos = await provider.getUtxos(registryAddress);
|
||||
const ogmiosUtxo = allUtxos.find(u => u.assets[unit] && u.assets[unit] > 0n);
|
||||
if (ogmiosUtxo) {
|
||||
utxo = ogmiosUtxo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!utxo) throw new Error(`Agent not found: no UTxO holds NFT ${unit}. The agent may not exist or may have deregistered.`);
|
||||
if (!utxo.datum) throw new Error('Registry UTxO found but has no inline datum.');
|
||||
const profile = parseAgentDatum(utxo.datum, `${utxo.txHash}#${utxo.outputIndex}`, utxo.assets);
|
||||
@@ -138,7 +160,7 @@ function validateCapabilities(capabilities) {
|
||||
|
||||
async function initLucid(mnemonic, accountIndex = 0) {
|
||||
const provider = newProvider();
|
||||
const lucid = await Lucid.new(provider, 'Mainnet');
|
||||
const lucid = await Lucid(provider, 'Mainnet');
|
||||
if (!mnemonic) throw new Error('mnemonic is required');
|
||||
const trimmed = mnemonic.trim();
|
||||
const words = trimmed.split(/\s+/);
|
||||
@@ -146,11 +168,7 @@ async function initLucid(mnemonic, accountIndex = 0) {
|
||||
if (!validLengths.includes(words.length)) {
|
||||
throw new Error(`Invalid mnemonic: Expected 12, 15, 18, 21 or 24 words, got ${words.length}`);
|
||||
}
|
||||
lucid.selectWalletFromSeed(trimmed, { accountIndex });
|
||||
|
||||
// Store the full wallet address so credential-based UTxO lookups resolve correctly
|
||||
const address = await lucid.wallet.address();
|
||||
provider.setWalletAddress(address);
|
||||
lucid.selectWallet.fromSeed(trimmed, { accountIndex });
|
||||
|
||||
return lucid;
|
||||
}
|
||||
@@ -159,10 +177,8 @@ async function initLucid(mnemonic, accountIndex = 0) {
|
||||
let _registryAddress = null;
|
||||
async function getRegistryAddress() {
|
||||
if (_registryAddress) return _registryAddress;
|
||||
const provider = newProvider();
|
||||
const lucid = await Lucid.new(provider, 'Mainnet');
|
||||
const registryScript = { type: 'PlutusV3', script: REGISTRY_SCRIPT_CBOR };
|
||||
_registryAddress = lucid.utils.validatorToAddress(lucidCompat(registryScript));
|
||||
const registryScript = { type: 'PlutusV3' as const, script: REGISTRY_SCRIPT_CBOR };
|
||||
_registryAddress = validatorToAddress('Mainnet', registryScript);
|
||||
return _registryAddress;
|
||||
}
|
||||
|
||||
@@ -308,12 +324,12 @@ ${capList}
|
||||
if (!safetyCheck.allowed) throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`);
|
||||
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const walletAddress = await lucid.wallet.address();
|
||||
const addressDetails = lucid.utils.getAddressDetails(walletAddress);
|
||||
const walletAddress = await lucid.wallet().address();
|
||||
const addressDetails = getAddressDetails(walletAddress);
|
||||
const vkeyHash = addressDetails.paymentCredential?.hash;
|
||||
if (!vkeyHash) throw new Error('Cannot derive payment key hash from wallet address');
|
||||
|
||||
const utxos = await lucid.wallet.getUtxos();
|
||||
const utxos = await lucid.utxosAt(await lucid.wallet().address());
|
||||
const seedUtxo = utxos.find(u => {
|
||||
const keys = Object.keys(u.assets);
|
||||
return keys.length === 1 && keys[0] === 'lovelace' && u.assets['lovelace'] >= MIN_AP3X_DEPOSIT + 2_000_000n;
|
||||
@@ -323,18 +339,18 @@ ${capList}
|
||||
const nftAssetName = deriveNftAssetName(seedUtxo.txHash, seedUtxo.outputIndex);
|
||||
const nftUnit = `${REGISTRY_POLICY_ID}${nftAssetName}`;
|
||||
const datum = buildAgentDatum(vkeyHash, name, description, capabilities, framework, endpoint);
|
||||
const registryScript = { type: "PlutusV3", script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryAddress = lucid.utils.validatorToAddress(lucidCompat(registryScript));
|
||||
const registryScript = { type: 'PlutusV3' as const, script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryAddress = validatorToAddress('Mainnet', registryScript);
|
||||
const registerRedeemer = Data.to(new Constr(0, [new Constr(0, [seedUtxo.txHash, BigInt(seedUtxo.outputIndex)])]));
|
||||
|
||||
const tx = await lucid.newTx()
|
||||
.collectFrom([seedUtxo])
|
||||
.mintAssets({ [nftUnit]: 1n }, registerRedeemer)
|
||||
.attachMintingPolicy(lucidCompat(registryScript))
|
||||
.payToContract(registryAddress, { inline: datum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.attach.MintingPolicy(registryScript)
|
||||
.pay.ToAddressWithData(registryAddress, { kind: "inline", value: datum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.addSigner(walletAddress)
|
||||
.complete();
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signedTx = await tx.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
safetyLayer.recordTransaction(txHash, Number(MIN_AP3X_DEPOSIT), registryAddress);
|
||||
|
||||
@@ -398,8 +414,8 @@ Save your Agent DID — you'll need it to update, deregister, or let other agent
|
||||
if (capabilities !== undefined) validateCapabilities(capabilities);
|
||||
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const walletAddress = await lucid.wallet.address();
|
||||
const vkeyHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash;
|
||||
const walletAddress = await lucid.wallet().address();
|
||||
const vkeyHash = getAddressDetails(walletAddress).paymentCredential?.hash;
|
||||
if (!vkeyHash) throw new Error('Cannot derive payment key hash from wallet address');
|
||||
|
||||
const provider = newProvider();
|
||||
@@ -414,17 +430,17 @@ Save your Agent DID — you'll need it to update, deregister, or let other agent
|
||||
const newEndpoint = endpoint ?? profile.endpoint;
|
||||
|
||||
const newDatum = buildAgentDatum(vkeyHash, newName, newDesc, newCaps, newFramework, newEndpoint, profile.registeredAt);
|
||||
const registryScript = { type: 'PlutusV3', script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryAddress = lucid.utils.validatorToAddress(lucidCompat(registryScript));
|
||||
const registryScript = { type: 'PlutusV3' as const, script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryAddress = validatorToAddress('Mainnet', registryScript);
|
||||
const spendRedeemer = Data.to(new Constr(0, [])); // Update
|
||||
|
||||
const tx = await lucid.newTx()
|
||||
.collectFrom([utxo], spendRedeemer)
|
||||
.attachSpendingValidator(lucidCompat(registryScript))
|
||||
.payToContract(registryAddress, { inline: newDatum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.attach.SpendingValidator(registryScript)
|
||||
.pay.ToAddressWithData(registryAddress, { kind: "inline", value: newDatum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.addSigner(walletAddress)
|
||||
.complete();
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signedTx = await tx.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
safetyLayer.recordTransaction(txHash, 0, registryAddress);
|
||||
|
||||
@@ -479,26 +495,26 @@ The agent's profile has been updated on-chain. The identity NFT and deposit are
|
||||
if (rateLimited) return rateLimited;
|
||||
try {
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const walletAddress = await lucid.wallet.address();
|
||||
const vkeyHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash;
|
||||
const walletAddress = await lucid.wallet().address();
|
||||
const vkeyHash = getAddressDetails(walletAddress).paymentCredential?.hash;
|
||||
if (!vkeyHash) throw new Error('Cannot derive payment key hash from wallet address');
|
||||
|
||||
const provider = newProvider();
|
||||
const { profile, utxo, nftUnit } = await resolveAgentUtxo(provider, agent_id);
|
||||
verifyOwnership(profile, vkeyHash);
|
||||
|
||||
const registryScript = { type: 'PlutusV3', script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryScript = { type: 'PlutusV3' as const, script: REGISTRY_SCRIPT_CBOR };
|
||||
const spendRedeemer = Data.to(new Constr(1, [])); // Deregister
|
||||
const mintRedeemer = Data.to(new Constr(1, [])); // Burn
|
||||
|
||||
const tx = await lucid.newTx()
|
||||
.collectFrom([utxo], spendRedeemer)
|
||||
.attachSpendingValidator(lucidCompat(registryScript))
|
||||
.attach.SpendingValidator(registryScript)
|
||||
.mintAssets({ [nftUnit]: -1n }, mintRedeemer)
|
||||
.attachMintingPolicy(lucidCompat(registryScript))
|
||||
.attach.MintingPolicy(registryScript)
|
||||
.addSigner(walletAddress)
|
||||
.complete();
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signedTx = await tx.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
safetyLayer.recordTransaction(txHash, 0, walletAddress);
|
||||
|
||||
@@ -548,14 +564,14 @@ The agent's identity NFT has been burned and the 10 AP3X deposit has been return
|
||||
if (rateLimited) return rateLimited;
|
||||
try {
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const walletAddress = await lucid.wallet.address();
|
||||
const vkeyHash = lucid.utils.getAddressDetails(walletAddress).paymentCredential?.hash;
|
||||
const walletAddress = await lucid.wallet().address();
|
||||
const vkeyHash = getAddressDetails(walletAddress).paymentCredential?.hash;
|
||||
if (!vkeyHash) throw new Error('Cannot derive payment key hash from wallet address');
|
||||
|
||||
// Validate new owner address
|
||||
let newOwnerDetails;
|
||||
try {
|
||||
newOwnerDetails = lucid.utils.getAddressDetails(new_owner_address);
|
||||
newOwnerDetails = getAddressDetails(new_owner_address);
|
||||
} catch {
|
||||
throw new Error(`Invalid new owner address: "${new_owner_address}". Must be a valid bech32 address.`);
|
||||
}
|
||||
@@ -574,17 +590,17 @@ The agent's identity NFT has been burned and the 10 AP3X deposit has been return
|
||||
profile.capabilities, profile.framework, profile.endpoint,
|
||||
profile.registeredAt
|
||||
);
|
||||
const registryScript = { type: 'PlutusV3', script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryAddress = lucid.utils.validatorToAddress(lucidCompat(registryScript));
|
||||
const registryScript = { type: 'PlutusV3' as const, script: REGISTRY_SCRIPT_CBOR };
|
||||
const registryAddress = validatorToAddress('Mainnet', registryScript);
|
||||
const spendRedeemer = Data.to(new Constr(0, [])); // Update (transfer uses Update redeemer)
|
||||
|
||||
const tx = await lucid.newTx()
|
||||
.collectFrom([utxo], spendRedeemer)
|
||||
.attachSpendingValidator(lucidCompat(registryScript))
|
||||
.payToContract(registryAddress, { inline: newDatum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.attach.SpendingValidator(registryScript)
|
||||
.pay.ToAddressWithData(registryAddress, { kind: "inline", value: newDatum }, { lovelace: MIN_AP3X_DEPOSIT, [nftUnit]: 1n })
|
||||
.addSigner(walletAddress)
|
||||
.complete();
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signedTx = await tx.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
safetyLayer.recordTransaction(txHash, 0, registryAddress);
|
||||
|
||||
@@ -640,17 +656,17 @@ Ownership has been transferred. The new owner can now update, transfer, or dereg
|
||||
if (!profile.ownerVkeyHash) throw new Error('Could not parse agent owner from registry datum');
|
||||
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const senderAddress = await lucid.wallet.address();
|
||||
const recipientAddress = lucid.utils.credentialToAddress({ type: 'Key', hash: profile.ownerVkeyHash });
|
||||
const senderAddress = await lucid.wallet().address();
|
||||
const recipientAddress = credentialToAddress('Mainnet', { type: 'Key', hash: profile.ownerVkeyHash });
|
||||
const minAda = 2_000_000n;
|
||||
const safetyCheck = safetyLayer.checkTransaction(Number(minAda));
|
||||
if (!safetyCheck.allowed) throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`);
|
||||
|
||||
const tx = await lucid.newTx()
|
||||
.payToAddress(recipientAddress, { lovelace: minAda })
|
||||
.attachMetadata(AGENT_MESSAGE_LABEL, { msg: ['a2a'], from: senderAddress, to: agent_id, type: message_type, payload })
|
||||
.pay.ToAddress(recipientAddress, { lovelace: minAda })
|
||||
.attachMetadata(AGENT_MESSAGE_LABEL, { msg: ['a2a'], from: metadataStr(senderAddress), to: metadataStr(agent_id), type: message_type, payload: metadataStr(payload) })
|
||||
.complete();
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signedTx = await tx.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
safetyLayer.recordTransaction(txHash, Number(minAda), recipientAddress);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Custom Lucid Provider backed by Ogmios HTTP JSON-RPC + Vector submit-api.
|
||||
*
|
||||
* Implements the lucid-cardano Provider interface so we can use Lucid's
|
||||
* Implements the @lucid-evolution/lucid Provider interface so we can use Lucid's
|
||||
* transaction building/signing without depending on Blockfrost.
|
||||
*/
|
||||
import fetch from 'cross-fetch';
|
||||
@@ -12,8 +12,8 @@ import type {
|
||||
OutRef,
|
||||
Credential,
|
||||
Delegation,
|
||||
// @ts-ignore - type aliases used as string in lucid
|
||||
} from 'lucid-cardano';
|
||||
EvalRedeemer,
|
||||
} from '@lucid-evolution/lucid';
|
||||
|
||||
interface OgmiosProviderConfig {
|
||||
ogmiosUrl: string;
|
||||
@@ -25,7 +25,6 @@ export class OgmiosProvider implements Provider {
|
||||
private ogmiosUrl: string;
|
||||
private submitUrl: string;
|
||||
private koiosUrl: string | undefined;
|
||||
private walletAddress: string | undefined;
|
||||
|
||||
constructor(config: OgmiosProviderConfig) {
|
||||
// Strip trailing slashes for consistent URL building
|
||||
@@ -34,14 +33,6 @@ export class OgmiosProvider implements Provider {
|
||||
this.koiosUrl = config.koiosUrl?.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the wallet's full bech32 address so credential-based lookups
|
||||
* (used internally by Lucid during tx building) can resolve to it.
|
||||
*/
|
||||
setWalletAddress(address: string): void {
|
||||
this.walletAddress = address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON-RPC request to Ogmios
|
||||
*/
|
||||
@@ -93,7 +84,9 @@ export class OgmiosProvider implements Provider {
|
||||
collateralPercentage: result.collateralPercentage ?? 150,
|
||||
maxCollateralInputs: result.maxCollateralInputs ?? 3,
|
||||
costModels: this.parseCostModels(result.plutusCostModels ?? result.costModels ?? {}),
|
||||
minfeeRefscriptCostPerByte: result.minFeeReferenceScripts?.base ?? 15,
|
||||
minFeeRefScriptCostPerByte: result.minFeeReferenceScripts?.base ?? 15,
|
||||
drepDeposit: BigInt(result.delegateRepresentativeDeposit?.ada?.lovelace ?? 500000000),
|
||||
govActionDeposit: BigInt(result.governanceActionDeposit?.ada?.lovelace ?? 100000000000),
|
||||
} as ProtocolParameters;
|
||||
}
|
||||
|
||||
@@ -125,21 +118,14 @@ export class OgmiosProvider implements Provider {
|
||||
}
|
||||
|
||||
async getUtxos(addressOrCredential: string | Credential): Promise<UTxO[]> {
|
||||
let address: string;
|
||||
if (typeof addressOrCredential === 'string') {
|
||||
address = addressOrCredential;
|
||||
} else if (this.walletAddress) {
|
||||
// Lucid's wallet internally calls getUtxos(paymentCredential) during tx building.
|
||||
// Ogmios requires a full bech32 address, so use the stored wallet address.
|
||||
address = this.walletAddress;
|
||||
} else {
|
||||
if (typeof addressOrCredential !== 'string') {
|
||||
throw new Error(
|
||||
'Credential-based UTxO lookup requires a wallet address. ' +
|
||||
'Call provider.setWalletAddress(addr) after selecting a wallet.'
|
||||
'Credential-based UTxO lookup is not supported by Ogmios. ' +
|
||||
'Please provide a bech32 address string.'
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.rpc('queryLedgerState/utxo', { addresses: [address] });
|
||||
const result = await this.rpc('queryLedgerState/utxo', { addresses: [addressOrCredential] });
|
||||
return this.ogmiosUtxosToLucid(result);
|
||||
}
|
||||
|
||||
@@ -149,23 +135,33 @@ export class OgmiosProvider implements Provider {
|
||||
}
|
||||
|
||||
async getUtxoByUnit(unit: string): Promise<UTxO> {
|
||||
// This requires an indexer; try Koios if available
|
||||
const policyId = unit.slice(0, 56);
|
||||
const assetName = unit.slice(56);
|
||||
|
||||
// Try Koios indexer first (fastest path)
|
||||
if (this.koiosUrl) {
|
||||
const policyId = unit.slice(0, 56);
|
||||
const assetName = unit.slice(56);
|
||||
const response = await fetch(`${this.koiosUrl}/api/v1/asset_utxos`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ _asset_list: [[policyId, assetName]] }),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.length > 0) {
|
||||
return this.koiosUtxoToLucid(data[0]);
|
||||
try {
|
||||
const response = await fetch(`${this.koiosUrl}/api/v1/asset_utxos`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ _asset_list: [[policyId, assetName]] }),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.length > 0) {
|
||||
return this.koiosUtxoToLucid(data[0]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Koios unreachable — caller can fall back to scanning UTxOs
|
||||
}
|
||||
}
|
||||
throw new Error('getUtxoByUnit requires an indexer (Koios). Not available or asset not found.');
|
||||
|
||||
throw new Error(
|
||||
`Asset ${policyId.slice(0, 8)}...${assetName ? assetName.slice(0, 8) + '...' : '(empty)'} not found. ` +
|
||||
`${this.koiosUrl ? 'Koios returned no results.' : 'No Koios indexer configured.'} ` +
|
||||
`The asset may not exist on-chain yet (wait for TX confirmation) or may have been burned.`
|
||||
);
|
||||
}
|
||||
|
||||
async getUtxosByOutRef(outRefs: OutRef[]): Promise<UTxO[]> {
|
||||
@@ -263,9 +259,31 @@ export class OgmiosProvider implements Provider {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a transaction (lucid-evolution Provider interface).
|
||||
* Returns execution units for script validation as EvalRedeemer[].
|
||||
*/
|
||||
async evaluateTx(tx: string, additionalUTxOs?: UTxO[]): Promise<EvalRedeemer[]> {
|
||||
const result = await this.rpc('evaluateTransaction', {
|
||||
transaction: { cbor: tx },
|
||||
});
|
||||
// Parse result to EvalRedeemer[] format
|
||||
if (Array.isArray(result)) {
|
||||
return result.map((item: any) => ({
|
||||
ex_units: {
|
||||
mem: item.budget?.memory ?? 0,
|
||||
steps: item.budget?.cpu ?? 0,
|
||||
},
|
||||
redeemer_index: item.validator?.index ?? 0,
|
||||
redeemer_tag: item.validator?.purpose ?? 'spend',
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a transaction without submitting (dry run).
|
||||
* Returns execution units for script validation.
|
||||
* Backward-compatible alias — returns raw Ogmios response.
|
||||
*/
|
||||
async evaluateTransaction(txCborHex: string): Promise<any> {
|
||||
return this.rpc('evaluateTransaction', {
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { Lucid, fromText, Data, applyDoubleCborEncoding } from 'lucid-cardano';
|
||||
import type { SpendingValidator } from 'lucid-cardano';
|
||||
import { Lucid, fromText, Data, applyDoubleCborEncoding, validatorToAddress, validatorToScriptHash, getAddressDetails } from '@lucid-evolution/lucid';
|
||||
import type { SpendingValidator } from '@lucid-evolution/lucid';
|
||||
|
||||
/**
|
||||
* Lucid v0.10.x lacks PlutusV3 support. This helper downcasts PlutusV3 to
|
||||
* PlutusV2 for hashing/address derivation (same blake2b-224 algorithm).
|
||||
* Use this when passing validators to any Lucid method.
|
||||
*/
|
||||
function lucidCompat(validator: SpendingValidator): SpendingValidator {
|
||||
if ((validator.type as string) === 'PlutusV3') {
|
||||
return { ...validator, type: 'PlutusV2' as const };
|
||||
}
|
||||
return validator;
|
||||
}
|
||||
import * as dotenv from 'dotenv';
|
||||
import { resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
@@ -83,7 +72,7 @@ async function initLucid(mnemonic: string, accountIndex: number = 0) {
|
||||
});
|
||||
|
||||
// Vector uses --mainnet flag, so addresses are addr1... format
|
||||
const lucid = await Lucid.new(provider, 'Mainnet');
|
||||
const lucid = await Lucid(provider, 'Mainnet');
|
||||
|
||||
if (!mnemonic) {
|
||||
throw new Error('mnemonic is required');
|
||||
@@ -97,23 +86,20 @@ async function initLucid(mnemonic: string, accountIndex: number = 0) {
|
||||
throw new Error(`Invalid mnemonic: Expected 12, 15, 18, 21 or 24 words, got ${words.length}`);
|
||||
}
|
||||
|
||||
lucid.selectWalletFromSeed(trimmedMnemonic, { accountIndex });
|
||||
lucid.selectWallet.fromSeed(trimmedMnemonic, { accountIndex });
|
||||
|
||||
const address = await lucid.wallet.address();
|
||||
const address = await lucid.wallet().address();
|
||||
if (!address) {
|
||||
throw new Error('Failed to derive address from mnemonic');
|
||||
}
|
||||
|
||||
// Store the full wallet address so credential-based UTxO lookups resolve correctly
|
||||
provider.setWalletAddress(address);
|
||||
|
||||
return lucid;
|
||||
}
|
||||
|
||||
// Get wallet info
|
||||
export async function getWalletInfo(mnemonic: string, accountIndex: number = 0): Promise<VectorWalletInfo> {
|
||||
const lucid = await initLucid(mnemonic, accountIndex);
|
||||
const address = await lucid.wallet.address();
|
||||
const address = await lucid.wallet().address();
|
||||
const utxos = await lucid.utxosAt(address);
|
||||
|
||||
let adaBalance = '0';
|
||||
@@ -187,31 +173,26 @@ export async function sendAda(
|
||||
}
|
||||
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const senderAddress = await lucid.wallet.address();
|
||||
const senderAddress = await lucid.wallet().address();
|
||||
|
||||
// Validate recipient address
|
||||
try {
|
||||
// @ts-ignore
|
||||
lucid.utils.getAddressDetails(recipientAddress);
|
||||
getAddressDetails(recipientAddress);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
// Build transaction
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx()
|
||||
.payToAddress(recipientAddress, { lovelace: BigInt(lovelaceAmount) });
|
||||
.pay.ToAddress(recipientAddress, { lovelace: BigInt(lovelaceAmount) });
|
||||
|
||||
if (metadata) {
|
||||
const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
|
||||
// @ts-ignore
|
||||
tx = tx.attachMetadata(674, parsedMetadata);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
tx = await tx.complete();
|
||||
// @ts-ignore
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signBuilder = await tx.complete();
|
||||
const signedTx = await signBuilder.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
|
||||
// Record in safety layer
|
||||
@@ -251,11 +232,10 @@ export async function sendTokens(
|
||||
}
|
||||
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const senderAddress = await lucid.wallet.address();
|
||||
const senderAddress = await lucid.wallet().address();
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
lucid.utils.getAddressDetails(recipientAddress);
|
||||
getAddressDetails(recipientAddress);
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
@@ -272,17 +252,14 @@ export async function sendTokens(
|
||||
? BigInt(Math.floor(adaAmount * 1_000_000))
|
||||
: BigInt(2_000_000); // Default min ADA
|
||||
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx()
|
||||
.payToAddress(recipientAddress, {
|
||||
.pay.ToAddress(recipientAddress, {
|
||||
lovelace: outputLovelace,
|
||||
...assets,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
tx = await tx.complete();
|
||||
// @ts-ignore
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signBuilder = await tx.complete();
|
||||
const signedTx = await signBuilder.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
|
||||
safetyLayer.recordTransaction(txHash, Number(outputLovelace), recipientAddress);
|
||||
@@ -325,7 +302,6 @@ export async function buildTransaction(
|
||||
|
||||
const lucid = await initLucid(mnemonic);
|
||||
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx();
|
||||
|
||||
for (const output of outputs) {
|
||||
@@ -337,27 +313,22 @@ export async function buildTransaction(
|
||||
assets[unit] = BigInt(qty);
|
||||
}
|
||||
}
|
||||
tx = tx.payToAddress(output.address, assets);
|
||||
tx = tx.pay.ToAddress(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 txJson = (completedTx as any).toJSON?.() ?? {};
|
||||
const fee = txJson?.body?.fee ?? '0';
|
||||
const txHash = completedTx.toHash();
|
||||
// @ts-ignore
|
||||
const txCbor = completedTx.toString();
|
||||
const txCbor = completedTx.toCBOR();
|
||||
|
||||
if (submit) {
|
||||
// @ts-ignore
|
||||
const signedTx = await completedTx.sign().complete();
|
||||
const signedTx = await completedTx.sign.withWallet().complete();
|
||||
const submittedHash = await signedTx.submit();
|
||||
|
||||
safetyLayer.recordTransaction(submittedHash, totalLovelace, outputs.map(o => o.address).join(', '));
|
||||
@@ -405,21 +376,16 @@ export async function deployContract(
|
||||
script: applyDoubleCborEncoding(scriptCbor),
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const scriptAddress = lucid.utils.validatorToAddress(lucidCompat(validator));
|
||||
// @ts-ignore
|
||||
const scriptHash = lucid.utils.validatorToScriptHash(lucidCompat(validator));
|
||||
const scriptAddress = validatorToAddress('Mainnet', validator);
|
||||
const scriptHash = validatorToScriptHash(validator);
|
||||
|
||||
const datum = initialDatum || Data.void();
|
||||
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx()
|
||||
.payToContract(scriptAddress, { inline: datum }, { lovelace: BigInt(lovelaceAmount) });
|
||||
.pay.ToAddressWithData(scriptAddress, { kind: "inline", value: datum }, { lovelace: BigInt(lovelaceAmount) });
|
||||
|
||||
// @ts-ignore
|
||||
tx = await tx.complete();
|
||||
// @ts-ignore
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signBuilder = await tx.complete();
|
||||
const signedTx = await signBuilder.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
|
||||
safetyLayer.recordTransaction(txHash, lovelaceAmount, scriptAddress);
|
||||
@@ -446,15 +412,14 @@ export async function interactWithContract(
|
||||
assets: Record<string, string> | null = null,
|
||||
): Promise<VectorInteractContractResult> {
|
||||
const lucid = await initLucid(mnemonic);
|
||||
const walletAddress = await lucid.wallet.address();
|
||||
const walletAddress = await lucid.wallet().address();
|
||||
|
||||
const validator: SpendingValidator = {
|
||||
type: scriptType as any,
|
||||
script: applyDoubleCborEncoding(scriptCbor),
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const scriptAddress = lucid.utils.validatorToAddress(lucidCompat(validator));
|
||||
const scriptAddress = validatorToAddress('Mainnet', validator);
|
||||
|
||||
if (action === 'lock') {
|
||||
const safetyCheck = safetyLayer.checkTransaction(lovelaceAmount);
|
||||
@@ -470,14 +435,11 @@ export async function interactWithContract(
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx()
|
||||
.payToContract(scriptAddress, { inline: datumData }, outputAssets);
|
||||
.pay.ToAddressWithData(scriptAddress, { kind: "inline", value: datumData }, outputAssets);
|
||||
|
||||
// @ts-ignore
|
||||
tx = await tx.complete();
|
||||
// @ts-ignore
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signBuilder = await tx.complete();
|
||||
const signedTx = await signBuilder.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
|
||||
safetyLayer.recordTransaction(txHash, lovelaceAmount, scriptAddress);
|
||||
@@ -492,9 +454,9 @@ export async function interactWithContract(
|
||||
// SPEND: collect from script
|
||||
let scriptUtxos;
|
||||
if (utxoRef) {
|
||||
scriptUtxos = await lucid.provider.getUtxosByOutRef([utxoRef]);
|
||||
scriptUtxos = await lucid.utxosByOutRef([utxoRef]);
|
||||
} else {
|
||||
scriptUtxos = await lucid.provider.getUtxos(scriptAddress);
|
||||
scriptUtxos = await lucid.utxosAt(scriptAddress);
|
||||
}
|
||||
|
||||
if (!scriptUtxos || scriptUtxos.length === 0) {
|
||||
@@ -503,27 +465,23 @@ export async function interactWithContract(
|
||||
|
||||
const redeemerData = redeemer || Data.void();
|
||||
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx()
|
||||
.collectFrom(scriptUtxos, redeemerData)
|
||||
.attachSpendingValidator(lucidCompat(validator))
|
||||
.addSigner(walletAddress);
|
||||
|
||||
let completedTx;
|
||||
try {
|
||||
// @ts-ignore
|
||||
tx = await tx.complete();
|
||||
completedTx = await lucid.newTx()
|
||||
.collectFrom(scriptUtxos, redeemerData)
|
||||
.attach.SpendingValidator(validator)
|
||||
.addSigner(walletAddress)
|
||||
.complete();
|
||||
} catch (err) {
|
||||
// Retry without native UPLC evaluator if it fails
|
||||
// @ts-ignore
|
||||
tx = await lucid.newTx()
|
||||
completedTx = await lucid.newTx()
|
||||
.collectFrom(scriptUtxos, redeemerData)
|
||||
.attachSpendingValidator(lucidCompat(validator))
|
||||
.attach.SpendingValidator(validator)
|
||||
.addSigner(walletAddress)
|
||||
.complete({ nativeUplc: false });
|
||||
.complete({ localUPLCEval: false });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const signedTx = await tx.sign().complete();
|
||||
const signedTx = await completedTx.sign.withWallet().complete();
|
||||
const txHash = await signedTx.submit();
|
||||
|
||||
// No spend recording for collecting — funds are coming back to wallet
|
||||
@@ -541,7 +499,6 @@ export async function interactWithContract(
|
||||
export function registerVectorTools(server: McpServer) {
|
||||
|
||||
// vector_get_balance — Get balance for any address
|
||||
// @ts-ignore: MCP SDK deep type instantiation
|
||||
server.tool(
|
||||
"vector_get_balance",
|
||||
"Get ADA and token balances for a Vector address",
|
||||
@@ -615,7 +572,6 @@ ${tokens.length > 0 ? `Token Holdings (${tokens.length}):\n${tokenList}` : 'No t
|
||||
);
|
||||
|
||||
// vector_get_address — Get the agent's wallet address and balance
|
||||
// @ts-ignore: MCP SDK deep type instantiation
|
||||
server.tool(
|
||||
"vector_get_address",
|
||||
"Get the Vector wallet address, balance, and token holdings derived from a mnemonic",
|
||||
@@ -664,7 +620,6 @@ ${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length}
|
||||
);
|
||||
|
||||
// vector_get_utxos — List UTxOs for an address or the wallet
|
||||
// @ts-ignore: MCP SDK deep type instantiation
|
||||
server.tool(
|
||||
"vector_get_utxos",
|
||||
"List unspent transaction outputs (UTxOs) for a Vector address or a wallet derived from a mnemonic",
|
||||
@@ -692,7 +647,7 @@ ${walletInfo.tokens.length > 0 ? `## Token Holdings (${walletInfo.tokens.length}
|
||||
} else {
|
||||
if (!mnemonic) throw new Error('Provide either address or mnemonic');
|
||||
const lucid = await initLucid(mnemonic);
|
||||
queryAddress = await lucid.wallet.address();
|
||||
queryAddress = await lucid.wallet().address();
|
||||
utxos = await lucid.utxosAt(queryAddress);
|
||||
}
|
||||
|
||||
@@ -734,7 +689,6 @@ ${utxoList}`,
|
||||
);
|
||||
|
||||
// vector_send_apex — Send APEX with safety limits (or craft unsigned TX)
|
||||
// @ts-ignore: MCP SDK deep type instantiation
|
||||
server.tool(
|
||||
"vector_send_apex",
|
||||
"Send ADA from a wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting (transaction-crafter mode).",
|
||||
@@ -754,19 +708,15 @@ ${utxoList}`,
|
||||
if (unsigned_only) {
|
||||
const lovelaceAmount = BigInt(Math.floor(amount * 1_000_000));
|
||||
const lucid = await initLucid(mnemonic);
|
||||
// @ts-ignore
|
||||
let txBuilder = lucid.newTx().payToAddress(recipientAddress, { lovelace: lovelaceAmount });
|
||||
let txBuilder = lucid.newTx().pay.ToAddress(recipientAddress, { lovelace: lovelaceAmount });
|
||||
if (metadata) {
|
||||
const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
|
||||
// @ts-ignore
|
||||
txBuilder = txBuilder.attachMetadata(674, parsedMetadata);
|
||||
}
|
||||
// @ts-ignore
|
||||
const tx = await txBuilder.complete();
|
||||
// @ts-ignore
|
||||
const fee = tx.fee;
|
||||
// @ts-ignore
|
||||
const txCbor = tx.toString();
|
||||
const txJson = (tx as any).toJSON?.() ?? {};
|
||||
const fee = txJson?.body?.fee ?? '0';
|
||||
const txCbor = tx.toCBOR();
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
@@ -819,7 +769,6 @@ Amount: ${result.amount} ADA
|
||||
);
|
||||
|
||||
// vector_send_tokens — Send native tokens with safety limits (or craft unsigned TX)
|
||||
// @ts-ignore: MCP SDK deep type instantiation
|
||||
server.tool(
|
||||
"vector_send_tokens",
|
||||
"Send Vector native tokens from a wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting.",
|
||||
@@ -848,14 +797,12 @@ Amount: ${result.amount} ADA
|
||||
const outputLovelace = adaAmount
|
||||
? BigInt(Math.floor(adaAmount * 1_000_000))
|
||||
: BigInt(2_000_000);
|
||||
// @ts-ignore
|
||||
const tx = await lucid.newTx()
|
||||
.payToAddress(recipientAddress, { lovelace: outputLovelace, [unit]: BigInt(amount) })
|
||||
.pay.ToAddress(recipientAddress, { lovelace: outputLovelace, [unit]: BigInt(amount) })
|
||||
.complete();
|
||||
// @ts-ignore
|
||||
const fee = tx.fee;
|
||||
// @ts-ignore
|
||||
const txCbor = tx.toString();
|
||||
const txJson = (tx as any).toJSON?.() ?? {};
|
||||
const fee = txJson?.body?.fee ?? '0';
|
||||
const txCbor = tx.toCBOR();
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
@@ -916,7 +863,6 @@ Included ADA: ${result.ada} ADA
|
||||
);
|
||||
|
||||
// vector_get_spend_limits — Check safety layer status
|
||||
// @ts-ignore: MCP SDK deep type instantiation
|
||||
server.tool(
|
||||
"vector_get_spend_limits",
|
||||
"Check current spend limits and remaining daily budget",
|
||||
@@ -961,7 +907,6 @@ ${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.",
|
||||
@@ -1032,7 +977,6 @@ Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction
|
||||
);
|
||||
|
||||
// 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",
|
||||
@@ -1060,7 +1004,6 @@ Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction
|
||||
if (!mnemonic) throw new Error('mnemonic is required when building from outputs');
|
||||
const lucid = await initLucid(mnemonic);
|
||||
|
||||
// @ts-ignore
|
||||
let tx = lucid.newTx();
|
||||
for (const output of outputs) {
|
||||
const assets: Record<string, bigint> = { lovelace: BigInt(output.lovelace) };
|
||||
@@ -1069,22 +1012,19 @@ Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction
|
||||
assets[unit] = BigInt(qty);
|
||||
}
|
||||
}
|
||||
tx = tx.payToAddress(output.address, assets);
|
||||
tx = tx.pay.ToAddress(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();
|
||||
const txJson = (completedTx as any).toJSON?.() ?? {};
|
||||
feeFromBuild = String(txJson?.body?.fee ?? '0');
|
||||
const signedTx = await completedTx.sign.withWallet().complete();
|
||||
cborToEvaluate = signedTx.toCBOR();
|
||||
}
|
||||
|
||||
if (!cborToEvaluate) {
|
||||
@@ -1165,7 +1105,6 @@ No transaction was submitted to the network.`,
|
||||
);
|
||||
|
||||
// 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",
|
||||
@@ -1185,7 +1124,7 @@ No transaction was submitted to the network.`,
|
||||
if (!queryAddress) {
|
||||
if (!mnemonic) throw new Error('Provide either address or mnemonic');
|
||||
const lucid = await initLucid(mnemonic);
|
||||
queryAddress = await lucid.wallet.address();
|
||||
queryAddress = await lucid.wallet().address();
|
||||
}
|
||||
|
||||
const provider = new OgmiosProvider({
|
||||
@@ -1240,7 +1179,6 @@ ${txList}
|
||||
);
|
||||
|
||||
// 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",
|
||||
@@ -1299,7 +1237,6 @@ Funds locked at the script address. Use vector_interact_contract to interact wit
|
||||
);
|
||||
|
||||
// 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",
|
||||
|
||||
@@ -2,13 +2,14 @@ import { describe, test, before, after } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { startServer, stopServer, callTool, getMnemonic, wait, ServerContext } from './setup.ts';
|
||||
|
||||
// Always-succeeds PlutusV3 validator (accepts any datum/redeemer/context)
|
||||
const ALWAYS_SUCCEEDS_V3 = '49480100002221200101';
|
||||
// Always-succeeds PlutusV2 validator (accepts any datum/redeemer/context, returns True)
|
||||
const ALWAYS_SUCCEEDS_V2 = '49480100002221200101';
|
||||
|
||||
let ctx: ServerContext;
|
||||
let mnemonic: string;
|
||||
let walletAddress: string;
|
||||
let walletHasAda = false;
|
||||
let walletBalanceAda = 0;
|
||||
let agentDid: string | null = null;
|
||||
|
||||
before(async () => {
|
||||
@@ -62,6 +63,7 @@ describe('Wallet Tools', () => {
|
||||
const balanceMatch = text.match(/ADA Balance:\s*([\d.]+)/);
|
||||
if (balanceMatch && parseFloat(balanceMatch[1]) > 0) {
|
||||
walletHasAda = true;
|
||||
walletBalanceAda = parseFloat(balanceMatch[1]);
|
||||
console.log(`Wallet funded: ${balanceMatch[1]} ADA`);
|
||||
} else {
|
||||
console.log('Wallet has 0 ADA - transaction tests will verify error handling');
|
||||
@@ -108,6 +110,41 @@ describe('History & Limits', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── UTxO Consolidation ─────────────────────────────────────────────────────
|
||||
// Repeated test runs fragment the wallet into many small UTxOs. Plutus script
|
||||
// transactions need collateral (≥5 ADA in ≤3 inputs). Consolidate up-front so
|
||||
// later tests don't fail from fragmentation.
|
||||
|
||||
describe('UTxO Consolidation', () => {
|
||||
test('consolidate wallet UTxOs', { timeout: 120_000 }, async () => {
|
||||
if (!walletHasAda) return;
|
||||
const utxoText = await callTool(ctx.client, 'vector_get_utxos', { mnemonic });
|
||||
const countMatch = utxoText.match(/Total:\s*(\d+)/);
|
||||
const utxoCount = countMatch ? parseInt(countMatch[1], 10) : 0;
|
||||
if (utxoCount <= 3) {
|
||||
console.log(`Only ${utxoCount} UTxO(s) — no consolidation needed`);
|
||||
return;
|
||||
}
|
||||
// Send most of the balance to self, forcing Lucid to consume many inputs.
|
||||
// This reduces the UTxO set to ~2 (output + change).
|
||||
const consolidateAmount = Math.floor(walletBalanceAda) - 2; // leave buffer for fees
|
||||
if (consolidateAmount < 2) {
|
||||
console.log(`Balance too low (${walletBalanceAda} ADA) to consolidate`);
|
||||
return;
|
||||
}
|
||||
console.log(`${utxoCount} UTxOs detected — consolidating ${consolidateAmount} ADA to self...`);
|
||||
const text = await callTool(ctx.client, 'vector_send_apex', {
|
||||
recipientAddress: walletAddress,
|
||||
amount: consolidateAmount,
|
||||
mnemonic,
|
||||
});
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Transaction Hash:/, 'consolidate UTxOs');
|
||||
console.log('Waiting 10s for consolidation tx to confirm...');
|
||||
await wait(10);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Transaction Tools ──────────────────────────────────────────────────────
|
||||
|
||||
describe('Transaction Tools', () => {
|
||||
@@ -187,8 +224,8 @@ describe('Smart Contract Tools', () => {
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_deploy_contract', {
|
||||
scriptCbor: ALWAYS_SUCCEEDS_V3,
|
||||
scriptType: 'PlutusV3',
|
||||
scriptCbor: ALWAYS_SUCCEEDS_V2,
|
||||
scriptType: 'PlutusV2',
|
||||
mnemonic,
|
||||
});
|
||||
console.log(text);
|
||||
@@ -201,8 +238,8 @@ describe('Smart Contract Tools', () => {
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_interact_contract', {
|
||||
scriptCbor: ALWAYS_SUCCEEDS_V3,
|
||||
scriptType: 'PlutusV3',
|
||||
scriptCbor: ALWAYS_SUCCEEDS_V2,
|
||||
scriptType: 'PlutusV2',
|
||||
action: 'lock',
|
||||
mnemonic,
|
||||
datum: 'd87980',
|
||||
@@ -218,8 +255,8 @@ describe('Smart Contract Tools', () => {
|
||||
await wait(10);
|
||||
}
|
||||
const text = await callTool(ctx.client, 'vector_interact_contract', {
|
||||
scriptCbor: ALWAYS_SUCCEEDS_V3,
|
||||
scriptType: 'PlutusV3',
|
||||
scriptCbor: ALWAYS_SUCCEEDS_V2,
|
||||
scriptType: 'PlutusV2',
|
||||
action: 'spend',
|
||||
mnemonic,
|
||||
redeemer: 'd87980',
|
||||
@@ -268,7 +305,7 @@ describe('Agent Network Tools', () => {
|
||||
|
||||
test('vector_get_agent_profile', { timeout: 120_000 }, async () => {
|
||||
// Use a known DID format even if registration failed, to test the tool responds
|
||||
const testDid = agentDid || `did:vector:agent:${ALWAYS_SUCCEEDS_V3}:${'a'.repeat(64)}`;
|
||||
const testDid = agentDid || `did:vector:agent:${ALWAYS_SUCCEEDS_V2}:${'a'.repeat(64)}`;
|
||||
const text = await callTool(ctx.client, 'vector_get_agent_profile', { agent_id: testDid });
|
||||
console.log(text);
|
||||
assertSuccessOrKnownError(text, /Agent Profile/, 'vector_get_agent_profile');
|
||||
@@ -312,7 +349,7 @@ describe('Agent Network Tools', () => {
|
||||
|
||||
test('vector_message_agent', { timeout: 120_000 }, async () => {
|
||||
// Use a known DID format even if registration failed, to test the tool responds
|
||||
const testDid = agentDid || `did:vector:agent:${ALWAYS_SUCCEEDS_V3}:${'a'.repeat(64)}`;
|
||||
const testDid = agentDid || `did:vector:agent:${ALWAYS_SUCCEEDS_V2}:${'a'.repeat(64)}`;
|
||||
if (walletHasAda && agentDid) {
|
||||
console.log('Waiting 10s for UTxOs to settle before message...');
|
||||
await wait(10);
|
||||
|
||||
Reference in New Issue
Block a user