Merge branch 'main' into feat/engine-code-links

This commit is contained in:
muzam1l
2022-08-19 15:07:44 +05:30
115 changed files with 6250 additions and 6080 deletions

View File

@@ -1,25 +1,24 @@
import toast from "react-hot-toast";
import state, { FaucetAccountRes } from '../index';
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",
];
'Alice',
'Bob',
'Carol',
'Carlos',
'Charlie',
'Dan',
'Dave',
'David',
'Faythe',
'Frank',
'Grace',
'Heidi',
'Judy',
'Olive',
'Peggy',
'Walter'
]
/* This function adds faucet account to application global state.
* It calls the /api/faucet endpoint which in send a HTTP POST to
@@ -30,22 +29,22 @@ export const names = [
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
if (typeof window === undefined) return
const toastId = showToast ? toast.loading("Creating account") : "";
const toastId = showToast ? toast.loading('Creating account') : ''
const res = await fetch(`${window.location.origin}/api/faucet`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
method: 'POST'
})
const json: FaucetAccountRes | { error: string } = await res.json()
if ('error' in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
return toast.error(json.error, { id: toastId })
} else {
return;
return
}
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
toast.success('New account created', { id: toastId })
}
const currNames = state.accounts.map(acc => acc.name);
const currNames = state.accounts.map(acc => acc.name)
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
@@ -55,36 +54,35 @@ export const addFaucetAccount = async (name?: string, showToast: boolean = false
hooks: [],
isLoading: false,
version: '2'
});
})
}
};
}
// fetch initial faucets
(async function fetchFaucets() {
;(async function fetchFaucets() {
if (typeof window !== 'undefined') {
if (state.accounts.length === 0) {
await addFaucetAccount();
await addFaucetAccount()
// setTimeout(() => {
// addFaucetAccount();
// }, 10000);
}
}
})();
})()
export const addFunds = async (address: string) => {
const toastId = toast.loading("Requesting funds");
const toastId = toast.loading('Requesting funds')
const res = await fetch(`${window.location.origin}/api/faucet?account=${address}`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
return toast.error(json.error, { id: toastId });
method: 'POST'
})
const json: FaucetAccountRes | { error: string } = await res.json()
if ('error' in json) {
return toast.error(json.error, { id: toastId })
} else {
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId });
const currAccount = state.accounts.find(acc => acc.address === address);
toast.success(`Funds added (${json.xrp} XRP)`, { id: toastId })
const currAccount = state.accounts.find(acc => acc.address === address)
if (currAccount) {
currAccount.xrp = (Number(currAccount.xrp) + (json.xrp * 1000000)).toString();
currAccount.xrp = (Number(currAccount.xrp) + json.xrp * 1000000).toString()
}
}
}
}

View File

@@ -1,30 +1,30 @@
import toast from "react-hot-toast";
import Router from 'next/router';
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";
import state from '../index'
import { saveFile } from './saveFile'
import { decodeBinary } from '../../utils/decodeBinary'
import { ref } from 'valtio'
/* compileCode sends the code of the active file to compile endpoint
* If all goes well you will get base64 encoded wasm file back with
* some extra logging information if we can provide it. This function
* some extra logging information if we can provide it. This function
* also decodes the returned wasm and creates human readable WAT file
* out of it and store both in global state.
*/
export const compileCode = async (activeId: number) => {
// Save the file to global state
saveFile(false, activeId);
saveFile(false, activeId)
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!");
throw Error('Missing env!')
}
// Bail out if we're already compiling
if (state.compiling) {
// if compiling is ongoing return // TODO Inform user about it.
return;
return
}
// Set loading state to true
state.compiling = true;
state.compiling = true
state.logs = []
const file = state.files[activeId]
try {
@@ -32,29 +32,29 @@ export const compileCode = async (activeId: number) => {
let res: Response
try {
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json'
},
body: JSON.stringify({
output: "wasm",
output: 'wasm',
compress: true,
strip: state.compileOptions.strip,
files: [
{
type: "c",
type: 'c',
options: state.compileOptions.optimizationLevel || '-O2',
name: file.name,
src: file.content,
},
],
}),
});
src: file.content
}
]
})
})
} catch (error) {
throw Error("Something went wrong, check your network connection and try again!")
throw Error('Something went wrong, check your network connection and try again!')
}
const json = await res.json();
state.compiling = false;
const json = await res.json()
state.compiling = false
if (!json.success) {
const errors = [json.message]
if (json.tasks && json.tasks.length > 0) {
@@ -62,65 +62,63 @@ export const compileCode = async (activeId: number) => {
if (!task.success) {
errors.push(task?.console)
}
});
})
}
throw errors
}
try {
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
const bufferData = await decodeBinary(json.output)
// Import wabt from and create human readable version of wasm file and
// put it into state
const ww = (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
readDebugNames: true
})
myModule.applyNames()
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
const wast = myModule.toText({ foldExprs: false, inlineExport: false })
file.compiledContent = ref(bufferData);
file.lastCompiled = new Date();
file.compiledContent = ref(bufferData)
file.lastCompiled = new Date()
file.compiledValueSnapshot = file.content
file.compiledWatContent = wast;
file.compiledWatContent = wast
} catch (error) {
throw Error("Invalid compilation result produced, check your code for errors and try again!")
throw Error('Invalid compilation result produced, check your code for errors and try again!')
}
toast.success("Compiled successfully!", { position: "bottom-center" });
toast.success('Compiled successfully!', { position: 'bottom-center' })
state.logs.push({
type: "success",
type: 'success',
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
});
link: Router.asPath.replace('develop', 'deploy'),
linkText: 'Go to deploy'
})
} catch (err) {
console.log(err);
console.log(err)
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({
type: "error",
message,
});
type: 'error',
message
})
})
} else if (err instanceof Error) {
state.logs.push({
type: 'error',
message: err.message
})
} else {
state.logs.push({
type: 'error',
message: 'Something went wrong, come back later!'
})
}
else if (err instanceof Error) {
state.logs.push({
type: "error",
message: err.message,
});
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, come back later!",
});
}
state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
state.compiling = false
toast.error(`Error occurred while compiling!`, { position: 'bottom-center' })
file.containsErrors = true
}
};
}

