diff --git a/Dockerfile b/Dockerfile index daa2224..3b80e17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Start from a rust base image -FROM rust:1.76.0 as base +FROM rust:1.86.0 as base # Set the current directory WORKDIR /app @@ -20,13 +20,16 @@ RUN cargo install cargo-make RUN apt-get --yes update RUN apt-get --yes upgrade ENV NVM_DIR /usr/local/nvm -ENV NODE_VERSION v18.16.1 +ENV NODE_VERSION v22.14.0 RUN mkdir -p /usr/local/nvm && apt-get update && echo "y" | apt-get install curl RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION" ENV NODE_PATH $NVM_DIR/versions/node/$NODE_VERSION/bin ENV PATH $NODE_PATH:$PATH +# Create public directory if it doesn't exist +RUN mkdir -p /app/packages/frontend/public + # Install dependencies RUN cargo make deps-wasm RUN cargo make deps-npm @@ -35,6 +38,7 @@ RUN cargo make deps-npm RUN cargo make build-server RUN cargo make build-bindings RUN cargo make build-app +RUN cargo make build-frontend RUN cargo make build-backend @@ -45,8 +49,21 @@ FROM nestybox/ubuntu-jammy-systemd-docker:latest # Copy the built files COPY --from=builder /app/packages/app/dist /app/packages/app/dist +COPY --from=builder /app/packages/frontend/.next /app/packages/frontend/.next +COPY --from=builder /app/packages/frontend/public /app/packages/frontend/public +COPY --from=builder /app/packages/frontend/package.json /app/packages/frontend/package.json COPY --from=builder /app/target/release/backend /app/target/release/backend +# Install Node.js in the final image for running Next.js +RUN apt-get update && apt-get install -y curl +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION v22.14.0 +RUN mkdir -p /usr/local/nvm +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash +RUN /bin/bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm use --delete-prefix $NODE_VERSION" +ENV NODE_PATH $NVM_DIR/versions/node/$NODE_VERSION/bin +ENV PATH $NODE_PATH:$PATH + # Startup scripts COPY sysbox/on-start.sh /usr/bin RUN chmod +x /usr/bin/on-start.sh diff --git a/Makefile.toml b/Makefile.toml index 8db8519..2fcd8fa 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -14,7 +14,7 @@ npm install [tasks.deps-docker] script = ''' -docker pull ghcr.io/hyperledger/solang@sha256:8776a9bd756664f7bf8414710d1a799799bf6fedc1c8f9f0bda17e76749dea7a +docker pull ghcr.io/hyperledger/solang@sha256:e6f687910df5dd9d4f5285aed105ae0e6bcae912db43e8955ed4d8344d49785d ''' [tasks.deps] @@ -118,7 +118,7 @@ docker build -t solang-playground . script = ''' docker run \ --runtime=sysbox-runc \ - --name playground \ + --name playground-makee \ --detach \ --volume /tmp:/tmp \ --publish 9000:9000 \ diff --git a/crates/backend/src/services/sandbox.rs b/crates/backend/src/services/sandbox.rs index b11d1f1..6f2c5a6 100644 --- a/crates/backend/src/services/sandbox.rs +++ b/crates/backend/src/services/sandbox.rs @@ -14,7 +14,8 @@ use tokio::process::Command; use crate::services::{CompilationRequest, CompilationResult}; const TIMEOUT: Duration = Duration::from_secs(60); -const DOCKER_IMAGE_BASE_NAME: &str = "ghcr.io/hyperledger/solang"; +// const DOCKER_IMAGE_BASE_NAME: &str = "ghcr.io/hyperledger-solang/solang@sha256:e6f687910df5dd9d4f5285aed105ae0e6bcae912db43e8955ed4d8344d49785d"; +const DOCKER_IMAGE_BASE_NAME: &str = "ghcr.io/hyperledger-solang/solang:latest"; const DOCKER_WORKDIR: &str = "/builds/contract/"; const DOCKER_OUTPUT: &str = "/playground-result"; @@ -29,11 +30,12 @@ macro_rules! docker_command { /// Builds the compile command using solang docker image pub fn build_compile_command(input_file: &Path, output_dir: &Path) -> Command { + println!("ip file: {:?}\nop dir: {:?}", input_file, output_dir); // Base docker command let mut cmd = docker_command!( "run", "--detach", - "--rm", + // "--rm", "-it", "--cap-drop=ALL", "--cap-add=DAC_OVERRIDE", @@ -65,15 +67,12 @@ pub fn build_compile_command(input_file: &Path, output_dir: &Path) -> Command { cmd.arg("--volume").arg(&mount_output_dir); // Using the solang image - cmd.arg(format!( - "{}@sha256:8776a9bd756664f7bf8414710d1a799799bf6fedc1c8f9f0bda17e76749dea7a", - DOCKER_IMAGE_BASE_NAME - )); + cmd.arg(DOCKER_IMAGE_BASE_NAME); // Building the compile command let remove_command = format!("rm -rf {}*.wasm {}*.contract", DOCKER_OUTPUT, DOCKER_OUTPUT); let compile_command = format!( - "solang compile --target polkadot -o /playground-result {} > /playground-result/stdout.log 2> /playground-result/stderr.log", + "solang compile --target soroban -o /playground-result {} > /playground-result/stdout.log 2> /playground-result/stderr.log", file_name ); let sh_command = format!("{} && {}", remove_command, compile_command); @@ -102,7 +101,9 @@ impl Sandbox { fs::set_permissions(&output_dir, PermissionsExt::from_mode(0o777)) .context("failed to set output permissions")?; - + + File::create(&input_file).context("failed to create input file")?; + Ok(Sandbox { scratch, input_file, @@ -115,14 +116,15 @@ impl Sandbox { self.write_source_code(&req.source)?; let command = build_compile_command(&self.input_file, &self.output_dir); - println!("Executing command: \n{:#?}", command); + // println!("Executing command: \n{:#?}", command); let output = run_command(command)?; + println!("out: {:?}", output); let file = fs::read_dir(&self.output_dir) .context("failed to read output directory")? .flatten() .map(|entry| entry.path()) - .find(|path| path.extension() == Some(OsStr::new("contract"))); + .find(|path| path.extension() == Some(OsStr::new("wasm"))); // The file `stdout.log` is in the same directory as the contract file let compile_log_stdout_file_path = fs::read_dir(&self.output_dir) @@ -187,9 +189,16 @@ impl Sandbox { /// A helper function to write the source code to the input file fn write_source_code(&self, code: &str) -> Result<()> { + println!("writing to {:?}", self.input_file); fs::write(&self.input_file, code).context("failed to write source code")?; + match fs::read_to_string(&self.input_file) { + Ok(content) => println!("Successfully read: {:?}", content), + Err(e) => eprintln!("Error reading file: {}", e), + } fs::set_permissions(&self.input_file, PermissionsExt::from_mode(0o777)) .context("failed to set source permissions")?; + let s: String = code.chars().take(40).collect(); + println!("Code: {:?}", s); println!("Wrote {} bytes of source to {}", code.len(), self.input_file.display()); Ok(()) } @@ -197,6 +206,7 @@ impl Sandbox { /// Reads a file from the given path fn read(path: &Path) -> Result>> { + println!("reading: {:?}", path); let f = match File::open(path) { Ok(f) => f, Err(ref e) if e.kind() == ErrorKind::NotFound => return Ok(None), @@ -204,7 +214,7 @@ fn read(path: &Path) -> Result>> { }; let mut f = BufReader::new(f); let metadata = fs::metadata(path).expect("failed to read metadata"); - + println!("meta: {:?}", metadata); let mut buffer = vec![0; metadata.len() as usize]; f.read_exact(&mut buffer).expect("buffer overflow"); Ok(Some(buffer)) @@ -219,14 +229,15 @@ async fn run_command(mut command: Command) -> Result { use std::os::unix::process::ExitStatusExt; let timeout = TIMEOUT; - println!("executing command!"); + println!("executing command: {:?}", command); let output = command.output().await.context("failed to start compiler")?; let stdout = String::from_utf8_lossy(&output.stdout); let id = stdout.lines().next().context("missing compiler ID")?.trim(); let stderr = &output.stderr; - + let mut command = docker_command!("wait", id); + println!("ID: {:?}\nwait: {:?}", id, command); let timed_out = match tokio::time::timeout(timeout, command.output()).await { Ok(Ok(o)) => { @@ -239,17 +250,19 @@ async fn run_command(mut command: Command) -> Result { }; let mut command = docker_command!("logs", id); + println!("logs: {:?}", command); let mut output = command.output().await.context("failed to get output from compiler")?; - - let mut command = docker_command!( - "rm", // Kills container if still running - "--force", id - ); - command.stdout(std::process::Stdio::null()); - command.status().await.context("failed to remove compiler")?; + println!("op: {:?}", output); + // let mut command = docker_command!( + // "rm", // Kills container if still running + // "--force", id + // ); + // println!("rm: {:?}", command); + // command.stdout(std::process::Stdio::null()); + // command.status().await.context("failed to remove compiler")?; let code = timed_out.context("compiler timed out")?; - + println!("timedout: {:?}", code); output.status = code; output.stderr = stderr.to_owned(); diff --git a/docker-compose.yml b/docker-compose.yml index f648c97..a9129a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,7 @@ services: - /tmp:/tmp ports: - "9000:9000" + - "3000:3000" # Expose Next.js port + environment: + - DOCKER_ENV=true runtime: sysbox-runc diff --git a/packages/app/src/app.ts b/packages/app/src/app.ts index ea22d4f..c3dd768 100644 --- a/packages/app/src/app.ts +++ b/packages/app/src/app.ts @@ -58,7 +58,7 @@ export const downloadBlob = (code: number[]): void => { const blob = new Blob([new Uint8Array(code).buffer]); const a = document.createElement('a'); - a.download = 'result.contract'; + a.download = 'result.wasm'; a.href = URL.createObjectURL(blob); a.dataset.downloadurl = ['application/json', a.download, a.href].join(':'); a.style.display = 'none'; @@ -171,6 +171,7 @@ export default class App { ); console.log("Compilation result: ", result); + client.printToConsole(proto.MessageType.Info, "Compilation result: " + JSON.stringify(result)); // If the compilation was successful, download the wasm blob and print a success message if (result.type === 'OK') { diff --git a/packages/frontend/next.config.ts b/packages/frontend/next.config.ts index da728e3..15878c0 100644 --- a/packages/frontend/next.config.ts +++ b/packages/frontend/next.config.ts @@ -3,14 +3,19 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ async rewrites() { + // In Docker, use the backend service running on the same container + const backendUrl = process.env.DOCKER_ENV + ? "http://localhost:9000" + : "http://localhost:4444"; + return [ { source: "/compile", - destination: "http://localhost:4444/compile", + destination: `${backendUrl}/compile`, }, { source: "/health", - destination: "http://localhost:4444/health", + destination: `${backendUrl}/health`, }, ]; }, diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 10c5a2d..66a7b20 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "set NODE_ENV=production & next build", "start": "next start", "lint": "next lint" }, @@ -37,8 +37,8 @@ "next": "15.0.3", "next-auth": "^5.0.0-beta.25", "next-themes": "^0.4.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^18", + "react-dom": "^18", "react-icons": "^5.4.0", "sonner": "^1.7.4", "tailwind-merge": "^2.5.5", diff --git a/packages/frontend/src/app/state.ts b/packages/frontend/src/app/state.ts index 66fe0e3..85d5318 100644 --- a/packages/frontend/src/app/state.ts +++ b/packages/frontend/src/app/state.ts @@ -30,7 +30,8 @@ export const useSettingsStore = create( export enum SidebarView { FILE_EXPLORER = "FILE-EXPLORER", SETTINGS = "SETTINGS", - CONTRACT = "CONTRACT", + COMPILE = "COMPILE", + DEPLOY = "DEPLOY", } export const useAppStore = create( diff --git a/packages/frontend/src/components/CompileExplorer.tsx b/packages/frontend/src/components/CompileExplorer.tsx new file mode 100644 index 0000000..7323faf --- /dev/null +++ b/packages/frontend/src/components/CompileExplorer.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { store } from "@/state"; +import { useSelector } from "@xstate/store/react"; +import React, { useEffect, useState } from "react"; +import Hide from "./Hide"; +import { Button } from "./ui/button"; +import { useExplorerItem } from "@/state/hooks"; +import { FileType } from "@/types/explorer"; +import { get } from "lodash"; +import useCompile from "@/hooks/useCompile"; + +function CompileExplorer() { + const { compileFile } = useCompile(); + + const selected = useSelector(store, (state) => state.context.currentFile); + const obj = useSelector(store, (state) => get(state.context, selected || '')) as FileType; + const [name, setName] = useState(''); + + + const [isSelected, setSelected] = useState(!1); + + console.log('[tur] selected', selected) + + useEffect(() => { + if(selected && selected !== 'home') { + setName(obj.name); + setSelected(!0); + } + }, [selected]) + + const handleCompile = async () => { + const result = await compileFile(); + selected && selected !== 'home' && + store.send({ type: "addCompiled", path: selected, name }); + console.log('[tur] compilation result', result); + } + + return ( +
+
+

