Merge branch 'main' into fix/renaming-ext

This commit is contained in:
muzam1l
2022-08-19 15:14:52 +05:30
111 changed files with 5511 additions and 5967 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,21 +1,21 @@
import { getFileExtention } from '../../utils/helpers';
import state, { IFile } from '../index';
import { getFileExtention } from '../../utils/helpers'
import state, { IFile } from '../index'
const languageMapping: Record<string, string | undefined> = {
'ts': 'typescript',
'js': 'javascript',
'md': 'markdown',
'c': 'c',
'h': 'c',
ts: 'typescript',
js: 'javascript',
md: 'markdown',
c: 'c',
h: 'c'
}
export const createNewFile = (name: string) => {
const ext = getFileExtention(name) || ''
const emptyFile: IFile = { name, language: languageMapping[ext] || '', content: '' };
state.files.push(emptyFile);
state.active = state.files.length - 1;
};
const emptyFile: IFile = { name, language: languageMapping[ext] || '', 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)
@@ -25,4 +25,4 @@ export const renameFile = (oldName: string, nwName: string) => {
const language = languageMapping[ext] || ''
file.name = nwName
file.language = language
};
}

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,53 +1,51 @@
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 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'
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 (
@@ -56,30 +54,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
)?.map((aa) => ({
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: {
@@ -89,82 +86,74 @@ 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
})
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",
type: 'success',
message: ref(
<>
[{submitRes.engine_result}] {submitRes.engine_result_message}{" "}
Transaction hash:{" "}
[{submitRes.engine_result}] {submitRes.engine_result_message} Transaction hash:{' '}
<Link
as="a"
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${submitRes.tx_json?.hash}`}
@@ -174,113 +163,110 @@ export const deployHook = async (
{submitRes.tx_json?.hash}
</Link>
</>
),
)
// message: `[${submitRes.engine_result}] ${submitRes.engine_result_message} Validated ledger index: ${submitRes.validated_ledger_index}`,
});
})
} else {
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);
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,57 +1,62 @@
import { Octokit } from "@octokit/core";
import state, { IFile } 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 files from Github Gists based on gistId and stores them in global state
*/
export const fetchFiles = async (gistId: string) => {
if (!gistId || state.files.length) return
state.loading = true;
state.loading = true
state.logs.push({
type: "log",
message: `Fetching Gist with id: ${gistId}`,
});
type: 'log',
message: `Fetching Gist with id: ${gistId}`
})
try {
const res = await octokit
.request("GET /gists/{gist_id}", { gist_id: gistId })
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)
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 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 }> = {};
const headerFiles: Record<string, { filename: string; content: string; language: string }> =
{}
Object.entries(headerJson).forEach(([key, value]) => {
const fname = `${key}.h`;
const fname = `${key}.h`
headerFiles[fname] = { filename: fname, content: value as string, language: 'C' }
})
const files = {
...res.data.files,
...headerFiles
};
res.data.files = files;
}
res.data.files = files
}
if (!res.data.files) throw Error("No files could be fetched from given gist id!")
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 || "",
}));
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() || '';
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> = {
@@ -63,20 +68,22 @@ export const fetchFiles = async (gistId: string) => {
// 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: "success",
message: "Fetched successfully ✅",
});
state.files = files;
state.gistId = gistId;
state.gistOwner = res.data.owner?.login;
type: 'success',
message: 'Fetched successfully ✅'
})
state.files = files
state.gistId = gistId
state.gistOwner = res.data.owner?.login
const gistName = files.find(file => file.language === 'c' || file.language === 'javascript')?.name || "untitled";
const gistName =
files.find(file => file.language === 'c' || file.language === 'javascript')?.name ||
'untitled'
state.gistName = gistName
} catch (err) {
console.error(err)
@@ -84,9 +91,9 @@ export const fetchFiles = async (gistId: string) => {
if (err instanceof Error) message = err.message
else message = `Something went wrong, try again later!`
state.logs.push({
type: "error",
message: `Error: ${message}`,
});
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

@@ -1,57 +1,66 @@
import { derive, sign } from "xrpl-accountlib";
import { derive, sign } from 'xrpl-accountlib'
import state from '..'
import type { IAccount } from "..";
import type { IAccount } from '..'
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 (account: IAccount, txOptions: TransactionOptions, options?: OtherOptions) => {
if (!state.client) throw Error('XRPL client not initalized')
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`,
});
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,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)
}

View File

@@ -1 +1 @@
export * from './templates'
export * from './templates'

View File