View File

@@ -1,24 +1,28 @@
import state, { IFile } from '../index';
import state, { IFile } from '../index'
const languageMapping = {
'ts': 'typescript',
'js': 'javascript',
'md': 'markdown',
'c': 'c',
'h': 'c',
'other': ''
ts: 'typescript',
js: 'javascript',
md: 'markdown',
c: 'c',
h: 'c',
other: ''
} /* Initializes empty file to global state */
export const createNewFile = (name: string) => {
const tempName = name.split('.');
const fileExt = tempName[tempName.length - 1] || 'other';
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
state.files.push(emptyFile);
state.active = state.files.length - 1;
};
const tempName = name.split('.')
const fileExt = tempName[tempName.length - 1] || 'other'
const emptyFile: IFile = {
name,
language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'],
content: ''
}
state.files.push(emptyFile)
state.active = state.files.length - 1
}
export const renameFile = (oldName: string, nwName: string) => {
const file = state.files.find(file => file.name === oldName)
if (!file) throw Error(`No file exists with name ${oldName}`)
file.name = nwName
};
}

View File

@@ -1,24 +1,24 @@
import state, { transactionsState } from '..';
import state, { transactionsState } from '..'
export const deleteAccount = (addr?: string) => {
if (!addr) return;
const index = state.accounts.findIndex(acc => acc.address === addr);
if (index === -1) return;
state.accounts.splice(index, 1);
if (!addr) return
const index = state.accounts.findIndex(acc => acc.address === addr)
if (index === -1) return
state.accounts.splice(index, 1)
// update selected accounts
transactionsState.transactions
.filter(t => t.state.selectedAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedAccount;
if (!acc) return;
acc.label = acc.value;
});
transactionsState.transactions
.filter(t => t.state.selectedDestAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedDestAccount;
if (!acc) return;
acc.label = acc.value;
});
};
// update selected accounts
transactionsState.transactions
.filter(t => t.state.selectedAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedAccount
if (!acc) return
acc.label = acc.value
})
transactionsState.transactions
.filter(t => t.state.selectedDestAccount?.value === addr)
.forEach(t => {
const acc = t.state.selectedDestAccount
if (!acc) return
acc.label = acc.value
})
}

View File

