Compare commits

...

17 Commits

Author SHA1 Message Date
muzam1l
7f6b989f15 Update tx state accounts on delete account. 2022-08-02 15:23:11 +05:30
muzamil
6ee1a09aaa Merge pull request #259 from XRPLF/feat/deploy-default-fields
Deploy config default fields from source files.
2022-07-29 15:59:01 +05:30
muzam1l
dd2228fb35 Reset deploy form fields when file changes. 2022-07-29 14:33:28 +05:30
muzam1l
ca52a5e064 Enforce required prop in default tags. 2022-07-28 18:12:58 +05:30
muzam1l
df0f8abe62 Add required margin to param field. 2022-07-27 17:31:43 +05:30
muzam1l
a6c4db1951 Invoke options defaults. 2022-07-26 17:05:33 +05:30
muzam1l
1c91003164 Deploy config default fields from source files. 2022-07-25 20:22:58 +05:30
muzamil
66be0efbbd Merge pull request #255 from XRPLF/feat/tab-renames
Implement file renaming.
2022-07-22 16:35:47 +05:30
muzamil
9ab64ec062 Merge pull request #256 from XRPLF/fix/compile-result
Fix incorrect compilation result.
2022-07-22 14:43:55 +05:30
muzamil
e77a5e234f Merge pull request #257 from XRPLF/fix/account-select
Fixes #240.
2022-07-22 14:43:32 +05:30
muzamil
d2f618512a Merge pull request #258 from XRPLF/fix/acc-import-limit
Remove account import limit.
2022-07-22 14:43:01 +05:30
muzam1l
1ee8dcb536 Select comp: remove hightlight from selected option 2022-07-21 16:58:07 +05:30
muzam1l
b2b7059774 Remove account import limit. 2022-07-21 15:31:06 +05:30
muzam1l
41ba096ef9 Account selector change. 2022-07-21 15:26:37 +05:30
muzam1l
8b72086c04 Differentiate between netwrok error and invalid binary error 2022-07-21 15:18:28 +05:30
muzam1l
895b34cc68 Fix compile result message on invalid wasm 2022-07-21 15:03:16 +05:30
muzamil
3897f2d823 Merge pull request #246 from XRPLF/feat/tabs-comp
Tabs and Context menu components.
2022-07-20 17:46:04 +05:30
9 changed files with 276 additions and 153 deletions

View File

