Compare commits

..

117 Commits

Author SHA1 Message Date
muzam1l
a3d094e873 Fix some more spelling errors. 2022-07-13 20:11:21 +05:30
muzam1l
ef70bfb13a Fix spelling error 2022-07-13 19:49:54 +05:30
muzam1l
c26c7c13d1 Improve compile error handling. 2022-07-13 16:50:47 +05:30
muzam1l
fc461ddd0d Content changed warning on deploy page. 2022-07-13 16:17:31 +05:30
muzam1l
c7001f6089 'Exit editor mode' button in wat editor. 2022-07-13 15:40:08 +05:30
muzam1l
243cbfec08 Upgrade hooks editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
1295e7fa41 Deploy editor to comp 2022-07-13 15:09:45 +05:30
muzam1l
793623d216 Migrate transaction json editor to comp. 2022-07-13 15:09:45 +05:30
muzam1l
0cde0eb240 Monaco component 2022-07-13 15:09:45 +05:30
muzamil
e2acb48e03 Merge pull request #236 from XRPLF/feat/jsdoc-to-ui
User declaration of input fields via JSDOC in script files.
2022-07-12 16:53:27 +05:30
muzam1l
a4373bb970 fix spelling error 2022-07-12 12:40:35 +05:30
muzam1l
cfb791092a Update template files of some examples to forked versions. 2022-07-11 14:44:35 +05:30
muzamil
3fcbac5ed9 Merge pull request #231 from XRPLF/feat/create-account-name
Allow passing desired name while creating account.
2022-07-11 14:29:38 +05:30
muzam1l
c40b272ce8 Fix disabled prop behaviour 2022-07-07 23:34:14 +05:30
muzam1l
860ff66a8a fix error in error handler 2022-07-07 23:25:53 +05:30
muzam1l
f4f700bea1 Handle required prop on fields. 2022-07-06 19:30:25 +05:30
muzam1l
789bc00ac3 Enhance account secret label! 2022-07-06 19:03:29 +05:30
muzam1l
6a0aabdeda Handle jsdoc errors 2022-07-05 19:38:26 +05:30
muzam1l
175b6266e8 Add script error handling 2022-07-05 19:09:08 +05:30
muzam1l
621482e2ee enhance empty log display 2022-07-05 18:52:45 +05:30
muzam1l
e55f48bc83 Use Single LogBox comp for scripts too 2022-07-05 18:45:52 +05:30
muzam1l
3e9e26a46a Data variables in process.env instead of process 2022-07-05 16:58:07 +05:30
muzam1l
f0e730bb9b Remove secret tag on type Account 2022-07-05 16:35:23 +05:30
muzam1l
6ce4828fc6 Remove console logs 2022-07-05 16:25:15 +05:30
muzam1l
bb0a246ae5 User declaration of input fields via JSDOC in script files. 2022-07-05 16:02:15 +05:30
Valtteri Karesto
3af2bad536 Make ping interval longer (#232)
* Make ping interval 45s

Co-authored-by: Vaclav Barta <vbarta@mangrove.cz>
2022-07-04 12:22:06 +02:00
muzam1l
4f1b877db0 Added optional tag to create account label. 2022-07-01 19:41:52 +05:30
muzamil
0289d64f5e Merge pull request #233 from XRPLF/fix/tab-names
Fix tab names
2022-07-01 19:29:33 +05:30
muzamil
868a0bcf78 Merge pull request #234 from XRPLF/feat/account-in-deploy-dialog
Add account selectable in deploy dialog.
2022-07-01 18:51:04 +05:30
muzam1l
aab2476a05 Add account selectable in deploy dialog. 2022-07-01 18:06:01 +05:30
muzam1l
cb25986d72 update transaction tab labels 2022-07-01 17:30:57 +05:30
muzam1l
309ad57173 Skip auto appneding test file extension. 2022-07-01 17:26:10 +05:30
muzam1l
53afb1d3d1 Fix html erros. 2022-07-01 17:08:34 +05:30
muzam1l
31ff7c0e28 Name field in import account dialog. 2022-07-01 16:43:15 +05:30
muzam1l
dfa35df465 reset input value on submit 2022-07-01 16:27:13 +05:30
muzam1l
f163b052e1 Allow passing desired name while creating account. 2022-07-01 14:33:28 +05:30
Valtteri Karesto
25c5b9c015 Merge pull request #229 from XRPLF/feat/links-to-explorer
Link from hashes/addresses to Hook Explorer
2022-06-30 15:40:57 +03:00
Valtteri Karesto
407e3946ce Added underline on hover to links 2022-06-30 15:30:43 +03:00
Valtteri Karesto
dc5b0d71eb Simplified hook state, since endpoint now works with hookhashes 2022-06-30 08:54:43 +03:00
Valtteri Karesto
3fd6c3f50e Remove debug code 2022-06-29 18:03:26 +03:00
Valtteri Karesto
ec8bfc5eee Add links to account modal 2022-06-29 15:26:33 +03:00
Valtteri Karesto
b4a0bcb90d Merge pull request #227 from XRPLF/feat/remember-deploy-values
Remember deploy values / Add feedback button
2022-06-29 14:08:47 +03:00
Valtteri Karesto
2c729e2aa4 Update button text 2022-06-29 14:04:06 +03:00
Valtteri Karesto
1cb2542170 Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/remember-deploy-values 2022-06-29 13:47:41 +03:00
Wietse Wind
00b309df34 Merge pull request #228 from XRPLF/feature/add-plausible-analytics
Add plausible analytics to builder
2022-06-29 12:10:58 +02:00
Joni Juup
a6fc730de6 add plausible analytics to builder 2022-06-29 12:58:17 +03:00
Valtteri Karesto
2245c5a221 Test gh integration 2022-06-29 12:18:38 +03:00
Valtteri Karesto
60c33661ad Add proper defaultvalue 2022-06-29 12:14:52 +03:00
Valtteri Karesto
ea21c85038 Add noopener and noreferrer to link 2022-06-29 11:37:22 +03:00
Valtteri Karesto
5478f43609 persist deploy values in memory 2022-06-29 11:32:15 +03:00
Valtteri Karesto
a9b64abb85 Add feedback button, show modal only on homepage 2022-06-29 11:31:46 +03:00
Valtteri Karesto
c6ced424d8 Merge pull request #226 from XRPLF/feat/long-navigation-support
Fix #215 scrollbar issues
2022-06-28 14:59:35 +03:00
Valtteri Karesto
3a1159cffc Make thumbs more visible 2022-06-28 14:49:34 +03:00
Valtteri Karesto
3136de1bd1 Slight style adjustments 2022-06-28 14:38:59 +03:00
Valtteri Karesto
67ffd3f1b4 Fix #215 scrollbar issues 2022-06-28 14:01:08 +03:00
Valtteri Karesto
8508cb69c4 Merge pull request #224 from XRPLF/feat/debug-stream-fixes
Feat/debug stream fixes
2022-06-28 11:31:03 +03:00
Valtteri Karesto
89217d2633 Remove console.log 2022-06-28 11:03:56 +03:00
Valtteri Karesto
ba1b64391c ping socket connection 2022-06-28 09:36:45 +03:00
Valtteri Karesto
098d919a77 Bring back dispose 2022-06-27 15:03:49 +03:00
Valtteri Karesto
b2af37ab4b Use reconnecting-websocket and refactor debug stream 2022-06-27 15:03:42 +03:00
Valtteri Karesto
dcb7e94e86 New gists provided by XRPL (#223)
* Prepare logic for new gists

* Remove unused imports

* updated gist IDs for xrplfgists

* Add macro.h to apiheaderfiles

* Update headers

Co-authored-by: Vaclav Barta <vbarta@mangrove.cz>
2022-06-27 08:25:25 +02:00
Valtteri Karesto
67848b3d8d Merge pull request #222 from XRPLF/fix/suggest-button-disabled
Fix/suggest button disabled
2022-06-23 11:12:04 +03:00
Valtteri Karesto
31a86263a1 Disable suggest if no account selected 2022-06-22 14:42:50 +03:00
Valtteri Karesto
4d0025afc1 Fix splitscreen error 2022-06-22 14:42:39 +03:00
Valtteri Karesto
f85bd2398d Merge pull request #220 from XRPLF/fix/decimals-again
fixes #201
2022-06-22 12:28:12 +03:00
Valtteri Karesto
a2a6596cc5 Prevent pasting decimals 2022-06-22 12:16:36 +03:00
Valtteri Karesto
37208ce97e fixes #201 2022-06-22 12:03:43 +03:00
Valtteri Karesto
bf4042926d Merge pull request #218 from XRPLF/feat/ui-fixes
Fixes #213 and fixes #200
2022-06-22 11:41:31 +03:00
Valtteri Karesto
3ccc1c16ac Fixed deploy 2022-06-22 11:32:07 +03:00
Valtteri Karesto
135f0c91a1 Fixes #213 and fixes #200 2022-06-22 11:06:15 +03:00
Valtteri Karesto
8f5786e242 Merge pull request #216 from XRPLF/feat/change-default-optimization
fixes #214 change default optimization
2022-06-22 09:56:15 +03:00
Valtteri Karesto
810eb4ca27 Add new default to compileCode as well 2022-06-22 09:52:47 +03:00
Valtteri Karesto
e6574f9f12 fixes #214 change default optimization 2022-06-22 09:47:30 +03:00
Valtteri Karesto
1a6726fabf Merge pull request #212 from XRPLF/feat/improve-supp-js
Improve supplementary JS feature
2022-06-21 11:28:42 +03:00
Valtteri Karesto
89f8671217 Clear log should now work 2022-06-21 10:53:52 +03:00
Valtteri Karesto
fb5259221b Changed color of starting running 2022-06-21 09:43:29 +03:00
Valtteri Karesto
fd17f59616 Show LogBoxForScrips if js file active 2022-06-20 23:54:56 +03:00
Valtteri Karesto
91bbc7ea61 Catch template errors, add better labels, styling 2022-06-20 23:54:33 +03:00
Valtteri Karesto
783d832c6d Remove export for unused component 2022-06-20 23:53:32 +03:00
Valtteri Karesto
698ca376e7 Add showbuttons prop to LogBoxForScripts 2022-06-20 23:53:15 +03:00
Valtteri Karesto
bfd9e21ab8 Remove unused component 2022-06-20 23:52:45 +03:00
Valtteri Karesto
e46411f245 Rename template helpers 2022-06-20 14:53:30 +03:00
Valtteri Karesto
08447c6b29 Add support for select parameters 2022-06-20 14:16:16 +03:00
Valtteri Karesto
9216cc6bf7 When downloading zip, include wasm if it exists 2022-06-20 11:01:13 +03:00
Valtteri Karesto
5108b08e39 Do not show scripts panel if no supplementary scripts 2022-06-20 10:40:47 +03:00
Valtteri Karesto
6c46a4f809 Merge pull request #211 from XRPLF/feat/user-provided-scripts
Feat/user provided scripts
2022-06-20 10:16:06 +03:00
Valtteri Karesto
0ea88f0d32 Remove unused imports 2022-06-17 21:48:32 +03:00
Valtteri Karesto
4c2e1f36f3 Add empty line 2022-06-17 21:42:50 +03:00
Valtteri Karesto
fa5315fc0e Fix errors on development 2022-06-17 17:32:21 +03:00
Valtteri Karesto
eda8b1550c If not string, stringify it 2022-06-17 13:42:19 +03:00
Valtteri Karesto
742b11374f Just empty body 2022-06-16 10:12:40 +03:00
Valtteri Karesto
d16e83dcfa No need for specific log() or error() functions anymore 2022-06-16 00:50:16 +03:00
Valtteri Karesto
155aa57784 Fix warning about useLayoutEffect 2022-06-16 00:49:43 +03:00
Valtteri Karesto
b88b6da7d9 Add one more example env value 2022-06-16 00:49:17 +03:00
Valtteri Karesto
fa13f7e282 Add logic for scripts 2022-06-15 15:15:08 +03:00
Valtteri Karesto
f1a43ef758 Add dependencies and fix handlebars errors 2022-06-15 15:14:51 +03:00
Vaclav Barta
4217813fd7 take default header names from /api/header-files JSON (#210)
This is a client-side upgrade for the server-side XRPLF/xrpl-hooks-compiler#17 - not hard-coding expected header names but taking them from the compilation backend.
2022-06-13 08:22:55 +02:00
muzamil
c588f7b1f3 Merge pull request #209 from XRPLF/feat/json-in-ui
Display tx json in UI inside textarea.
2022-06-10 16:50:28 +05:30
muzam1l
985e8ee820 Merge branch 'main' into feat/json-in-ui 2022-06-10 16:50:08 +05:30
muzamil
8832e76a0a Merge pull request #208 from XRPLF/feat/default-tx
Set payment as default transaction
2022-06-10 16:47:01 +05:30
muzam1l
9777f1dbd1 Increase vert. padding of textarea 2022-06-08 15:03:51 +05:30
muzam1l
213d468aab Merge branch 'main' into feat/json-in-ui 2022-06-07 19:06:57 +05:30
muzam1l
46becb0e7b Display tx json in UI inside textarea. 2022-06-07 19:06:13 +05:30
Valtteri Karesto
fad6bd100a Merge pull request #205 from XRPLF/feat/prevent-decimals
Prevent inputing decimals
2022-06-07 14:55:05 +03:00
muzam1l
5a11f83fea Set payment as default transaction 2022-06-07 00:52:59 +05:30
Valtteri Karesto
525338abf7 Merge branch 'main' of github.com:eqlabs/xrpl-hooks-ide into feat/prevent-decimals 2022-06-03 17:21:00 +03:00
Valtteri Karesto
ea977816a4 Merge pull request #203 from XRPLF/feat/tab-ordering
Sort the files after fetching
2022-06-03 17:20:27 +03:00
Valtteri Karesto
0ee599a2b6 Update sort 2022-06-03 16:50:31 +03:00
Valtteri Karesto
02c59f8d79 Update sorting function 2022-06-03 16:34:46 +03:00
Valtteri Karesto
3d5b77e60a Fix issue if no filenames 2022-06-03 16:34:36 +03:00
Valtteri Karesto
92a167d47a Prevent also pasting 2022-06-03 15:33:46 +03:00
Valtteri Karesto
d41e263942 Merge pull request #204 from XRPLF/feat/remove-cleaner-ui
Remove cleaner options from UI
2022-06-03 15:08:50 +03:00
Valtteri Karesto
bd1226fe90 Remove log 2022-06-02 16:35:18 +03:00
Valtteri Karesto
57403e42dd Adjustments to sorting 2022-06-02 16:34:53 +03:00
Valtteri Karesto
2b42a96c4a Update ordering 2022-06-02 16:15:39 +03:00
Valtteri Karesto
80d6bb691d Prevent inputing decimals 2022-06-02 15:22:28 +03:00
Valtteri Karesto
4a22861860 Sort the files after fetching 2022-06-02 14:04:52 +03:00
37 changed files with 1471 additions and 644 deletions

View File

@@ -1,4 +1,5 @@
NEXTAUTH_URL=https://example.com
NEXTAUTH_SECRET="1234"
GITHUB_SECRET=""
GITHUB_ID=""
NEXT_PUBLIC_COMPILE_API_ENDPOINT="http://localhost:9000/api/build"

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ yarn-error.log*
# vercel
.vercel
.vscode

View File

@@ -108,3 +108,5 @@ To learn more about Next.js, take a look at the following resources:
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

View File

@@ -31,6 +31,7 @@ import transactionsData from "../content/transactions.json";
import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook";
import { capitalize } from "../utils/helpers";
export const AccountDialog = ({
activeAccountAddress,
@@ -42,12 +43,12 @@ export const AccountDialog = ({
const snap = useSnapshot(state);
const [showSecret, setShowSecret] = useState(false);
const activeAccount = snap.accounts.find(
(account) => account.address === activeAccountAddress
account => account.address === activeAccountAddress
);
return (
<Dialog
open={Boolean(activeAccountAddress)}
onOpenChange={(open) => {
onOpenChange={open => {
setShowSecret(false);
!open && setActiveAccountAddress(null);
}}
@@ -99,7 +100,7 @@ export const AccountDialog = ({
tabIndex={-1}
onClick={() => {
const index = state.accounts.findIndex(
(acc) => acc.address === activeAccount?.address
acc => acc.address === activeAccount?.address
);
state.accounts.splice(index, 1);
}}
@@ -116,9 +117,16 @@ export const AccountDialog = ({
<Text
css={{
fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}}
>
{activeAccount?.address}
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
rel="noopener noreferrer"
>
{activeAccount?.address}
</a>
</Text>
</Flex>
<Flex css={{ marginLeft: "auto", color: "$mauve12" }}>
@@ -158,7 +166,7 @@ export const AccountDialog = ({
}}
ghost
size="xs"
onClick={() => setShowSecret((curr) => !curr)}
onClick={() => setShowSecret(curr => !curr)}
>
{showSecret ? "Hide" : "Show"}
</Button>
@@ -215,7 +223,11 @@ export const AccountDialog = ({
</Button>
</Text>
</Flex>
<Flex css={{ marginLeft: "auto" }}>
<Flex
css={{
marginLeft: "auto",
}}
>
<a
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${activeAccount?.address}`}
target="_blank"
@@ -237,10 +249,22 @@ export const AccountDialog = ({
<Text
css={{
fontFamily: "$monospace",
a: { "&:hover": { textDecoration: "underline" } },
}}
>
{activeAccount && activeAccount.hooks.length > 0
? activeAccount.hooks.map((i) => truncate(i, 12)).join(",")
? activeAccount.hooks.map(i => {
return (
<a
key={i}
href={`https://${process.env.NEXT_PUBLIC_EXPLORER_URL}/${i}`}
target="_blank"
rel="noopener noreferrer"
>
{truncate(i, 12)}
</a>
);
})
: ""}
</Text>
</Flex>
@@ -278,7 +302,7 @@ interface AccountProps {
showHookStats?: boolean;
}
const Accounts: FC<AccountProps> = (props) => {
const Accounts: FC<AccountProps> = props => {
const snap = useSnapshot(state);
const [activeAccountAddress, setActiveAccountAddress] = useState<
string | null
@@ -286,7 +310,7 @@ const Accounts: FC<AccountProps> = (props) => {
useEffect(() => {
const fetchAccInfo = async () => {
if (snap.clientStatus === "online") {
const requests = snap.accounts.map((acc) =>
const requests = snap.accounts.map(acc =>
snap.client?.send({
id: `hooks-builder-req-info-${acc.address}`,
command: "account_info",
@@ -299,7 +323,7 @@ const Accounts: FC<AccountProps> = (props) => {
const balance = res?.account_data?.Balance as string;
const sequence = res?.account_data?.Sequence as number;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
acc => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.xrp = balance;
@@ -307,7 +331,7 @@ const Accounts: FC<AccountProps> = (props) => {
accountToUpdate.error = null;
} else {
const oldAccount = state.accounts.find(
(acc) => acc.address === res?.account
acc => acc.address === res?.account
);
if (oldAccount) {
oldAccount.xrp = "0";
@@ -318,7 +342,7 @@ const Accounts: FC<AccountProps> = (props) => {
}
}
});
const objectRequests = snap.accounts.map((acc) => {
const objectRequests = snap.accounts.map(acc => {
return snap.client?.send({
id: `hooks-builder-req-objects-${acc.address}`,
command: "account_objects",
@@ -329,7 +353,7 @@ const Accounts: FC<AccountProps> = (props) => {
objectResponses.forEach((res: any) => {
const address = res?.account as string;
const accountToUpdate = state.accounts.find(
(acc) => acc.address === address
acc => acc.address === address
);
if (accountToUpdate) {
accountToUpdate.hooks =
@@ -393,9 +417,7 @@ const Accounts: FC<AccountProps> = (props) => {
<Wallet size="15px" /> <Text css={{ lineHeight: 1 }}>Accounts</Text>
</Heading>
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
<Button ghost size="sm" onClick={() => addFaucetAccount(true)}>
Create
</Button>
<ImportAccountDialog type="create" />
<ImportAccountDialog />
</Flex>
</Flex>
@@ -412,7 +434,7 @@ const Accounts: FC<AccountProps> = (props) => {
overflowY: "auto",
}}
>
{snap.accounts.map((account) => (
{snap.accounts.map(account => (
<Flex
column
key={account.address + account.name}
@@ -465,7 +487,7 @@ const Accounts: FC<AccountProps> = (props) => {
{!props.hideDeployBtn && (
<div
className="hook-deploy-button"
onClick={(e) => {
onClick={e => {
e.stopPropagation();
}}
>
@@ -491,31 +513,71 @@ const Accounts: FC<AccountProps> = (props) => {
);
};
export const transactionsOptions = transactionsData.map((tx) => ({
export const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
}));
const ImportAccountDialog = () => {
const [value, setValue] = useState("");
const ImportAccountDialog = ({
type = "import",
}: {
type?: "import" | "create";
}) => {
const [secret, setSecret] = useState("");
const [name, setName] = useState("");
const btnText = type === "import" ? "Import" : "Create";
const title = type === "import" ? "Import Account" : "Create Account";
const handleSubmit = async () => {
if (type === "create") {
const value = capitalize(name);
await addFaucetAccount(value, true);
setName("");
setSecret("");
return;
}
importAccount(secret, name);
setName("");
setSecret("");
};
return (
<Dialog>
<DialogTrigger asChild>
<Button ghost size="sm">
Import
{btnText}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Import account</DialogTitle>
<DialogDescription>
<Label>Add account secret</Label>
<Input
name="secret"
type="password"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</DialogDescription>
<DialogContent aria-describedby={undefined}>
<DialogTitle css={{ mb: "$4" }}>{title}</DialogTitle>
<Flex column>
<Box css={{ mb: "$2" }}>
<Label>
Account name <Text muted>(optional)</Text>
</Label>
<Input
name="name"
type="text"
autoComplete="off"
autoCapitalize="on"
value={name}
onChange={e => setName(e.target.value)}
/>
</Box>
{type === "import" && (
<Box>
<Label>Account secret</Label>
<Input
required
name="secret"
type="password"
autoComplete="new-password"
value={secret}
onChange={e => setSecret(e.target.value)}
/>
</Box>
)}
</Flex>
<Flex
css={{
@@ -528,14 +590,8 @@ const ImportAccountDialog = () => {
<Button outline>Cancel</Button>
</DialogClose>
<DialogClose asChild>
<Button
variant="primary"
onClick={() => {
importAccount(value);
setValue("");
}}
>
Import account
<Button type="submit" variant="primary" onClick={handleSubmit}>
{title}
</Button>
</DialogClose>
</Flex>

View File

@@ -1,5 +1,7 @@
import { useCallback, useEffect } from "react";
import { useEffect } from "react";
import ReconnectingWebSocket, { CloseEvent } from "reconnecting-websocket";
import { proxy, ref, useSnapshot } from "valtio";
import { subscribeKey } from "valtio/utils";
import { Select } from ".";
import state, { ILog, transactionsState } from "../state";
import { extractJSON } from "../utils/json";
@@ -15,7 +17,7 @@ export interface IStreamState {
status: "idle" | "opened" | "closed";
statusChangeTimestamp?: number;
logs: ILog[];
socket?: WebSocket;
socket?: ReconnectingWebSocket;
}
export const streamState = proxy<IStreamState>({
@@ -24,12 +26,85 @@ export const streamState = proxy<IStreamState>({
logs: [] as ILog[],
});
const onOpen = (account: ISelect | null) => {
if (!account) {
return;
}
// streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account?.value}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
// 999 = closed websocket connection by switching account
if (e.code !== 4999) {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
}
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
// Ping returns just account address, if we get that
// response we don't need to log anything
if (event.data !== streamState.selectedAccount?.value) {
pushLog(event.data);
}
};
let interval: NodeJS.Timer | null = null;
const addListeners = (account: ISelect | null) => {
if (account?.value && streamState.socket?.url.endsWith(account?.value)) {
return;
}
streamState.logs = [];
if (account?.value) {
if (interval) {
clearInterval(interval);
}
if (streamState.socket) {
streamState.socket?.removeEventListener("open", () => onOpen(account));
streamState.socket?.removeEventListener("close", onClose);
streamState.socket?.removeEventListener("error", onError);
streamState.socket?.removeEventListener("message", onMessage);
}
streamState.socket = ref(
new ReconnectingWebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account?.value}`
)
);
if (streamState.socket) {
interval = setInterval(() => {
streamState.socket?.send("");
}, 45000);
}
streamState.socket.addEventListener("open", () => onOpen(account));
streamState.socket.addEventListener("close", onClose);
streamState.socket.addEventListener("error", onError);
streamState.socket.addEventListener("message", onMessage);
}
};
subscribeKey(streamState, "selectedAccount", addListeners);
const DebugStream = () => {
const { selectedAccount, logs, socket } = useSnapshot(streamState);
const { selectedAccount, logs } = useSnapshot(streamState);
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
const { accounts } = useSnapshot(state);
const accountOptions = accounts.map(acc => ({
const accountOptions = accounts.map((acc) => ({
label: acc.name,
value: acc.address,
}));
@@ -42,117 +117,21 @@ const DebugStream = () => {
options={accountOptions}
hideSelectedOptions
value={selectedAccount}
onChange={acc => (streamState.selectedAccount = acc as any)}
onChange={(acc) => {
streamState.socket?.close(
4999,
"Old connection closed because user switched account"
);
streamState.selectedAccount = acc as any;
}}
css={{ width: "100%" }}
/>
</>
);
useEffect(() => {
const account = selectedAccount?.value;
if (account && (!socket || !socket.url.endsWith(account))) {
socket?.close();
streamState.socket = ref(
new WebSocket(
`wss://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/${account}`
)
);
} else if (!account && socket) {
socket.close();
streamState.socket = undefined;
}
}, [selectedAccount?.value, socket]);
const onMount = useCallback(async () => {
// deliberately using `proxy` values and not the `useSnapshot` ones to have no dep list
const acc = streamState.selectedAccount;
const status = streamState.status;
if (status === "opened" && acc) {
// fetch the missing ones
try {
const url = `https://${process.env.NEXT_PUBLIC_DEBUG_STREAM_URL}/recent/${acc?.value}`;
// TODO Remove after api sets cors properly
const res = await fetch("/api/proxy", {
method: "POST",
body: JSON.stringify({ url }),
headers: {
"Content-Type": "application/json",
},
});
if (!res.ok) return;
const body = await res.json();
if (!body?.logs) return;
const start = streamState.statusChangeTimestamp || 0;
streamState.logs = [];
pushLog(`Debug stream opened for account ${acc.value}`, {
type: "success",
});
const logs = Object.entries(body.logs).filter(([tm]) => +tm >= start);
logs.forEach(([tm, log]) => pushLog(log));
} catch (error) {
console.error(error);
}
}
}, []);
useEffect(() => {
onMount();
}, [onMount]);
useEffect(() => {
const account = selectedAccount?.value;
const socket = streamState.socket;
if (!socket) return;
const onOpen = () => {
streamState.logs = [];
streamState.status = "opened";
streamState.statusChangeTimestamp = Date.now();
pushLog(`Debug stream opened for account ${account}`, {
type: "success",
});
};
const onError = () => {
pushLog("Something went wrong! Check your connection and try again.", {
type: "error",
});
};
const onClose = (e: CloseEvent) => {
pushLog(`Connection was closed. [code: ${e.code}]`, {
type: "error",
});
streamState.selectedAccount = null;
streamState.status = "closed";
streamState.statusChangeTimestamp = Date.now();
};
const onMessage = (event: any) => {
pushLog(event.data);
};
socket.addEventListener("open", onOpen);
socket.addEventListener("close", onClose);
socket.addEventListener("error", onError);
socket.addEventListener("message", onMessage);
return () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("close", onClose);
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
};
}, [selectedAccount?.value, socket]);
useEffect(() => {
const account = transactionsState.transactions.find(
tx => tx.header === activeTxTab
(tx) => tx.header === activeTxTab
)?.state.selectedAccount;
if (account && account.value !== streamState.selectedAccount?.value)

View File

@@ -1,7 +1,6 @@
import React, { useRef, useState } from "react";
import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import React, { useState } from "react";
import { useSnapshot } from "valtio";
import { useTheme } from "next-themes";
import { useRouter } from "next/router";
import NextLink from "next/link";
@@ -10,31 +9,25 @@ import filesize from "filesize";
import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import state from "../state";
import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from ".";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
import Monaco from "./Monaco";
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
const DeployEditor = () => {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const snap = useSnapshot(state);
const router = useRouter();
const { theme } = useTheme();
const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active];
const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color =
compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -43,6 +36,10 @@ const DeployEditor = () => {
? "$warning"
: "$success";
const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErros = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && (
<Flex
column
@@ -60,15 +57,30 @@ const DeployEditor = () => {
{activeFile?.lastCompiled && (
<ReactTimeAgo date={activeFile.lastCompiled} locale="en-US" />
)}
{activeFile.compiledContent?.byteLength && (
<Text css={{ ml: "$2", color }}>
({filesize(activeFile.compiledContent.byteLength)})
</Text>
)}
</Flex>
{activeFile.compiledContent?.byteLength &&
activeFile.compiledContent?.byteLength >= 64000 && (
<Flex css={{ flexDirection: "column", py: "$3", pb: "$1" }}>
<Text css={{ ml: "$2", color: "$error" }}>
File size is larger than 64kB, cannot set hook!
</Text>
</Flex>
)}
<Button variant="link" onClick={() => setShowContent(true)}>
View as WAT-file
</Button>
{isContentChanged && (
<Text warning>
File contents were changed after last compile, compile again to
incorporate your latest changes in the build.
</Text>
)}
</Flex>
);
const NoContentView = !snap.loading && router.isReady && (
@@ -88,7 +100,7 @@ const DeployEditor = () => {
</Text>
);
const isContent =
snap.files?.filter((file) => file.compiledWatContent).length > 0 &&
snap.files?.filter(file => file.compiledWatContent).length > 0 &&
router.isReady;
return (
<Box
@@ -115,32 +127,38 @@ const DeployEditor = () => {
) : !showContent ? (
CompiledStatView
) : (
<Editor
<Monaco
className="hooks-editor"
defaultLanguage={"wat"}
language={"wat"}
path={`file://tmp/c/${snap.files?.[snap.active]?.name}.wat`}
value={snap.files?.[snap.active]?.compiledWatContent || ""}
beforeMount={(monaco) => {
path={`file://tmp/c/${activeFile?.name}.wat`}
value={activeFile?.compiledWatContent || ""}
beforeMount={monaco => {
monaco.languages.register({ id: "wat" });
monaco.languages.setLanguageConfiguration("wat", wat.config);
monaco.languages.setMonarchTokensProvider("wat", wat.tokens);
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}}
onMount={(editor, monaco) => {
editorRef.current = editor;
onMount={editor => {
editor.updateOptions({
glyphMargin: true,
readOnly: true,
});
}}
theme={theme === "dark" ? "dark" : "light"}
overlay={
<Flex
css={{
m: "$1",
ml: "auto",
fontSize: "$sm",
color: "$textMuted",
}}
>
<Link onClick={() => setShowContent(false)}>
Exit editor mode
</Link>
</Flex>
}
/>
)}
</Container>

View File

@@ -1,103 +0,0 @@
import React, { useRef, useLayoutEffect } from "react";
import { useSnapshot } from "valtio";
import { Play, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { compileCode } from "../state/actions";
import state from "../state";
import Button from "./Button";
import Heading from "./Heading";
const Footer = () => {
const snap = useSnapshot(state);
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [snap.logs, stayScrolled]);
return (
<Box
as="footer"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Heading
as="h3"
css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
>
DEVELOPMENT LOG
</Heading>
<Button
ghost
size="xs"
css={{
position: "absolute",
right: "$3",
top: "$2",
color: "$mauve10",
}}
onClick={() => {
state.logs = [];
}}
>
<Prohibit size="14px" />
</Button>
<Box
as="pre"
ref={logRef}
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "auto",
wordWrap: "break-word",
py: 3,
}}
>
{snap.logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText>
<LogText>{log.message}</LogText>
</Box>
))}
</Box>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Container>
</Box>
);
};
export default Footer;

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import {
Plus,
Share,
@@ -101,7 +101,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
if (!filename) {
return { error: "You need to add filename" };
}
if (snap.files.find(file => file.name === filename)) {
if (snap.files.find((file) => file.name === filename)) {
return { error: "Filename already exists." };
}
@@ -132,22 +132,55 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
createNewFile(filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const files = snap.files;
return (
<Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex
id="kissa"
ref={scrollRef}
css={{
overflowX: "scroll",
overflowY: "hidden",
py: "$3",
pb: "$0",
flex: 1,
"&::-webkit-scrollbar": {
height: 0,
background: "transparent",
height: "0.3em",
background: "rgba(0,0,0,.0)",
},
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(0,0,0,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(0,0,0,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
".dark &": {
"&::-webkit-scrollbar": {
background: "rgba(0,0,0,.0)",
},
"&::-webkit-scrollbar-gutter": "stable",
"&::-webkit-scrollbar-thumb": {
backgroundColor: "rgba(255,255,255,.2)",
outline: "0px",
borderRadius: "9999px",
},
scrollbarColor: "rgba(255,255,255,.2) rgba(0,0,0,0)",
scrollbarGutter: "stable",
scrollbarWidth: "thin",
},
}}
onWheelCapture={(e) => {
if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY;
}
}}
>
<Container css={{ flex: 1 }}>
<Container css={{ flex: 1 }} ref={containerRef}>
<Stack
css={{
gap: "$3",
@@ -233,8 +266,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
<Label>Filename</Label>
<Input
value={filename}
onChange={e => setFilename(e.target.value)}
onKeyPress={e => {
onChange={(e) => setFilename(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleConfirm();
}
@@ -509,8 +542,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
type="number"
min="1"
value={editorSettings.tabSize}
onChange={e =>
setEditorSettings(curr => ({
onChange={(e) =>
setEditorSettings((curr) => ({
...curr,
tabSize: Number(e.target.value),
}))

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useRef } from "react";
import { useSnapshot, ref } from "valtio";
import Editor, { loader } from "@monaco-editor/react";
import type monaco from "monaco-editor";
import { ArrowBendLeftUp } from "phosphor-react";
import { useTheme } from "next-themes";
@@ -8,8 +7,6 @@ import { useRouter } from "next/router";
import Box from "./Box";
import Container from "./Container";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
import { saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants";
import state from "../state";
@@ -22,16 +19,12 @@ import { listen } from "@codingame/monaco-jsonrpc";
import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
import Monaco from "./Monaco";
import { saveAllFiles } from '../state/actions/saveFile';
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path;
if (apiHeaderFiles.find((h) => currPath?.endsWith(h))) {
if (apiHeaderFiles.find(h => currPath?.endsWith(h))) {
editor.updateOptions({ readOnly: true });
} else {
editor.updateOptions({ readOnly: false });
@@ -48,7 +41,7 @@ const setMarkers = (monacoE: typeof monaco) => {
.getModelMarkers({})
// Filter out the markers that are hooks specific
.filter(
(marker) =>
marker =>
typeof marker?.code === "string" &&
// Take only markers that starts with "hooks-"
marker?.code?.includes("hooks-")
@@ -62,16 +55,16 @@ const setMarkers = (monacoE: typeof monaco) => {
// Add decoration (aka extra hoverMessages) to markers in the
// exact same range (location) where the markers are
const models = monacoE.editor.getModels();
models.forEach((model) => {
models.forEach(model => {
decorations[model.id] = model?.deltaDecorations(
decorations?.[model.id] || [],
markers
.filter((marker) =>
.filter(marker =>
marker?.resource.path
.split("/")
.includes(`${state.files?.[state.active]?.name}`)
)
.map((marker) => ({
.map(marker => ({
range: new monacoE.Range(
marker.startLineNumber,
marker.startColumn,
@@ -119,6 +112,13 @@ const HooksEditor = () => {
setMarkers(monacoRef.current);
}
}, [snap.active]);
useEffect(() => {
return () => {
saveAllFiles();
};
}, []);
const file = snap.files[snap.active];
return (
<Box
css={{
@@ -133,16 +133,16 @@ const HooksEditor = () => {
>
<EditorNavigation />
{snap.files.length > 0 && router.isReady ? (
<Editor
className="hooks-editor"
<Monaco
keepCurrentModel
defaultLanguage={snap.files?.[snap.active]?.language}
language={snap.files?.[snap.active]?.language}
path={`file:///work/c/${snap.files?.[snap.active]?.name}`}
defaultValue={snap.files?.[snap.active]?.content}
beforeMount={(monaco) => {
defaultLanguage={file?.language}
language={file?.language}
path={`file:///work/c/${file?.name}`}
defaultValue={file?.content}
// onChange={val => (state.files[snap.active].content = val)} // Auto save?
beforeMount={monaco => {
if (!snap.editorCtx) {
snap.files.forEach((file) =>
snap.files.forEach(file =>
monaco.editor.createModel(
file.content,
file.language,
@@ -167,13 +167,13 @@ const HooksEditor = () => {
// listen when the web socket is opened
listen({
webSocket: webSocket as WebSocket,
onConnection: (connection) => {
onConnection: connection => {
// create and start the language client
const languageClient = createLanguageClient(connection);
const disposable = languageClient.start();
connection.onClose(() => {
try {
// disposable.stop();
disposable.dispose();
} catch (err) {
console.log("err", err);
@@ -183,7 +183,6 @@ const HooksEditor = () => {
});
}
// // hook editor to global state
// editor.updateOptions({
// minimap: {
// enabled: false,
@@ -192,10 +191,6 @@ const HooksEditor = () => {
// });
if (!state.editorCtx) {
state.editorCtx = ref(monaco.editor);
// @ts-expect-error
monaco.editor.defineTheme("dark", dark);
// @ts-expect-error
monaco.editor.defineTheme("light", light);
}
}}
onMount={(editor, monaco) => {
@@ -223,13 +218,13 @@ const HooksEditor = () => {
});
// Hacky way to hide Peek menu
editor.onContextMenu((e) => {
editor.onContextMenu(e => {
const host =
document.querySelector<HTMLElement>(".shadow-root-host");
const contextMenuItems =
host?.shadowRoot?.querySelectorAll("li.action-item");
contextMenuItems?.forEach((k) => {
contextMenuItems?.forEach(k => {
// If menu item contains "Peek" lets hide it
if (k.querySelector(".action-label")?.textContent === "Peek") {
// @ts-expect-error

View File

@@ -6,7 +6,7 @@ import {
useState,
useCallback,
} from "react";
import { Notepad, Prohibit } from "phosphor-react";
import { IconProps, Notepad, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import NextLink from "next/link";
@@ -24,6 +24,7 @@ interface ILogBox {
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
Icon?: FC<IconProps>;
}
const LogBox: FC<ILogBox> = ({
@@ -33,6 +34,7 @@ const LogBox: FC<ILogBox> = ({
children,
renderNav,
enhanced,
Icon = Notepad,
}) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -82,14 +84,14 @@ const LogBox: FC<ILogBox> = ({
gap: "$3",
}}
>
<Notepad size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
<Icon size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex
row
align="center"
css={{
width: "50%", // TODO make it max without breaking layout!
}}
// css={{
// maxWidth: "100%", // TODO make it max without breaking layout!
// }}
>
{renderNav?.()}
</Flex>
@@ -162,11 +164,11 @@ export const Log: FC<ILog> = ({
(str?: string): ReactNode => {
if (!str || !accounts.length) return null;
const pattern = `(${accounts.map((acc) => acc.address).join("|")})`;
const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
const res = regexifyString({
pattern: new RegExp(pattern, "gim"),
decorator: (match, idx) => {
const name = accounts.find((acc) => acc.address === match)?.name;
const name = accounts.find(acc => acc.address === match)?.name;
return (
<Link
key={match + idx}
@@ -188,13 +190,13 @@ export const Log: FC<ILog> = ({
);
let message: ReactNode;
if (typeof _message === 'string') {
if (typeof _message === "string") {
_message = _message.trim().replace(/\n /gi, "\n");
message = enrichAccounts(_message)
}
else {
message = _message
if (_message) message = enrichAccounts(_message);
else message = <Text muted>{'""'}</Text>
} else {
message = _message;
}
const jsonData = enrichAccounts(_jsonData);

75
components/Monaco.tsx Normal file
View File

@@ -0,0 +1,75 @@
import Editor, { loader, EditorProps, Monaco } from "@monaco-editor/react";
import { CSS } from "@stitches/react";
import type monaco from "monaco-editor";
import { useTheme } from "next-themes";
import { FC, MutableRefObject, ReactNode } from "react";
import { Flex } from ".";
import dark from "../theme/editor/amy.json";
import light from "../theme/editor/xcode_default.json";
export type MonacoProps = EditorProps & {
id?: string;
rootProps?: { css: CSS } & Record<string, any>;
overlay?: ReactNode;
editorRef?: MutableRefObject<monaco.editor.IStandaloneCodeEditor>;
monacoRef?: MutableRefObject<typeof monaco>;
};
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
const Monaco: FC<MonacoProps> = ({
id,
path = `file:///${id}`,
className = id,
language = "json",
overlay,
editorRef,
monacoRef,
beforeMount,
rootProps,
...rest
}) => {
const { theme } = useTheme();
const setTheme = (monaco: Monaco) => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
};
return (
<Flex
fluid
column
{...rootProps}
css={{
position: "relative",
height: "100%",
...rootProps?.css,
}}
>
<Editor
className={className}
language={language}
path={path}
beforeMount={monaco => {
beforeMount?.(monaco);
setTheme(monaco);
}}
theme={theme === "dark" ? "dark" : "light"}
{...rest}
/>
{overlay && (
<Flex
css={{ position: "absolute", bottom: 0, right: 0, width: "100%" }}
>
{overlay}
</Flex>
)}
</Flex>
);
};
export default Monaco;

View File

@@ -30,12 +30,6 @@ import PanelBox from "./PanelBox";
import { templateFileIds } from "../state/constants";
import { styled } from "../stitches.config";
import Starter from "../components/icons/Starter";
import Firewall from "../components/icons/Firewall";
import Notary from "../components/icons/Notary";
import Carbon from "../components/icons/Carbon";
import Peggy from "../components/icons/Peggy";
const ImageWrapper = styled(Flex, {
position: "relative",
mt: "$2",
@@ -301,66 +295,18 @@ const Navigation = () => {
},
}}
>
<PanelBox
as="a"
href={`/develop/${templateFileIds.starter}`}
>
<ImageWrapper>
<Starter />
</ImageWrapper>
<Heading>Starter</Heading>
{Object.values(templateFileIds).map((template) => (
<PanelBox
key={template.id}
as="a"
href={`/develop/${template.id}`}
>
<ImageWrapper>{template.icon()}</ImageWrapper>
<Heading>{template.name}</Heading>
<Text>
Just a basic starter with essential imports, just
accepts any transaction coming through
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.firewall}`}
css={{ alignItems: "flex-start" }}
>
<ImageWrapper>
<Firewall />
</ImageWrapper>
<Heading>Firewall</Heading>
<Text>
This Hook essentially checks a blacklist of accounts
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.notary}`}
>
<ImageWrapper>
<Notary />
</ImageWrapper>
<Heading>Notary</Heading>
<Text>
Collecting signatures for multi-sign transactions
</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.carbon}`}
>
<ImageWrapper>
<Carbon />
</ImageWrapper>
<Heading>Carbon</Heading>
<Text>Send a percentage of sum to an address</Text>
</PanelBox>
<PanelBox
as="a"
href={`/develop/${templateFileIds.peggy}`}
>
<ImageWrapper>
<Peggy />
</ImageWrapper>
<Heading>Peggy</Heading>
<Text>An oracle based stable coin hook</Text>
</PanelBox>
<Text>{template.description}</Text>
</PanelBox>
))}
</Flex>
</Flex>
<DialogClose asChild>
@@ -394,6 +340,8 @@ const Navigation = () => {
height: 0,
background: "transparent",
},
scrollbarColor: "transparent",
scrollbarWidth: "none",
}}
>
<Stack

View File

@@ -0,0 +1,338 @@
import { Play, X } from "phosphor-react";
import {
HTMLInputTypeAttribute,
useCallback,
useEffect,
useState,
} from "react";
import state, { IAccount, IFile, ILog } from "../../state";
import Button from "../Button";
import Box from "../Box";
import Input, { Label } from "../Input";
import Stack from "../Stack";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogTitle,
DialogDescription,
DialogClose,
} from "../Dialog";
import Flex from "../Flex";
import { useSnapshot } from "valtio";
import Select from "../Select";
import Text from "../Text";
import { saveFile } from "../../state/actions/saveFile";
import { getErrors, getTags } from "../../utils/comment-parser";
import toast from "react-hot-toast";
const generateHtmlTemplate = (code: string, data?: Record<string, any>) => {
let processString: string | undefined;
const process = { env: { NODE_ENV: "production" } } as any;
if (data) {
Object.keys(data).forEach(key => {
process.env[key] = data[key];
});
}
processString = JSON.stringify(process);
return `
<html>
<head>
<script>
var log = console.log;
var errorLog = console.error;
var infoLog = console.info;
var warnLog = console.warn
console.log = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'log', args: args || [] }, '*');
log.apply(console, args);
}
console.error = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'error', args: args || [] }, '*');
errorLog.apply(console, args);
}
console.info = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'info', args: args || [] }, '*');
infoLog.apply(console, args);
}
console.warn = function(){
var args = Array.from(arguments);
parent.window.postMessage({ type: 'warning', args: args || [] }, '*');
warnLog.apply(console, args);
}
var process = '${processString || "{}"}';
process = JSON.parse(process);
window.process = process
function windowErrorHandler(event) {
event.preventDefault() // to prevent automatically logging to console
console.error(event.error?.toString())
}
window.addEventListener('error', windowErrorHandler);
</script>
<script type="module">
${code}
</script>
</head>
<body>
</body>
</html>
`;
};
type Fields = Record<
string,
{
name: string;
value: string;
type?: "Account" | `Account.${keyof IAccount}` | HTMLInputTypeAttribute;
description?: string;
required?: boolean;
}
>;
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const snap = useSnapshot(state);
const [templateError, setTemplateError] = useState("");
const [fields, setFields] = useState<Fields>({});
const [iFrameCode, setIframeCode] = useState("");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const getFields = useCallback(() => {
const inputTags = ["input", "param", "arg", "argument"];
const tags = getTags(content)
.filter(tag => inputTags.includes(tag.tag))
.filter(tag => !!tag.name);
let _fields = tags.map(tag => ({
name: tag.name,
value: tag.default || "",
type: tag.type,
description: tag.description,
required: !tag.optional,
}));
const fields: Fields = _fields.reduce((acc, field) => {
acc[field.name] = field;
return acc;
}, {} as Fields);
const error = getErrors(content);
if (error) setTemplateError(error.message);
else setTemplateError("");
return fields;
}, [content]);
const runScript = useCallback(() => {
try {
let data: any = {};
Object.keys(fields).forEach(key => {
data[key] = fields[key].value;
});
const template = generateHtmlTemplate(content, data);
setIframeCode(template);
state.scriptLogs = [
...snap.scriptLogs,
{ type: "success", message: "Started running..." },
];
} catch (err) {
state.scriptLogs = [
...snap.scriptLogs,
// @ts-expect-error
{ type: "error", message: err?.message || "Could not parse template" },
];
}
}, [content, fields, snap.scriptLogs]);
useEffect(() => {
const handleEvent = (e: any) => {
if (e.data.type === "log" || e.data.type === "error") {
const data: ILog[] = e.data.args.map((msg: any) => ({
type: e.data.type,
message: typeof msg === "string" ? msg : JSON.stringify(msg, null, 2),
}));
state.scriptLogs = [...snap.scriptLogs, ...data];
}
};
window.addEventListener("message", handleEvent);
return () => window.removeEventListener("message", handleEvent);
}, [snap.scriptLogs]);
useEffect(() => {
const defaultFields = getFields() || {};
setFields(defaultFields);
}, [content, setFields, getFields]);
const accOptions = snap.accounts?.map(acc => ({
...acc,
label: acc.name,
value: acc.address,
}));
const isDisabled = Object.values(fields).some(
field => field.required && !field.value
);
const handleRun = useCallback(() => {
if (isDisabled)
return toast.error("Please fill in all the required fields.");
state.scriptLogs = [];
runScript();
setIsDialogOpen(false);
}, [isDisabled, runScript]);
return (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button
variant="primary"
onClick={() => {
saveFile(false);
setIframeCode("");
}}
>
<Play weight="bold" size="16px" /> {name}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Run {name} script</DialogTitle>
<DialogDescription>
<Box>
You are about to run scripts provided by the developer of the
hook, make sure you trust the author before you continue.
</Box>
{templateError && (
<Box
as="span"
css={{
display: "block",
color: "$error",
mt: "$3",
whiteSpace: "pre",
}}
>
{templateError}
</Box>
)}
{Object.keys(fields).length > 0 && (
<Box css={{ mt: "$4", mb: 0 }}>
Fill in the following parameters to run the script.
</Box>
)}
</DialogDescription>
<Stack css={{ width: "100%" }}>
{Object.keys(fields).map(key => {
const { name, value, type, description, required } = fields[key];
const isAccount = type?.startsWith("Account");
const isAccountSecret = type === "Account.secret";
const accountField =
(isAccount && type?.split(".")[1]) || "address";
return (
<Box key={name} css={{ width: "100%" }}>
<Label
css={{ display: "flex", justifyContent: "space-between" }}
>
<span>
{description || name} {required && <Text error>*</Text>}
</span>
{isAccountSecret && (
<Text error small css={{ alignSelf: "end" }}>
can access account secret key
</Text>
)}
</Label>
{isAccount ? (
<Select
css={{ mt: "$1" }}
options={accOptions}
onChange={(val: any) => {
setFields({
...fields,
[key]: {
...fields[key],
value: val[accountField],
},
});
}}
value={accOptions.find(
(acc: any) => acc[accountField] === value
)}
/>
) : (
<Input
type={type || "text"}
value={value}
css={{ mt: "$1" }}
onChange={e => {
setFields({
...fields,
[key]: { ...fields[key], value: e.target.value },
});
}}
/>
)}
</Box>
);
})}
<Flex
css={{ justifyContent: "flex-end", width: "100%", gap: "$3" }}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button
variant="primary"
isDisabled={isDisabled}
onClick={handleRun}
>
Run script
</Button>
</Flex>
</Stack>
<DialogClose asChild>
<Box
css={{
position: "absolute",
top: "$1",
right: "$1",
cursor: "pointer",
background: "$mauve1",
display: "flex",
borderRadius: "$full",
p: "$1",
}}
>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
{iFrameCode && (
<iframe
style={{ display: "none" }}
srcDoc={iFrameCode}
sandbox="allow-scripts"
/>
)}
</>
);
};
export default RunScript;

View File

@@ -22,12 +22,12 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions";
import { useSnapshot } from "valtio";
import state from "../state";
import state, { SelectOption } from "../state";
import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee";
const transactionOptions = Object.keys(tts).map((key) => ({
const transactionOptions = Object.keys(tts).map(key => ({
label: key,
value: key as keyof TTS,
}));
@@ -56,9 +56,22 @@ export type SetHookData = {
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => {
const snap = useSnapshot(state);
const account = snap.accounts.find((acc) => acc.address === accountAddress);
const activeFile = snap.files[snap.active]?.compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
}));
const [selectedAccount, setSelectedAccount] = useState(
accountOptions.find(acc => acc.value === accountAddress)
);
const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value
);
const {
register,
handleSubmit,
@@ -68,11 +81,13 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
getValues,
formState: { errors },
} = useForm<SetHookData>({
defaultValues: {
HookNamespace:
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter((to) => to.label === "ttPAYMENT"),
},
defaultValues: snap.deployValues?.[activeFile?.name]
? snap.deployValues[activeFile?.name]
: {
HookNamespace:
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
},
});
const { fields, append, remove } = useFieldArray({
control,
@@ -80,15 +95,30 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
});
const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee");
// Update value if activeWat changes
useEffect(() => {
setValue(
"HookNamespace",
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
);
const defaultValue = snap.deployValues?.[activeFile?.name]
? snap.deployValues?.[activeFile?.name].HookNamespace
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "";
setValue("HookNamespace", defaultValue);
setFormInitialized(true);
}, [snap.activeWat, snap.files, setValue]);
}, [
snap.activeWat,
snap.files,
setValue,
activeFile?.name,
snap.deployValues,
]);
useEffect(() => {
if (
watchedFee &&
(watchedFee.includes(".") || watchedFee.includes(","))
) {
setValue("Fee", watchedFee.replaceAll(".", "").replaceAll(",", ""));
}
}, [watchedFee, setValue]);
// const {
// fields: grantFields,
// append: grantAppend,
@@ -100,7 +130,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch(
"HookNamespace",
snap.files?.[snap.active]?.name?.split(".")?.[0] || ""
snap.deployValues?.[activeFile?.name]
? snap.deployValues?.[activeFile?.name].HookNamespace
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
);
const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace);
@@ -121,21 +153,28 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
}
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", res.base_fee);
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
})();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]);
if (!account) {
return null;
}
const tooLargeFile = () => {
const activeFile = snap.files[snap.active].compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
return Boolean(
activeFile?.compiledContent?.byteLength &&
activeFile?.compiledContent?.byteLength >= 64000
);
};
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
const currAccount = state.accounts.find(
(acc) => acc.address === account.address
(acc) => acc.address === account?.address
);
if (!account) return;
if (currAccount) currAccount.isLoading = true;
const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false;
@@ -155,8 +194,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase
variant={"secondary"}
disabled={
!account ||
account.isLoading ||
!snap.files.filter((file) => file.compiledWatContent).length
!snap.files.filter(file => file.compiledWatContent).length ||
tooLargeFile()
}
>
Set Hook
@@ -167,14 +208,22 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<DialogTitle>Deploy configuration</DialogTitle>
<DialogDescription as="div">
<Stack css={{ width: "100%", flex: 1 }}>
<Box css={{ width: "100%" }}>
<Label>Account</Label>
<Select
instanceId="deploy-account"
placeholder="Select account"
hideSelectedOptions
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
/>
</Box>
<Box css={{ width: "100%" }}>
<Label>Invoke on transactions</Label>
<Controller
name="Invoke"
control={control}
defaultValue={transactionOptions.filter(
(to) => to.label === "ttPAYMENT"
)}
render={({ field }) => (
<Select
{...field}
@@ -191,9 +240,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Input
{...register("HookNamespace", { required: true })}
autoComplete={"off"}
defaultValue={
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
}
/>
{errors.HookNamespace?.type === "required" && (
<Box css={{ display: "inline", color: "$red11" }}>
@@ -256,6 +302,12 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
type="number"
{...register("Fee", { required: true })}
autoComplete={"off"}
onKeyPress={e => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
}}
step="1"
defaultValue={10000}
css={{
"-moz-appearance": "textfield",
@@ -282,8 +334,9 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
alignContent: "center",
display: "flex",
}}
onClick={async (e) => {
onClick={async e => {
e.preventDefault();
if (!account) return;
setEstimateLoading(true);
const formValues = getValues();
try {
@@ -295,7 +348,12 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", res.base_fee);
setValue(
"Fee",
Math.round(
Number(res.base_fee || "")
).toString()
);
}
}
} catch (err) {}
@@ -377,7 +435,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Button
variant="primary"
type="submit"
isLoading={account.isLoading}
isLoading={account?.isLoading}
>
Set Hook
</Button>

View File

@@ -31,13 +31,15 @@ interface TabProps {
// TODO customise messages shown
interface Props {
label?: string;
activeIndex?: number;
activeHeader?: string;
headless?: boolean;
children: ReactElement<TabProps>[];
keepAllAlive?: boolean;
defaultExtension?: string;
forceDefaultExtension?: boolean;
appendDefaultExtension?: boolean;
allowedExtensions?: string[];
onCreateNewTab?: (name: string) => any;
onCloseTab?: (index: number, header?: string) => any;
onChangeActive?: (index: number, header?: string) => any;
@@ -46,6 +48,7 @@ interface Props {
export const Tab = (props: TabProps) => null;
export const Tabs = ({
label = "Tab",
children,
activeIndex,
activeHeader,
@@ -55,7 +58,8 @@ export const Tabs = ({
onCloseTab,
onChangeActive,
defaultExtension = "",
forceDefaultExtension,
appendDefaultExtension = false,
allowedExtensions,
}: Props) => {
const [active, setActive] = useState(activeIndex || 0);
const tabs: TabProps[] = children.map(elem => elem.props);
@@ -86,9 +90,13 @@ export const Tabs = ({
if (tabs.find(tab => tab.header === tabname)) {
return { error: "Name already exists." };
}
const ext = tabname.split(".").pop() || "";
if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" };
}
return { error: null };
},
[tabs]
[allowedExtensions, tabs]
);
const handleActiveChange = useCallback(
@@ -101,9 +109,11 @@ export const Tabs = ({
const handleCreateTab = useCallback(() => {
// add default extension in case omitted
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
if (forceDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = _tabname + defaultExtension;
let _tabname = tabname.includes(".")
? tabname
: `${tabname}.${defaultExtension}`;
if (appendDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
const chk = validateTabname(_tabname);
@@ -122,7 +132,7 @@ export const Tabs = ({
}, [
tabname,
defaultExtension,
forceDefaultExtension,
appendDefaultExtension,
validateTabname,
onCreateNewTab,
handleActiveChange,
@@ -206,13 +216,13 @@ export const Tabs = ({
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new tab</DialogTitle>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
<DialogDescription>
<Label>Tabname</Label>
<Label>{label} name</Label>
<Input
value={tabname}
onChange={e => setTabname(e.target.value)}

View File

@@ -7,20 +7,35 @@ const Text = styled("span", {
variants: {
small: {
true: {
fontSize: '$xs'
}
fontSize: "$xs",
},
},
muted: {
true: {
color: '$mauve9'
}
color: "$mauve9",
},
},
error: {
true: {
color: "$error",
},
},
warning: {
true: {
color: "$warning",
},
},
monospace: {
true: {
fontFamily: '$monospace'
}
}
}
fontFamily: "$monospace",
},
},
block: {
true: {
display: "block",
},
},
},
});
export default Text;

115
components/Textarea.tsx Normal file
View File

@@ -0,0 +1,115 @@
import { styled } from "../stitches.config";
export const Textarea = styled("textarea", {
// Reset
appearance: "none",
borderWidth: "0",
boxSizing: "border-box",
fontFamily: "inherit",
outline: "none",
width: "100%",
flex: "1",
backgroundColor: "$mauve4",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "$sm",
p: "$2",
fontSize: "$md",
lineHeight: 1,
color: "$mauve12",
boxShadow: `0 0 0 1px $colors$mauve8`,
WebkitTapHighlightColor: "rgba(0,0,0,0)",
"&::before": {
boxSizing: "border-box",
},
"&::after": {
boxSizing: "border-box",
},
fontVariantNumeric: "tabular-nums",
"&:-webkit-autofill": {
boxShadow: "inset 0 0 0 1px $colors$blue6, inset 0 0 0 100px $colors$blue3",
},
"&:-webkit-autofill::first-line": {
fontFamily: "$untitled",
color: "$mauve12",
},
"&:focus": {
boxShadow: `0 0 0 1px $colors$mauve10`,
"&:-webkit-autofill": {
boxShadow: `0 0 0 1px $colors$mauve10`,
},
},
"&::placeholder": {
color: "$mauve9",
},
"&:disabled": {
pointerEvents: "none",
backgroundColor: "$mauve2",
color: "$mauve8",
cursor: "not-allowed",
"&::placeholder": {
color: "$mauve7",
},
},
variants: {
variant: {
ghost: {
boxShadow: "none",
backgroundColor: "transparent",
"@hover": {
"&:hover": {
boxShadow: "inset 0 0 0 1px $colors$mauve7",
},
},
"&:focus": {
backgroundColor: "$loContrast",
boxShadow: `0 0 0 1px $colors$mauve10`,
},
"&:disabled": {
backgroundColor: "transparent",
},
"&:read-only": {
backgroundColor: "transparent",
},
},
deep: {
backgroundColor: "$deep",
boxShadow: "none",
},
},
state: {
invalid: {
boxShadow: "inset 0 0 0 1px $colors$crimson7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$crimson8, 0px 0px 0px 1px $colors$crimson8",
},
},
valid: {
boxShadow: "inset 0 0 0 1px $colors$grass7",
"&:focus": {
boxShadow:
"inset 0px 0px 0px 1px $colors$grass8, 0px 0px 0px 1px $colors$grass8",
},
},
},
cursor: {
default: {
cursor: "default",
"&:focus": {
cursor: "text",
},
},
text: {
cursor: "text",
},
},
},
});
export default Textarea;

View File

@@ -1,9 +1,4 @@
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { FC, useCallback, useEffect, useState } from "react";
import { useTheme } from "next-themes";
import dark from "../../theme/editor/amy.json";
import light from "../../theme/editor/xcode_default.json";
import { useSnapshot } from "valtio";
import state, {
prepareState,
@@ -11,18 +6,13 @@ import state, {
TransactionState,
} from "../../state";
import Text from "../Text";
import Flex from "../Flex";
import { Link } from "..";
import { Flex, Link } from "..";
import { showAlert } from "../../state/actions/showAlert";
import { parseJSON } from "../../utils/json";
import { extractSchemaProps } from "../../utils/schema";
import amountSchema from "../../content/amount-schema.json";
loader.config({
paths: {
vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.30.1/min/vs",
},
});
import Monaco from "../Monaco";
import type monaco from "monaco-editor";
interface JsonProps {
value?: string;
@@ -40,7 +30,6 @@ export const TxJson: FC<JsonProps> = ({
}) => {
const { editorSettings, accounts } = useSnapshot(state);
const { editorValue = value, estimatedFee } = txState;
const { theme } = useTheme();
const [hasUnsaved, setHasUnsaved] = useState(false);
const [currTxType, setCurrTxType] = useState<string | undefined>(
txState.selectedTransaction?.value
@@ -95,9 +84,6 @@ export const TxJson: FC<JsonProps> = ({
});
};
const path = `file:///${header}`;
const monaco = useMonaco();
const getSchemas = useCallback(async (): Promise<any[]> => {
const txObj = transactionsData.find(
td => td.TransactionType === currTxType
@@ -177,55 +163,63 @@ export const TxJson: FC<JsonProps> = ({
];
}, [accounts, currTxType, estimatedFee, header]);
const [monacoInst, setMonacoInst] = useState<typeof monaco>();
useEffect(() => {
if (!monaco) return;
if (!monacoInst) return;
getSchemas().then(schemas => {
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
monacoInst.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas,
});
});
}, [getSchemas, monaco]);
}, [getSchemas, monacoInst]);
return (
<Flex
fluid
column
css={{ height: "calc(100% - 45px)", position: "relative" }}
>
<Editor
className="hooks-editor"
language={"json"}
path={path}
height="100%"
beforeMount={monaco => {
monaco.editor.defineTheme("dark", dark as any);
monaco.editor.defineTheme("light", light as any);
}}
value={editorValue}
onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
<Monaco
rootProps={{
css: { height: "calc(100% - 45px)" },
}}
language={"json"}
id={header}
height="100%"
value={editorValue}
onChange={val => setState({ editorValue: val })}
onMount={(editor, monaco) => {
editor.updateOptions({
minimap: { enabled: false },
glyphMargin: true,
tabSize: editorSettings.tabSize,
dragAndDrop: true,
fontSize: 14,
});
// register onExit cb
const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue()));
}}
theme={theme === "dark" ? "dark" : "light"}
/>
{hasUnsaved && (
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
This file has unsaved changes.{" "}
<Link onClick={() => saveState(editorValue, currTxType)}>save</Link>{" "}
<Link onClick={discardChanges}>discard</Link>
</Text>
)}
</Flex>
setMonacoInst(monaco);
// register onExit cb
const model = editor.getModel();
model?.onWillDispose(() => onExit(model.getValue()));
}}
overlay={
hasUnsaved ? (
<Flex
row
align="center"
css={{ fontSize: "$xs", color: "$textMuted", ml: 'auto' }}
>
<Text muted small>
This file has unsaved changes.
</Text>
<Link
css={{ ml: "$1" }}
onClick={() => saveState(editorValue, currTxType)}
>
save
</Link>
<Link css={{ ml: "$1" }} onClick={discardChanges}>
discard
</Link>
</Flex>
) : undefined
}
/>
);
};

View File

@@ -1,4 +1,4 @@
import { FC, useCallback, useState } from "react";
import { FC, useCallback, useEffect, useState } from "react";
import Container from "../Container";
import Flex from "../Flex";
import Input from "../Input";
@@ -15,6 +15,7 @@ import { useSnapshot } from "valtio";
import state from "../../state";
import { streamState } from "../DebugStream";
import { Button } from "..";
import Textarea from "../Textarea";
interface UIProps {
setState: (
@@ -37,6 +38,7 @@ export const TxUI: FC<UIProps> = ({
txFields,
} = txState;
const transactionsOptions = transactionsData.map(tx => ({
value: tx.TransactionType,
label: tx.TransactionType,
@@ -96,13 +98,16 @@ export const TxUI: FC<UIProps> = ({
[estimateFee, handleSetField]
);
const handleChangeTxType = (tt: SelectOption) => {
setState({ selectedTransaction: tt });
const handleChangeTxType = useCallback(
(tt: SelectOption) => {
setState({ selectedTransaction: tt });
const newState = resetOptions(tt.value);
const newState = resetOptions(tt.value);
handleEstimateFee(newState, true);
};
handleEstimateFee(newState, true);
},
[handleEstimateFee, resetOptions, setState]
);
const specialFields = ["TransactionType", "Account", "Destination"];
@@ -110,6 +115,21 @@ export const TxUI: FC<UIProps> = ({
k => !specialFields.includes(k)
) as [keyof TxFields];
const switchToJson = () =>
setState({ editorSavedValue: null, viewType: "json" });
// default tx
useEffect(() => {
if (selectedTransaction?.value) return;
const defaultOption = transactionsOptions.find(
tt => tt.value === "Payment"
);
if (defaultOption) {
handleChangeTxType(defaultOption);
}
}, [handleChangeTxType, selectedTransaction?.value, transactionsOptions]);
return (
<Container
css={{
@@ -196,7 +216,7 @@ export const TxUI: FC<UIProps> = ({
let value: string | undefined;
if (typeof _value === "object") {
if (_value.$type === "json" && typeof _value.$value === "object") {
value = JSON.stringify(_value.$value);
value = JSON.stringify(_value.$value, null, 2);
} else {
value = _value.$value.toString();
}
@@ -204,9 +224,13 @@ export const TxUI: FC<UIProps> = ({
value = _value?.toString();
}
let isXrp = typeof _value === "object" && _value.$type === "xrp";
const isXrp = typeof _value === "object" && _value.$type === "xrp";
const isJson = typeof _value === "object" && _value.$type === "json";
const isFee = field === "Fee";
let rows = isJson
? (value?.match(/\n/gm)?.length || 0) + 1
: undefined;
if (rows && rows > 5) rows = 5;
return (
<Flex column key={field} css={{ mb: "$2", pr: "1px" }}>
<Flex
@@ -221,18 +245,63 @@ export const TxUI: FC<UIProps> = ({
<Text muted css={{ mr: "$3" }}>
{field + (isXrp ? " (XRP)" : "")}:{" "}
</Text>
<Input
value={value}
onChange={e => {
handleSetField(field, e.target.value);
}}
css={{ width: "70%", flex: "inherit" }}
/>
{isJson ? (
<Textarea
rows={rows}
value={value}
spellCheck={false}
onChange={switchToJson}
css={{
width: "70%",
flex: "inherit",
resize: "vertical",
}}
/>
) : (
<Input
type={isFee ? "number" : "text"}
value={value}
onChange={e => {
if (isFee) {
const val = e.target.value
.replaceAll(".", "")
.replaceAll(",", "");
handleSetField(field, val);
} else {
handleSetField(field, e.target.value);
}
}}
onKeyPress={
isFee
? e => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
}
: undefined
}
css={{
width: "70%",
flex: "inherit",
"-moz-appearance": "textfield",
"&::-webkit-outer-spin-button": {
"-webkit-appearance": "none",
margin: 0,
},
"&::-webkit-inner-spin-button ": {
"-webkit-appearance": "none",
margin: 0,
},
}}
/>
)}
{isFee && (
<Button
size="xs"
variant="primary"
outline
disabled={txState.txIsDisabled}
isDisabled={txState.txIsDisabled}
isLoading={feeLoading}
css={{
position: "absolute",

View File

@@ -12,6 +12,5 @@ export { default as Box } from "./Box";
export { default as Button } from "./Button";
export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog";
export * from "./DropdownMenu";

View File

@@ -12,7 +12,7 @@
"dependencies": {
"@codingame/monaco-jsonrpc": "^0.3.1",
"@codingame/monaco-languageclient": "^0.17.0",
"@monaco-editor/react": "^4.4.1",
"@monaco-editor/react": "^4.4.5",
"@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1",
@@ -23,8 +23,9 @@
"@radix-ui/react-popover": "^0.1.6",
"@radix-ui/react-switch": "^0.1.5",
"@radix-ui/react-tooltip": "^0.1.7",
"@stitches/react": "^1.2.6-0",
"@stitches/react": "^1.2.8",
"base64-js": "^1.5.1",
"comment-parser": "^1.3.1",
"dinero.js": "^1.9.1",
"file-saver": "^2.0.5",
"filesize": "^8.0.7",
@@ -35,6 +36,7 @@
"monaco-editor": "^0.33.0",
"next": "^12.0.4",
"next-auth": "^4.0.0-beta.5",
"next-plausible": "^3.2.0",
"next-themes": "^0.1.1",
"normalize-url": "^7.0.2",
"octokit": "^1.7.0",

View File

@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
import { Toaster } from "react-hot-toast";
import { useRouter } from "next/router";
import { IdProvider } from "@radix-ui/react-id";
import PlausibleProvider from "next-plausible";
import { darkTheme, css } from "../stitches.config";
import Navigation from "../components/Navigation";
@@ -17,6 +18,8 @@ import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en.json";
import { useSnapshot } from "valtio";
import Alert from "../components/AlertDialog";
import { Button, Flex } from "../components";
import { ChatCircleText } from "phosphor-react";
TimeAgo.setDefaultLocale(en.locale);
TimeAgo.addLocale(en);
@@ -37,7 +40,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
if (
!gistId &&
router.isReady &&
!router.pathname.includes("/sign-in") &&
router.pathname.includes("/develop") &&
!snap.files.length &&
!snap.mainModalShowed
) {
@@ -114,6 +117,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
media="(prefers-color-scheme: light)"
/>
</Head>
<IdProvider>
<SessionProvider session={session}>
<ThemeProvider
@@ -125,23 +129,40 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
dark: darkTheme.className,
}}
>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: "$mauve1",
color: "$mauve10",
fontSize: "$sm",
zIndex: 9999,
".dark &": {
backgroundColor: "$mauve4",
color: "$mauve12",
},
})(),
}}
/>
<Alert />
<PlausibleProvider
domain="hooks-builder.xrpl.org"
trackOutboundLinks
>
<Navigation />
<Component {...pageProps} />
<Toaster
toastOptions={{
className: css({
backgroundColor: "$mauve1",
color: "$mauve10",
fontSize: "$sm",
zIndex: 9999,
".dark &": {
backgroundColor: "$mauve4",
color: "$mauve12",
},
})(),
}}
/>
<Alert />
<Flex
as="a"
href="https://github.com/XRPLF/Hooks/discussions"
target="_blank"
rel="noopener noreferrer"
css={{ position: "fixed", right: "$4", bottom: "$4" }}
>
<Button size="sm" variant="primary" outline>
<ChatCircleText size={14} style={{ marginRight: "0px" }} />
Bugs & Discussions
</Button>
</Flex>
</PlausibleProvider>
</ThemeProvider>
</SessionProvider>
</IdProvider>

View File

@@ -1,7 +1,7 @@
import { Label } from "@radix-ui/react-label";
import type { NextPage } from "next";
import dynamic from "next/dynamic";
import { Gear, Play } from "phosphor-react";
import { FileJs, Gear, Play } from "phosphor-react";
import Hotkeys from "react-hot-keys";
import Split from "react-split";
import { useSnapshot } from "valtio";
@@ -9,6 +9,7 @@ import { ButtonGroup, Flex } from "../../components";
import Box from "../../components/Box";
import Button from "../../components/Button";
import Popover from "../../components/Popover";
import RunScript from "../../components/RunScript";
import state from "../../state";
import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
@@ -159,7 +160,7 @@ const Home: NextPage = () => {
>
<main style={{ display: "flex", flex: 1, position: "relative" }}>
<HooksEditor />
{snap.files[snap.active]?.name?.split(".")?.[1].toLowerCase() ===
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"c" && (
<Hotkeys
keyName="command+b,ctrl+b"
@@ -196,20 +197,61 @@ const Home: NextPage = () => {
</Flex>
</Hotkeys>
)}
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Flex
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
gap: "$2",
}}
>
<RunScript file={snap.files[snap.active]} />
</Flex>
</Hotkeys>
)}
</main>
<Box
css={{
display: "flex",
background: "$mauve1",
position: "relative",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Box>
<Flex css={{ width: "100%" }}>
<Flex
css={{
flex: 1,
background: "$mauve1",
position: "relative",
borderRight: "1px solid $mauve8",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Flex>
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Flex
css={{
flex: 1,
}}
>
<LogBox
Icon={FileJs}
title="Script Log"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
</Flex>
</Split>
);
};

View File

@@ -6,6 +6,9 @@ import Transaction from "../../components/Transaction";
import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTransaction } from "../../state";
import { useEffect, useState } from "react";
import { FileJs } from "phosphor-react";
import RunScript from '../../components/RunScript';
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
ssr: false,
@@ -19,14 +22,42 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
});
const Test = () => {
// This and useEffect is here to prevent useLayoutEffect warnings from react-split
const [showComponent, setShowComponent] = useState(false);
const { transactionLogs } = useSnapshot(state);
const { transactions, activeHeader } = useSnapshot(transactionsState);
const snap = useSnapshot(state);
useEffect(() => {
setShowComponent(true);
}, []);
if (!showComponent) {
return null;
}
const hasScripts = Boolean(
snap.files.filter(f => f.name.toLowerCase()?.endsWith(".js")).length
);
const renderNav = () => (
<Flex css={{ gap: "$3" }}>
{snap.files
.filter(f => f.name.endsWith(".js"))
.map(file => (
<RunScript file={file} key={file.name} />
))}
</Flex>
);
return (
<Container css={{ px: 0 }}>
<Split
direction="vertical"
sizes={getSplit("testVertical") || [50, 50]}
sizes={
hasScripts && getSplit("testVertical")?.length === 2
? [50, 20, 30]
: hasScripts
? [50, 20, 50]
: [50, 50]
}
gutterSize={4}
gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }}
@@ -56,14 +87,15 @@ const Test = () => {
>
<Box css={{ width: "55%", px: "$2" }}>
<Tabs
label="Transaction"
activeHeader={activeHeader}
// TODO make header a required field
onChangeActive={(idx, header) => {
if (header) transactionsState.activeHeader = header;
}}
keepAllAlive
forceDefaultExtension
defaultExtension=".json"
defaultExtension="json"
allowedExtensions={["json"]}
onCreateNewTab={header => modifyTransaction(header, {})}
onCloseTab={(idx, header) =>
header && modifyTransaction(header, undefined)
@@ -71,10 +103,7 @@ const Test = () => {
>
{transactions.map(({ header, state }) => (
<Tab key={header} header={header}>
<Transaction
state={state}
header={header}
/>
<Transaction state={state} header={header} />
</Tab>
))}
</Tabs>
@@ -84,8 +113,25 @@ const Test = () => {
</Box>
</Split>
</Flex>
<Flex row fluid>
{hasScripts ? (
<Flex
as="div"
css={{
borderTop: "1px solid $mauve6",
background: "$mauve1",
flexDirection: "column",
}}
>
<LogBox
Icon={FileJs}
title="Helper scripts"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
renderNav={renderNav}
/>
</Flex>
) : null}
<Flex>
<Split
direction="horizontal"
sizes={[50, 50]}

View File

@@ -27,7 +27,7 @@ export const names = [
* new account with 10 000 XRP. Hooks Testnet /newcreds endpoint
* is protected with CORS so that's why we did our own endpoint
*/
export const addFaucetAccount = async (showToast: boolean = false) => {
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
// Lets limit the number of faucet accounts to 5 for now
if (state.accounts.length > 5) {
return toast.error("You can only have maximum 6 accounts");
@@ -52,7 +52,7 @@ export const addFaucetAccount = async (showToast: boolean = false) => {
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: names.filter(name => !currNames.includes(name))[0],
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,

View File

@@ -14,19 +14,21 @@ import { ref } from "valtio";
*/
export const compileCode = async (activeId: number) => {
// Save the file to global state
saveFile(false);
saveFile(false, activeId);
if (!process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT) {
throw Error("Missing env!");
}
// Bail out if we're already compiling
if (state.compiling) {
// if compiling is ongoing return
// if compiling is ongoing return // TODO Inform user about it.
return;
}
// Set loading state to true
state.compiling = true;
state.logs = []
const file = state.files[activeId]
try {
file.containsErrors = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
headers: {
@@ -39,9 +41,9 @@ export const compileCode = async (activeId: number) => {
files: [
{
type: "c",
options: state.compileOptions.optimizationLevel || '-O0',
name: state.files[activeId].name,
src: state.files[activeId].content,
options: state.compileOptions.optimizationLevel || '-O2',
name: file.name,
src: file.content,
},
],
}),
@@ -49,15 +51,15 @@ export const compileCode = async (activeId: number) => {
const json = await res.json();
state.compiling = false;
if (!json.success) {
state.logs.push({ type: "error", message: json.message });
const errors = [json.message]
if (json.tasks && json.tasks.length > 0) {
json.tasks.forEach((task: any) => {
if (!task.success) {
state.logs.push({ type: "error", message: task?.console });
errors.push(task?.console)
}
});
}
return toast.error(`Couldn't compile!`, { position: "bottom-center" });
throw errors
}
state.logs.push({
type: "success",
@@ -67,8 +69,9 @@ export const compileCode = async (activeId: number) => {
});
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
state.files[state.active].compiledContent = ref(bufferData);
state.files[state.active].lastCompiled = new Date();
file.compiledContent = ref(bufferData);
file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
// Import wabt from and create human readable version of wasm file and
// put it into state
import("wabt").then((wabt) => {
@@ -84,10 +87,23 @@ export const compileCode = async (activeId: number) => {
});
} catch (err) {
console.log(err);
state.logs.push({
type: "error",
message: "Error occured while compiling!",
});
if (err instanceof Array && typeof err[0] === 'string') {
err.forEach(message => {
state.logs.push({
type: "error",
message,
});
})
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, check your connection try again later!",
});
}
state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true
}
};

View File

@@ -54,15 +54,15 @@ export const prepareDeployHookTx = async (
account: IAccount & { name?: string },
data: SetHookData
) => {
if (
!state.files ||
state.files.length === 0 ||
!state.files?.[state.active]?.compiledContent
) {
const activeFile = state.files[state.active]?.compiledContent
? state.files[state.active]
: state.files.filter((file) => file.compiledContent)[0];
if (!state.files || state.files.length === 0) {
return;
}
if (!state.files?.[state.active]?.compiledContent) {
if (!activeFile?.compiledContent) {
return;
}
if (!state.client) {
@@ -99,7 +99,7 @@ export const prepareDeployHookTx = async (
{
Hook: {
CreateCode: arrayBufferToHex(
state.files?.[state.active]?.compiledContent
activeFile?.compiledContent
).toUpperCase(),
HookOn: calculateHookOn(hookOnValues),
HookNamespace,
@@ -126,6 +126,10 @@ export const deployHook = async (
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);
if (!tx) {
return;
@@ -185,7 +189,7 @@ export const deployHook = async (
console.log(err);
state.deployLogs.push({
type: "error",
message: "Error occured while deploying",
message: "Error occurred while deploying",
});
}
if (currentAccount) {
@@ -268,10 +272,10 @@ export const deleteHook = async (account: IAccount & { name?: string }) => {
}
} catch (err) {
console.log(err);
toast.error("Error occured while deleting hoook", { id: toastId });
toast.error("Error occurred while deleting hook", { id: toastId });
state.deployLogs.push({
type: "error",
message: "Error occured while deleting hook",
message: "Error occurred while deleting hook",
});
}
if (currentAccount) {

View File

@@ -8,11 +8,12 @@ export const downloadAsZip = async () => {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }));
const zipped = await createZip(files);
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 occured while creating zip file, try again later')
toast.error('Error occurred while creating zip file, try again later')
} finally {
state.zipLoading = false
}

View File

@@ -19,20 +19,22 @@ export const fetchFiles = (gistId: string) => {
octokit
.request("GET /gists/{gist_id}", { gist_id: gistId })
.then(async res => {
if (!Object.values(templateFileIds).includes(gistId)) {
if (!Object.values(templateFileIds).map(v => v.id).includes(gistId)) {
return res
}
// in case of templates, fetch header file(s) and append to res
let resHeaderJson;
try {
const resHeader = await fetch(`${process.env.NEXT_PUBLIC_COMPILE_API_BASE_URL}/api/header-files`);
if (resHeader.ok) {
resHeaderJson = await resHeader.json();
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,
'hookapi.h': res.data.files?.['hookapi.h'] || { filename: 'hookapi.h', content: resHeaderJson.hookapi, language: 'C' },
'hookmacro.h': res.data.files?.['hookmacro.h'] || { filename: 'hookmacro.h', content: resHeaderJson.hookmacro, language: 'C' },
'sfcodes.h': res.data.files?.['sfcodes.h'] || { filename: 'sfcodes.h', content: resHeaderJson.sfcodes, language: 'C' },
...headerFiles
};
res.data.files = files;
}
@@ -58,6 +60,29 @@ export const fetchFiles = (gistId: string) => {
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({
@@ -89,4 +114,4 @@ export const fetchFiles = (gistId: string) => {
return;
}
state.loading = false;
};
};

View File

@@ -5,7 +5,7 @@ import state from '../index';
import { names } from './addFaucetAccount';
// Adds test account to global state with secret key
export const importAccount = (secret: string) => {
export const importAccount = (secret: string, name?: string) => {
if (!secret) {
return toast.error("You need to add secret!");
}
@@ -19,7 +19,7 @@ export const importAccount = (secret: string) => {
if (err?.message) {
toast.error(err.message)
} else {
toast.error('Error occured while importing account')
toast.error('Error occurred while importing account')
}
return;
}
@@ -27,7 +27,7 @@ export const importAccount = (secret: string) => {
return toast.error(`Couldn't create account!`);
}
state.accounts.push({
name: names[state.accounts.length],
name: name || names[state.accounts.length],
address: account.address || "",
secret: account.secret.familySeed || "",
xrp: "0",

View File

@@ -2,14 +2,15 @@ import toast from "react-hot-toast";
import state from '../index';
// Saves the current editor content to global state
export const saveFile = (showToast: boolean = true) => {
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 file = state.files[activeId || state.active]
if (state.files.length > 0) {
state.files[state.active].content = currentModel?.getValue() || "";
file.content = currentModel?.getValue() || "";
}
if (showToast) {
toast.success("Saved successfully", { position: "bottom-center" });

View File

@@ -1,20 +1,41 @@
// export const templateFileIds = {
// 'starter': '1d14e51e2e02dc0a508cb0733767a914', // TODO currently same as accept
// 'firewall': 'bcd6d0c0fcbe52545ddb802481ff9d26',
// 'notary': 'a789c75f591eeab7932fd702ed8cf9ea',
// 'carbon': '43925143fa19735d8c6505c34d3a6a47',
// 'peggy': 'ceaf352e2a65741341033ab7ef05c448',
// 'headers': '9b448e8a55fab11ef5d1274cb59f9cf3'
// }
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': '1f7d2963d9e342ea092286115274f3e3',
'firewall': '70edec690f0de4dd315fad1f4f996d8c',
'notary': '3d5677768fe8a54c4f6317e185d9ba66',
'carbon': 'a9fbcaf1b816b198c7fc0f62962bebf2',
'doubler': '56b86174aeb70b2b48eee962bad3e355',
'peggy': 'd21298a37e1550b781682014762a567b',
'headers': '55f639bce59a49c58c45e663776b5138'
'starter': {
id: '9106f1fe60482d90475bfe8f1315affe',
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: '5941c19dce3e147948f564e224553c02',
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', 'hookmacro.h']
export const apiHeaderFiles = ['hookapi.h', 'sfcodes.h', 'macro.h', 'extern.h', 'error.h'];

View File

@@ -13,9 +13,11 @@ export interface IFile {
name: string;
language: string;
content: string;
compiledValueSnapshot?: string
compiledContent?: ArrayBuffer | null;
compiledWatContent?: string | null;
lastCompiled?: Date
containsErrors?: boolean
}
export interface FaucetAccountRes {
@@ -52,6 +54,8 @@ export interface ILog {
defaultCollapsed?: boolean
}
export type DeployValue = Record<IFile['name'], any>;
export interface IState {
files: IFile[];
gistId?: string | null;
@@ -66,6 +70,7 @@ export interface IState {
logs: ILog[];
deployLogs: ILog[];
transactionLogs: ILog[];
scriptLogs: ILog[];
editorCtx?: typeof monaco.editor;
editorSettings: {
tabSize: number;
@@ -81,7 +86,8 @@ export interface IState {
compileOptions: {
optimizationLevel: '-O0' | '-O1' | '-O2' | '-O3' | '-O4' | '-Os';
strip: boolean
}
},
deployValues: DeployValue
}
// let localStorageState: null | string = null;
@@ -96,6 +102,7 @@ let initialState: IState = {
logs: [],
deployLogs: [],
transactionLogs: [],
scriptLogs: [],
editorCtx: undefined,
gistId: undefined,
gistOwner: undefined,
@@ -112,9 +119,10 @@ let initialState: IState = {
mainModalShowed: false,
accounts: [],
compileOptions: {
optimizationLevel: '-O0',
optimizationLevel: '-O2',
strip: true
}
},
deployValues: {}
};
let localStorageAccounts: string | null = null;

24
utils/comment-parser.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Spec, parse, Problem } from "comment-parser"
export const getTags = (source?: string): Spec[] => {
if (!source) return []
const blocks = parse(source)
const tags = blocks.reduce(
(acc, block) => acc.concat(block.tags),
[] as Spec[]
);
return tags
}
export const getErrors = (source?: string): Error | undefined => {
if (!source) return undefined
const blocks = parse(source)
const probs = blocks.reduce(
(acc, block) => acc.concat(block.problems),
[] as Problem[]
);
if (!probs.length) return undefined
const errors = probs.map(prob => `[${prob.code}] on line ${prob.line}: ${prob.message}`)
const error = new Error(`The following error(s) occurred while parsing JSDOC: \n${errors.join('\n')}`)
return error
}

View File

@@ -6,4 +6,10 @@ export const guessZipFileName = (files: File[]) => {
let parts = (files.filter(f => f.name.endsWith('.c'))[0]?.name || 'hook').split('.')
parts = parts.length > 1 ? parts.slice(0, -1) : parts
return parts.join('')
}
export const capitalize = (value?: string) => {
if (!value) return '';
return value[0].toLocaleUpperCase() + value.slice(1);
}

View File

@@ -1,6 +1,5 @@
import { MessageConnection } from "@codingame/monaco-jsonrpc";
import { MonacoLanguageClient, ErrorAction, CloseAction, createConnection } from "@codingame/monaco-languageclient";
import Router from "next/router";
import normalizeUrl from "normalize-url";
import ReconnectingWebSocket from "reconnecting-websocket";
@@ -14,11 +13,7 @@ export function createLanguageClient(connection: MessageConnection): MonacoLangu
errorHandler: {
error: () => ErrorAction.Continue,
closed: () => {
if (Router.pathname.includes('/develop')) {
return CloseAction.Restart
} else {
return CloseAction.DoNotRestart
}
return CloseAction.DoNotRestart
}
},

View File

@@ -202,19 +202,19 @@
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@monaco-editor/loader@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.0.tgz#659fbaf1d612ea67b2a0519a18612d1c4813e444"
integrity sha512-N3mGq1ktC3zh7WUx3NGO+PSDdNq9Vspk/41rEmRdrCqV9vNbBTRzAOplmUpNQsi+hmTs++ERMBobMERb8Kb+3g==
"@monaco-editor/loader@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.2.tgz#04effbb87052d19cd7d3c9d81c0635490f9bb6d8"
integrity sha512-BTDbpHl3e47r3AAtpfVFTlAi7WXv4UQ/xZmz8atKl4q7epQV5e7+JbigFDViWF71VBi4IIBdcWP57Hj+OWuc9g==
dependencies:
state-local "^1.0.6"
"@monaco-editor/react@^4.4.1":
version "4.4.1"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.1.tgz#2e2b9b369f3082b0e14f47cdbe35658fd56c7c7d"
integrity sha512-95E/XPC4dbm/7qdkhSsU/a1kRgcn2PYhRTVIc+/cixWCZrwRURW1DRPaIZ2lOawBJ6kAOLywxuD4A4UmbT0ZIw==
"@monaco-editor/react@^4.4.5":
version "4.4.5"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.5.tgz#beabe491efeb2457441a00d1c7651c653697f65b"
integrity sha512-IImtzU7sRc66OOaQVCG+5PFHkSWnnhrUWGBuH6zNmH2h0YgmAhcjHZQc/6MY9JWEbUtVF1WPBMJ9u1XuFbRrVA==
dependencies:
"@monaco-editor/loader" "^1.3.0"
"@monaco-editor/loader" "^1.3.2"
prop-types "^15.7.2"
"@next/env@12.1.0":
@@ -917,10 +917,10 @@
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz"
integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==
"@stitches/react@^1.2.6-0":
version "1.2.7"
resolved "https://registry.npmjs.org/@stitches/react/-/react-1.2.7.tgz"
integrity sha512-6AxpUag7OW55ANzRnuy7R15FEyQeZ66fytVo3BBilFIU0mfo3t49CAMcEAL/A1SbhSj/FCdWkn/XrbjGBTJTzg==
"@stitches/react@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
integrity sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==
"@types/aws-lambda@^8.10.83":
version "8.10.93"
@@ -1525,6 +1525,11 @@ color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
comment-parser@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b"
integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@@ -2964,6 +2969,11 @@ next-auth@^4.0.0-beta.5:
preact-render-to-string "^5.1.19"
uuid "^8.3.2"
next-plausible@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-3.2.0.tgz#d801346253e0c1cf64a02b9fc3a42050455cbc47"
integrity sha512-OlYcLXBG3kKd/fKMpm8SZ5IkUKSFm1/8t7cv6e5bewIqlpdZpdWuSrjbdJpbmutb2KPLXHzilKp09zmDGjy9KQ==
next-themes@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.1.1.tgz#122113a458bf1d1be5ffed66778ab924c106f82a"