@@ -1,54 +1,52 @@
import { derive, sign } from "xrpl-accountlib";
import toast from "react-hot-toast";
import { derive, sign } from 'xrpl-accountlib'
import toast from 'react-hot-toast'
import state, { IAccount } from "../index";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import { Link } from "../../components";
import { ref } from "valtio";
import estimateFee from "../../utils/estimateFee";
import { SetHookData } from "../../utils/setHook";
import ResultLink from "../../components/ResultLink";
import state, { IAccount } from '../index'
import calculateHookOn, { TTS } from '../../utils/hookOnCalculator'
import { Link } from '../../components'
import { ref } from 'valtio'
import estimateFee from '../../utils/estimateFee'
import { SetHookData } from '../../utils/setHook'
import ResultLink from '../../components/ResultLink'
export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string);
const hashBuffer = await crypto.subtle.digest("SHA-256", utf8);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map(bytes => bytes.toString(16).padStart(2, "0"))
.join("");
return hashHex;
};
const utf8 = new TextEncoder().encode(string)
const hashBuffer = await crypto.subtle.digest('SHA-256', utf8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(bytes => bytes.toString(16).padStart(2, '0')).join('')
return hashHex
}
function toHex(str: string) {
var result = "";
var result = ''
for (var i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16);
result += str.charCodeAt(i).toString(16)
}
return result.toUpperCase();
return result.toUpperCase()
}
function arrayBufferToHex(arrayBuffer?: ArrayBuffer | null) {
if (!arrayBuffer) {
return "";
return ''
}
if (
typeof arrayBuffer !== "object" ||
typeof arrayBuffer !== 'object' ||
arrayBuffer === null ||
typeof arrayBuffer.byteLength !== "number"
typeof arrayBuffer.byteLength !== 'number'
) {
throw new TypeError("Expected input to be an ArrayBuffer");
throw new TypeError('Expected input to be an ArrayBuffer')
}
var view = new Uint8Array(arrayBuffer);
var result = "";
var value;
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;
value = view[i].toString(16)
result += value.length === 1 ? '0' + value : value
}
return result;
return result
}
export const prepareDeployHookTx = async (
@@ -57,30 +55,29 @@ export const prepareDeployHookTx = async (
) => {
const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active]
: state.files.filter(file => file.compiledContent)[0];
: state.files.filter(file => file.compiledContent)[0]
if (!state.files || state.files.length === 0) {
return;
return
}
if (!activeFile?.compiledContent) {
return;
return
}
if (!state.client) {
return;
return
}
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase();
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value);
const { HookParameters } = data;
const HookNamespace = (await sha256(data.HookNamespace)).toUpperCase()
const hookOnValues: (keyof TTS)[] = data.Invoke.map(tt => tt.value)
const { HookParameters } = data
const filteredHookParameters = HookParameters.filter(
hp =>
hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
hp => hp.HookParameter.HookParameterName && hp.HookParameter.HookParameterValue
)?.map(aa => ({
HookParameter: {
HookParameterName: toHex(aa.HookParameter.HookParameterName || ""),
HookParameterValue: aa.HookParameter.HookParameterValue || "",
},
}));
HookParameterName: toHex(aa.HookParameter.HookParameterName || ''),
HookParameterValue: aa.HookParameter.HookParameterValue || ''
}
}))
// const filteredHookGrants = HookGrants.filter(hg => hg.HookGrant.Authorize || hg.HookGrant.HookHash).map(hg => {
// return {
// HookGrant: {
@@ -90,79 +87,71 @@ export const prepareDeployHookTx = async (
// }
// }
// });
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
const tx = {
Account: account.address,
TransactionType: "SetHook",
TransactionType: 'SetHook',
Sequence: account.sequence,
Fee: data.Fee,
Hooks: [
{
Hook: {
CreateCode: arrayBufferToHex(
activeFile?.compiledContent
).toUpperCase(),
CreateCode: arrayBufferToHex(activeFile?.compiledContent).toUpperCase(),
HookOn: calculateHookOn(hookOnValues),
HookNamespace,
HookApiVersion: 0,
Flags: 1,
// ...(filteredHookGrants.length > 0 && { HookGrants: filteredHookGrants }),
...(filteredHookParameters.length > 0 && {
HookParameters: filteredHookParameters,
}),
},
},
],
};
return tx;
HookParameters: filteredHookParameters
})
}
}
]
}
return tx
}
};
}
/* deployHook function turns the wasm binary into
* hex string, signs the transaction and deploys it to
* Hooks testnet.
*/
export const deployHook = async (
account: IAccount & { name?: string },
data: SetHookData
) => {
if (typeof window !== "undefined") {
export const deployHook = async (account: IAccount & { name?: string }, data: SetHookData) => {
if (typeof window !== 'undefined') {
const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active]
: state.files.filter(file => file.compiledContent)[0];
state.deployValues[activeFile.name] = data;
const tx = await prepareDeployHookTx(account, data);
: state.files.filter(file => file.compiledContent)[0]
state.deployValues[activeFile.name] = data
const tx = await prepareDeployHookTx(account, data)
if (!tx) {
return;
return
}
if (!state.client) {
return;
return
}
const keypair = derive.familySeed(account.secret);
const keypair = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, keypair);
const currentAccount = state.accounts.find(
acc => acc.address === account.address
);
const { signedTransaction } = sign(tx, keypair)
const currentAccount = state.accounts.find(acc => acc.address === account.address)
if (currentAccount) {
currentAccount.isLoading = true;
currentAccount.isLoading = true
}
let submitRes;
let submitRes
try {
submitRes = await state.client?.send({
command: "submit",
tx_blob: signedTransaction,
});
command: 'submit',
tx_blob: signedTransaction
})
const txHash = submitRes.tx_json?.hash;
const txHash = submitRes.tx_json?.hash
const resultMsg = ref(
<>
[<ResultLink result={submitRes.engine_result} />]{" "}
{submitRes.engine_result_message}{" "}
[<ResultLink result={submitRes.engine_result} />] {submitRes.engine_result_message}{' '}
{txHash && (
<>
Transaction hash:{" "}
Transaction hash:{' '}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${txHash}`}
@@ -174,123 +163,120 @@ export const deployHook = async (
</>
)}
</>
);
if (submitRes.engine_result === "tesSUCCESS") {
)
if (submitRes.engine_result === 'tesSUCCESS') {
state.deployLogs.push({
type: "success",
message: "Hook deployed successfully ✅",
});
type: 'success',
message: 'Hook deployed successfully ✅'
})
state.deployLogs.push({
type: "success",
message: resultMsg,
});
type: 'success',
message: resultMsg
})
} else if (submitRes.engine_result) {
state.deployLogs.push({
type: "error",
message: resultMsg,
});
type: 'error',
message: resultMsg
})
} else {
state.deployLogs.push({
type: "error",
message: `[${submitRes.error}] ${submitRes.error_exception}`,
});
type: 'error',
message: `[${submitRes.error}] ${submitRes.error_exception}`
})
}
} catch (err) {
console.log(err);
console.log(err)
state.deployLogs.push({
type: "error",
message: "Error occurred while deploying",
});
type: 'error',
message: 'Error occurred while deploying'
})
}
if (currentAccount) {
currentAccount.isLoading = false;
currentAccount.isLoading = false
}
return submitRes;
return submitRes
}
};
}
export const deleteHook = async (account: IAccount & { name?: string }) => {
if (!state.client) {
return;
return
}
const currentAccount = state.accounts.find(
acc => acc.address === account.address
);
const currentAccount = state.accounts.find(acc => acc.address === account.address)
if (currentAccount?.isLoading || !currentAccount?.hooks.length) {
return;
return
}
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
const tx = {
Account: account.address,
TransactionType: "SetHook",
TransactionType: 'SetHook',
Sequence: account.sequence,
Fee: "100000",
Fee: '100000',
Hooks: [
{
Hook: {
CreateCode: "",
Flags: 1,
},
},
],
};
CreateCode: '',
Flags: 1
}
}
]
}
const keypair = derive.familySeed(account.secret);
const keypair = derive.familySeed(account.secret)
try {
// Update tx Fee value with network estimation
const res = await estimateFee(tx, account);
tx["Fee"] = res?.base_fee ? res?.base_fee : "1000";
const res = await estimateFee(tx, account)
tx['Fee'] = res?.base_fee ? res?.base_fee : '1000'
} catch (err) {
// use default value what you defined earlier
console.log(err);
console.log(err)
}
const { signedTransaction } = sign(tx, keypair);
const { signedTransaction } = sign(tx, keypair)
if (currentAccount) {
currentAccount.isLoading = true;
currentAccount.isLoading = true
}
let submitRes;
const toastId = toast.loading("Deleting hook...");
let submitRes
const toastId = toast.loading('Deleting hook...')
try {
submitRes = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
command: 'submit',
tx_blob: signedTransaction
})
if (submitRes.engine_result === "tesSUCCESS") {
toast.success("Hook deleted successfully ✅", { id: toastId });
if (submitRes.engine_result === 'tesSUCCESS') {
toast.success('Hook deleted successfully ✅', { id: toastId })
state.deployLogs.push({
type: "success",
message: "Hook deleted successfully ✅",
});
type: 'success',
message: 'Hook deleted successfully ✅'
})
state.deployLogs.push({
type: "success",
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
currentAccount.hooks = [];
type: 'success',
message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`
})
currentAccount.hooks = []
} else {
toast.error(
`${submitRes.engine_result_message || submitRes.error_exception}`,
{ id: toastId }
);
toast.error(`${submitRes.engine_result_message || submitRes.error_exception}`, {
id: toastId
})
state.deployLogs.push({
type: "error",
type: 'error',
message: `[${submitRes.engine_result || submitRes.error}] ${
submitRes.engine_result_message || submitRes.error_exception
}`,
});
}`
})
}
} catch (err) {
console.log(err);
toast.error("Error occurred while deleting hook", { id: toastId });
console.log(err)
toast.error('Error occurred while deleting hook', { id: toastId })
state.deployLogs.push({
type: "error",
message: "Error occurred while deleting hook",
});
type: 'error',
message: 'Error occurred while deleting hook'
})
}
if (currentAccount) {
currentAccount.isLoading = false;
currentAccount.isLoading = false
}
return submitRes;
return submitRes
}
};
}

View File

@@ -1,20 +1,22 @@
import { createZip } from '../../utils/zip';
import { guessZipFileName } from '../../utils/helpers';
import { createZip } from '../../utils/zip'
import { guessZipFileName } from '../../utils/helpers'
import state from '..'
import toast from 'react-hot-toast';
import toast from 'react-hot-toast'
export const downloadAsZip = async () => {
try {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }));
const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }));
const zipped = await createZip([...files, ...wasmFiles]);
const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName);
} catch (error) {
toast.error('Error occurred while creating zip file, try again later')
} finally {
state.zipLoading = false
}
};
try {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }))
const wasmFiles = state.files
.filter(i => i.compiledContent)
.map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }))
const zipped = await createZip([...files, ...wasmFiles])
const zipFileName = guessZipFileName(files)
zipped.saveFile(zipFileName)
} catch (error) {
toast.error('Error occurred while creating zip file, try again later')
} finally {
state.zipLoading = false
}
}

View File

@@ -1,117 +1,99 @@
import { Octokit } from "@octokit/core";
import Router from "next/router";
import state from '../index';
import { templateFileIds } from '../constants';
import { Octokit } from '@octokit/core'
import state, { IFile } from '../index'
import { templateFileIds } from '../constants'
const octokit = new Octokit();
const octokit = new Octokit()
/* Fetches Gist files from Githug Gists based on
* gistId and stores the content in global state
/**
* Fetches files from Github Gists based on gistId and stores them in global state
*/
export const fetchFiles = (gistId: string) => {
state.loading = true;
if (gistId && !state.files.length) {
export const fetchFiles = async (gistId: string) => {
if (!gistId || state.files.length) return
state.loading = true
state.logs.push({
type: 'log',
message: `Fetching Gist with id: ${gistId}`
})
try {
const res = await octokit.request('GET /gists/{gist_id}', { gist_id: gistId })
const isTemplate = (id: string) =>
Object.values(templateFileIds)
.map(v => v.id)
.includes(id)
if (isTemplate(gistId)) {
// fetch headers
const headerRes = await fetch(
`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`
)
if (!headerRes.ok) throw Error('Failed to fetch headers')
const headerJson = await headerRes.json()
const headerFiles: Record<string, { filename: string; content: string; language: string }> =
{}
Object.entries(headerJson).forEach(([key, value]) => {
const fname = `${key}.h`
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
})
const files = {
...res.data.files,
...headerFiles
}
res.data.files = files
}
if (!res.data.files) throw Error('No files could be fetched from given gist id!')
const files: IFile[] = Object.keys(res.data.files).map(filename => ({
name: res.data.files?.[filename]?.filename || 'untitled.c',
language: res.data.files?.[filename]?.language?.toLowerCase() || '',
content: res.data.files?.[filename]?.content || ''
}))
files.sort((a, b) => {
const aBasename = a.name.split('.')?.[0]
const aExt = a.name.split('.').pop() || ''
const bBasename = b.name.split('.')?.[0]
const bExt = b.name.split('.').pop() || ''
// default priority is undefined == 0
const extPriority: Record<string, number> = {
c: 3,
md: 2,
h: -1
}
// Sort based on extention priorities
const comp = (extPriority[bExt] || 0) - (extPriority[aExt] || 0)
if (comp !== 0) return comp
// Otherwise fallback to alphabetical sorting
return aBasename.localeCompare(bBasename)
})
state.logs.push({
type: "log",
message: `Fetching Gist with id: ${gistId}`,
});
type: 'success',
message: 'Fetched successfully ✅'
})
state.files = files
state.gistId = gistId
state.gistOwner = res.data.owner?.login
octokit
.request("GET /gists/{gist_id}", { gist_id: gistId })
.then(async res => {
if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) {
return res
}
// in case of templates, fetch header file(s) and append to res
try {
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
if (resHeader.ok) {
const resHeaderJson = await resHeader.json()
const headerFiles: Record<string, { filename: string; content: string; language: string }> = {};
Object.entries(resHeaderJson).forEach(([key, value]) => {
const fname = `${key}.h`;
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
})
const files = {
...res.data.files,
...headerFiles
};
res.data.files = files;
}
} catch (err) {
console.log(err)
}
return res;
// If you want to load templates from GIST instad, uncomment the code below and comment the code above.
// return octokit.request("GET /gists/{gist_id}", { gist_id: templateFileIds.headers }).then(({ data: { files: headerFiles } }) => {
// const files = { ...res.data.files, ...headerFiles }
// console.log(headerFiles)
// res.data.files = files
// return res
// })
})
.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 || "untitled.c",
language: res.data.files?.[filename]?.language?.toLowerCase() || "",
content: res.data.files?.[filename]?.content || "",
}));
// Sort files so that the source files are first
// In case of other files leave the order as it its
files.sort((a, b) => {
const aBasename = a.name.split('.')?.[0];
const aCext = a.name?.toLowerCase().endsWith('.c');
const bBasename = b.name.split('.')?.[0];
const bCext = b.name?.toLowerCase().endsWith('.c');
// If a has c extension and b doesn't move a up
if (aCext && !bCext) {
return -1;
}
if (!aCext && bCext) {
return 1
}
// Otherwise fallback to default sorting based on basename
if (aBasename > bBasename) {
return 1;
}
if (bBasename > aBasename) {
return -1;
}
return 0;
})
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) => {
// console.error(err)
state.loading = false;
state.logs.push({
type: "error",
message: `Couldn't find Gist with id: ${gistId}`,
});
return;
});
return;
const gistName =
files.find(file => file.language === 'c' || file.language === 'javascript')?.name ||
'untitled'
state.gistName = gistName
} catch (err) {
console.error(err)
let message: string
if (err instanceof Error) message = err.message
else message = `Something went wrong, try again later!`
state.logs.push({
type: 'error',
message: `Error: ${message}`
})
}
state.loading = false;
};
state.loading = false
}

