Extract actions to separate files

This commit is contained in:
Valtteri Karesto
2021-12-13 22:23:37 +02:00
parent 59637e32fe
commit 4d4b96bede
21 changed files with 620 additions and 585 deletions

View File

@@ -1,6 +1,7 @@
import toast from "react-hot-toast";
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 Container from "./Container";
import Heading from "./Heading";

View File

@@ -10,7 +10,7 @@ import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { state } from "../state";
import state from "../state";
import EditorNavigation from "./EditorNavigation";
import Text from "./Text";

View File

@@ -6,7 +6,8 @@ import useStayScrolled from "react-stay-scrolled";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { compileCode, state } from "../state";
import { compileCode } from "../state/actions";
import state from "../state";
import Button from "./Button";
import Heading from "./Heading";

View File

@@ -28,10 +28,10 @@ import { useSnapshot } from "valtio";
import {
createNewFile,
state,
syncToGist,
updateEditorSettings,
} from "../state";
} from "../state/actions";
import state from "../state";
import Box from "./Box";
import Button from "./Button";
import Container from "./Container";

View File

@@ -10,7 +10,8 @@ import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.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 Text from "./Text";

View File

@@ -12,7 +12,7 @@ import Flex from "./Flex";
import Container from "./Container";
import Box from "./Box";
import ThemeChanger from "./ThemeChanger";
import { state } from "../state";
import state from "../state";
import Heading from "./Heading";
import Text from "./Text";
import Spinner from "./Spinner";

View File

@@ -10,7 +10,8 @@ import { IdProvider } from "@radix-ui/react-id";
import { darkTheme, css } from "../stitches.config";
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) {
const router = useRouter();

View File

@@ -2,7 +2,7 @@ import React from "react";
import dynamic from "next/dynamic";
import Flex from "../../components/Flex";
import { useSnapshot } from "valtio";
import { state } from "../../state";
import state from "../../state";
const DeployEditor = dynamic(() => import("../../components/DeployEditor"), {
ssr: false,

View File

@@ -4,7 +4,8 @@ import Hotkeys from "react-hot-keys";
import { Play } from "phosphor-react";
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 Box from "../../components/Box";

575
state.ts
View File

@@ -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;
}
});
}

View 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,
});
}
};

View 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;
}
};

View 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;
};

View 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;
}
}
};

View 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;
};

View 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
View 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
View 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" });
}
};

View 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;

View 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
View 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