Compile Explorer

+
+
+ + {/*
+ { + keys.map(k => ( +
+

toggleCollapsed(e, k)} + > + {`${k.substring(0, 5)}..${k.substring(50)}`} +

+
+ { + deployed[k].map(item => ( + + )) + } +
+
+ ) + ) + } +
*/} + + {/* +
+

No Function or IDL Specified

+
+
*/} +
+
+ ); +} + +export default CompileExplorer; diff --git a/packages/frontend/src/components/ContractExplorer.tsx b/packages/frontend/src/components/ContractExplorer.tsx deleted file mode 100644 index 79e8d68..0000000 --- a/packages/frontend/src/components/ContractExplorer.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; - -import { store } from "@/state"; -import { useSelector } from "@xstate/store/react"; -import React from "react"; -import Hide from "./Hide"; -import InvokeFunction from "./InvokeFunction"; - -function ContractExplorer() { - const idl = useSelector(store, (state) => state.context.contract?.methods) || []; - - return ( -
-
-

Contract Explorer

-
-
-
- {idl.map((item) => ( - - ))} -
- - -
-

No Function or IDL Specified

-
-
-
-
- ); -} - -export default ContractExplorer; diff --git a/packages/frontend/src/components/DeployExplorer.tsx b/packages/frontend/src/components/DeployExplorer.tsx new file mode 100644 index 0000000..8667c33 --- /dev/null +++ b/packages/frontend/src/components/DeployExplorer.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { store } from "@/state"; +import { useSelector } from "@xstate/store/react"; +import React, { useEffect, useState } from "react"; +import Hide from "./Hide"; +import InvokeFunction from "./InvokeFunction"; +import useDeploy from "@/hooks/useDeploy"; +import { Button } from "./ui/button"; +import { Select, SelectContent, SelectIcon, SelectItem, SelectItemText, SelectPortal, SelectTrigger, SelectValue, SelectViewport } from "@radix-ui/react-select"; +import { ArchiveX, ChevronDownIcon, Copy, LucideDelete } from "lucide-react"; +import useCompile from "@/hooks/useCompile"; + +function DeployExplorer() { + const {compileFile} = useCompile(); + + const {deployWasm} = useDeploy(); + const currFileTabSelected = useSelector(store, (state) => state.context.currentFile); + const compiled = useSelector(store, (state) => state.context.compiled); + const currWasm = useSelector(store, (state) => state.context.currentWasm); + const deployed = useSelector(store, (state) => state.context.contract?.deployed) || {}; + const [keys, setKeys] = useState([]); + const [copied, setCopied] = useState(!1); + const [selected, setSelected] = useState(compiled.length ? compiled[0].path : ''); + + useEffect(() => { + console.log('[tur] current file tab:', currFileTabSelected) + currFileTabSelected && setSelected(currFileTabSelected || '') + }, [currFileTabSelected]) + + useEffect(() => { + console.log('[tur] compiled updated:', compiled) + }, [compiled]) + + const handleSelect = (event: React.ChangeEvent) => { + const v = event.target.value; + console.log("[tur] Custom selected:", v); + setSelected(v); + store.send({ type: "setCurrentPath", path: v }); + }; + + useEffect(() => { + const ks = Object.keys(deployed) + console.log('[tur] useEffect deployed:', ks) + setKeys(ks || []) + }, [deployed]) + + const toggleCollapsed = (e: React.MouseEvent, k: string) => { + // const target = e.target as HTMLElement; + // const div = target.nextElementSibling as HTMLElement | null; + + // if(div) { + // div.style.display = div.style.display === 'none' ? 'block' : 'none' + // } + const span = e.currentTarget as HTMLElement; + const parentP = span.parentElement; + const containerDiv = parentP?.parentElement; + + const hiddenDiv = containerDiv?.querySelector('div') as HTMLElement | null; + + if (hiddenDiv) { + hiddenDiv.style.display = hiddenDiv.style.display === 'none' ? 'block' : 'none'; + } + } + + const handleDeploy = async () => { + let contract: Buffer | null = null; + console.log('[tur] selected path', selected) + console.log('[tur] curr wasm path', currWasm.path) + if(selected.indexOf(currWasm.path) == -1) { + console.log('[tur] so compiling..') + const res = await compileFile(); + contract = res.data; + } + const result = await deployWasm(contract) + console.log('[tur] deployed?', result) + } + + const handleCopy = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + setCopied(!0); + setTimeout(() => setCopied(!1), 1500); + console.log('[tur] copied!') + } catch (err) { + console.error("Copy failed", err); + } + }; + + const handleRemoveDeployed = async (k: string) => { + console.log('[tur] keys:', keys) + store.send({ type: 'deleteDeployed', addr: k}) + } + + return ( +
+
+

Deploy Explorer

+
+
+
+
+

CONTRACTS

+ + +
+
+
+
+ +
+
+ {keys.length ?

DEPLOYED

: <>} +
+ { + keys.map(k => ( +
+

+ toggleCollapsed(e, k)}> + {`${k.substring(0, 5)}..${k.substring(50)}`} + + handleCopy(k)} /> + {/* {copied && Copied!} */} + handleRemoveDeployed(k)}/> +

+
+ { + deployed[k] && deployed[k].map(item => ( + + )) + } +
+
+ ) + ) + } +
+ + {/* +
+

No Function or IDL Specified

+
+
*/} +
+
+ ); +} + +export default DeployExplorer; diff --git a/packages/frontend/src/components/Header.tsx b/packages/frontend/src/components/Header.tsx index 870f4b5..d3de583 100644 --- a/packages/frontend/src/components/Header.tsx +++ b/packages/frontend/src/components/Header.tsx @@ -1,16 +1,20 @@ "use client"; -import { cn, downloadBlob } from "@/lib/utils"; +import { useEffect, useRef, useState } from "react"; import { FaPlay, FaTimes } from "react-icons/fa"; -import { useAddConsole } from "@/app/state"; -import { useExplorerItem, useFileContent } from "@/state/hooks"; +import { Keypair, Networks } from "@stellar/stellar-sdk"; import { useSelector } from "@xstate/store/react"; +import deployStellerContract from "@/lib/deploy-steller"; +import generateIdl from "@/lib/idl-wasm"; +import { cn } from "@/lib/utils"; import { store } from "@/state"; -import IconButton from "./IconButton"; -import { useEffect, useRef } from "react"; +import { useExplorerItem, useFileContent } from "@/state/hooks"; import { logger } from "@/state/utils"; -import Hide from "./Hide"; import DeployToSteller from "./DeployToSteller"; +import Hide from "./Hide"; +import IconButton from "./IconButton"; +import useCompile from "@/hooks/useCompile"; +import useDeploy from "@/hooks/useDeploy"; function TabItem({ path }: { path: string }) { const file = useExplorerItem(path); @@ -74,68 +78,38 @@ function TabHome({ path }: { path: string }) { } function Header() { + const { compileFile } = useCompile(); + const { deployWasm } = useDeploy(); const code = useFileContent(); const tabs = useSelector(store, (state) => state.context.tabs); const containerRef = useRef(null); + const [contract, setContract] = useState(null); - async function handleCompile() { - if (!code) { - return logger.error("Error: No Source Code Found"); - } - - logger.info("Compiling contract..."); - - const opts: RequestInit = { - method: "POST", - mode: "cors", - credentials: "same-origin", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - source: code, - }), - }; - - const { result, success, message } = await fetch("/compile", opts).then(async (res) => { - console.log(res); - const result = await res.json().catch(() => null); - - if (!result) { - return { - success: false, - message: res.statusText, - result: null, - }; - } - - return { - success: res.ok, - message: res.statusText, - result: result, - }; - }); + const handleCompile = async () => { + const result = await compileFile() + setContract(result.data) + console.log('[tur] scompilation result', result) + } - if (success) { - if (result.type === "SUCCESS") { - const wasm = result.payload.wasm; - downloadBlob(wasm); - logger.info("Contract compiled successfully!"); - } else { - const message = result.payload.compile_stderr; - logger.error(message); - } - } else { - logger.error(message); - } + const handleDeploy = async () => { + const result = await deployWasm(contract) + console.log('[tur] deployed?', result) } return (
- +
+
+
-
{[...tabs].map((tab) => ( diff --git a/packages/frontend/src/components/InvokeFunction.tsx b/packages/frontend/src/components/InvokeFunction.tsx index f7c00f3..77392d7 100644 --- a/packages/frontend/src/components/InvokeFunction.tsx +++ b/packages/frontend/src/components/InvokeFunction.tsx @@ -58,13 +58,14 @@ const defaultState = { result: { type: "", value: "" }, name: "", }; -function InvokeFunction({ method }: { method: FunctionSpec }) { +function InvokeFunction({ contractAddress, method }: { contractAddress: string, method: FunctionSpec }) { const [sg, setSignature] = useState>(defaultState); const [args, setArgs] = useState>({}); - const contractAddress = useSelector(store, (state) => state.context.contract?.address); + // const contractAddress = useSelector(store, (state) => state.context.contract?.address); const [logs, setLogs] = useState(["Hello wrold", "Mango World"]); const toastId = useId(); const [block, setBlock] = useState(false); + const [invkRetVal, setInvkRetVal] = useState(null); const handleInputChange = (name: string, value: string, type: string, subType: string) => { setArgs((prev) => ({ @@ -131,7 +132,9 @@ function InvokeFunction({ method }: { method: FunctionSpec }) { logger.info(`TxId: ${result.hash}`); logger.info(`Contract Logs: \n${logs.join("\n")}`); if (response.returnValue) { - logger.info(`TX Result: ${scValToNative(response.returnValue)}`); + const retVal = scValToNative(response.returnValue); + setInvkRetVal(retVal); + logger.info(`TX Result: ${retVal}`); const logSignature = createLogSingnature(method, args, scValToNative(response.returnValue)); setBlock(false); setSignature(logSignature); @@ -147,7 +150,7 @@ function InvokeFunction({ method }: { method: FunctionSpec }) { } catch (error: any) { toast.error(`Error: ${error.message}`, { id: toastId }); } - + setInvkRetVal(null); setBlock(false); }; @@ -165,8 +168,9 @@ function InvokeFunction({ method }: { method: FunctionSpec }) { - diff --git a/packages/frontend/src/components/SidePanel.tsx b/packages/frontend/src/components/SidePanel.tsx index 7855127..14fec63 100644 --- a/packages/frontend/src/components/SidePanel.tsx +++ b/packages/frontend/src/components/SidePanel.tsx @@ -5,9 +5,17 @@ import SolangLogo from "@/assets/image/solang-logo.png"; import { Button } from "./ui/button"; import { FaCog } from "react-icons/fa"; import { SidebarView, useAppStore } from "@/app/state"; -import { Files, FunctionSquare, LucideFiles, SquareFunction, Star } from "lucide-react"; +import { Files, FunctionSquare, LucideFiles, SquareFunction, Star, RefreshCcw, SquarePlay } from "lucide-react"; +import useCompile from "@/hooks/useCompile"; +import useDeploy from "@/hooks/useDeploy"; +import { useState } from "react"; function SidePanel() { + const { compileFile } = useCompile(); + const { deployWasm } = useDeploy(); + const [contract, setContract] = useState(null); + + const setSidebar = useAppStore((state) => state.setSidebar); return (
@@ -18,8 +26,11 @@ function SidePanel() { - +
diff --git a/packages/frontend/src/components/Sidebar.tsx b/packages/frontend/src/components/Sidebar.tsx index 758573b..cb6339e 100644 --- a/packages/frontend/src/components/Sidebar.tsx +++ b/packages/frontend/src/components/Sidebar.tsx @@ -8,7 +8,8 @@ import { store } from "@/state"; // import ContractExplorer from "./ContractExplorer"; import dynamic from "next/dynamic"; -const ContractExplorer = dynamic(() => import("./ContractExplorer"), { ssr: false }); +const CompileExplorer = dynamic(() => import("./CompileExplorer"), { ssr: false }); +const DeployExplorer = dynamic(() => import("./DeployExplorer"), { ssr: false }); function Sidebar() { const { sidebar } = useAppStore(); @@ -18,13 +19,18 @@ function Sidebar() { return ; } - if (sidebar === SidebarView.CONTRACT) { - return ; + if (sidebar === SidebarView.COMPILE) { + return ; + } + + if (sidebar === SidebarView.DEPLOY) { + return ; } return (
+
); } diff --git a/packages/frontend/src/hooks/useCompile.tsx b/packages/frontend/src/hooks/useCompile.tsx new file mode 100644 index 0000000..6b163a8 --- /dev/null +++ b/packages/frontend/src/hooks/useCompile.tsx @@ -0,0 +1,96 @@ +import { EditorContext } from "@/context/EditorProvider"; +import { useFileContent } from "@/state/hooks"; +import { useSelector } from "@xstate/store/react"; +import { useContext } from "react"; +import { store } from "@/state"; +import { logger } from "@/state/utils"; +import { Keypair, Networks } from "@stellar/stellar-sdk"; +import generateIdl from "@/lib/idl-wasm"; +import deployStellerContract from "@/lib/deploy-steller"; + +export interface ICompilationResult { + data: null | Buffer, + err: null | string +} + +function useCompile() { + const code = useFileContent(); + const selected = useSelector(store, (state) => state.context.currentFile); + + const compileFile = async (): Promise => { + console.log('[tur] [compileFile] code:', code) + if (!code) { + const err ="Error: No Source Code Found" + logger.error(err); + return { + data: null, + err + } + } + + logger.info("Compiling contract..."); + + const opts: RequestInit = { + method: "POST", + mode: "cors", + credentials: "same-origin", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source: code, + }), + }; + + const { result, success, message } = await fetch("/compile", opts).then(async (res) => { + console.log(res); + console.log(store); + const result = await res.json().catch(() => null); + + if (!result) { + return { + success: false, + message: res.statusText, + result: null, + }; + } + + return { + success: res.ok, + message: res.statusText, + result: result, + }; + }); + + let err = ""; + + if (success) { + if (result.type === "SUCCESS") { + const wasm = result.payload.wasm; + store.send({ type: "updateCurrentWasm", path: selected || '', buff: wasm }); + logger.info("Contract compiled successfully!"); + return { + data: wasm, + err: null + }; + } else { + const message = result.payload.compile_stderr; + logger.error(message); + err = message + } + } else { + logger.error(message); + err = message + } + console.log('[tur] compilatiion error:', err) + return { + data: null, + err + } + } + + return { + compileFile + } + +} + +export default useCompile; diff --git a/packages/frontend/src/hooks/useDeploy.tsx b/packages/frontend/src/hooks/useDeploy.tsx new file mode 100644 index 0000000..b608adf --- /dev/null +++ b/packages/frontend/src/hooks/useDeploy.tsx @@ -0,0 +1,56 @@ + +import { EditorContext } from "@/context/EditorProvider"; +import { useFileContent } from "@/state/hooks"; +import { useSelector } from "@xstate/store/react"; +import { useContext } from "react"; +import { store } from "@/state"; +import { logger } from "@/state/utils"; +import { Keypair, Networks } from "@stellar/stellar-sdk"; +import generateIdl from "@/lib/idl-wasm"; +import deployStellerContract from "@/lib/deploy-steller"; +import { FunctionSpec } from "@/types/idl"; +import useCompile from "./useCompile"; + + +function useDeploy() { + const {compileFile} = useCompile(); + const selected = useSelector(store, (state) => state.context.currentFile); + const currWasm = useSelector(store, (state) => state.context.currentWasm); + + + const deployWasm = async (contract: null | Buffer) => { + console.log('[tur] deploying', contract) + + if(currWasm.path.indexOf(selected || '') > -1) { + contract = currWasm.buff + } else if(!contract && selected && selected !== 'explorer') { + const r = await compileFile(); + contract = r.data + } + + if (!contract) { + return; + } + try { + const keypair = Keypair.random(); + + logger.info("Deploying contract..."); + const idl = await generateIdl(contract); + const fltrd = idl.filter((i: FunctionSpec) => i.name.indexOf('constructor') == -1); + store.send({ type: "updateContract", methods: fltrd }); + const contractAddress = await deployStellerContract(contract, keypair, Networks.TESTNET); + logger.info("Contract deployed successfully!"); + contractAddress && store.send({ type: "updateContract", address: contractAddress }); + + } catch { + return !1 + } + return !0 + } + + return { + deployWasm + } +} + +export default useDeploy; \ No newline at end of file diff --git a/packages/frontend/src/lib/utils.ts b/packages/frontend/src/lib/utils.ts index 5b19755..abcf8d0 100644 --- a/packages/frontend/src/lib/utils.ts +++ b/packages/frontend/src/lib/utils.ts @@ -9,7 +9,7 @@ export const downloadBlob = (code: number[]): void => { const blob = new Blob([new Uint8Array(code).buffer]); const a = document.createElement("a"); - a.download = "result.contract"; + a.download = "result.wasm"; a.href = URL.createObjectURL(blob); a.dataset.downloadurl = ["application/json", a.download, a.href].join(":"); a.style.display = "none"; diff --git a/packages/frontend/src/pages/_error.js b/packages/frontend/src/pages/_error.js new file mode 100644 index 0000000..cc05f55 --- /dev/null +++ b/packages/frontend/src/pages/_error.js @@ -0,0 +1,16 @@ +function Error({ statusCode }) { + return ( +

+ {statusCode + ? `An error ${statusCode} occurred on server` + : 'An error occurred on client'} +

+ ) + } + + Error.getInitialProps = ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404 + return { statusCode } + } + + export default Error \ No newline at end of file diff --git a/packages/frontend/src/state/context.ts b/packages/frontend/src/state/context.ts index f5b335c..83859d0 100644 --- a/packages/frontend/src/state/context.ts +++ b/packages/frontend/src/state/context.ts @@ -2,6 +2,7 @@ import { ExpNodeType, FolderType } from "@/types/explorer"; import { LogType } from "@/types/log"; import { Monaco } from "@monaco-editor/react"; import { Contract, IDL } from "@/types/idl"; +import { ICompiled, ICurrentWasm } from "@/types/contracts"; export * from "@stellar/stellar-sdk"; export * as contract from "@stellar/stellar-sdk/contract"; export * as rpc from "@stellar/stellar-sdk/rpc"; @@ -37,7 +38,17 @@ export const context = { invoking: false, address: null, methods: [], + // for deploy explorer's deployed instance list + // to invoke functions therein + deployed: {}, } as Contract, + // for deploy explorer's drop down list + // if the list has something, we can select one and press deploy btn + compiled: [] as ICompiled[], + currentWasm: { + path: '', + buff: null, + } as ICurrentWasm, }; export type Context = typeof context; diff --git a/packages/frontend/src/state/events.ts b/packages/frontend/src/state/events.ts index 1f78669..7a9ef73 100644 --- a/packages/frontend/src/state/events.ts +++ b/packages/frontend/src/state/events.ts @@ -7,7 +7,8 @@ import { Monaco } from "@monaco-editor/react"; import { createPath } from "./utils"; import { MessageType } from "vscode-languageserver-protocol"; import { nanoid } from "nanoid"; -import { Contract, IDL } from "@/types/idl"; +import { Contract, ContractsDeployed, IDL } from "@/types/idl"; +import { ICompiled, ICurrentWasm } from "@/types/contracts"; export const events = { toggleFolder: (context: Context, event: { path: string }) => { @@ -36,6 +37,7 @@ export const events = { } } }, + addDeployedContract(context: Context, event: { basePath: string; name: string, contract: any }) {}, addFile(context: Context, event: { basePath: string; name: string; content: string }) { const path = createPath(event.basePath, event.name); const file = { @@ -65,6 +67,7 @@ export const events = { } satisfies FolderType; }, + deleteFile(context: Context, event: { path: string; basePath: string }) { const folder = get(context, event.basePath) as FolderType; const file = get(context, event.path) as FileType; @@ -72,6 +75,7 @@ export const events = { events.removeTab(context, { path: event.path }); delete folder.items[file.name]; delete context.files[event.path]; + context.compiled = context.compiled.filter(c => c.path !== event.path) }, deleteFolder(context: Context, event: { path: string }) { unset(context, event.path); @@ -130,6 +134,32 @@ export const events = { // context.contract.methods = event.idl; // }, updateContract(context: Context, event: Partial) { + const addr = event.address; + if(addr && Object.keys(context.contract.deployed).indexOf(addr || '') == -1) { + context.contract.deployed[addr] = context.contract.methods + } Object.assign(context.contract, event); }, + + deleteDeployed(context: Context, event: {addr: string}) { + console.log('[tur] remove deployed:', event.addr); + const copy = { ...context.contract.deployed }; + console.log('[tur] copy:', copy, copy[event.addr]); + + delete copy[event.addr]; + context.contract.deployed = copy; + + }, + + addCompiled(context: Context, event: Partial) { + const d = {} as ICompiled; + Object.assign(d, event); + const x = context.compiled.filter(c => c.path == event.path); + if(x.length == 0) context.compiled.push(d); + }, + + updateCurrentWasm(context: Context, event: Partial) { + console.log('[tur] event updateCurrentWasm:', event) + Object.assign(context.currentWasm, event) + } }; diff --git a/packages/frontend/src/types/contracts.ts b/packages/frontend/src/types/contracts.ts new file mode 100644 index 0000000..d0b3686 --- /dev/null +++ b/packages/frontend/src/types/contracts.ts @@ -0,0 +1,9 @@ +export interface ICompiled { + path: string, + name: string, +} + +export interface ICurrentWasm { + path: string, + buff: Buffer | null, +} \ No newline at end of file diff --git a/packages/frontend/src/types/idl.ts b/packages/frontend/src/types/idl.ts index fb4b79e..5add947 100644 --- a/packages/frontend/src/types/idl.ts +++ b/packages/frontend/src/types/idl.ts @@ -8,6 +8,10 @@ export interface FunctionSpec { outputs: OutputSpec[]; } +export interface ContractsDeployed { + [key: string]: IDL; +} + interface InputSpec { doc: string; name: string; @@ -38,4 +42,5 @@ export interface Contract { address: string | null; methods: IDL; invoking: boolean; + deployed: ContractsDeployed; } diff --git a/solidity/counter_demo.abi b/solidity/counter_demo.abi new file mode 100644 index 0000000..26e5c03 --- /dev/null +++ b/solidity/counter_demo.abi @@ -0,0 +1 @@ +[{"name":"count","type":"function","inputs":[],"outputs":[{"name":"count","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"name":"decrement","type":"function","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"nonpayable"}] \ No newline at end of file diff --git a/solidity/counter_demo.sol b/solidity/counter_demo.sol new file mode 100755 index 0000000..cb2b489 --- /dev/null +++ b/solidity/counter_demo.sol @@ -0,0 +1,12 @@ +contract counter_demo { + + + uint64 public count = 1; + + function decrement() public returns (uint64){ + count -= 1; + return count; + } + + +} \ No newline at end of file diff --git a/solidity/counter_demo.wasm b/solidity/counter_demo.wasm new file mode 100644 index 0000000..61d8543 Binary files /dev/null and b/solidity/counter_demo.wasm differ diff --git a/solidity/hello_world.wasm b/solidity/hello_world.wasm new file mode 100755 index 0000000..f7232c0 Binary files /dev/null and b/solidity/hello_world.wasm differ diff --git a/solidity/soroban_token_contract.wasm b/solidity/soroban_token_contract.wasm new file mode 100755 index 0000000..2be33cb Binary files /dev/null and b/solidity/soroban_token_contract.wasm differ diff --git a/sysbox/on-start.sh b/sysbox/on-start.sh index 06b05a1..1bc2fbd 100755 --- a/sysbox/on-start.sh +++ b/sysbox/on-start.sh @@ -5,7 +5,6 @@ dockerd > /var/log/dockerd.log 2>&1 & sleep 2 # pull solang image -docker pull ghcr.io/hyperledger/solang@sha256:8776a9bd756664f7bf8414710d1a799799bf6fedc1c8f9f0bda17e76749dea7a +docker pull ghcr.io/hyperledger/solang@sha256:e6f687910df5dd9d4f5285aed105ae0e6bcae912db43e8955ed4d8344d49785d -# start backend server -./app/target/release/backend --port 9000 --host 0.0.0.0 --frontend_folder /app/packages/app/dist +cargo make run