View File

@@ -1,40 +1,40 @@
import toast from "react-hot-toast";
import { derive, XRPL_Account } from "xrpl-accountlib";
import toast from 'react-hot-toast'
import { derive, XRPL_Account } from 'xrpl-accountlib'
import state from '../index';
import { names } from './addFaucetAccount';
import state from '../index'
import { names } from './addFaucetAccount'
// Adds test account to global state with secret key
export const importAccount = (secret: string, name?: string) => {
if (!secret) {
return toast.error("You need to add secret!");
return toast.error('You need to add secret!')
}
if (state.accounts.find((acc) => acc.secret === secret)) {
return toast.error("Account already added!");
if (state.accounts.find(acc => acc.secret === secret)) {
return toast.error('Account already added!')
}
let account: XRPL_Account | null = null;
let account: XRPL_Account | null = null
try {
account = derive.familySeed(secret);
account = derive.familySeed(secret)
} catch (err: any) {
if (err?.message) {
toast.error(err.message)
} else {
toast.error('Error occurred while importing account')
}
return;
return
}
if (!account || !account.secret.familySeed) {
return toast.error(`Couldn't create account!`);
return toast.error(`Couldn't create account!`)
}
state.accounts.push({
name: name || names[state.accounts.length],
address: account.address || "",
secret: account.secret.familySeed || "",
xrp: "0",
address: account.address || '',
secret: account.secret.familySeed || '',
xrp: '0',
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
return toast.success("Account imported successfully!");
};
})
return toast.success('Account imported successfully!')
}