@@ -32,6 +32,7 @@ import { SetHookDialog } from "./SetHookDialog";
import { addFunds } from "../state/actions/addFaucetAccount";
import { deleteHook } from "../state/actions/deployHook";
import { capitalize } from "../utils/helpers";
import { deleteAccount } from '../state/actions/deleteAccount';
export const AccountDialog = ({
activeAccountAddress,
@@ -99,10 +100,7 @@ export const AccountDialog = ({
css={{ ml: "auto", mr: "$9" }}
tabIndex={-1}
onClick={() => {
const index = state.accounts.findIndex(
acc => acc.address === activeAccount?.address
);
state.accounts.splice(index, 1);
deleteAccount(activeAccount?.address);
}}
>
Delete Account <Trash size="15px" />

View File

@@ -162,7 +162,7 @@ export const Log: FC<ILog> = ({
const enrichAccounts = useCallback(
(str?: string): ReactNode => {
if (!str || !accounts.length) return null;
if (!str || !accounts.length) return str;
const pattern = `(${accounts.map(acc => acc.address).join("|")})`;
const res = regexifyString({

View File

@@ -91,7 +91,7 @@ const Select = forwardRef<any, Props>((props, ref) => {
...provided,
color: colors.searchText,
backgroundColor:
state.isSelected || state.isFocused
state.isFocused
? colors.activeLight
: colors.dropDownBg,
":hover": {

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import { Plus, Trash, X } from "phosphor-react";
import Button from "./Button";
import Box from "./Box";
import { Button, Box, Text } from ".";
import { Stack, Flex, Select } from ".";
import {
Dialog,
@@ -19,48 +18,30 @@ import {
useForm,
} from "react-hook-form";
import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions";
import { useSnapshot } from "valtio";
import state, { IFile, 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 => ({
label: key,
value: key as keyof TTS,
}));
export type SetHookData = {
Invoke: {
value: keyof TTS;
label: string;
}[];
Fee: string;
HookNamespace: string;
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
import {
getParameters,
getInvokeOptions,
transactionOptions,
SetHookData,
} from "../utils/setHook";
import { capitalize } from "../utils/helpers";
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => {
const snap = useSnapshot(state);
const [estimateLoading, setEstimateLoading] = useState(false);
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const compiledFiles = snap.files.filter(file => file.compiledContent);
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
label: acc.name,
value: acc.address,
@@ -75,12 +56,23 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const getHookNamespace = useCallback(
() =>
activeFile && snap.deployValues[activeFile.name]
? snap.deployValues[activeFile.name].HookNamespace
: activeFile?.name.split(".")[0] || "",
(activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
activeFile?.name.split(".")[0] ||
"",
[activeFile, snap.deployValues]
);
const getDefaultValues = useCallback((): Partial<SetHookData> => {
const content = activeFile?.compiledValueSnapshot;
return (
(activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: getHookNamespace(),
Invoke: getInvokeOptions(content),
HookParameters: getParameters(content),
}
);
}, [activeFile, getHookNamespace, snap.deployValues]);
const {
register,
handleSubmit,
@@ -88,29 +80,25 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
watch,
setValue,
getValues,
reset,
formState: { errors },
} = useForm<SetHookData>({
defaultValues: (activeFile && snap.deployValues[activeFile.name]) || {
HookNamespace: activeFile?.name.split(".")[0] || "",
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
},
defaultValues: getDefaultValues(),
});
const { fields, append, remove } = useFieldArray({
control,
name: "HookParameters", // unique name for your Field Array
});
const [formInitialized, setFormInitialized] = useState(false);
const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee");
// Update value if activeFile changes
// Reset form if activeFile changes
useEffect(() => {
if (!activeFile) return;
const defaultValue = getHookNamespace();
const defaultValues = getDefaultValues();
setValue("HookNamespace", defaultValue);
setFormInitialized(true);
}, [setValue, activeFile, snap.deployValues, getHookNamespace]);
reset(defaultValues);
}, [activeFile, getDefaultValues, reset]);
useEffect(() => {
if (
@@ -141,23 +129,19 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
calculateHashedValue();
}, [namespace, calculateHashedValue]);
// Calculate initial fee estimate when modal opens
useEffect(() => {
if (formInitialized && account) {
(async () => {
const formValues = getValues();
const tx = await prepareDeployHookTx(account, formValues);
if (!tx) {
return;
}
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
})();
const calculateFee = useCallback(async () => {
if (!account) return;
const formValues = getValues();
const tx = await prepareDeployHookTx(account, formValues);
if (!tx) {
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formInitialized]);
const res = await estimateFee(tx, account);
if (res && res.base_fee) {
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
}
}, [account, getValues, setValue]);
const tooLargeFile = () => {
return Boolean(
@@ -172,6 +156,12 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
);
if (!account) return;
if (currAccount) currAccount.isLoading = true;
data.HookParameters.forEach(param => {
delete param.$metaData;
return param;
});
const res = await deployHook(account, data);
if (currAccount) currAccount.isLoading = false;
@@ -181,8 +171,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
}
toast.error(`Transaction failed! (${res?.engine_result_message})`);
};
const onOpenChange = useCallback((open: boolean) => {
setIsSetHookDialogOpen(open);
if (open) calculateFee();
}, [calculateFee]);
return (
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<Button
ghost
@@ -206,7 +202,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Select
instanceId="deploy-account"
placeholder="Select account"
hideSelectedOptions
options={accountOptions}
value={selectedAccount}
onChange={(acc: any) => setSelectedAccount(acc)}
@@ -252,22 +247,39 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
<Stack>
{fields.map((field, index) => (
<Stack key={field.id}>
<Input
// important to include key with field's id
placeholder="Parameter name"
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
<Flex column>
<Flex row>
<Input
// important to include key with field's id
placeholder="Parameter name"
readOnly={field.$metaData?.required}
{...register(
`HookParameters.${index}.HookParameter.HookParameterName`
)}
/>
<Input
css={{ mx: "$2" }}
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`,
{ required: field.$metaData?.required }
)}
/>
<Button
onClick={() => remove(index)}
variant="destroy"
>
<Trash weight="regular" size="16px" />
</Button>
</Flex>
{errors.HookParameters?.[index]?.HookParameter
?.HookParameterValue?.type === "required" && (
<Text error>This field is required</Text>
)}
/>
<Input
placeholder="Value (hex-quoted)"
{...register(
`HookParameters.${index}.HookParameter.HookParameterValue`
)}
/>
<Button onClick={() => remove(index)} variant="destroy">
<Trash weight="regular" size="16px" />
</Button>
<Label css={{ fontSize: "$sm", mt: "$1" }}>
{capitalize(field.$metaData?.description)}
</Label>
</Flex>
</Stack>
))}
<Button

View File

@@ -28,40 +28,34 @@ export const names = [
* is protected with CORS so that's why we did our own endpoint
*/
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");
}
if (typeof window !== 'undefined') {
if (typeof window === undefined) return
const toastId = showToast ? toast.loading("Creating account") : "";
const res = await fetch(`${window.location.origin}/api/faucet`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
return;
}
const toastId = showToast ? toast.loading("Creating account") : "";
const res = await fetch(`${window.location.origin}/api/faucet`, {
method: "POST",
});
const json: FaucetAccountRes | { error: string } = await res.json();
if ("error" in json) {
if (showToast) {
return toast.error(json.error, { id: toastId });
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
return;
}
} else {
if (showToast) {
toast.success("New account created", { id: toastId });
}
const currNames = state.accounts.map(acc => acc.name);
state.accounts.push({
name: name || names.filter(name => !currNames.includes(name))[0],
xrp: (json.xrp || 0 * 1000000).toString(),
address: json.address,
secret: json.secret,
sequence: 1,
hooks: [],
isLoading: false,
version: '2'
});
}
};

View File

@@ -29,25 +29,30 @@ export const compileCode = async (activeId: number) => {
const file = state.files[activeId]
try {
file.containsErrors = false
const res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
output: "wasm",
compress: true,
strip: state.compileOptions.strip,
files: [
{
type: "c",
options: state.compileOptions.optimizationLevel || '-O2',
name: file.name,
src: file.content,
},
],
}),
});
let res: Response
try {
res = await fetch(process.env.NEXT_PUBLIC_COMPILE_API_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
output: "wasm",
compress: true,
strip: state.compileOptions.strip,
files: [
{
type: "c",
options: state.compileOptions.optimizationLevel || '-O2',
name: file.name,
src: file.content,
},
],
}),
});
} catch (error) {
throw Error("Something went wrong, check your network connection and try again!")
}
const json = await res.json();
state.compiling = false;
if (!json.success) {
@@ -61,29 +66,34 @@ export const compileCode = async (activeId: number) => {
}
throw errors
}
state.logs.push({
type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
});
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
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) => {
const ww = wabt.default();
try {
// Decode base64 encoded wasm that is coming back from the endpoint
const bufferData = await decodeBinary(json.output);
// Import wabt from and create human readable version of wasm file and
// put it into state
const ww = (await import('wabt')).default()
const myModule = ww.readWasm(new Uint8Array(bufferData), {
readDebugNames: true,
});
myModule.applyNames();
const wast = myModule.toText({ foldExprs: false, inlineExport: false });
state.files[state.active].compiledWatContent = wast;
toast.success("Compiled successfully!", { position: "bottom-center" });
file.compiledContent = ref(bufferData);
file.lastCompiled = new Date();
file.compiledValueSnapshot = file.content
file.compiledWatContent = wast;
} catch (error) {
throw Error("Invalid compilation result produced, check your code for errors and try again!")
}
toast.success("Compiled successfully!", { position: "bottom-center" });
state.logs.push({
type: "success",
message: `File ${state.files?.[activeId]?.name} compiled successfully. Ready to deploy.`,
link: Router.asPath.replace("develop", "deploy"),
linkText: "Go to deploy",
});
} catch (err) {
console.log(err);
@@ -96,12 +106,19 @@ export const compileCode = async (activeId: number) => {
});
})
}
else if (err instanceof Error) {
state.logs.push({
type: "error",
message: err.message,
});
}
else {
state.logs.push({
type: "error",
message: "Something went wrong, check your connection try again later!",
message: "Something went wrong, come back later!",
});
}
state.compiling = false;
toast.error(`Error occurred while compiling!`, { position: "bottom-center" });
file.containsErrors = true

View File

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

View File

@@ -3,10 +3,10 @@ import toast from "react-hot-toast";
import state, { IAccount } from "../index";
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
import { SetHookData } from "../../components/SetHookDialog";
import { Link } from "../../components";
import { ref } from "valtio";
import estimateFee from "../../utils/estimateFee";
import { SetHookData } from '../../utils/setHook';
export const sha256 = async (string: string) => {
const utf8 = new TextEncoder().encode(string);

78
utils/setHook.ts Normal file
View File

@@ -0,0 +1,78 @@
import { getTags } from './comment-parser';
import { tts, TTS } from './hookOnCalculator';
export const transactionOptions = Object.keys(tts).map(key => ({
label: key,
value: key as keyof TTS,
}));
export type SetHookData = {
Invoke: {
value: keyof TTS;
label: string;
}[];
Fee: string;
HookNamespace: string;
HookParameters: {
HookParameter: {
HookParameterName: string;
HookParameterValue: string;
};
$metaData?: any;
}[];
// HookGrants: {
// HookGrant: {
// Authorize: string;
// HookHash: string;
// };
// }[];
};
export const getParameters = (content?: string) => {
const fieldTags = ["field", "param", "arg", "argument"];
const tags = getTags(content)
.filter(tag => fieldTags.includes(tag.tag))
.filter(tag => !!tag.name);
const paramters: SetHookData["HookParameters"] = tags.map(tag => ({
HookParameter: {
HookParameterName: tag.name,
HookParameterValue: tag.default || "",
},
$metaData: {
description: tag.description,
required: !tag.optional
},
}));
return paramters;
};
export const getInvokeOptions = (content?: string) => {
const invokeTags = ["invoke", "invoke-on"];
const options = getTags(content)
.filter(tag => invokeTags.includes(tag.tag))
.reduce((cumm, curr) => {
const combined = curr.type || `${curr.name} ${curr.description}`
const opts = combined.split(' ')
return cumm.concat(opts as any)
}, [] as (keyof TTS)[])
.filter(opt => Object.keys(tts).includes(opt))
const invokeOptions: SetHookData['Invoke'] = options.map(opt => ({
label: opt,
value: opt
}))
// default
if (!invokeOptions.length) {
const payment = transactionOptions.find(tx => tx.value === "ttPAYMENT")
if (payment) return [payment]
}
return invokeOptions;
};