Extract actions to separate files
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import { addFaucetAccount, deployHook, importAccount, state } from "../state";
|
import { addFaucetAccount, deployHook, importAccount } from "../state/actions";
|
||||||
|
import state from "../state";
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import Heading from "./Heading";
|
import Heading from "./Heading";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Box from "./Box";
|
|||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import dark from "../theme/editor/amy.json";
|
import dark from "../theme/editor/amy.json";
|
||||||
import light from "../theme/editor/xcode_default.json";
|
import light from "../theme/editor/xcode_default.json";
|
||||||
import { state } from "../state";
|
import state from "../state";
|
||||||
|
|
||||||
import EditorNavigation from "./EditorNavigation";
|
import EditorNavigation from "./EditorNavigation";
|
||||||
import Text from "./Text";
|
import Text from "./Text";
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import useStayScrolled from "react-stay-scrolled";
|
|||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import LogText from "./LogText";
|
import LogText from "./LogText";
|
||||||
import { compileCode, state } from "../state";
|
import { compileCode } from "../state/actions";
|
||||||
|
import state from "../state";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import Heading from "./Heading";
|
import Heading from "./Heading";
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ import { useSnapshot } from "valtio";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
createNewFile,
|
createNewFile,
|
||||||
state,
|
|
||||||
syncToGist,
|
syncToGist,
|
||||||
updateEditorSettings,
|
updateEditorSettings,
|
||||||
} from "../state";
|
} from "../state/actions";
|
||||||
|
import state from "../state";
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import Box from "./Box";
|
|||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import dark from "../theme/editor/amy.json";
|
import dark from "../theme/editor/amy.json";
|
||||||
import light from "../theme/editor/xcode_default.json";
|
import light from "../theme/editor/xcode_default.json";
|
||||||
import { saveFile, state } from "../state";
|
import { saveFile } from "../state/actions";
|
||||||
|
import state from "../state";
|
||||||
|
|
||||||
import EditorNavigation from "./EditorNavigation";
|
import EditorNavigation from "./EditorNavigation";
|
||||||
import Text from "./Text";
|
import Text from "./Text";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Flex from "./Flex";
|
|||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import ThemeChanger from "./ThemeChanger";
|
import ThemeChanger from "./ThemeChanger";
|
||||||
import { state } from "../state";
|
import state from "../state";
|
||||||
import Heading from "./Heading";
|
import Heading from "./Heading";
|
||||||
import Text from "./Text";
|
import Text from "./Text";
|
||||||
import Spinner from "./Spinner";
|
import Spinner from "./Spinner";
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { IdProvider } from "@radix-ui/react-id";
|
|||||||
|
|
||||||
import { darkTheme, css } from "../stitches.config";
|
import { darkTheme, css } from "../stitches.config";
|
||||||
import Navigation from "../components/Navigation";
|
import Navigation from "../components/Navigation";
|
||||||
import { fetchFiles, state } from "../state";
|
import { fetchFiles } from "../state/actions";
|
||||||
|
import state from "../state";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Flex from "../../components/Flex";
|
import Flex from "../../components/Flex";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import { state } from "../../state";
|
import state from "../../state";
|
||||||
|
|
||||||
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
|
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import Hotkeys from "react-hot-keys";
|
|||||||
import { Play } from "phosphor-react";
|
import { Play } from "phosphor-react";
|
||||||
|
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import { compileCode, state } from "../../state";
|
import { compileCode } from "../../state/actions";
|
||||||
|
import state from "../../state";
|
||||||
import Button from "../../components/Button";
|
import Button from "../../components/Button";
|
||||||
import Box from "../../components/Box";
|
import Box from "../../components/Box";
|
||||||
|
|
||||||
|
|||||||
575
state.ts
575
state.ts
@@ -1,575 +0,0 @@
|
|||||||
import { proxy, ref, subscribe } from "valtio";
|
|
||||||
import { devtools } from "valtio/utils";
|
|
||||||
import { Octokit } from "@octokit/core";
|
|
||||||
import type monaco from "monaco-editor";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import Router from "next/router";
|
|
||||||
import type { Session } from "next-auth";
|
|
||||||
import { decodeBinary } from "./utils/decodeBinary";
|
|
||||||
|
|
||||||
import { derive, sign } from "xrpl-accountlib";
|
|
||||||
import { XrplClient } from "xrpl-client";
|
|
||||||
|
|
||||||
const octokit = new Octokit();
|
|
||||||
|
|
||||||
interface File {
|
|
||||||
name: string;
|
|
||||||
language: string;
|
|
||||||
content: string;
|
|
||||||
compiledContent?: ArrayBuffer | null;
|
|
||||||
compiledWatContent?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FaucetAccountRes {
|
|
||||||
address: string;
|
|
||||||
secret: string;
|
|
||||||
xrp: number;
|
|
||||||
hash: string;
|
|
||||||
code: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAccount {
|
|
||||||
name: string;
|
|
||||||
address: string;
|
|
||||||
secret: string;
|
|
||||||
xrp: string;
|
|
||||||
sequence: number;
|
|
||||||
hooks: string[];
|
|
||||||
isLoading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILog {
|
|
||||||
type: "error" | "warning" | "log" | "success";
|
|
||||||
message: string;
|
|
||||||
link?: string;
|
|
||||||
linkText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
files: File[];
|
|
||||||
gistId?: string | null;
|
|
||||||
gistOwner?: string | null;
|
|
||||||
gistName?: string | null;
|
|
||||||
active: number;
|
|
||||||
activeWat: number;
|
|
||||||
loading: boolean;
|
|
||||||
gistLoading: boolean;
|
|
||||||
compiling: boolean;
|
|
||||||
logs: ILog[];
|
|
||||||
deployLogs: ILog[];
|
|
||||||
editorCtx?: typeof monaco.editor;
|
|
||||||
editorSettings: {
|
|
||||||
tabSize: number;
|
|
||||||
};
|
|
||||||
client: XrplClient | null;
|
|
||||||
clientStatus: "offline" | "online";
|
|
||||||
mainModalOpen: boolean;
|
|
||||||
accounts: IAccount[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const names = [
|
|
||||||
"Alice",
|
|
||||||
"Bob",
|
|
||||||
"Carol",
|
|
||||||
"Carlos",
|
|
||||||
"Charlie",
|
|
||||||
"Dan",
|
|
||||||
"Dave",
|
|
||||||
"David",
|
|
||||||
"Faythe",
|
|
||||||
"Frank",
|
|
||||||
"Grace",
|
|
||||||
"Heidi",
|
|
||||||
"Judy",
|
|
||||||
"Olive",
|
|
||||||
"Peggy",
|
|
||||||
"Walter",
|
|
||||||
];
|
|
||||||
|
|
||||||
// let localStorageState: null | string = null;
|
|
||||||
let initialState = {
|
|
||||||
files: [],
|
|
||||||
active: 0,
|
|
||||||
activeWat: 0,
|
|
||||||
loading: false,
|
|
||||||
compiling: false,
|
|
||||||
logs: [],
|
|
||||||
deployLogs: [],
|
|
||||||
editorCtx: undefined,
|
|
||||||
gistId: undefined,
|
|
||||||
gistOwner: undefined,
|
|
||||||
gistName: undefined,
|
|
||||||
gistLoading: false,
|
|
||||||
editorSettings: {
|
|
||||||
tabSize: 2,
|
|
||||||
},
|
|
||||||
client: null,
|
|
||||||
clientStatus: "offline" as "offline",
|
|
||||||
mainModalOpen: false,
|
|
||||||
accounts: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
let localStorageAccounts: string | null = null;
|
|
||||||
let initialAccounts: IAccount[] = [];
|
|
||||||
// Check if there's a persited accounts in localStorage
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
try {
|
|
||||||
localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
|
|
||||||
} catch (err) {
|
|
||||||
console.log(`localStorage state broken`);
|
|
||||||
localStorage.removeItem("hooksIdeAccounts");
|
|
||||||
}
|
|
||||||
if (localStorageAccounts) {
|
|
||||||
initialAccounts = JSON.parse(localStorageAccounts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize state
|
|
||||||
export const state = proxy<IState>({
|
|
||||||
...initialState,
|
|
||||||
accounts: initialAccounts.length > 0 ? initialAccounts : [],
|
|
||||||
logs: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize socket connection
|
|
||||||
const client = new XrplClient("wss://hooks-testnet.xrpl-labs.com");
|
|
||||||
|
|
||||||
client.on("online", () => {
|
|
||||||
state.client = ref(client);
|
|
||||||
state.clientStatus = "online";
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("offline", () => {
|
|
||||||
state.clientStatus = "offline";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch content from Githug Gists
|
|
||||||
export const fetchFiles = (gistId: string) => {
|
|
||||||
state.loading = true;
|
|
||||||
if (gistId && !state.files.length) {
|
|
||||||
state.logs.push({
|
|
||||||
type: "log",
|
|
||||||
message: `Fetching Gist with id: ${gistId}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
octokit
|
|
||||||
.request("GET /gists/{gist_id}", { gist_id: gistId })
|
|
||||||
.then((res) => {
|
|
||||||
if (res.data.files && Object.keys(res.data.files).length > 0) {
|
|
||||||
const files = Object.keys(res.data.files).map((filename) => ({
|
|
||||||
name: res.data.files?.[filename]?.filename || "noname.c",
|
|
||||||
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
|
|
||||||
content: res.data.files?.[filename]?.content || "",
|
|
||||||
}));
|
|
||||||
state.loading = false;
|
|
||||||
if (files.length > 0) {
|
|
||||||
state.logs.push({
|
|
||||||
type: "success",
|
|
||||||
message: "Fetched successfully ✅",
|
|
||||||
});
|
|
||||||
state.files = files;
|
|
||||||
state.gistId = gistId;
|
|
||||||
state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
|
|
||||||
state.gistOwner = res.data.owner?.login;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// Open main modal if now files
|
|
||||||
state.mainModalOpen = true;
|
|
||||||
}
|
|
||||||
return Router.push({ pathname: "/develop" });
|
|
||||||
}
|
|
||||||
state.loading = false;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
state.loading = false;
|
|
||||||
state.logs.push({
|
|
||||||
type: "error",
|
|
||||||
message: `Couldn't find Gist with id: ${gistId}`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.loading = false;
|
|
||||||
// return state.files = initFiles
|
|
||||||
};
|
|
||||||
|
|
||||||
export const syncToGist = async (
|
|
||||||
session?: Session | null,
|
|
||||||
createNewGist?: boolean
|
|
||||||
) => {
|
|
||||||
let files: Record<string, { filename: string; content: string }> = {};
|
|
||||||
state.gistLoading = true;
|
|
||||||
|
|
||||||
if (!session || !session.user) {
|
|
||||||
state.gistLoading = false;
|
|
||||||
return toast.error("You need to be logged in!");
|
|
||||||
}
|
|
||||||
const toastId = toast.loading("Pushing to Gist");
|
|
||||||
if (!state.files || !state.files.length) {
|
|
||||||
state.gistLoading = false;
|
|
||||||
return toast.error(`You need to create some files we can push to gist`, {
|
|
||||||
id: toastId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
state.gistId &&
|
|
||||||
session?.user.username === state.gistOwner &&
|
|
||||||
!createNewGist
|
|
||||||
) {
|
|
||||||
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
|
|
||||||
gist_id: state.gistId,
|
|
||||||
});
|
|
||||||
if (currentFilesRes.data.files) {
|
|
||||||
Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
|
|
||||||
files[`${filename}`] = { filename, content: "" };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
state.files.forEach((file) => {
|
|
||||||
files[`${file.name}`] = { filename: file.name, content: file.content };
|
|
||||||
});
|
|
||||||
// Update existing Gist
|
|
||||||
octokit
|
|
||||||
.request("PATCH /gists/{gist_id}", {
|
|
||||||
gist_id: state.gistId,
|
|
||||||
files,
|
|
||||||
headers: {
|
|
||||||
authorization: `token ${session?.accessToken || ""}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
state.gistLoading = false;
|
|
||||||
return toast.success("Updated to gist successfully!", { id: toastId });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
state.gistLoading = false;
|
|
||||||
return toast.error(`Could not update Gist, try again later!`, {
|
|
||||||
id: toastId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Not Gist of the current user or it isn't Gist yet
|
|
||||||
state.files.forEach((file) => {
|
|
||||||
files[`${file.name}`] = { filename: file.name, content: file.content };
|
|
||||||
});
|
|
||||||
octokit
|
|
||||||
.request("POST /gists", {
|
|
||||||
files,
|
|
||||||
public: true,
|
|
||||||
headers: {
|
|
||||||
authorization: `token ${session?.accessToken || ""}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
state.gistLoading = false;
|
|
||||||
state.gistOwner = res.data.owner?.login;
|
|
||||||
state.gistId = res.data.id;
|
|
||||||
state.gistName = Array.isArray(res.data.files)
|
|
||||||
? Object.keys(res.data?.files)?.[0]
|
|
||||||
: "Untitled";
|
|
||||||
Router.push({ pathname: `/develop/${res.data.id}` });
|
|
||||||
return toast.success("Created new gist successfully!", { id: toastId });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
state.gistLoading = false;
|
|
||||||
return toast.error(`Could not create Gist, try again later!`, {
|
|
||||||
id: toastId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateEditorSettings = (
|
|
||||||
editorSettings: IState["editorSettings"]
|
|
||||||
) => {
|
|
||||||
state.editorCtx?.getModels().forEach((model) => {
|
|
||||||
model.updateOptions({
|
|
||||||
...editorSettings,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return (state.editorSettings = editorSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveFile = (showToast: boolean = true) => {
|
|
||||||
const editorModels = state.editorCtx?.getModels();
|
|
||||||
const currentModel = editorModels?.find((editorModel) => {
|
|
||||||
return editorModel.uri.path === `/c/${state.files[state.active].name}`;
|
|
||||||
});
|
|
||||||
if (state.files.length > 0) {
|
|
||||||
state.files[state.active].content = currentModel?.getValue() || "";
|
|
||||||
}
|
|
||||||
if (showToast) {
|
|
||||||
toast.success("Saved successfully", { position: "bottom-center" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createNewFile = (name: string) => {
|
|
||||||
const emptyFile: File = { name, language: "c", content: "" };
|
|
||||||
state.files.push(emptyFile);
|
|
||||||
state.active = state.files.length - 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const compileCode = async (activeId: number) => {
|
|
||||||
saveFile(false);
|
|
||||||
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
|
|
||||||
throw Error("Missing env!");
|
|
||||||
}
|
|
||||||
if (state.compiling) {
|
|
||||||
// if compiling is ongoing return
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.compiling = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
output: "wasm",
|
|
||||||
compress: true,
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
type: "c",
|
|
||||||
name: state.files[activeId].name,
|
|
||||||
options: "-O0",
|
|
||||||
src: state.files[activeId].content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
state.compiling = false;
|
|
||||||
if (!json.success) {
|
|
||||||
state.logs.push({ type: "error", message: json.message });
|
|
||||||
if (json.tasks && json.tasks.length > 0) {
|
|
||||||
json.tasks.forEach((task: any) => {
|
|
||||||
if (!task.success) {
|
|
||||||
state.logs.push({ type: "error", message: task?.console });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
|
|
||||||
}
|
|
||||||
state.logs.push({
|
|
||||||
type: "success",
|
|
||||||
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
|
|
||||||
link: Router.asPath.replace("develop", "deploy"),
|
|
||||||
linkText: "Go to deploy",
|
|
||||||
});
|
|
||||||
const bufferData = await decodeBinary(json.output);
|
|
||||||
state.files[state.active].compiledContent = ref(bufferData);
|
|
||||||
|
|
||||||
import("wabt").then((wabt) => {
|
|
||||||
const ww = wabt.default();
|
|
||||||
const myModule = ww.readWasm(new Uint8Array(bufferData), {
|
|
||||||
readDebugNames: true,
|
|
||||||
});
|
|
||||||
myModule.applyNames();
|
|
||||||
|
|
||||||
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
|
|
||||||
state.files[state.active].compiledWatContent = wast;
|
|
||||||
toast.success("Compiled successfully!", { position: "bottom-center" });
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
state.logs.push({
|
|
||||||
type: "error",
|
|
||||||
message: "Error occured while compiling!",
|
|
||||||
});
|
|
||||||
state.compiling = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addFaucetAccount = async (showToast: boolean = false) => {
|
|
||||||
if (state.accounts.length > 4) {
|
|
||||||
return toast.error("You can only have maximum 5 accounts");
|
|
||||||
}
|
|
||||||
const toastId = showToast ? toast.loading("Creating account") : "";
|
|
||||||
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/faucet`, {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
const json: FaucetAccountRes | { error: string } = await res.json();
|
|
||||||
if ("error" in json) {
|
|
||||||
if (showToast) {
|
|
||||||
return toast.error(json.error, { id: toastId });
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (showToast) {
|
|
||||||
toast.success("New account created", { id: toastId });
|
|
||||||
}
|
|
||||||
state.accounts.push({
|
|
||||||
name: names[state.accounts.length],
|
|
||||||
xrp: (json.xrp || 0 * 1000000).toString(),
|
|
||||||
address: json.address,
|
|
||||||
secret: json.secret,
|
|
||||||
sequence: 1,
|
|
||||||
hooks: [],
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
|
|
||||||
if (!arrayBuffer) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
typeof arrayBuffer !== "object" ||
|
|
||||||
arrayBuffer === null ||
|
|
||||||
typeof arrayBuffer.byteLength !== "number"
|
|
||||||
) {
|
|
||||||
throw new TypeError("Expected input to be an ArrayBuffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
var view = new Uint8Array(arrayBuffer);
|
|
||||||
var result = "";
|
|
||||||
var value;
|
|
||||||
|
|
||||||
for (var i = 0; i < view.length; i++) {
|
|
||||||
value = view[i].toString(16);
|
|
||||||
result += value.length === 1 ? "0" + value : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
export const deployHook = async (account: IAccount & { name?: string }) => {
|
|
||||||
if (
|
|
||||||
!state.files ||
|
|
||||||
state.files.length === 0 ||
|
|
||||||
!state.files?.[state.active]?.compiledContent
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state.files?.[state.active]?.compiledContent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const tx = {
|
|
||||||
Account: account.address,
|
|
||||||
TransactionType: "SetHook",
|
|
||||||
CreateCode: arrayBufferToHex(
|
|
||||||
state.files?.[state.active]?.compiledContent
|
|
||||||
).toUpperCase(),
|
|
||||||
HookOn: "0000000000000000",
|
|
||||||
Sequence: account.sequence,
|
|
||||||
Fee: "1000",
|
|
||||||
};
|
|
||||||
const keypair = derive.familySeed(account.secret);
|
|
||||||
const { signedTransaction } = sign(tx, keypair);
|
|
||||||
const currentAccount = state.accounts.find(
|
|
||||||
(acc) => acc.address === account.address
|
|
||||||
);
|
|
||||||
if (currentAccount) {
|
|
||||||
currentAccount.isLoading = true;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const submitRes = await client.send({
|
|
||||||
command: "submit",
|
|
||||||
tx_blob: signedTransaction,
|
|
||||||
});
|
|
||||||
if (submitRes.engine_result === "tesSUCCESS") {
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "success",
|
|
||||||
message: "Hook deployed successfully ✅",
|
|
||||||
});
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "success",
|
|
||||||
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "error",
|
|
||||||
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
state.deployLogs.push({
|
|
||||||
type: "error",
|
|
||||||
message: "Error occured while deploying",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (currentAccount) {
|
|
||||||
currentAccount.isLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendXrp = async (account: IAccount & { name?: string }) => {
|
|
||||||
const tx = {
|
|
||||||
TransactionType: "Payment",
|
|
||||||
Account: state.accounts[2].address,
|
|
||||||
Fee: "1000",
|
|
||||||
Destination: state.accounts[1].address,
|
|
||||||
Amount: "133701337",
|
|
||||||
Sequence: state.accounts[2].sequence,
|
|
||||||
};
|
|
||||||
const keypair = derive.familySeed(state.accounts[2].secret);
|
|
||||||
const { signedTransaction } = sign(tx, keypair);
|
|
||||||
await client.send({
|
|
||||||
command: "submit",
|
|
||||||
tx_blob: signedTransaction,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//@ts-expect-errors
|
|
||||||
window.sendXrp = sendXrp;
|
|
||||||
|
|
||||||
export const importAccount = (secret: string) => {
|
|
||||||
if (!secret) {
|
|
||||||
return toast.error("You need to add secret!");
|
|
||||||
}
|
|
||||||
if (state.accounts.find((acc) => acc.secret === secret)) {
|
|
||||||
return toast.error("Account already added!");
|
|
||||||
}
|
|
||||||
const account = derive.familySeed(secret);
|
|
||||||
if (!account.secret.familySeed) {
|
|
||||||
return toast.error(`Couldn't create account!`);
|
|
||||||
}
|
|
||||||
state.accounts.push({
|
|
||||||
name: names[state.accounts.length],
|
|
||||||
address: account.address || "",
|
|
||||||
secret: account.secret.familySeed || "",
|
|
||||||
xrp: "0",
|
|
||||||
sequence: 1,
|
|
||||||
hooks: [],
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
return toast.success("Account imported successfully!");
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
window.importAccount = importAccount;
|
|
||||||
|
|
||||||
// fetch initial faucets
|
|
||||||
(async function fetchFaucets() {
|
|
||||||
if (state.accounts.length < 2) {
|
|
||||||
await addFaucetAccount();
|
|
||||||
setTimeout(() => {
|
|
||||||
addFaucetAccount();
|
|
||||||
}, 10000);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
devtools(state, "Files State");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
subscribe(state, () => {
|
|
||||||
const { accounts, active } = state;
|
|
||||||
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
|
||||||
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
|
||||||
if (!state.files[active]?.compiledWatContent) {
|
|
||||||
state.activeWat = 0;
|
|
||||||
} else {
|
|
||||||
state.activeWat = active;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
52
state/actions/addFaucetAccount.ts
Normal file
52
state/actions/addFaucetAccount.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import toast from "react-hot-toast";
|
||||||
|
import state, { FaucetAccountRes } from '../index';
|
||||||
|
|
||||||
|
export const names = [
|
||||||
|
"Alice",
|
||||||
|
"Bob",
|
||||||
|
"Carol",
|
||||||
|
"Carlos",
|
||||||
|
"Charlie",
|
||||||
|
"Dan",
|
||||||
|
"Dave",
|
||||||
|
"David",
|
||||||
|
"Faythe",
|
||||||
|
"Frank",
|
||||||
|
"Grace",
|
||||||
|
"Heidi",
|
||||||
|
"Judy",
|
||||||
|
"Olive",
|
||||||
|
"Peggy",
|
||||||
|
"Walter",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const addFaucetAccount = async (showToast: boolean = false) => {
|
||||||
|
if (state.accounts.length > 4) {
|
||||||
|
return toast.error("You can only have maximum 5 accounts");
|
||||||
|
}
|
||||||
|
const toastId = showToast ? toast.loading("Creating account") : "";
|
||||||
|
const res = await fetch(`${process.env.NEXT_PUBLIC_SITE_URL}/api/faucet`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
const json: FaucetAccountRes | { error: string } = await res.json();
|
||||||
|
if ("error" in json) {
|
||||||
|
if (showToast) {
|
||||||
|
return toast.error(json.error, { id: toastId });
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (showToast) {
|
||||||
|
toast.success("New account created", { id: toastId });
|
||||||
|
}
|
||||||
|
state.accounts.push({
|
||||||
|
name: names[state.accounts.length],
|
||||||
|
xrp: (json.xrp || 0 * 1000000).toString(),
|
||||||
|
address: json.address,
|
||||||
|
secret: json.secret,
|
||||||
|
sequence: 1,
|
||||||
|
hooks: [],
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
79
state/actions/compileCode.ts
Normal file
79
state/actions/compileCode.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Router from 'next/router';
|
||||||
|
|
||||||
|
import state from "../index";
|
||||||
|
import { saveFile } from "./saveFile";
|
||||||
|
import { decodeBinary } from "../../utils/decodeBinary";
|
||||||
|
import { ref } from "valtio";
|
||||||
|
|
||||||
|
export const compileCode = async (activeId: number) => {
|
||||||
|
saveFile(false);
|
||||||
|
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
|
||||||
|
throw Error("Missing env!");
|
||||||
|
}
|
||||||
|
if (state.compiling) {
|
||||||
|
// if compiling is ongoing return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.compiling = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
output: "wasm",
|
||||||
|
compress: true,
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
type: "c",
|
||||||
|
name: state.files[activeId].name,
|
||||||
|
options: "-O0",
|
||||||
|
src: state.files[activeId].content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
state.compiling = false;
|
||||||
|
if (!json.success) {
|
||||||
|
state.logs.push({ type: "error", message: json.message });
|
||||||
|
if (json.tasks && json.tasks.length > 0) {
|
||||||
|
json.tasks.forEach((task: any) => {
|
||||||
|
if (!task.success) {
|
||||||
|
state.logs.push({ type: "error", message: task?.console });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
|
||||||
|
}
|
||||||
|
state.logs.push({
|
||||||
|
type: "success",
|
||||||
|
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
|
||||||
|
link: Router.asPath.replace("develop", "deploy"),
|
||||||
|
linkText: "Go to deploy",
|
||||||
|
});
|
||||||
|
const bufferData = await decodeBinary(json.output);
|
||||||
|
state.files[state.active].compiledContent = ref(bufferData);
|
||||||
|
|
||||||
|
import("wabt").then((wabt) => {
|
||||||
|
const ww = wabt.default();
|
||||||
|
const myModule = ww.readWasm(new Uint8Array(bufferData), {
|
||||||
|
readDebugNames: true,
|
||||||
|
});
|
||||||
|
myModule.applyNames();
|
||||||
|
|
||||||
|
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
|
||||||
|
state.files[state.active].compiledWatContent = wast;
|
||||||
|
toast.success("Compiled successfully!", { position: "bottom-center" });
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
state.logs.push({
|
||||||
|
type: "error",
|
||||||
|
message: "Error occured while compiling!",
|
||||||
|
});
|
||||||
|
state.compiling = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
7
state/actions/createNewFile.ts
Normal file
7
state/actions/createNewFile.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import state, { IFile } from '../index';
|
||||||
|
|
||||||
|
export const createNewFile = (name: string) => {
|
||||||
|
const emptyFile: IFile = { name, language: "c", content: "" };
|
||||||
|
state.files.push(emptyFile);
|
||||||
|
state.active = state.files.length - 1;
|
||||||
|
};
|
||||||
93
state/actions/deployHook.ts
Normal file
93
state/actions/deployHook.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { derive, sign } from "xrpl-accountlib";
|
||||||
|
|
||||||
|
import state, { IAccount } from "../index";
|
||||||
|
|
||||||
|
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
|
||||||
|
if (!arrayBuffer) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof arrayBuffer !== "object" ||
|
||||||
|
arrayBuffer === null ||
|
||||||
|
typeof arrayBuffer.byteLength !== "number"
|
||||||
|
) {
|
||||||
|
throw new TypeError("Expected input to be an ArrayBuffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
var view = new Uint8Array(arrayBuffer);
|
||||||
|
var result = "";
|
||||||
|
var value;
|
||||||
|
|
||||||
|
for (var i = 0; i < view.length; i++) {
|
||||||
|
value = view[i].toString(16);
|
||||||
|
result += value.length === 1 ? "0" + value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
export const deployHook = async (account: IAccount & { name?: string }) => {
|
||||||
|
if (
|
||||||
|
!state.files ||
|
||||||
|
state.files.length === 0 ||
|
||||||
|
!state.files?.[state.active]?.compiledContent
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.files?.[state.active]?.compiledContent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const tx = {
|
||||||
|
Account: account.address,
|
||||||
|
TransactionType: "SetHook",
|
||||||
|
CreateCode: arrayBufferToHex(
|
||||||
|
state.files?.[state.active]?.compiledContent
|
||||||
|
).toUpperCase(),
|
||||||
|
HookOn: "0000000000000000",
|
||||||
|
Sequence: account.sequence,
|
||||||
|
Fee: "1000",
|
||||||
|
};
|
||||||
|
const keypair = derive.familySeed(account.secret);
|
||||||
|
const { signedTransaction } = sign(tx, keypair);
|
||||||
|
const currentAccount = state.accounts.find(
|
||||||
|
(acc) => acc.address === account.address
|
||||||
|
);
|
||||||
|
if (currentAccount) {
|
||||||
|
currentAccount.isLoading = true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const submitRes = await state.client.send({
|
||||||
|
command: "submit",
|
||||||
|
tx_blob: signedTransaction,
|
||||||
|
});
|
||||||
|
if (submitRes.engine_result === "tesSUCCESS") {
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: "success",
|
||||||
|
message: "Hook deployed successfully ✅",
|
||||||
|
});
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: "success",
|
||||||
|
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: "error",
|
||||||
|
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
state.deployLogs.push({
|
||||||
|
type: "error",
|
||||||
|
message: "Error occured while deploying",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (currentAccount) {
|
||||||
|
currentAccount.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
55
state/actions/fetchFiles.ts
Normal file
55
state/actions/fetchFiles.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Octokit } from "@octokit/core";
|
||||||
|
import Router from "next/router";
|
||||||
|
import state from '../index';
|
||||||
|
|
||||||
|
const octokit = new Octokit();
|
||||||
|
|
||||||
|
// Fetch content from Githug Gists
|
||||||
|
export const fetchFiles = (gistId: string) => {
|
||||||
|
state.loading = true;
|
||||||
|
if (gistId && !state.files.length) {
|
||||||
|
state.logs.push({
|
||||||
|
type: "log",
|
||||||
|
message: `Fetching Gist with id: ${gistId}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
octokit
|
||||||
|
.request("GET /gists/{gist_id}", { gist_id: gistId })
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.files && Object.keys(res.data.files).length > 0) {
|
||||||
|
const files = Object.keys(res.data.files).map((filename) => ({
|
||||||
|
name: res.data.files?.[filename]?.filename || "noname.c",
|
||||||
|
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
|
||||||
|
content: res.data.files?.[filename]?.content || "",
|
||||||
|
}));
|
||||||
|
state.loading = false;
|
||||||
|
if (files.length > 0) {
|
||||||
|
state.logs.push({
|
||||||
|
type: "success",
|
||||||
|
message: "Fetched successfully ✅",
|
||||||
|
});
|
||||||
|
state.files = files;
|
||||||
|
state.gistId = gistId;
|
||||||
|
state.gistName = Object.keys(res.data.files)?.[0] || "untitled";
|
||||||
|
state.gistOwner = res.data.owner?.login;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Open main modal if now files
|
||||||
|
state.mainModalOpen = true;
|
||||||
|
}
|
||||||
|
return Router.push({ pathname: "/develop" });
|
||||||
|
}
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
state.loading = false;
|
||||||
|
state.logs.push({
|
||||||
|
type: "error",
|
||||||
|
message: `Couldn't find Gist with id: ${gistId}`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.loading = false;
|
||||||
|
};
|
||||||
28
state/actions/importAccount.ts
Normal file
28
state/actions/importAccount.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { derive } from "xrpl-accountlib";
|
||||||
|
|
||||||
|
import state from '../index';
|
||||||
|
import { names } from './addFaucetAccount';
|
||||||
|
|
||||||
|
export const importAccount = (secret: string) => {
|
||||||
|
if (!secret) {
|
||||||
|
return toast.error("You need to add secret!");
|
||||||
|
}
|
||||||
|
if (state.accounts.find((acc) => acc.secret === secret)) {
|
||||||
|
return toast.error("Account already added!");
|
||||||
|
}
|
||||||
|
const account = derive.familySeed(secret);
|
||||||
|
if (!account.secret.familySeed) {
|
||||||
|
return toast.error(`Couldn't create account!`);
|
||||||
|
}
|
||||||
|
state.accounts.push({
|
||||||
|
name: names[state.accounts.length],
|
||||||
|
address: account.address || "",
|
||||||
|
secret: account.secret.familySeed || "",
|
||||||
|
xrp: "0",
|
||||||
|
sequence: 1,
|
||||||
|
hooks: [],
|
||||||
|
isLoading: false,
|
||||||
|
});
|
||||||
|
return toast.success("Account imported successfully!");
|
||||||
|
};
|
||||||
21
state/actions/index.ts
Normal file
21
state/actions/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { addFaucetAccount } from "./addFaucetAccount";
|
||||||
|
import { compileCode } from "./compileCode";
|
||||||
|
import { createNewFile } from "./createNewFile";
|
||||||
|
import { deployHook } from "./deployHook";
|
||||||
|
import { fetchFiles } from "./fetchFiles";
|
||||||
|
import { importAccount } from "./importAccount";
|
||||||
|
import { saveFile } from "./saveFile";
|
||||||
|
import { syncToGist } from "./syncToGist";
|
||||||
|
import { updateEditorSettings } from "./updateEditorSettings";
|
||||||
|
|
||||||
|
export {
|
||||||
|
addFaucetAccount,
|
||||||
|
compileCode,
|
||||||
|
createNewFile,
|
||||||
|
deployHook,
|
||||||
|
fetchFiles,
|
||||||
|
importAccount,
|
||||||
|
saveFile,
|
||||||
|
syncToGist,
|
||||||
|
updateEditorSettings
|
||||||
|
};
|
||||||
15
state/actions/saveFile.ts
Normal file
15
state/actions/saveFile.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import toast from "react-hot-toast";
|
||||||
|
import state from '../index';
|
||||||
|
|
||||||
|
export const saveFile = (showToast: boolean = true) => {
|
||||||
|
const editorModels = state.editorCtx?.getModels();
|
||||||
|
const currentModel = editorModels?.find((editorModel) => {
|
||||||
|
return editorModel.uri.path === `/c/${state.files[state.active].name}`;
|
||||||
|
});
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
state.files[state.active].content = currentModel?.getValue() || "";
|
||||||
|
}
|
||||||
|
if (showToast) {
|
||||||
|
toast.success("Saved successfully", { position: "bottom-center" });
|
||||||
|
}
|
||||||
|
};
|
||||||
97
state/actions/syncToGist.ts
Normal file
97
state/actions/syncToGist.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import type { Session } from "next-auth";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { Octokit } from "@octokit/core";
|
||||||
|
import Router from "next/router";
|
||||||
|
|
||||||
|
import state from '../index';
|
||||||
|
|
||||||
|
const octokit = new Octokit();
|
||||||
|
|
||||||
|
export const syncToGist = async (
|
||||||
|
session?: Session | null,
|
||||||
|
createNewGist?: boolean
|
||||||
|
) => {
|
||||||
|
let files: Record<string, { filename: string; content: string }> = {};
|
||||||
|
state.gistLoading = true;
|
||||||
|
|
||||||
|
if (!session || !session.user) {
|
||||||
|
state.gistLoading = false;
|
||||||
|
return toast.error("You need to be logged in!");
|
||||||
|
}
|
||||||
|
const toastId = toast.loading("Pushing to Gist");
|
||||||
|
if (!state.files || !state.files.length) {
|
||||||
|
state.gistLoading = false;
|
||||||
|
return toast.error(`You need to create some files we can push to gist`, {
|
||||||
|
id: toastId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
state.gistId &&
|
||||||
|
session?.user.username === state.gistOwner &&
|
||||||
|
!createNewGist
|
||||||
|
) {
|
||||||
|
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
|
||||||
|
gist_id: state.gistId,
|
||||||
|
});
|
||||||
|
if (currentFilesRes.data.files) {
|
||||||
|
Object.keys(currentFilesRes?.data?.files).forEach((filename) => {
|
||||||
|
files[`${filename}`] = { filename, content: "" };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.files.forEach((file) => {
|
||||||
|
files[`${file.name}`] = { filename: file.name, content: file.content };
|
||||||
|
});
|
||||||
|
// Update existing Gist
|
||||||
|
octokit
|
||||||
|
.request("PATCH /gists/{gist_id}", {
|
||||||
|
gist_id: state.gistId,
|
||||||
|
files,
|
||||||
|
headers: {
|
||||||
|
authorization: `token ${session?.accessToken || ""}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
state.gistLoading = false;
|
||||||
|
return toast.success("Updated to gist successfully!", { id: toastId });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
state.gistLoading = false;
|
||||||
|
return toast.error(`Could not update Gist, try again later!`, {
|
||||||
|
id: toastId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Not Gist of the current user or it isn't Gist yet
|
||||||
|
state.files.forEach((file) => {
|
||||||
|
files[`${file.name}`] = { filename: file.name, content: file.content };
|
||||||
|
});
|
||||||
|
octokit
|
||||||
|
.request("POST /gists", {
|
||||||
|
files,
|
||||||
|
public: true,
|
||||||
|
headers: {
|
||||||
|
authorization: `token ${session?.accessToken || ""}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
state.gistLoading = false;
|
||||||
|
state.gistOwner = res.data.owner?.login;
|
||||||
|
state.gistId = res.data.id;
|
||||||
|
state.gistName = Array.isArray(res.data.files)
|
||||||
|
? Object.keys(res.data?.files)?.[0]
|
||||||
|
: "Untitled";
|
||||||
|
Router.push({ pathname: `/develop/${res.data.id}` });
|
||||||
|
return toast.success("Created new gist successfully!", { id: toastId });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
state.gistLoading = false;
|
||||||
|
return toast.error(`Could not create Gist, try again later!`, {
|
||||||
|
id: toastId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default syncToGist;
|
||||||
12
state/actions/updateEditorSettings.ts
Normal file
12
state/actions/updateEditorSettings.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import state, { IState } from '../index';
|
||||||
|
|
||||||
|
export const updateEditorSettings = (
|
||||||
|
editorSettings: IState["editorSettings"]
|
||||||
|
) => {
|
||||||
|
state.editorCtx?.getModels().forEach((model) => {
|
||||||
|
model.updateOptions({
|
||||||
|
...editorSettings,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (state.editorSettings = editorSettings);
|
||||||
|
};
|
||||||
146
state/index.ts
Normal file
146
state/index.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { proxy, ref, subscribe } from "valtio";
|
||||||
|
import { devtools } from 'valtio/utils'
|
||||||
|
import type monaco from "monaco-editor";
|
||||||
|
import { XrplClient } from "xrpl-client";
|
||||||
|
import { addFaucetAccount } from "./actions/addFaucetAccount";
|
||||||
|
|
||||||
|
export interface IFile {
|
||||||
|
name: string;
|
||||||
|
language: string;
|
||||||
|
content: string;
|
||||||
|
compiledContent?: ArrayBuffer | null;
|
||||||
|
compiledWatContent?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FaucetAccountRes {
|
||||||
|
address: string;
|
||||||
|
secret: string;
|
||||||
|
xrp: number;
|
||||||
|
hash: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAccount {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
secret: string;
|
||||||
|
xrp: string;
|
||||||
|
sequence: number;
|
||||||
|
hooks: string[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILog {
|
||||||
|
type: "error" | "warning" | "log" | "success";
|
||||||
|
message: string;
|
||||||
|
link?: string;
|
||||||
|
linkText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IState {
|
||||||
|
files: IFile[];
|
||||||
|
gistId?: string | null;
|
||||||
|
gistOwner?: string | null;
|
||||||
|
gistName?: string | null;
|
||||||
|
active: number;
|
||||||
|
activeWat: number;
|
||||||
|
loading: boolean;
|
||||||
|
gistLoading: boolean;
|
||||||
|
compiling: boolean;
|
||||||
|
logs: ILog[];
|
||||||
|
deployLogs: ILog[];
|
||||||
|
editorCtx?: typeof monaco.editor;
|
||||||
|
editorSettings: {
|
||||||
|
tabSize: number;
|
||||||
|
};
|
||||||
|
client: XrplClient | null;
|
||||||
|
clientStatus: "offline" | "online";
|
||||||
|
mainModalOpen: boolean;
|
||||||
|
accounts: IAccount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// let localStorageState: null | string = null;
|
||||||
|
let initialState = {
|
||||||
|
files: [],
|
||||||
|
active: 0,
|
||||||
|
activeWat: 0,
|
||||||
|
loading: false,
|
||||||
|
compiling: false,
|
||||||
|
logs: [],
|
||||||
|
deployLogs: [],
|
||||||
|
editorCtx: undefined,
|
||||||
|
gistId: undefined,
|
||||||
|
gistOwner: undefined,
|
||||||
|
gistName: undefined,
|
||||||
|
gistLoading: false,
|
||||||
|
editorSettings: {
|
||||||
|
tabSize: 2,
|
||||||
|
},
|
||||||
|
client: null,
|
||||||
|
clientStatus: "offline" as "offline",
|
||||||
|
mainModalOpen: false,
|
||||||
|
accounts: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let localStorageAccounts: string | null = null;
|
||||||
|
let initialAccounts: IAccount[] = [];
|
||||||
|
// Check if there's a persited accounts in localStorage
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
try {
|
||||||
|
localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`localStorage state broken`);
|
||||||
|
localStorage.removeItem("hooksIdeAccounts");
|
||||||
|
}
|
||||||
|
if (localStorageAccounts) {
|
||||||
|
initialAccounts = JSON.parse(localStorageAccounts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize state
|
||||||
|
const state = proxy<IState>({
|
||||||
|
...initialState,
|
||||||
|
accounts: initialAccounts.length > 0 ? initialAccounts : [],
|
||||||
|
logs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize socket connection
|
||||||
|
const client = new XrplClient("wss://hooks-testnet.xrpl-labs.com");
|
||||||
|
|
||||||
|
client.on("online", () => {
|
||||||
|
state.client = ref(client);
|
||||||
|
state.clientStatus = "online";
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("offline", () => {
|
||||||
|
state.clientStatus = "offline";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// fetch initial faucets
|
||||||
|
(async function fetchFaucets() {
|
||||||
|
if (state.accounts.length < 2) {
|
||||||
|
await addFaucetAccount();
|
||||||
|
setTimeout(() => {
|
||||||
|
addFaucetAccount();
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
devtools(state, "Files State");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
subscribe(state, () => {
|
||||||
|
const { accounts, active } = state;
|
||||||
|
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
||||||
|
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
||||||
|
if (!state.files[active]?.compiledWatContent) {
|
||||||
|
state.activeWat = 0;
|
||||||
|
} else {
|
||||||
|
state.activeWat = active;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export default state
|
||||||
Reference in New Issue
Block a user