View File

@@ -1,14 +1,14 @@
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";
import { downloadAsZip } from "./downloadAsZip";
import { sendTransaction } from "./sendTransaction";
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'
import { downloadAsZip } from './downloadAsZip'
import { sendTransaction } from './sendTransaction'
export {
addFaucetAccount,
@@ -22,4 +22,4 @@ export {
updateEditorSettings,
downloadAsZip,
sendTransaction
};
}

View File

@@ -1,5 +1,5 @@
import { snapshot } from "valtio"
import state from ".."
import { snapshot } from 'valtio'
import state from '..'
export type SplitSize = number[]
@@ -12,4 +12,3 @@ export const getSplit = (splitId: string): SplitSize | null => {
const split = splits[splitId]
return split ? split : null
}

View File

@@ -1,28 +1,28 @@
import toast from "react-hot-toast";
import state from '../index';
import toast from 'react-hot-toast'
import state from '../index'
// Saves the current editor content to global state
export const saveFile = (showToast: boolean = true, activeId?: number) => {
const editorModels = state.editorCtx?.getModels();
const sought = '/' + state.files[state.active].name;
const currentModel = editorModels?.find((editorModel) => {
return editorModel.uri.path.endsWith(sought);
});
const editorModels = state.editorCtx?.getModels()
const sought = '/' + state.files[state.active].name
const currentModel = editorModels?.find(editorModel => {
return editorModel.uri.path.endsWith(sought)
})
const file = state.files[activeId || state.active]
if (state.files.length > 0) {
file.content = currentModel?.getValue() || "";
file.content = currentModel?.getValue() || ''
}
if (showToast) {
toast.success("Saved successfully", { position: "bottom-center" });
toast.success('Saved successfully', { position: 'bottom-center' })
}
};
}
export const saveAllFiles = () => {
const editorModels = state.editorCtx?.getModels();
const editorModels = state.editorCtx?.getModels()
state.files.forEach(file => {
const currentModel = editorModels?.find(model => model.uri.path.endsWith('/' + file.name))
if (currentModel) {
file.content = currentModel?.getValue() || '';
file.content = currentModel?.getValue() || ''
}
})
}

