UNPKG

stellar-mcp-cli

Version:

CLI tool that transforms any Soroban Smart Contract into an AI-ready Model Context Protocol (MCP) server

193 lines (169 loc) 5.6 kB
import { Address, nativeToScVal, xdr, rpc as SorobanRpc, TransactionBuilder, contract } from '@stellar/stellar-sdk'; import { Client as SacClient } from 'sac-sdk'; export const createContractClient = async (contractId: string, networkPassphrase: string, rpcUrl: string): Promise<contract.Client> => { return contract.Client.from({ contractId, networkPassphrase, rpcUrl }) } export const createSACClient = async (contractId: string, networkPassphrase: string, rpcUrl: string): Promise<SacClient> => { return new SacClient({ contractId, rpcUrl, networkPassphrase, }) } /** * Convert a string to a Symbol ScVal */ export const stringToSymbol = (val: string) => { return nativeToScVal(val, { type: "symbol" }); }; /** * Convert a number to a u64 ScVal with 2 decimal precision */ export const numberToU64 = (val: number) => { const num = parseInt((val * 100).toFixed(0)); return nativeToScVal(num, { type: "u64" }); }; /** * Convert a number to an i128 ScVal with 2 decimal precision */ export const numberToI128 = (val: number) => { const num = parseInt((val * 100).toFixed(0)); return nativeToScVal(num, { type: "i128" }); }; /** * Convert a Stellar address to ScVal */ export function addressToScVal(addressStr: string) { // Validate and convert the address const address = Address.fromString(addressStr); // Convert to ScVal return nativeToScVal(address); } /** * Convert a string to an i128 ScVal * This is useful for handling large numbers that exceed JavaScript's number precision */ export function i128ToScVal(value: string) { return nativeToScVal(value, { type: "i128" }); } /** * Convert a string to a u128 ScVal * This is useful for handling large numbers that exceed JavaScript's number precision */ export function u128ToScVal(value: string) { return nativeToScVal(value, { type: "u128" }); } /** * Convert a boolean to an ScVal */ export function boolToScVal(value: boolean) { return xdr.ScVal.scvBool(value); } /** * Convert a number to a u32 ScVal */ export function u32ToScVal(value: number) { return xdr.ScVal.scvU32(value); } /** * Configuration for transaction submission */ export interface SubmitTransactionConfig { server: SorobanRpc.Server; networkPassphrase: string; maxRetries?: number; pollingIntervalMs?: number; } /** * Submit a transaction and wait for its completion */ export async function submitTransaction( signedXdr: string, config: SubmitTransactionConfig ): Promise<{ status: 'SUCCESS' | 'FAILED' | 'TIMEOUT', hash?: string, resultXdr?: string, resultMetaXdr?: string, response?: any }> { const { server, networkPassphrase, maxRetries = 10, pollingIntervalMs = 2000 } = config; try { // Reconstruct and submit the transaction const signedTx = TransactionBuilder.fromXDR(signedXdr, networkPassphrase); const submittedTx = await server.sendTransaction(signedTx); console.log('Transaction submitted:', submittedTx.hash); // Wait for transaction completion let retries = 0; while (retries < maxRetries) { try { const tx = await server.getTransaction(submittedTx.hash); if (tx.status === "SUCCESS") { return { status: 'SUCCESS', hash: submittedTx.hash, resultXdr: typeof tx.resultXdr === 'string' ? tx.resultXdr : undefined, resultMetaXdr: typeof tx.resultMetaXdr === 'string' ? tx.resultMetaXdr : undefined, response: tx }; } else if (tx.status === "FAILED") { return { status: 'FAILED', hash: submittedTx.hash, resultXdr: typeof tx.resultXdr === 'string' ? tx.resultXdr : undefined, resultMetaXdr: typeof tx.resultMetaXdr === 'string' ? tx.resultMetaXdr : undefined, response: tx }; } await new Promise(resolve => setTimeout(resolve, pollingIntervalMs)); retries++; } catch (error: any) { console.error('Error checking transaction status:', error.message); // Check for XDR parsing error (Bad union switch) if (error.message.includes('Bad union switch')) { // Even though we can't parse the response, the transaction might still be processing // We should return a TIMEOUT status to avoid blocking indefinitely await new Promise(resolve => setTimeout(resolve, pollingIntervalMs)); retries++; // After some retries, just return the transaction hash for manual checking if (retries >= maxRetries / 2) { return { status: 'TIMEOUT', hash: submittedTx.hash, response: { message: "Transaction submitted but status check failed due to XDR parsing error" } }; } continue; } if (error.message.includes("404") || error.message.includes("NOT_FOUND")) { await new Promise(resolve => setTimeout(resolve, pollingIntervalMs)); retries++; continue; } throw error; } } return { status: 'TIMEOUT', hash: submittedTx.hash }; } catch (error: any) { console.error('Submit transaction error:', error); throw new Error(`Transaction submission failed: ${error.message}`); } } /** * Replace bigint values with their string representation */ export const replacer = (key: string, value: any) => { return typeof value === 'bigint' ? `${value}n` : value; };