@@ -1,41 +1,41 @@
import Carbon from "../../components/icons/Carbon";
import Firewall from "../../components/icons/Firewall";
import Notary from "../../components/icons/Notary";
import Peggy from "../../components/icons/Peggy";
import Starter from "../../components/icons/Starter";
import Carbon from '../../components/icons/Carbon'
import Firewall from '../../components/icons/Firewall'
import Notary from '../../components/icons/Notary'
import Peggy from '../../components/icons/Peggy'
import Starter from '../../components/icons/Starter'
export const templateFileIds = {
'starter': {
id: '1f8109c80f504e6326db2735df2f0ad6', // Forked
name: 'Starter',
description: 'Just a basic starter with essential imports, just accepts any transaction coming through',
icon: Starter
},
'firewall': {
id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
name: 'Firewall',
description: 'This Hook essentially checks a blacklist of accounts',
icon: Firewall
},
'notary': {
id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
name: 'Notary',
description: 'Collecting signatures for multi-sign transactions',
icon: Notary
},
'carbon': {
id: '953662b22d065449f8ab6f69bc2afe41', // Forked
name: 'Carbon',
description: 'Send a percentage of sum to an address',
icon: Carbon
},
'peggy': {
id: '049784a83fa068faf7912f663f7b6471', // Forked
name: 'Peggy',
description: 'An oracle based stable coin hook',
icon: Peggy
},
starter: {
id: '1f8109c80f504e6326db2735df2f0ad6', // Forked
name: 'Starter',
description:
'Just a basic starter with essential imports, just accepts any transaction coming through',
icon: Starter
},
firewall: {
id: '1cc30f39c8a0b9c55b88c312669ca45e', // Forked
name: 'Firewall',
description: 'This Hook essentially checks a blacklist of accounts',
icon: Firewall
},
notary: {
id: '87b6f5a8c2f5038fb0f20b8b510efa10', // Forked
name: 'Notary',
description: 'Collecting signatures for multi-sign transactions',
icon: Notary
},
carbon: {
id: '953662b22d065449f8ab6f69bc2afe41', // Forked
name: 'Carbon',
description: 'Send a percentage of sum to an address',
icon: Carbon
},
peggy: {
id: '049784a83fa068faf7912f663f7b6471', // Forked
name: 'Peggy',
description: 'An oracle based stable coin hook',
icon: Peggy
}
}
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h'];
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h']

View File