View File

@@ -0,0 +1,66 @@
import { derive, sign } from 'xrpl-accountlib'
import state from '..'
import type { IAccount } from '..'
interface TransactionOptions {
TransactionType: string
Account?: string
Fee?: string
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string
}
export const sendTransaction = async (
account: IAccount,
txOptions: TransactionOptions,
options?: OtherOptions
) => {
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = '1000', ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence,
Fee, // TODO auto-fillable default
...opts
}
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, signedAccount)
const response = await state.client.send({
command: 'submit',
tx_blob: signedTransaction
})
if (response.engine_result === 'tesSUCCESS') {
state.transactionLogs.push({
type: 'success',
message: `${logPrefix}[${response.engine_result}] ${response.engine_result_message}`
})
} else {
state.transactionLogs.push({
type: 'error',
message: `${logPrefix}[${response.error || response.engine_result}] ${
response.error_exception || response.engine_result_message
}`
})
}
const currAcc = state.accounts.find(acc => acc.address === account.address)
if (currAcc && response.account_sequence_next) {
currAcc.sequence = response.account_sequence_next
}
} catch (err) {
console.error(err)
state.transactionLogs.push({
type: 'error',
message:
err instanceof Error
? `${logPrefix}Error: ${err.message}`
: `${logPrefix}Something went wrong, try again later`
})
}
}

