diff --git a/src/components/dashboard/TransactionBuilder.jsx b/src/components/dashboard/TransactionBuilder.jsx index a16f466b..5be10f59 100644 --- a/src/components/dashboard/TransactionBuilder.jsx +++ b/src/components/dashboard/TransactionBuilder.jsx @@ -7,6 +7,7 @@ import { getCachedUserTransactionTemplates, upsertUserTransactionTemplate, } from "../../lib/transactionTemplateVault.ts"; +import { fetchContractData } from "../../lib/stellar"; import { Copy, Play, Download, AlertCircle, CheckCircle, ArrowDown, GripVertical, Trash2, Plus, Zap } from "lucide-react"; function Panel({ title, subtitle, children }) { @@ -153,6 +154,11 @@ export default function TransactionBuilder() { const [isSimulating, setIsSimulating] = useState(false); const [showXDR, setShowXDR] = useState(false); const [draggedIndex, setDraggedIndex] = useState(null); + const [inspectContractId, setInspectContractId] = useState(""); + const [inspectContractKey, setInspectContractKey] = useState(""); + const [inspectContractData, setInspectContractData] = useState(null); + const [inspectContractLoading, setInspectContractLoading] = useState(false); + const [inspectContractError, setInspectContractError] = useState(""); function addOperation() { setOperations([ @@ -208,6 +214,27 @@ export default function TransactionBuilder() { loadTemplate(selectedTemplateId); setSelectedTemplateId(null); }, [selectedTemplateId]); + + async function handleFetchContractData() { + if (!inspectContractId.trim()) return; + + setInspectContractError(""); + setInspectContractData(null); + setInspectContractLoading(true); + + try { + const data = await fetchContractData( + inspectContractId, + inspectContractKey, + network + ); + setInspectContractData(data); + } catch (error) { + setInspectContractError(error.message || "Failed to fetch contract data"); + } finally { + setInspectContractLoading(false); + } + } // Drag and drop handlers function handleDragStart(index) { @@ -645,6 +672,112 @@ export default function TransactionBuilder() { ); + case "invokeHostFunction": { + const args = op.params.args || []; + return ( + <> + + + updateOperation(op.id, "contractId", e.target.value) + } + placeholder="C... contract address" + style={textInputStyle(hasErrors)} + /> + + + + updateOperation(op.id, "functionName", e.target.value) + } + placeholder="increment" + style={textInputStyle()} + /> + +
+
+ + + +
+ {args.map((arg, idx) => ( +
+ + { + const newArgs = [...args]; + newArgs[idx] = { ...arg, value: e.target.value }; + updateOperation(op.id, "args", newArgs); + }} + placeholder={arg.type === "bool" ? "true/false" : "Argument value"} + style={textInputStyle()} + /> + +
+ ))} +
+ + ); + } + default: return (
@@ -715,6 +848,90 @@ export default function TransactionBuilder() {
+ {/* Contract State Inspection */} + +
+
+ + setInspectContractId(e.target.value)} + placeholder="C... contract address" + style={textInputStyle()} + /> + +
+
+ + setInspectContractKey(e.target.value)} + placeholder="Storage key (string or JSON)" + style={textInputStyle()} + /> + +
+
+ +
+
+ {inspectContractError && ( +
+ {inspectContractError} +
+ )} + {inspectContractData && ( +
+
+
+ Key +
+
+                {JSON.stringify(inspectContractData.key, null, 2)}
+              
+
+
+
+ Value +
+
+                {JSON.stringify(inspectContractData.value, null, 2)}
+              
+
+
+ )} +
+ {/* Transaction Settings */}
diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts index 6f7b7456..7226e520 100644 --- a/src/lib/stellar.ts +++ b/src/lib/stellar.ts @@ -914,6 +914,40 @@ export async function fetchContractInfo( } } +export async function fetchContractData( + contractId: string, + key: StellarSdk.xdr.ScVal | string, + network: NetworkName = 'testnet', + durability: StellarSdk.SorobanRpc.Durability = StellarSdk.SorobanRpc.Durability.Persistent +): Promise { + const server = getSorobanServer(network); + + let scValKey; + if (typeof key === "string") { + try { + // Try to parse from JSON first + const parsed = JSON.parse(key); + scValKey = StellarSdk.nativeToScVal(parsed); + } catch { + // If JSON fails, treat as string + scValKey = StellarSdk.nativeToScVal(key, { type: "string" }); + } + } else { + scValKey = key; + } + + try { + const result = await server.getContractData(contractId, scValKey, durability); + return { + key: StellarSdk.scValToNative(result.key), + value: StellarSdk.scValToNative(result.val), + xdr: result.xdr + }; + } catch (e) { + throw new Error(`Failed to fetch contract data: ${(e as Error).message}`); + } +} + export interface ContractInvocationArg { type: 'string' | 'int' | 'address' | 'bool'; value: string; diff --git a/src/lib/transactionBuilder.js b/src/lib/transactionBuilder.js index 8fdd81ed..2a0ceb3e 100644 --- a/src/lib/transactionBuilder.js +++ b/src/lib/transactionBuilder.js @@ -23,6 +23,8 @@ export const OPERATION_TYPES = [ // Fee-bump, sponsorship, and clawback operations (#196) { value: "feeBump", label: "Fee-Bump Transaction" }, { value: "clawback", label: "Clawback" }, + // Contract invocation + { value: "invokeHostFunction", label: "Invoke Host Function (Contract Call)" }, ]; export function createOperation(type, params) { @@ -201,6 +203,25 @@ export function createOperation(type, params) { return op; } + case "invokeHostFunction": { + const contract = new StellarSdk.Contract(params.contractId); + const args = (params.args || []).map(arg => { + switch (arg.type) { + case "string": + return StellarSdk.nativeToScVal(arg.value, { type: "string" }); + case "int": + return StellarSdk.nativeToScVal(BigInt(arg.value), { type: "i128" }); + case "address": + return StellarSdk.Address.fromString(arg.value).toScVal(); + case "bool": + return StellarSdk.nativeToScVal(arg.value === "true", { type: "bool" }); + default: + throw new Error(`Unsupported argument type: ${arg.type}`); + } + }); + return contract.call(params.functionName, ...args); + } + default: throw new Error(`Unsupported operation type: ${type}`); }