@@ -1,92 +1,92 @@
import type monaco from "monaco-editor";
import { proxy, ref, subscribe } from "valtio";
import { devtools, subscribeKey } from 'valtio/utils';
import { XrplClient } from "xrpl-client";
import { SplitSize } from "./actions/persistSplits";
import type monaco from 'monaco-editor'
import { proxy, ref, subscribe } from 'valtio'
import { devtools, subscribeKey } from 'valtio/utils'
import { XrplClient } from 'xrpl-client'
import { SplitSize } from './actions/persistSplits'
declare module "valtio" {
function useSnapshot<T extends object>(p: T): T;
function snapshot<T extends object>(p: T): T;
declare module 'valtio' {
function useSnapshot<T extends object>(p: T): T
function snapshot<T extends object>(p: T): T
}
export interface IFile {
name: string;
language: string;
content: string;
name: string
language: string
content: string
compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null;
compiledContent?: ArrayBuffer | null
compiledWatContent?: string | null
lastCompiled?: Date
containsErrors?: boolean
}
export interface FaucetAccountRes {
address: string;
secret: string;
xrp: number;
hash: string;
code: string;
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;
version?: string;
name: string
address: string
secret: string
xrp: string
sequence: number
hooks: string[]
isLoading: boolean
version?: string
error?: {
message: string;
code: string;
} | null;
message: string
code: string
} | null
}
export interface ILog {
type: "error" | "warning" | "log" | "success";
message: string | JSX.Element;
key?: string;
jsonData?: any,
timestring?: string;
link?: string;
linkText?: string;
type: 'error' | 'warning' | 'log' | 'success'
message: string | JSX.Element
key?: string
jsonData?: any
timestring?: string
link?: string
linkText?: string
defaultCollapsed?: boolean
}
export type DeployValue = Record<IFile['name'], any>;
export type DeployValue = Record<IFile['name'], any>
export interface IState {
files: IFile[];
gistId?: string | null;
gistOwner?: string | null;
gistName?: string | null;
active: number;
activeWat: number;
loading: boolean;
gistLoading: boolean;
zipLoading: boolean;
compiling: boolean;
logs: ILog[];
deployLogs: ILog[];
transactionLogs: ILog[];
scriptLogs: ILog[];
editorCtx?: typeof monaco.editor;
files: IFile[]
gistId?: string | null
gistOwner?: string | null
gistName?: string | null
active: number
activeWat: number
loading: boolean
gistLoading: boolean
zipLoading: boolean
compiling: boolean
logs: ILog[]
deployLogs: ILog[]
transactionLogs: ILog[]
scriptLogs: ILog[]
editorCtx?: typeof monaco.editor
editorSettings: {
tabSize: number;
};
tabSize: number
}
splits: {
[id: string]: SplitSize
};
client: XrplClient | null;
clientStatus: "offline" | "online";
mainModalOpen: boolean;
mainModalShowed: boolean;
accounts: IAccount[];
}
client: XrplClient | null
clientStatus: 'offline' | 'online'
mainModalOpen: boolean
mainModalShowed: boolean
accounts: IAccount[]
compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os'
strip: boolean
},
}
deployValues: DeployValue
}
@@ -110,11 +110,11 @@ let initialState: IState = {
gistLoading: false,
zipLoading: false,
editorSettings: {
tabSize: 2,
tabSize: 2
},
splits: {},
client: null,
clientStatus: "offline" as "offline",
clientStatus: 'offline' as 'offline',
mainModalOpen: false,
mainModalShowed: false,
accounts: [],
@@ -123,23 +123,23 @@ let initialState: IState = {
strip: true
},
deployValues: {}
};
}
let localStorageAccounts: string | null = null;
let initialAccounts: IAccount[] = [];
let localStorageAccounts: string | null = null
let initialAccounts: IAccount[] = []
// TODO: What exactly should we store in localStorage? editorSettings, splits, accounts?
// Check if there's a persited accounts in localStorage
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
try {
localStorageAccounts = localStorage.getItem("hooksIdeAccounts");
localStorageAccounts = localStorage.getItem('hooksIdeAccounts')
} catch (err) {
console.log(`localStorage state broken`);
localStorage.removeItem("hooksIdeAccounts");
console.log(`localStorage state broken`)
localStorage.removeItem('hooksIdeAccounts')
}
if (localStorageAccounts) {
initialAccounts = JSON.parse(localStorageAccounts);
initialAccounts = JSON.parse(localStorageAccounts)
}
// filter out old accounts (they do not have version property at all)
// initialAccounts = initialAccounts.filter(acc => acc.version === '2');
@@ -149,36 +149,35 @@ if (typeof window !== "undefined") {
const state = proxy<IState>({
...initialState,
accounts: initialAccounts.length > 0 ? initialAccounts : [],
logs: [],
});
logs: []
})
// Initialize socket connection
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`);
const client = new XrplClient(`wss://${process.env.NEXT_PUBLIC_TESTNET_URL}`)
client.on("online", () => {
state.client = ref(client);
state.clientStatus = "online";
});
client.on('online', () => {
state.client = ref(client)
state.clientStatus = 'online'
})
client.on("offline", () => {
state.clientStatus = "offline";
});
client.on('offline', () => {
state.clientStatus = 'offline'
})
if (process.env.NODE_ENV !== "production") {
devtools(state, "Files State");
if (process.env.NODE_ENV !== 'production') {
devtools(state, 'Files State')
}
if (typeof window !== "undefined") {
if (typeof window !== 'undefined') {
subscribe(state.accounts, () => {
const { accounts } = state;
const { accounts } = state
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
});
localStorage.setItem('hooksIdeAccounts', JSON.stringify(accountsNoLoading))
})
const updateActiveWat = () => {
const filename = state.files[state.active]?.name
const compiledFiles = state.files.filter(
file => file.compiledContent)
const compiledFiles = state.files.filter(file => file.compiledContent)
const idx = compiledFiles.findIndex(file => file.name === filename)
if (idx !== -1) state.activeWat = idx

View File

@@ -1,260 +1,252 @@
import { proxy } from 'valtio';
import { deepEqual } from '../utils/object';
import transactionsData from "../content/transactions.json";
import state from '.';
import { showAlert } from "../state/actions/showAlert";
import { parseJSON } from '../utils/json';
import { proxy } from 'valtio'
import { deepEqual } from '../utils/object'
import transactionsData from '../content/transactions.json'
import state from '.'
import { showAlert } from '../state/actions/showAlert'
import { parseJSON } from '../utils/json'
export type SelectOption = {
value: string;
label: string;
};
export interface TransactionState {
selectedTransaction: SelectOption | null;
selectedAccount: SelectOption | null;
selectedDestAccount: SelectOption | null;
txIsLoading: boolean;
txIsDisabled: boolean;
txFields: TxFields;
viewType: 'json' | 'ui',
editorValue?: string,
estimatedFee?: string
value: string
label: string
}
export interface TransactionState {
selectedTransaction: SelectOption | null
selectedAccount: SelectOption | null
selectedDestAccount: SelectOption | null
txIsLoading: boolean
txIsDisabled: boolean
txFields: TxFields
viewType: 'json' | 'ui'
editorValue?: string
estimatedFee?: string
}
export type TxFields = Omit<
Partial<typeof transactionsData[0]>,
"Account" | "Sequence" | "TransactionType"
>;
Partial<typeof transactionsData[0]>,
'Account' | 'Sequence' | 'TransactionType'
>
export const defaultTransaction: TransactionState = {
selectedTransaction: null,
selectedAccount: null,
selectedDestAccount: null,
txIsLoading: false,
txIsDisabled: false,
txFields: {},
viewType: 'ui'
};
selectedTransaction: null,
selectedAccount: null,
selectedDestAccount: null,
txIsLoading: false,
txIsDisabled: false,
txFields: {},
viewType: 'ui'
}
export const transactionsState = proxy({
transactions: [
{
header: "test1.json",
state: { ...defaultTransaction },
},
],
activeHeader: "test1.json"
});
transactions: [
{
header: 'test1.json',
state: { ...defaultTransaction }
}
],
activeHeader: 'test1.json'
})
export const renameTxState = (oldName: string, nwName: string) => {
const tx = transactionsState.transactions.find(tx => tx.header === oldName);
const tx = transactionsState.transactions.find(tx => tx.header === oldName)
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`);
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`)
tx.header = nwName
tx.header = nwName
}
/**
* Simple transaction state changer
* @param header Unique key and tab name for the transaction tab
* @param partialTx partial transaction state, `undefined` deletes the transaction
*
*
*/
export const modifyTxState = (
header: string,
partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {}
header: string,
partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {}
) => {
const tx = transactionsState.transactions.find(tx => tx.header === header);
const tx = transactionsState.transactions.find(tx => tx.header === header)
if (partialTx === undefined) {
transactionsState.transactions = transactionsState.transactions.filter(
tx => tx.header !== header
);
return;
if (partialTx === undefined) {
transactionsState.transactions = transactionsState.transactions.filter(
tx => tx.header !== header
)
return
}
if (!tx) {
const state = {
...defaultTransaction,
...partialTx
}
transactionsState.transactions.push({
header,
state
})
return state
}
if (!tx) {
const state = {
...defaultTransaction,
...partialTx,
}
transactionsState.transactions.push({
header,
state,
});
return state;
if (opts.replaceState) {
const repTx: TransactionState = {
...defaultTransaction,
...partialTx
}
tx.state = repTx
return repTx
}
if (opts.replaceState) {
const repTx: TransactionState = {
...defaultTransaction,
...partialTx,
}
tx.state = repTx
return repTx
}
Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definitely safe!
const s = tx.state as any
const p = partialTx as any // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k]
})
Object.keys(partialTx).forEach(k => {
// Typescript mess here, but is definitely safe!
const s = tx.state as any;
const p = partialTx as any; // ? Make copy
if (!deepEqual(s[k], p[k])) s[k] = p[k];
});
return tx.state
};
return tx.state
}
// state to tx options
export const prepareTransaction = (data: any) => {
let options = { ...data };
let options = { ...data }
(Object.keys(options)).forEach(field => {
let _value = options[field];
// convert xrp
if (_value && typeof _value === "object" && _value.$type === "xrp") {
if (+_value.$value) {
options[field] = (+_value.$value * 1000000 + "") as any;
} else {
options[field] = undefined; // 👇 💀
}
}
// handle type: `json`
if (_value && typeof _value === "object" && _value.$type === "json") {
if (typeof _value.$value === "object") {
options[field] = _value.$value;
} else {
try {
options[field] = JSON.parse(_value.$value);
} catch (error) {
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
}`;
console.error(message)
options[field] = _value.$value
}
}
Object.keys(options).forEach(field => {
let _value = options[field]
// convert xrp
if (_value && typeof _value === 'object' && _value.$type === 'xrp') {
if (+_value.$value) {
options[field] = (+_value.$value * 1000000 + '') as any
} else {
options[field] = undefined // 👇 💀
}
}
// handle type: `json`
if (_value && typeof _value === 'object' && _value.$type === 'json') {
if (typeof _value.$value === 'object') {
options[field] = _value.$value
} else {
try {
options[field] = JSON.parse(_value.$value)
} catch (error) {
const message = `Input error for json field '${field}': ${
error instanceof Error ? error.message : ''
}`
console.error(message)
options[field] = _value.$value
}
}
}
// delete unnecessary fields
if (!options[field]) {
delete options[field];
}
});
// delete unnecessary fields
if (!options[field]) {
delete options[field]
}
})
return options
return options
}
// editor value to state
export const prepareState = (value: string, transactionType?: string) => {
const options = parseJSON(value);
if (!options) {
showAlert("Error!", {
body: "Cannot save editor with malformed transaction."
})
return
};
const options = parseJSON(value)
if (!options) {
showAlert('Error!', {
body: 'Cannot save editor with malformed transaction.'
})
return
}
const { Account, TransactionType, Destination, ...rest } = options;
let tx: Partial<TransactionState> = {};
const schema = getTxFields(transactionType)
const { Account, TransactionType, Destination, ...rest } = options
let tx: Partial<TransactionState> = {}
const schema = getTxFields(transactionType)
if (Account) {
const acc = state.accounts.find(acc => acc.address === Account);
if (acc) {
tx.selectedAccount = {
label: acc.name,
value: acc.address,
};
} else {
tx.selectedAccount = {
label: Account,
value: Account,
};
}
if (Account) {
const acc = state.accounts.find(acc => acc.address === Account)
if (acc) {
tx.selectedAccount = {
label: acc.name,
value: acc.address
}
} else {
tx.selectedAccount = null;
tx.selectedAccount = {
label: Account,
value: Account
}
}
} else {
tx.selectedAccount = null
}
if (TransactionType) {
tx.selectedTransaction = {
label: TransactionType,
value: TransactionType,
};
if (TransactionType) {
tx.selectedTransaction = {
label: TransactionType,
value: TransactionType
}
} else {
tx.selectedTransaction = null
}
if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination)
if (dest) {
tx.selectedDestAccount = {
label: dest.name,
value: dest.address
}
} else if (Destination) {
tx.selectedDestAccount = {
label: Destination,
value: Destination
}
} else {
tx.selectedTransaction = null;
tx.selectedDestAccount = null
}
} else if (Destination) {
rest.Destination = Destination
}
if (schema.Destination !== undefined) {
const dest = state.accounts.find(acc => acc.address === Destination);
if (dest) {
tx.selectedDestAccount = {
label: dest.name,
value: dest.address,
};
}
else if (Destination) {
tx.selectedDestAccount = {
label: Destination,
value: Destination,
};
}
else {
tx.selectedDestAccount = null
}
}
else if (Destination) {
rest.Destination = Destination
Object.keys(rest).forEach(field => {
const value = rest[field]
const schemaVal = schema[field as keyof TxFields]
const isXrp =
typeof value !== 'object' &&
schemaVal &&
typeof schemaVal === 'object' &&
schemaVal.$type === 'xrp'
if (isXrp) {
rest[field] = {
$type: 'xrp',
$value: +value / 1000000 // ! maybe use bigint?
}
} else if (typeof value === 'object') {
rest[field] = {
$type: 'json',
$value: value
}
}
})
Object.keys(rest).forEach(field => {
const value = rest[field];
const schemaVal = schema[field as keyof TxFields]
const isXrp = typeof value !== 'object' && schemaVal && typeof schemaVal === 'object' && schemaVal.$type === 'xrp'
if (isXrp) {
rest[field] = {
$type: "xrp",
$value: +value / 1000000, // ! maybe use bigint?
};
} else if (typeof value === "object") {
rest[field] = {
$type: "json",
$value: value,
};
}
});
tx.txFields = rest
tx.txFields = rest;
return tx
return tx
}
export const getTxFields = (tt?: string) => {
const txFields: TxFields | undefined = transactionsData.find(
tx => tx.TransactionType === tt
);
const txFields: TxFields | undefined = transactionsData.find(tx => tx.TransactionType === tt)
if (!txFields) return {}
if (!txFields) return {}
let _txFields = Object.keys(txFields)
.filter(
key => !["TransactionType", "Account", "Sequence"].includes(key)
)
.reduce<TxFields>(
(tf, key) => (
(tf[key as keyof TxFields] = (txFields as any)[key]), tf
),
{}
);
return _txFields
let _txFields = Object.keys(txFields)
.filter(key => !['TransactionType', 'Account', 'Sequence'].includes(key))
.reduce<TxFields>((tf, key) => ((tf[key as keyof TxFields] = (txFields as any)[key]), tf), {})
return _txFields
}
export { transactionsData }
export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
value: tx.TransactionType,
label: tx.TransactionType
}))
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')
export const defaultTransactionType = transactionsOptions.find(tt => tt.value === 'Payment')