View File

@@ -1,19 +1,19 @@
import { derive, sign } from "xrpl-accountlib";
import { derive, sign } from 'xrpl-accountlib'
import state from "..";
import type { IAccount } from "..";
import ResultLink from "../../components/ResultLink";
import { ref } from "valtio";
import state from '..'
import type { IAccount } from '..'
import ResultLink from '../../components/ResultLink'
import { ref } from 'valtio'
interface TransactionOptions {
TransactionType: string;
Account?: string;
Fee?: string;
Destination?: string;
[index: string]: any;
TransactionType: string
Account?: string
Fee?: string
Destination?: string
[index: string]: any
}
interface OtherOptions {
logPrefix?: string;
logPrefix?: string
}
export const sendTransaction = async (
@@ -21,58 +21,57 @@ export const sendTransaction = async (
txOptions: TransactionOptions,
options?: OtherOptions
) => {
if (!state.client) throw Error("XRPL client not initalized");
if (!state.client) throw Error('XRPL client not initalized')
const { Fee = "1000", ...opts } = txOptions;
const { Fee = '1000', ...opts } = txOptions
const tx: TransactionOptions = {
Account: account.address,
Sequence: account.sequence,
Fee, // TODO auto-fillable default
...opts,
};
const { logPrefix = "" } = options || {};
...opts
}
const { logPrefix = '' } = options || {}
try {
const signedAccount = derive.familySeed(account.secret);
const { signedTransaction } = sign(tx, signedAccount);
const signedAccount = derive.familySeed(account.secret)
const { signedTransaction } = sign(tx, signedAccount)
const response = await state.client.send({
command: "submit",
tx_blob: signedTransaction,
});
command: 'submit',
tx_blob: signedTransaction
})
const resultMsg = ref(
<>
{logPrefix}[<ResultLink result={response.engine_result} />]{" "}
{response.engine_result_message}
{logPrefix}[<ResultLink result={response.engine_result} />] {response.engine_result_message}
</>
);
if (response.engine_result === "tesSUCCESS") {
)
if (response.engine_result === 'tesSUCCESS') {
state.transactionLogs.push({
type: "success",
message: resultMsg,
});
type: 'success',
message: resultMsg
})
} else if (response.engine_result) {
state.transactionLogs.push({
type: "error",
message: resultMsg,
});
type: 'error',
message: resultMsg
})
} else {
state.transactionLogs.push({
type: "error",
message: `${logPrefix}[${response.error}] ${response.error_exception}`,
});
type: 'error',
message: `${logPrefix}[${response.error}] ${response.error_exception}`
})
}
const currAcc = state.accounts.find(acc => acc.address === account.address);
const currAcc = state.accounts.find(acc => acc.address === account.address)
if (currAcc && response.account_sequence_next) {
currAcc.sequence = response.account_sequence_next;
currAcc.sequence = response.account_sequence_next
}
} catch (err) {
console.error(err);
console.error(err)
state.transactionLogs.push({
type: "error",
type: 'error',
message:
err instanceof Error
? `${logPrefix}Error: ${err.message}`
: `${logPrefix}Something went wrong, try again later`,
});
: `${logPrefix}Something went wrong, try again later`
})
}
};
}

View File

