diff --git a/packages/builder/src/vector/vector.ts b/packages/builder/src/vector/vector.ts index 9cceda7..64f54da 100644 --- a/packages/builder/src/vector/vector.ts +++ b/packages/builder/src/vector/vector.ts @@ -1,7 +1,6 @@ import { z } from "zod/v4"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { Lucid, fromText, Data, applyDoubleCborEncoding, validatorToAddress, validatorToScriptHash, getAddressDetails } from '@lucid-evolution/lucid'; -import type { SpendingValidator } from '@lucid-evolution/lucid'; +import { Lucid } from '@lucid-evolution/lucid'; import * as dotenv from 'dotenv'; import { resolve } from 'path'; @@ -18,10 +17,12 @@ import { pollUntilConfirmed, buildConfirmationCheck } from './poll.js'; import { registerAgentNetworkTools } from './agent-network.js'; import { registerSelfImprovementTools } from './self-improvement.js'; import type { - VectorToken, VectorWalletInfo, VectorAdaTransactionResult, VectorTokenTransactionResult, - TxOutput, VectorBuildTransactionResult, VectorDryRunResult, VectorDeployContractResult, - VectorInteractContractResult, + VectorToken, VectorWalletInfo, VectorDryRunResult, } from '@apexfusion/vector-mcp-shared/types'; +import { + lucidForAddress, buildSendApex, buildSendTokens, buildMultiOutput, + buildDeployContract, buildInteractContract, +} from './build.js'; // Direct .env loading const __filename = fileURLToPath(import.meta.url); @@ -127,350 +128,13 @@ export async function getWalletInfo(mnemonic: string, accountIndex: number = 0): }; } -// Send AP3X transaction -export async function sendAda( - recipientAddress: string, - amountAda: number, - mnemonic: string, - metadata: any = null -): Promise { - if (!recipientAddress) { - throw new Error('Recipient address is required'); - } - - if (typeof amountAda !== 'number' || amountAda <= 0) { - throw new Error('Amount must be a positive number'); - } - - const lovelaceAmount = Math.floor(amountAda * 1_000_000); - - // Safety check - const safetyCheck = safetyLayer.checkTransaction(lovelaceAmount); - if (!safetyCheck.allowed) { - throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); - } - - const lucid = await initLucid(mnemonic); - const senderAddress = await lucid.wallet().address(); - - // Validate recipient address - try { - getAddressDetails(recipientAddress); - } catch (error) { - throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`); - } - - // Build transaction - let tx = lucid.newTx() - .pay.ToAddress(recipientAddress, { lovelace: BigInt(lovelaceAmount) }); - - if (metadata) { - const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; - tx = tx.attachMetadata(674, parsedMetadata); - } - - const signBuilder = await tx.complete(); - const signedTx = await signBuilder.sign.withWallet().complete(); - const txHash = await signedTx.submit(); - - // Record in safety layer - safetyLayer.recordTransaction(txHash, lovelaceAmount, recipientAddress); - - return { - txHash, - senderAddress, - recipientAddress, - amount: amountAda, - links: { - explorer: explorerTxLink(txHash), - }, - }; -} - -// Send tokens transaction -export async function sendTokens( - recipientAddress: string, - policyId: string, - assetName: string, - amount: string, - mnemonic: string, - adaAmount: number | null = null -): Promise { - if (!recipientAddress) throw new Error('Recipient address is required'); - if (!policyId) throw new Error('Policy ID is required'); - if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { - throw new Error('Amount must be a positive number'); - } - - // Safety check on AP3X portion - const adaLovelace = adaAmount ? Math.floor(adaAmount * 1_000_000) : 2_000_000; // min ~2 AP3X - const safetyCheck = safetyLayer.checkTransaction(adaLovelace); - if (!safetyCheck.allowed) { - throw new Error(`Safety limit exceeded: ${safetyCheck.reason}`); - } - - const lucid = await initLucid(mnemonic); - const senderAddress = await lucid.wallet().address(); - - try { - getAddressDetails(recipientAddress); - } catch (error) { - throw new Error(`Invalid recipient address: ${error instanceof Error ? error.message : String(error)}`); - } - - let assetNameHex = assetName; - if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) { - assetNameHex = fromText(assetName); - } - - const unit = `${policyId}${assetNameHex}`; - const assets = { [unit]: BigInt(amount) }; - - const outputLovelace = adaAmount - ? BigInt(Math.floor(adaAmount * 1_000_000)) - : BigInt(2_000_000); // Default min AP3X - - let tx = lucid.newTx() - .pay.ToAddress(recipientAddress, { - lovelace: outputLovelace, - ...assets, - }); - - const signBuilder = await tx.complete(); - const signedTx = await signBuilder.sign.withWallet().complete(); - const txHash = await signedTx.submit(); - - safetyLayer.recordTransaction(txHash, Number(outputLovelace), recipientAddress); - - const displayAssetName = assetName ? formatAssetName(assetNameHex) : ''; - - return { - txHash, - senderAddress, - recipientAddress, - token: { - policyId, - name: displayAssetName, - amount, - }, - ada: lovelaceToAda(outputLovelace), - links: { - explorer: explorerTxLink(txHash), - }, - }; -} - -// Build a complex multi-output transaction -export async function buildTransaction( - outputs: TxOutput[], - mnemonic: string, - metadata: any = null, - submit: boolean = false, -): Promise { - if (!outputs || outputs.length === 0) { - throw new Error('At least one output is required'); - } - - // Calculate total AP3X 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(mnemonic); - - 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.pay.ToAddress(output.address, assets); - } - - if (metadata) { - const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; - tx = tx.attachMetadata(674, parsedMetadata); - } - - const completedTx = await tx.complete(); - const txJson = (completedTx as any).toJSON?.() ?? {}; - const fee = txJson?.body?.fee ?? '0'; - const txHash = completedTx.toHash(); - const txCbor = completedTx.toCBOR(); - - if (submit) { - const signedTx = await completedTx.sign.withWallet().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, - mnemonic: 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(mnemonic); - - const validator: SpendingValidator = { - type: scriptType as any, - script: applyDoubleCborEncoding(scriptCbor), - }; - - const scriptAddress = validatorToAddress('Mainnet', validator); - const scriptHash = validatorToScriptHash(validator); - - const datum = initialDatum || Data.void(); - - let tx = lucid.newTx() - .pay.ToAddressWithData(scriptAddress, { kind: "inline", value: datum }, { lovelace: BigInt(lovelaceAmount) }); - - const signBuilder = await tx.complete(); - const signedTx = await signBuilder.sign.withWallet().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', - mnemonic: string, - 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(mnemonic); - const walletAddress = await lucid.wallet().address(); - - const validator: SpendingValidator = { - type: scriptType as any, - script: applyDoubleCborEncoding(scriptCbor), - }; - - const scriptAddress = validatorToAddress('Mainnet', 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); - } - } - - let tx = lucid.newTx() - .pay.ToAddressWithData(scriptAddress, { kind: "inline", value: datumData }, outputAssets); - - const signBuilder = await tx.complete(); - const signedTx = await signBuilder.sign.withWallet().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.utxosByOutRef([utxoRef]); - } else { - scriptUtxos = await lucid.utxosAt(scriptAddress); - } - - if (!scriptUtxos || scriptUtxos.length === 0) { - throw new Error(`No UTxOs found at script address ${scriptAddress}`); - } - - const redeemerData = redeemer || Data.void(); - - let completedTx; - try { - completedTx = await lucid.newTx() - .collectFrom(scriptUtxos, redeemerData) - .attach.SpendingValidator(validator) - .addSigner(walletAddress) - .complete(); - } catch (err) { - // Retry without native UPLC evaluator if it fails - completedTx = await lucid.newTx() - .collectFrom(scriptUtxos, redeemerData) - .attach.SpendingValidator(validator) - .addSigner(walletAddress) - .complete({ localUPLCEval: false }); - } - - const signedTx = await completedTx.sign.withWallet().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) }, - }; - } +// Shared provider for the keyless build_* tools (Task 5 reuses this). +function makeProvider(): OgmiosProvider { + return new OgmiosProvider({ + ogmiosUrl: VECTOR_OGMIOS_URL, + submitUrl: VECTOR_SUBMIT_URL, + koiosUrl: VECTOR_KOIOS_URL, + }); } // Register all Vector MCP tools @@ -667,66 +331,40 @@ ${utxoList}`, } ); - // vector_send_apex - Send APEX with safety limits (or craft unsigned TX) + // vector_build_send_apex - build an UNSIGNED AP3X payment (keyless) server.tool( - "vector_send_apex", - "Send AP3X from a wallet to a recipient address. Set unsigned_only=true to return unsigned CBOR without submitting (transaction-crafter mode).", + "vector_build_send_apex", + "Build an UNSIGNED transaction sending AP3X to a recipient. Takes no key material - sign the returned CBOR with your local signer (vector_signer_sign), then broadcast with vector_submit_transaction.", { + changeAddress: z.string().describe("Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address."), recipientAddress: z.string().describe("Recipient Vector address (addr1...)"), amount: z.number().min(1).describe("Amount of AP3X to send"), - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the sending wallet"), metadata: z.string().optional().describe("Optional transaction metadata in JSON format"), - unsigned_only: z.boolean().optional().default(false).describe("If true, return unsigned TX CBOR without signing or submitting"), }, - async ({ recipientAddress, amount, mnemonic, metadata, unsigned_only }) => { + async ({ changeAddress, recipientAddress, amount, metadata }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - if (unsigned_only) { - const lovelaceAmount = BigInt(Math.floor(amount * 1_000_000)); - const lucid = await initLucid(mnemonic); - let txBuilder = lucid.newTx().pay.ToAddress(recipientAddress, { lovelace: lovelaceAmount }); - if (metadata) { - const parsedMetadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata; - txBuilder = txBuilder.attachMetadata(674, parsedMetadata); - } - const tx = await txBuilder.complete(); - const txJson = (tx as any).toJSON?.() ?? {}; - const fee = txJson?.body?.fee ?? '0'; - const txCbor = tx.toCBOR(); - return { - content: [{ - type: "text", - text: `# Unsigned Transaction (Transaction-Crafter Mode) - -**Amount:** ${amount} AP3X (${lovelaceAmount} lovelace) -**To:** ${recipientAddress} -**Estimated Fee:** ${lovelaceToAda(fee)} AP3X - -**Unsigned TX CBOR:** -\`\`\` -${txCbor} -\`\`\` - -This transaction has NOT been submitted. Sign and submit it separately.`, - }], - }; - } - - const result = await sendAda(recipientAddress, amount, mnemonic, metadata); + const lucid = await lucidForAddress(makeProvider(), changeAddress); + const result = await buildSendApex(lucid, { recipientAddress, amountApex: amount, metadataJson: metadata }); return { content: [{ type: "text", - text: `# AP3X Transaction Successful + text: `# Unsigned Transaction Built -Transaction Hash: ${result.txHash} -From: ${result.senderAddress} -To: ${result.recipientAddress} -Amount: ${result.amount} AP3X +**Amount:** ${amount} AP3X +**To:** ${recipientAddress} +**Estimated Fee:** ${result.feeAda} AP3X +**Tx Hash:** ${result.txHash} -[View on Explorer](${result.links.explorer})`, +**Unsigned TX CBOR:** +\`\`\` +${result.txCbor} +\`\`\` + +Non-custodial flow: 1) sign locally with vector_signer_sign { txCbor } — your keys never leave your machine; 2) broadcast with vector_submit_transaction { signedTxCbor }; 3) confirm with vector_await_transaction { txHash }.`, }], }; } catch (err) { @@ -734,93 +372,56 @@ Amount: ${result.amount} AP3X return { content: [{ type: "text", - text: `Failed to send AP3X: ${error.message} + text: `Failed to build transaction: ${error.message} **Troubleshooting Tips:** -1. Check that your wallet has sufficient balance +1. Verify the change address is your funded wallet address (addr1...) 2. Verify the recipient address is correct (addr1...) -3. Check spend limits with vector_get_spend_limits -4. Verify the Ogmios endpoint is reachable`, +3. Ensure the wallet has enough AP3X for the amount plus fees`, }], }; } } ); - // vector_send_tokens - Send native tokens with safety limits (or craft unsigned TX) + // vector_build_send_tokens - build an UNSIGNED native-token transfer (keyless) 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.", + "vector_build_send_tokens", + "Build an UNSIGNED transaction sending Vector native tokens. Takes no key material - sign the returned CBOR with your local signer (vector_signer_sign), then broadcast with vector_submit_transaction.", { + changeAddress: z.string().describe("Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address."), recipientAddress: z.string().describe("Recipient Vector address (addr1...)"), - policyId: z.string().describe("Token policy ID"), - assetName: z.string().describe("Asset name (can be empty for policy-only tokens)"), + policyId: z.string().describe("Token policy ID (56 hex characters)"), + assetName: z.string().describe("Asset name (text or hex; can be empty for policy-only tokens)"), amount: z.string().describe("Amount of tokens to send"), - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the sending wallet"), - adaAmount: z.number().optional().describe("Optional AP3X to include (uses minimum required if not specified)"), - unsigned_only: z.boolean().optional().default(false).describe("If true, return unsigned TX CBOR without signing or submitting"), + adaAmount: z.number().optional().describe("Optional AP3X to include with the tokens (defaults to the 2 AP3X minimum)"), }, - async ({ recipientAddress, policyId, assetName, amount, mnemonic, adaAmount, unsigned_only }) => { + async ({ changeAddress, recipientAddress, policyId, assetName, amount, adaAmount }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - if (unsigned_only) { - const lucid = await initLucid(mnemonic); - let assetNameHex = assetName; - if (assetName && !/^[0-9a-fA-F]+$/.test(assetName)) { - assetNameHex = fromText(assetName); - } - const unit = `${policyId}${assetNameHex}`; - const outputLovelace = adaAmount - ? BigInt(Math.floor(adaAmount * 1_000_000)) - : BigInt(2_000_000); - const tx = await lucid.newTx() - .pay.ToAddress(recipientAddress, { lovelace: outputLovelace, [unit]: BigInt(amount) }) - .complete(); - const txJson = (tx as any).toJSON?.() ?? {}; - const fee = txJson?.body?.fee ?? '0'; - const txCbor = tx.toCBOR(); - return { - content: [{ - type: "text", - text: `# Unsigned Token Transaction (Transaction-Crafter Mode) - -**Token:** ${formatAssetName(assetNameHex) || policyId.substring(0,8) + '...'} -**Amount:** ${amount} -**To:** ${recipientAddress} -**Included AP3X:** ${lovelaceToAda(outputLovelace)} AP3X -**Estimated Fee:** ${lovelaceToAda(fee)} AP3X - -**Unsigned TX CBOR:** -\`\`\` -${txCbor} -\`\`\` - -This transaction has NOT been submitted. Sign and submit it separately.`, - }], - }; - } - - const result = await sendTokens(recipientAddress, policyId, assetName, amount, mnemonic, adaAmount); + const lucid = await lucidForAddress(makeProvider(), changeAddress); + const result = await buildSendTokens(lucid, { recipientAddress, policyId, assetName, amount, apexAmount: adaAmount }); + const displayName = assetName && !/^[0-9a-fA-F]+$/.test(assetName) ? assetName : (formatAssetName(assetName) || `${policyId.substring(0, 8)}...`); return { content: [{ type: "text", - text: `# Token Transaction Successful + text: `# Unsigned Token Transaction Built -Transaction Hash: ${result.txHash} -From: ${result.senderAddress} -To: ${result.recipientAddress} +**Token:** ${displayName} +**Amount:** ${amount} +**To:** ${recipientAddress} +**Estimated Fee:** ${result.feeAda} AP3X +**Tx Hash:** ${result.txHash} -Token Details: -- Policy ID: ${result.token.policyId} -- Asset Name: ${result.token.name || '(none)'} -- Amount: ${result.token.amount} +**Unsigned TX CBOR:** +\`\`\` +${result.txCbor} +\`\`\` -Included AP3X: ${result.ada} AP3X - -[View on Explorer](${result.links.explorer})`, +Non-custodial flow: 1) sign locally with vector_signer_sign { txCbor }; 2) broadcast with vector_submit_transaction { signedTxCbor }; 3) confirm with vector_await_transaction { txHash }.`, }], }; } catch (err) { @@ -828,13 +429,12 @@ Included AP3X: ${result.ada} AP3X return { content: [{ type: "text", - text: `Failed to send tokens: ${error.message} + text: `Failed to build token transaction: ${error.message} **Troubleshooting Tips:** -1. Check that your wallet has sufficient AP3X and token balance -2. Verify the policy ID and asset name are correct -3. Verify the recipient address is correct (addr1...) -4. Check spend limits with vector_get_spend_limits`, +1. Check that the wallet at changeAddress holds the token and enough AP3X +2. Verify the policy ID (56 hex chars) and asset name are correct +3. Verify the recipient address is correct (addr1...)`, }], }; } @@ -885,57 +485,44 @@ ${log.length > 0 ? `## Recent Transactions (last ${Math.min(5, log.length)}):\n$ } ); - // vector_build_transaction - Build a complex multi-output transaction + // vector_build_transaction - build an UNSIGNED multi-output transaction (keyless) 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.", + "Build an UNSIGNED multi-output transaction with optional metadata. Takes no key material and never submits - sign the returned CBOR with your local signer (vector_signer_sign), then broadcast with vector_submit_transaction.", { + changeAddress: z.string().describe("Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address."), outputs: z.array(z.object({ address: z.string().describe("Recipient Vector address"), lovelace: z.number().describe("Amount in lovelace (1 AP3X = 1,000,000 lovelace)"), assets: z.record(z.string(), z.string()).optional().describe("Optional native assets: { 'policyId+assetNameHex': 'quantity' }"), })).min(1).describe("Transaction outputs"), - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the signing wallet"), 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, mnemonic, metadata, submit }) => { + async ({ changeAddress, outputs, metadata }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - const result = await buildTransaction(outputs, mnemonic, metadata, submit); - - if (result.submitted) { - return { - content: [{ - type: "text", - text: `# Transaction Submitted - -Transaction Hash: ${result.txHash} -Fee: ${result.feeAda} AP3X -Outputs: ${result.outputCount} -Total AP3X Sent: ${result.totalAda} AP3X - -[View on Explorer](${result.links?.explorer})`, - }], - }; - } - + const lucid = await lucidForAddress(makeProvider(), changeAddress); + const result = await buildMultiOutput(lucid, { outputs, metadataJson: metadata }); + const totalLovelace = outputs.reduce((sum, o) => sum + o.lovelace, 0); return { content: [{ type: "text", - text: `# Transaction Built (Not Submitted) + text: `# Unsigned Transaction Built (Not Submitted) -Transaction Hash: ${result.txHash} -Fee: ${result.feeAda} AP3X -Outputs: ${result.outputCount} -Total AP3X: ${result.totalAda} AP3X +**Tx Hash:** ${result.txHash} +**Fee:** ${result.feeAda} AP3X +**Outputs:** ${outputs.length} +**Total AP3X:** ${lovelaceToAda(totalLovelace)} AP3X -CBOR (hex): ${result.txCbor.substring(0, 200)}${result.txCbor.length > 200 ? '...' : ''} +**Unsigned TX CBOR:** +\`\`\` +${result.txCbor} +\`\`\` -Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction again with submit=true to submit.`, +Use vector_dry_run with this CBOR to simulate it, sign it locally with vector_signer_sign, then broadcast with vector_submit_transaction.`, }], }; } catch (err) { @@ -947,8 +534,7 @@ Use vector_dry_run with this CBOR to simulate, or call vector_build_transaction **Troubleshooting Tips:** 1. Verify all recipient addresses are valid (addr1...) -2. Ensure wallet has enough AP3X for outputs + fees -3. Check spend limits with vector_get_spend_limits`, +2. Ensure the wallet at changeAddress has enough AP3X for outputs + fees`, }], }; } @@ -1251,44 +837,42 @@ ${txList} } ); - // vector_deploy_contract - Deploy a Plutus/Aiken smart contract + // vector_build_deploy_contract - build an UNSIGNED contract deployment (keyless) server.tool( - "vector_deploy_contract", - "Deploy a Plutus/Aiken smart contract to Vector by sending funds to its script address", + "vector_build_deploy_contract", + "Build an UNSIGNED transaction deploying a Plutus/Aiken smart contract (locks funds at its script address). Takes no key material - sign with your local signer, then broadcast with vector_submit_transaction.", { + changeAddress: z.string().describe("Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address."), scriptCbor: z.string().describe("Compiled Plutus/Aiken script in CBOR hex format"), scriptType: z.enum(["PlutusV1", "PlutusV2", "PlutusV3"]).describe("Script version"), - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the deploying wallet"), 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("AP3X to lock at the script address in lovelace (default: 2,000,000 = 2 AP3X)"), }, - async ({ scriptCbor, scriptType, mnemonic, initialDatum, lovelaceAmount }) => { + async ({ changeAddress, scriptCbor, scriptType, initialDatum, lovelaceAmount }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - const result = await deployContract( - scriptCbor, - scriptType, - mnemonic, - initialDatum || null, - lovelaceAmount || 2_000_000, - ); - + const lucid = await lucidForAddress(makeProvider(), changeAddress); + const result = await buildDeployContract(lucid, { scriptCbor, scriptType, initialDatum, lovelaceAmount }); return { content: [{ type: "text", - text: `# Smart Contract Deployed + text: `# Unsigned Contract Deployment Built -Transaction Hash: ${result.txHash} -Script Address: ${result.scriptAddress} -Script Hash: ${result.scriptHash} -Script Type: ${result.scriptType} +**Script Address:** ${result.scriptAddress} +**Script Hash:** ${result.scriptHash} +**Script Type:** ${result.scriptType} +**Estimated Fee:** ${result.feeAda} AP3X +**Tx Hash:** ${result.txHash} -Funds locked at the script address. Use vector_interact_contract to interact with this contract. +**Unsigned TX CBOR:** +\`\`\` +${result.txCbor} +\`\`\` -[View on Explorer](${result.links.explorer})`, +Sign locally with vector_signer_sign, broadcast with vector_submit_transaction, then use vector_build_interact_contract to interact with the deployed contract.`, }], }; } catch (err) { @@ -1296,28 +880,27 @@ Funds locked at the script address. Use vector_interact_contract to interact wit return { content: [{ type: "text", - text: `Failed to deploy contract: ${error.message} + text: `Failed to build contract deployment: ${error.message} **Troubleshooting Tips:** 1. Verify the script CBOR is valid hex (compiled Aiken or Plutus output) -2. Ensure wallet has sufficient AP3X 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`, +2. Ensure the wallet at changeAddress has sufficient AP3X for the locked amount + fees +3. Check that the script type matches the compiled version (PlutusV1/V2/V3)`, }], }; } } ); - // vector_interact_contract - Interact with a deployed smart contract + // vector_build_interact_contract - build an UNSIGNED contract interaction (keyless) server.tool( - "vector_interact_contract", - "Interact with a deployed Plutus/Aiken smart contract - lock AP3X or spend from it", + "vector_build_interact_contract", + "Build an UNSIGNED transaction interacting with a deployed Plutus/Aiken contract - lock AP3X at it or spend from it. Takes no key material - sign with your local signer, then broadcast with vector_submit_transaction.", { + changeAddress: z.string().describe("Your wallet address (source of funds, change, and the required signer for spends). Get it from your local signer's vector_signer_get_address."), 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"), - mnemonic: z.string().describe("15 or 24-word BIP39 mnemonic for the wallet"), 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 AP3X)"), @@ -1327,38 +910,33 @@ Funds locked at the script address. Use vector_interact_contract to interact wit }).optional().describe("Specific UTxO to spend from (optional, otherwise spends all UTxOs at script address)"), assets: z.record(z.string(), z.string()).optional().describe("Additional native assets for lock action: { 'policyId+assetNameHex': 'quantity' }"), }, - async ({ scriptCbor, scriptType, action, mnemonic, redeemer, datum, lovelaceAmount, utxoRef, assets }) => { + async ({ changeAddress, scriptCbor, scriptType, action, redeemer, datum, lovelaceAmount, utxoRef, assets }) => { const rateCheck = rateLimiter.check(); if (!rateCheck.allowed) { return { content: [{ type: "text", text: `Rate limit exceeded. Retry after ${rateCheck.retryAfterMs}ms.` }] }; } try { - const result = await interactWithContract( - scriptCbor, - scriptType, - action, - mnemonic, - redeemer || null, - datum || null, - lovelaceAmount || 2_000_000, - utxoRef || null, - assets || null, - ); - - const actionVerb = result.action === 'spend' ? 'collected from' : 'locked at'; - + const lucid = await lucidForAddress(makeProvider(), changeAddress); + const result = await buildInteractContract(lucid, { + scriptCbor, scriptType, action, changeAddress, redeemer, datum, lovelaceAmount, utxoRef, assets, + }); + const actionVerb = result.action === 'spend' ? 'collects from' : 'locks funds at'; return { content: [{ type: "text", - text: `# Contract Interaction Successful + text: `# Unsigned Contract Interaction Built -Transaction Hash: ${result.txHash} -Action: ${result.action} -Script Address: ${result.scriptAddress} +**Action:** ${result.action} (${actionVerb} the script address) +**Script Address:** ${result.scriptAddress} +**Estimated Fee:** ${result.feeAda} AP3X +**Tx Hash:** ${result.txHash} -Funds ${actionVerb} the script address. +**Unsigned TX CBOR:** +\`\`\` +${result.txCbor} +\`\`\` -[View on Explorer](${result.links.explorer})`, +Sign locally with vector_signer_sign { txCbor }, then broadcast with vector_submit_transaction { signedTxCbor }.`, }], }; } catch (err) { @@ -1366,14 +944,13 @@ Funds ${actionVerb} the script address. return { content: [{ type: "text", - text: `Failed to interact with contract: ${error.message} + text: `Failed to build contract interaction: ${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 AP3X and the datum matches the script's expectations -3. Spending requires collateral - ensure wallet has a pure-AP3X UTxO (no native tokens) -4. Verify the script CBOR matches the deployed script exactly -5. Check spend limits with vector_get_spend_limits`, +2. For 'lock': ensure the wallet at changeAddress has sufficient AP3X and the datum matches the script's expectations +3. Spending requires collateral - ensure the wallet has a pure-AP3X UTxO (no native tokens) +4. Verify the script CBOR matches the deployed script exactly`, }], }; } diff --git a/packages/builder/test/smoke/tool-inventory.test.ts b/packages/builder/test/smoke/tool-inventory.test.ts index ffcf808..4dfe1e9 100644 --- a/packages/builder/test/smoke/tool-inventory.test.ts +++ b/packages/builder/test/smoke/tool-inventory.test.ts @@ -15,8 +15,11 @@ import { startServer, stopServer, ServerContext } from '../setup.ts'; */ const EXPECTED_TOOLS = [ 'vector_await_transaction', + 'vector_build_deploy_contract', + 'vector_build_interact_contract', + 'vector_build_send_apex', + 'vector_build_send_tokens', 'vector_build_transaction', - 'vector_deploy_contract', 'vector_deregister_agent', 'vector_discover_agents', 'vector_dry_run', @@ -26,7 +29,6 @@ const EXPECTED_TOOLS = [ 'vector_get_spend_limits', 'vector_get_transaction_history', 'vector_get_utxos', - 'vector_interact_contract', 'vector_message_agent', 'vector_register_agent', 'vector_self_improvement_analyze_metrics', @@ -34,8 +36,6 @@ const EXPECTED_TOOLS = [ 'vector_self_improvement_critique', 'vector_self_improvement_endorse', 'vector_self_improvement_submit_proposal', - 'vector_send_apex', - 'vector_send_tokens', 'vector_submit_transaction', 'vector_transfer_agent', 'vector_update_agent', diff --git a/packages/builder/test/smoke/tool-schemas.snapshot.json b/packages/builder/test/smoke/tool-schemas.snapshot.json index e6de772..1dda661 100644 --- a/packages/builder/test/smoke/tool-schemas.snapshot.json +++ b/packages/builder/test/smoke/tool-schemas.snapshot.json @@ -18,9 +18,189 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, + "vector_build_deploy_contract": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address.", + "type": "string" + }, + "scriptCbor": { + "description": "Compiled Plutus/Aiken script in CBOR hex format", + "type": "string" + }, + "scriptType": { + "description": "Script version", + "type": "string", + "enum": [ + "PlutusV1", + "PlutusV2", + "PlutusV3" + ] + }, + "initialDatum": { + "description": "Initial datum as CBOR hex. Use 'd87980' for void/unit datum. Defaults to void if omitted.", + "type": "string" + }, + "lovelaceAmount": { + "description": "AP3X to lock at the script address in lovelace (default: 2,000,000 = 2 AP3X)", + "type": "number" + } + }, + "required": [ + "changeAddress", + "scriptCbor", + "scriptType" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "vector_build_interact_contract": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (source of funds, change, and the required signer for spends). Get it from your local signer's vector_signer_get_address.", + "type": "string" + }, + "scriptCbor": { + "description": "Compiled Plutus/Aiken script in CBOR hex", + "type": "string" + }, + "scriptType": { + "description": "Script version", + "type": "string", + "enum": [ + "PlutusV1", + "PlutusV2", + "PlutusV3" + ] + }, + "action": { + "description": "'spend' to collect UTxOs from the script, 'lock' to send funds to it", + "type": "string", + "enum": [ + "spend", + "lock" + ] + }, + "redeemer": { + "description": "Redeemer as CBOR hex (required for spend, use 'd87980' for void)", + "type": "string" + }, + "datum": { + "description": "Datum as CBOR hex (required for lock, use 'd87980' for void)", + "type": "string" + }, + "lovelaceAmount": { + "description": "Lovelace to lock (for lock action, default: 2,000,000 = 2 AP3X)", + "type": "number" + }, + "utxoRef": { + "description": "Specific UTxO to spend from (optional, otherwise spends all UTxOs at script address)", + "type": "object", + "properties": { + "txHash": { + "type": "string" + }, + "outputIndex": { + "type": "number" + } + }, + "required": [ + "txHash", + "outputIndex" + ] + }, + "assets": { + "description": "Additional native assets for lock action: { 'policyId+assetNameHex': 'quantity' }", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "changeAddress", + "scriptCbor", + "scriptType", + "action" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "vector_build_send_apex": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address.", + "type": "string" + }, + "recipientAddress": { + "description": "Recipient Vector address (addr1...)", + "type": "string" + }, + "amount": { + "description": "Amount of AP3X to send", + "type": "number", + "minimum": 1 + }, + "metadata": { + "description": "Optional transaction metadata in JSON format", + "type": "string" + } + }, + "required": [ + "changeAddress", + "recipientAddress", + "amount" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "vector_build_send_tokens": { + "type": "object", + "properties": { + "changeAddress": { + "description": "Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address.", + "type": "string" + }, + "recipientAddress": { + "description": "Recipient Vector address (addr1...)", + "type": "string" + }, + "policyId": { + "description": "Token policy ID (56 hex characters)", + "type": "string" + }, + "assetName": { + "description": "Asset name (text or hex; can be empty for policy-only tokens)", + "type": "string" + }, + "amount": { + "description": "Amount of tokens to send", + "type": "string" + }, + "adaAmount": { + "description": "Optional AP3X to include with the tokens (defaults to the 2 AP3X minimum)", + "type": "number" + } + }, + "required": [ + "changeAddress", + "recipientAddress", + "policyId", + "assetName", + "amount" + ], + "$schema": "http://json-schema.org/draft-07/schema#" + }, "vector_build_transaction": { "type": "object", "properties": { + "changeAddress": { + "description": "Your wallet address (source of funds; receives change). Get it from your local signer's vector_signer_get_address.", + "type": "string" + }, "outputs": { "description": "Transaction outputs", "minItems": 1, @@ -53,58 +233,14 @@ ] } }, - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the signing wallet", - "type": "string" - }, "metadata": { "description": "Optional JSON metadata (attached under label 674)", "type": "string" - }, - "submit": { - "description": "If true, sign and submit the transaction. If false/omitted, return unsigned CBOR for review.", - "type": "boolean" } }, "required": [ - "outputs", - "mnemonic" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "vector_deploy_contract": { - "type": "object", - "properties": { - "scriptCbor": { - "description": "Compiled Plutus/Aiken script in CBOR hex format", - "type": "string" - }, - "scriptType": { - "description": "Script version", - "type": "string", - "enum": [ - "PlutusV1", - "PlutusV2", - "PlutusV3" - ] - }, - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the deploying wallet", - "type": "string" - }, - "initialDatum": { - "description": "Initial datum as CBOR hex. Use 'd87980' for void/unit datum. Defaults to void if omitted.", - "type": "string" - }, - "lovelaceAmount": { - "description": "AP3X to lock at the script address in lovelace (default: 2,000,000 = 2 AP3X)", - "type": "number" - } - }, - "required": [ - "scriptCbor", - "scriptType", - "mnemonic" + "changeAddress", + "outputs" ], "$schema": "http://json-schema.org/draft-07/schema#" }, @@ -276,81 +412,6 @@ }, "$schema": "http://json-schema.org/draft-07/schema#" }, - "vector_interact_contract": { - "type": "object", - "properties": { - "scriptCbor": { - "description": "Compiled Plutus/Aiken script in CBOR hex", - "type": "string" - }, - "scriptType": { - "description": "Script version", - "type": "string", - "enum": [ - "PlutusV1", - "PlutusV2", - "PlutusV3" - ] - }, - "action": { - "description": "'spend' to collect UTxOs from the script, 'lock' to send funds to it", - "type": "string", - "enum": [ - "spend", - "lock" - ] - }, - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the wallet", - "type": "string" - }, - "redeemer": { - "description": "Redeemer as CBOR hex (required for spend, use 'd87980' for void)", - "type": "string" - }, - "datum": { - "description": "Datum as CBOR hex (required for lock, use 'd87980' for void)", - "type": "string" - }, - "lovelaceAmount": { - "description": "Lovelace to lock (for lock action, default: 2,000,000 = 2 AP3X)", - "type": "number" - }, - "utxoRef": { - "description": "Specific UTxO to spend from (optional, otherwise spends all UTxOs at script address)", - "type": "object", - "properties": { - "txHash": { - "type": "string" - }, - "outputIndex": { - "type": "number" - } - }, - "required": [ - "txHash", - "outputIndex" - ] - }, - "assets": { - "description": "Additional native assets for lock action: { 'policyId+assetNameHex': 'quantity' }", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "scriptCbor", - "scriptType", - "action", - "mnemonic" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, "vector_message_agent": { "type": "object", "properties": { @@ -655,81 +716,6 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "vector_send_apex": { - "type": "object", - "properties": { - "recipientAddress": { - "description": "Recipient Vector address (addr1...)", - "type": "string" - }, - "amount": { - "description": "Amount of AP3X to send", - "type": "number", - "minimum": 1 - }, - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the sending wallet", - "type": "string" - }, - "metadata": { - "description": "Optional transaction metadata in JSON format", - "type": "string" - }, - "unsigned_only": { - "description": "If true, return unsigned TX CBOR without signing or submitting", - "default": false, - "type": "boolean" - } - }, - "required": [ - "recipientAddress", - "amount", - "mnemonic" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, - "vector_send_tokens": { - "type": "object", - "properties": { - "recipientAddress": { - "description": "Recipient Vector address (addr1...)", - "type": "string" - }, - "policyId": { - "description": "Token policy ID", - "type": "string" - }, - "assetName": { - "description": "Asset name (can be empty for policy-only tokens)", - "type": "string" - }, - "amount": { - "description": "Amount of tokens to send", - "type": "string" - }, - "mnemonic": { - "description": "15 or 24-word BIP39 mnemonic for the sending wallet", - "type": "string" - }, - "adaAmount": { - "description": "Optional AP3X to include (uses minimum required if not specified)", - "type": "number" - }, - "unsigned_only": { - "description": "If true, return unsigned TX CBOR without signing or submitting", - "default": false, - "type": "boolean" - } - }, - "required": [ - "recipientAddress", - "policyId", - "assetName", - "amount", - "mnemonic" - ], - "$schema": "http://json-schema.org/draft-07/schema#" - }, "vector_submit_transaction": { "type": "object", "properties": {