@@ -1,23 +1,27 @@
import { ref } from 'valtio';
import { AlertState, alertState } from "../../components/AlertDialog";
import { ref } from 'valtio'
import { AlertState, alertState } from '../../components/AlertDialog'
export const showAlert = (title: string, opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}) => {
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
const body = (_body && typeof _body === 'object') ? ref(_body) : _body
const confirmPrefix = (_confirmPrefix && typeof _confirmPrefix === 'object') ? ref(_confirmPrefix) : _confirmPrefix
export const showAlert = (
title: string,
opts: Omit<Partial<AlertState>, 'title' | 'isOpen'> = {}
) => {
const { body: _body, confirmPrefix: _confirmPrefix, ...rest } = opts
const body = _body && typeof _body === 'object' ? ref(_body) : _body
const confirmPrefix =
_confirmPrefix && typeof _confirmPrefix === 'object' ? ref(_confirmPrefix) : _confirmPrefix
const nwState: AlertState = {
isOpen: true,
title,
body,
confirmPrefix,
cancelText: undefined,
confirmText: undefined,
onCancel: undefined,
onConfirm: undefined,
...rest,
}
Object.entries(nwState).forEach(([key, value]) => {
(alertState as any)[key] = value
})
}
const nwState: AlertState = {
isOpen: true,
title,
body,
confirmPrefix,
cancelText: undefined,
confirmText: undefined,
onCancel: undefined,
onConfirm: undefined,
...rest
}
Object.entries(nwState).forEach(([key, value]) => {
;(alertState as any)[key] = value
})
}

View File

@@ -1,104 +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 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';
import { saveAllFiles } from "./saveFile";
import state from '../index'
import { saveAllFiles } from './saveFile'
const octokit = new Octokit();
const octokit = new Octokit()
// Syncs the current files from the state to GitHub Gists.
export const syncToGist = async (
session?: Session | null,
createNewGist?: boolean
) => {
saveAllFiles();
let files: Record<string, { filename: string; content: string }> = {};
state.gistLoading = true;
export const syncToGist = async (session?: Session | null, createNewGist?: boolean) => {
saveAllFiles()
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!");
state.gistLoading = false
return toast.error('You need to be logged in!')
}
const toastId = toast.loading("Pushing to Gist");
const toastId = toast.loading('Pushing to Gist')
if (!state.files || !state.files.length) {
state.gistLoading = false;
state.gistLoading = false
return toast.error(`You need to create some files we can push to gist`, {
id: toastId,
});
id: toastId
})
}
if (
state.gistId &&
session?.user.username === state.gistOwner &&
!createNewGist
) {
if (state.gistId && session?.user.username === state.gistOwner && !createNewGist) {
// You can only remove files from Gist by updating file with empty contents
// So we need to fetch existing files and compare those to local state
// and then send empty content if we don't have matching files anymore
// on local state
const currentFilesRes = await octokit.request("GET /gists/{gist_id}", {
gist_id: state.gistId,
});
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: "" };
});
Object.keys(currentFilesRes?.data?.files).forEach(filename => {
files[`${filename}`] = { filename, content: '' }
})
}
state.files.forEach((file) => {
files[`${file.name}`] = { filename: file.name, content: file.content };
});
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
// Update existing Gist
octokit
.request("PATCH /gists/{gist_id}", {
.request('PATCH /gists/{gist_id}', {
gist_id: state.gistId,
files,
headers: {
authorization: `token ${session?.accessToken || ""}`,
},
authorization: `token ${session?.accessToken || ''}`
}
})
.then((res) => {
state.gistLoading = false;
return toast.success("Updated to gist successfully!", { id: toastId });
.then(res => {
state.gistLoading = false
return toast.success('Updated to gist successfully!', { id: toastId })
})
.catch((err) => {
console.log(err);
state.gistLoading = false;
.catch(err => {
console.log(err)
state.gistLoading = false
return toast.error(`Could not update Gist, try again later!`, {
id: toastId,
});
});
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 };
});
state.files.forEach(file => {
files[`${file.name}`] = { filename: file.name, content: file.content }
})
octokit
.request("POST /gists", {
.request('POST /gists', {
files,
public: true,
headers: {
authorization: `token ${session?.accessToken || ""}`,
},
authorization: `token ${session?.accessToken || ''}`
}
})
.then((res) => {
state.gistLoading = false;
state.gistOwner = res.data.owner?.login;
state.gistId = res.data.id;
.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 });
: '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;
.catch(err => {
console.log(err)
state.gistLoading = false
return toast.error(`Could not create Gist, try again later!`, {
id: toastId,
});
});
id: toastId
})
})
}
};
}
export default syncToGist;
export default syncToGist

View File

@@ -1,14 +1,12 @@
import state, { IState } from '../index';
import state, { IState } from '../index'
// Updates editor settings and stores them
// in global state
export const updateEditorSettings = (
editorSettings: IState["editorSettings"]
) => {
state.editorCtx?.getModels().forEach((model) => {
export const updateEditorSettings = (editorSettings: IState['editorSettings']) => {
state.editorCtx?.getModels().forEach(model => {
model.updateOptions({
...editorSettings,
});
});
return (state.editorSettings = editorSettings);
};
...editorSettings
})
})
return (state.editorSettings = editorSettings)
}